Zug Zug.
Guides

Deploy to production

Turnkey HTTPS with Caddy and a bundled Postgres — plus escape hatches for your own ingress or a managed database.

Caddy gets you an automatic Let's Encrypt certificate, and a bundled Postgres runs out of the box. Two escape hatches cover existing ingress and managed databases.

Prerequisites

  • A host with Docker and ports 80 + 443 open to the internet and free on the host (if another web server already binds them, Caddy can't, and ACME will fail).
  • A DNS A/AAAA record for your domain pointing at the host.

1. Configure

git clone https://github.com/Fredehagelund92/zugzug.git
cd zugzug
cp .env.prod.example .env

Edit .env:

  • DOMAIN — your domain (e.g. zugzug.acme.com).
  • ACME_EMAIL — your email (Let's Encrypt expiry notices).
  • ORIGINhttps://<DOMAIN> (must match; this makes cookies Secure).
  • Generate secrets:
    openssl rand -hex 32     # -> POSTGRES_PASSWORD
    openssl rand -base64 32  # -> ZUGZUG_CURSOR_KEY

2. Launch

docker compose -f compose.prod.yml up -d --build

Caddy provisions the certificate on first request (DNS must be live and 80/443 reachable). Open https://<DOMAIN> and create the first account — it becomes the admin. Do it immediately; to restrict further signups set ALLOWED_DOMAIN (or configure OIDC) and up -d again.

Escape hatches

Behind existing ingress / your own TLS — remove the caddy service from compose.prod.yml, publish app on a host port (e.g. ports: ["8080:80"]), terminate TLS upstream, and keep ORIGIN=https://<domain>.

Managed / external Postgres — remove the postgres service, set DATABASE_URL to your managed instance (e.g. postgres://user:pass@host:5432/db?sslmode=require), and remove the depends_on: postgres block from the server service (leaving it makes docker compose error with "undefined service postgres"). Recommended at scale for managed backups / PITR.

Operate

  • Back up Postgres — see backup & restore.
  • Warehouse (optional): set ATTACH_WAREHOUSE=true + MOTHERDUCK_TOKEN. If you enable direct write, validate against a staging warehouse first.
  • Updates: git pull && docker compose -f compose.prod.yml up -d --build. Migrations run automatically on server boot. Take a Postgres backup first (see below).

If an upgrade fails

Migrations are forward-only and run on boot. If one fails, the server exits instead of serving a half-migrated schema — so a bad upgrade takes the service down rather than corrupting it. To recover:

  1. Pin the previous release (check out the prior tag/commit) and docker compose -f compose.prod.yml up -d --build to get back online on the schema you were on.
  2. If the failed migration had already partially applied, restore the pre-upgrade dump — see Backup & restore.

This is why the update step above starts with a backup: it's the only way back from a migration that fails halfway.

On this page