Files
terdut-server/docs/deployment.md
T
Niklas Ye c28a9b7b49 Let the bootstrap hook wait for a database that is slow to start
The server exits when the database does not answer within about ten
seconds, so while the database is down the pod crash-loops with a restart
backoff that grows to five minutes, and /healthz only answers after it has
connected. The hook gave up after a fixed two minutes per attempt, and Flux's
default five-minute timeout came first anyway. A failed hook fails the
release and nothing runs it again until the next upgrade, so a database that
was merely slow left the install without an administrator.

Each attempt now waits bootstrap.waitSeconds (default 600), logging progress,
and the Job gets an activeDeadlineSeconds derived from it so a database that
never comes up still ends the hook. No change to the binary, API or schema.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-13 11:49:05 +02:00

4.6 KiB

Deployment

Running the server in a container and on Kubernetes with the Helm chart. Back to the README and the documentation index.

Docker

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:

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 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/upgrade hook that creates the first administrator with a generated password and stores username, password and api-key in the <release>-admin-key Secret. The password is generated once and reused, so recreating the database brings the same account back. Already-bootstrapped servers are left alone. Set false to create the first user yourself with POST /api/bootstrap
bootstrap.waitSeconds 600 How long each attempt of the hook waits for the server to answer /healthz, which it does only once the database is reachable. Keep Helm's --timeout (Flux spec.timeout) above it; if the timeout comes first the release is marked failed, but the Job keeps running and can still create the administrator
bootstrap.username / bootstrap.email admin / admin@example.com Account the hook creates
bootstrap.secretName "" Secret to keep the credentials in; empty means <release>-admin-key
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 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 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 and 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.