Appearance
Deploy
Everything runs on a single VPS — a Hetzner CX22-class box is plenty to start. Traefik terminates TLS with Let's Encrypt; the app trusts X-Forwarded-For only from the Docker network; the database is never exposed.
bash
# 0. Point your DNS A/AAAA records at the server; install Docker.
# 1. Shared proxy network + Traefik (once per host)
docker network create traefik
ACME_EMAIL=you@example.com docker compose -f deploy/traefik/docker-compose.yml up -d
# 2. Configure the app
cp .env.example .env
# set: APP_ENV=production, BASE_URL=https://app.example.com, DOMAIN=app.example.com,
# POSTGRES_PASSWORD=<random>, CSRF_KEY=$(openssl rand -hex 32),
# MAILER=resend + RESEND_API_KEY, STRIPE_* keys, SUPER_ADMIN_EMAIL=you@example.com
# 3. Ship it
docker compose -f docker-compose.prod.yml up -d --buildUpdates are a pull and a rebuild:
bash
git pull && docker compose -f docker-compose.prod.yml up -d --buildThe boot protects you
APP_ENV=production refuses to start with a missing CSRF_KEY, a plain-http BASE_URL, or a demo placeholder price ID in BILLING_PLANS. A misconfigured deployment fails loudly at boot, not quietly at the first payment.
Back up two things
The pgdata volume (pg_dump) and the uploads volume — blobs live outside the database, so a dump alone does not restore avatars. With STORAGE=s3 the object store holds them instead and only the database is yours to back up.
Webhooks in production
Point a Stripe webhook endpoint at https://app.example.com/api/billing/webhook and set its signing secret as STRIPE_WEBHOOK_SECRET. The events to subscribe to, and why: Stripe.
Releasing your own product
The release pipeline ships with the kit and publishes your product — the image name follows your repository, nothing to search-and-replace:
bash
git tag v1.0.0 && git push origin v1.0.0
gh release create v1.0.0 --generate-notesLinux amd64/arm64 binaries from one frontend build, a smoke test before anything is published, a multi-arch image on GHCR, checksums.txt, a CycloneDX SBOM, SLSA provenance and a keyless Cosign signature.