Kubernetes (Helm)
GOAT has a Helm chart for Kubernetes clusters, published at oci://ghcr.io/plan4better/charts/goat. This page summarises what the chart includes and what you have to provide. The chart README is the full reference for all values.
For a single server, the Docker Compose bundle is the recommended path. It includes the login server, object storage and HTTPS, which the Helm chart leaves to you.
This page describes chart version 0.7.0, which installs GOAT v3.0.3 by default.
Installβ
helm install goat oci://ghcr.io/plan4better/charts/goat \
--version 0.7.0 \
--namespace goat --create-namespace \
--values your-values.yaml \
--wait --timeout 25m
A first install pulls several large images, which is why the timeout is long. A fresh cluster needs this one command only.
What the chart includesβ
| Component | Default |
|---|---|
| core, web, geoapi, processes, catalog | on |
| Windmill server and the default worker | on |
Windmill workers tools, workflows and print | off |
| PostgreSQL through CloudNativePG (operator and cluster) | on, optional sub-chart |
| Redis | on, optional |
A shared data volume (data, 200 Gi, ReadWriteOnce) | on |
| Caddy for custom domains | off |
The chart does not include:
- Object storage. GOAT needs an S3-compatible store: your own, or the Garage the chart can run for you.
- Keycloak. If users should log in, you need an external Keycloak.
You can also point the chart at an existing PostgreSQL or Redis instead of the bundled ones; the chart README lists the SQL your PostgreSQL needs. If postgresql.external.host is a connection pooler in transaction mode, set postgresql.external.transactionPooler: true (POSTGRES_TRANSACTION_POOLER), so GOAT prepares no statements on the server and sends no settings the pooler might refuse, and set postgresql.external.ducklakeHost to the database itself: DuckLake keeps its catalog connections open, so they bypass the pooler. If the pooler listens on another port than the database (PgBouncer's default is 6432), also set postgresql.external.ducklakePort to the database's port.
Every service has memory requests and limits by default, and a CPU request without a limit. The chart README lists them; raise them under <service>.resources if a service needs more, for example geoapi on a large catalog, and set requests and limits together.
What you have to setβ
Public URLsβ
The browser calls core, geoapi, processes and catalog directly, so the web app needs their public addresses. Give each service an Ingress with a host (<service>.ingress.*), and the URLs are derived from it, or set them explicitly in web.publicUrls.api, .geoapi, .processes and .catalog.
Without these URLs, the web app shows a blank page.
Object storageβ
Describe your S3 store once in global.s3: the bucket for uploads, the provider, endpoint, region and whether it needs path-style URLs, and a Secret with the access key pair (global.s3.existingSecret). Core, geoapi, processes and the analysis workers (below) all get these settings.
Uploaded images, such as project and dataset thumbnails, avatars and dashboard images, go to a second bucket in the same store (global.s3.assets.bucket). Browsers load them straight from that bucket, so make it publicly readable and set its public URL in global.s3.assets.publicUrl. Outside AWS the URL is required. GOAT's own artwork ships in the web app and needs neither.
Without a store of your own, set garage.enabled: true: the chart runs a single-node Garage, as the Docker Compose bundle does, creates both buckets and a key pair, and fills global.s3 for it. Browsers reach it on the web app's host, uploads at /<bucket> and images at /<assets bucket>; garage.ingress.enabled: true sets up both routes for ingress-nginx. With another ingress controller, route them yourself (see the chart's README). It is one node without replication: back up its data volume.
Loginβ
Login is off by default (global.auth.enabled: false): every service then acts as one default user, so put such an installation behind your own access control. To switch login on, set global.auth.enabled: true and global.auth.existingSecret to a Secret with the keys server-url, realm, client-id, client-secret and nextauth-secret. One Secret serves all five services.
If other applications use the same realm, their tokens also pass GOAT's signature check. Set global.auth.allowedClients to the clients whose tokens GOAT accepts, for example [goat-web], and core, geoapi, processes and catalog answer a token of any other client with 401. The list must include GOAT's own client (client-id in the Secret): the web app signs users in with it, and the print worker and the services pass those tokens on. Without it core refuses to start. The list checks which client issued a token, not who the user is: if only some users may use GOAT, enforce that on GOAT's client for every flow it allows, or turn its Direct access grants off. See A realm shared with other applications.
Email and invitationsβ
emailsets the SMTP server core sends invitations through, with the password from a Secret (email.existingSecret). The links in these emails are built from the public web and API URLs.- Keycloak sends its own emails, such as password resets, with the SMTP settings of its realm. Configure the same server there.
global.auth.provisionInvitedUsers: truelets an invitation create the Keycloak account, and Keycloak emails a link to set the password. Use it when your realm has self-registration off. Core's Keycloak client then needs the realm-management rolesview-usersandmanage-users.global.auth.allowOrganizationCreation: falselets only accounts with the Keycloak realm rolesuperusercreate organizations; everyone else seesWaiting for an invitationuntil they are invited. This keeps people who sign in before their invitation from setting up an organization of their own, after which they could no longer be invited. Set up your organization, or give an account that role, before switching it off. A superuser can delete an organization that has at most one member and nothing in it withDELETE /api/v2/organizations/{organization_id}/permanent, so that its member can be invited; with Stripe billing on, cancel the Stripe customer named in the answer'sstripe_idin Stripe.global.caBundlenames a ConfigMap or Secret with a company CA certificate, for an SMTP relay, a Keycloak or pages the print worker opens that use a certificate from a private CA. The chart mounts it into core, web and theprint,toolsandworkflowsworkers.
Support ticketsβ
Optional and off by default. Support tickets hand problem reports from your users to an Odoo Helpdesk. The block odoo holds the connection (odoo.url, odoo.db), odoo.support the Helpdesk team (odoo.support.teamId) and the API key from a Secret (odoo.support.existingSecret). Tickets are on only when all of these are set. The key is issued by Plan4Better for its own Odoo, so leave the block empty on your own installation unless you have received one.
Without tickets, Report a problem opens an email to the address in web.supportEmail, which is also named in the message shown when support is temporarily unavailable. It defaults to support@plan4better.de; set your own address for a white-label installation.
Ticket messages can carry attachments of up to 55 MiB in one request. The Ingress in front of core must accept bodies of at least 56 MB on /api/v2/support and allow generous read timeouts. ingress-nginx limits bodies to 1 MB by default; raise it with nginx.ingress.kubernetes.io/proxy-body-size: "56m" and proxy-read-timeout in core.ingress.annotations.
Analysis workersβ
The default Windmill worker only handles small jobs. Analysis tools, dataset imports and workflows need the tools and workflows workers, and PDF printing needs the print worker. All three are off by default. To use them:
- Enable them with
windmill.workers.tools.enabled,windmill.workers.workflows.enabledandwindmill.workers.print.enabled. - The
toolsandworkflowsworkers are pinned to nodes labellednode.kubernetes.io/server-usage: geodatawith the tolerationgeodata=true:NoSchedule. Prepare such nodes, or overridenodeSelectorandtolerationsfor these workers. Otherwise their pods stayPendingandhelm install --waittimes out. - The chart gives these workers what GOAT's jobs need: the GOAT database, the S3 store, the catalog bucket, the address of the web app for printing and, with login on, the Keycloak client the print worker signs in with. Windmill passes only the variables listed in
WHITELIST_ENVSon to the jobs; the chart builds that list, including every variable you add to a worker'sconfigorextraEnv. - Enable
windmill.scriptSync.enabledto register GOAT's analysis tools and tasks in Windmill after the install. It is off by default. - Each Windmill container holds at most a few connections to its database: the server 10 and every worker 3 (
windmill.server.config.DATABASE_CONNECTIONS,windmill.workers.databaseConnections). Upgrading to a GOAT release with a newer Windmill migrates the Windmill database on start, one way: back it up first.
Storage for several nodesβ
The shared data volume defaults to ReadWriteOnce, which works on a single node. On a cluster with several nodes, set data.accessMode: ReadWriteMany with an RWX-capable storage class (data.storageClassName), or supply your own claim with data.existingClaim.
geoapi, processes and the Windmill workers all write to this volume, and a fresh RWX volume belongs to root, so they run as root. If your cluster does not allow that, see the chart README ("The shared data volume").
Further readingβ
The chart README covers the details: bootstrapping a fresh cluster, the external PostgreSQL setup, authentication precedence, the shared data volume, the region new projects open in (DEFAULT_PROJECT_VIEW_STATE), schema migrations and the upgrade notes between chart versions.