This page is the narrative walkthrough. DEPLOYMENT.md in the repository root is the complete, copy-pasteable reference — every command below comes from there.

What’s running

One image (docker/api.Dockerfile) serves api and all four worker roles plus beat — only the container command differs. postgres and dashboard build locally from this repo’s Dockerfiles (neither has a CI publish step yet); api/workers pull the same image CI already builds and pushes to GHCR on every merge to main.

Why Vault is still required

Kubernetes isn’t the reason Vault exists in this stack — many agents and integration routes call VaultClient at runtime to read and rotate per-tenant credentials (OAuth tokens, API keys), and production Settings validation refuses to boot without a real AppRole identity. On a VPS, Vault runs in server mode with a file storage backend (not the in-memory -dev mode local development uses) — persisted to a volume, initialized once, manually unsealed after a restart. scripts/vault-bootstrap-vps.sh is the adapted setup script — the same AppRole policies the Kubernetes path uses, minus the Kubernetes-auth steps that only make sense with a Vault Agent Injector sidecar.

TLS: Caddy, not Nginx + Certbot

docker/caddy/Caddyfile gets automatic Let’s Encrypt issuance and renewal for free — no separate Certbot container or cron renewal hook. Two site blocks, mael.in (dashboard) and api.mael.in (API); DNS must already point at the VPS before first start.

Deploy, rollback, backup — the scripts

scripts/vps-deploy.sh

Pull/build → migrate → up -d → health-check. One command, idempotent.

scripts/vps-rollback.sh

Image rolls back; database rolls forward — migrations are written backward-compatible specifically so this is safe.

scripts/vps-backup.sh

Nightly pg_dump -Fc via cron, 14-day retention, optional off-host sync (a single VPS’s disk is a single point of failure).

Observability on a VPS

The same LGTM stack, same alert rules, same dashboards as the Kubernetes path — see Observability — with two VPS-specific differences: every port binds to 127.0.0.1 only (reach it over an SSH tunnel, never expose directly on a public IP), and Promtail ships container stdout to Loki, since there’s no Kubernetes log-collection DaemonSet doing that job here.

Full DEPLOYMENT.md reference