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 callVaultClient 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 to127.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.