# Deployment _Running the server in a container and on Kubernetes with the Helm chart._ Back to the [README](../README.md) and the [documentation index](./README.md). ## Docker ```bash docker build -t terdut-server . docker run -p 8080:8080 \ -e TERDUT_DB_DSN='postgres://terdut:secret@host.docker.internal:5432/terdut?sslmode=disable' \ terdut-server ``` The server creates its own schema on startup and needs a reachable Postgres; it stores nothing on disk, so there is no volume to mount. ## Kubernetes A Helm chart is published from this repository as an OCI artifact, versioned in lockstep with the app — chart `x.y.z` is always app `vx.y.z`: ```bash helm upgrade --install terdut-server oci://git.ryuvia.com/niklas/terdut-server \ --version 0.9.2 \ --namespace terdut-server --create-namespace \ --set networking.hostname=terdut.example.com ``` The chart expects a [Gateway API](https://gateway-api.sigs.k8s.io/) Gateway named `envoy-main` in the `envoy-gateway-system` namespace to already exist — it renders an `HTTPRoute` against it rather than an `Ingress`. TLS is terminated at the gateway, so the server itself never sees a certificate. | Value | Default | Description | |---|---|---| | `networking.hostname` | `terdut.example.com` | Hostname the `HTTPRoute` serves | | `networking.listener` | `""` | Gateway listener (`sectionName`) to bind to. Empty attaches to every matching listener, **including plaintext HTTP** — set it to the HTTPS listener's name to serve TLS only | | `networking.servicePort` | `8080` | Port the route forwards to; keep in sync with `service.port` | | `bootstrap.enabled` | `true` | Runs a post-install hook that creates the first user and stores its API key in the `-admin-key` Secret. Already-bootstrapped servers are left alone | | `database.dsn` | `""` | **Required.** Postgres DSN, with no password in it. The chart provisions no database | | `database.passwordSecret.name` | `""` | Secret supplying `PGPASSWORD`. With the Zalando postgres operator, the Secret it generates for the role | | `database.passwordSecret.key` | `password` | Key within that Secret | The API key travels in an `Authorization: Bearer` header, so set `networking.listener` whenever the hostname is reachable outside a trusted network. ### The database The chart provisions no database: it takes a DSN and expects a Postgres that already exists. In this cluster the wrapper chart declares an `acid.zalan.do/v1 postgresql` CR; anywhere else, any reachable Postgres 14+ will do. The DSN carries no password. pgx falls back to libpq's environment variables for whatever the DSN leaves out, so the password arrives as `PGPASSWORD` from a Secret and never appears in values, in the rendered manifest or in `kubectl describe pod`. With the postgres operator that Secret is the one it generates for the role, so a rebuild mints a new password with nothing to keep in sync — the same wiring miniflux uses. The server migrates its own schema on startup, so a new database only has to exist and be writable. ### Backups Postgres is backed up where it runs, not from here. The database pod carries a [k8up](https://k8up.io/) `k8up.io/backupcommand` annotation that streams a `pg_dump`, the same way gitea and immich do in this cluster. ## On Kubernetes with the operator [terdut-operator](https://git.ryuvia.com/niklas/terdut-operator) runs a server for you from a `TerdutServer` object and manages its teams, escalation ladders, dead man's switches and alert sources as Kubernetes objects. It hands the server a generated key through `TERDUT_OPERATOR_KEY` (see [Configuration](./configuration.md) and [`SERVICE-ACCOUNTS.md`](../SERVICE-ACCOUNTS.md)), and the server then treats configuration as operator-managed (`TERDUT_OPERATOR_MODE`), refusing edits made by hand in the web UI. Use the Helm chart above for a plain install, the operator when you want that configuration in gitops.