Files
terdut-server/docs/deployment.md
T
Niklas Ye b7d296f6e9 Create the first administrator with a generated password kept in a Secret
The bootstrap hook created `admin` with no password, so the account could
only use its API key and could not sign in on the web UI or the TUI. It
also won the one-shot /api/bootstrap against whoever ran it by hand, and
failed with exit 1 when the Secret already existed, which fails the Helm
release.

The hook now generates a 32-character password, stores username, password
and api-key in the <release>-admin-key Secret, and reuses the stored
password on later runs, so recreating the database brings the same
account back. The password is written before the account is created, so a
crash in between cannot leave an administrator nobody has the password
for. When the server was bootstrapped by something else, it checks the
stored password against /api/login and removes only the password it
generated if that does not sign in, then exits cleanly. bootstrap.enabled:
false still removes the hook and its role.

Claude-Session: https://claude.ai/code/session_016mBLURvJoMuUEr9cB2RpUN
2026-10-10 14:36:15 +02:00

4.3 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.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.