Deployment

Docker image, environment setup, CI gates, and webhook-driven deploys.

The image

Genesis ships a multi-stage Dockerfile on the official Bun image: dependencies, build, and a lean runtime stage. On boot the container syncs the database schema (prisma db push) and starts the production server (serve.ts), which serves static client assets with immutable caching and hands everything else to the SSR handler on port 3000.

docker build -t my-app .
docker run -p 3000:3000 --env-file .env my-app

Build-time vs runtime variables

VITE_* variables are inlined into the client bundle during the image build, so they must be passed as build arguments (the Dockerfile declares them). Server variables are runtime-only. On platforms like Coolify, mark VITE_* variables as build variables and the rest as runtime.

Minimum production env

NODE_ENV=production
DATABASE_URL=...            # Postgres, or SQLite on a persistent volume
BETTER_AUTH_SECRET=...      # 32+ characters
BETTER_AUTH_URL=https://your-domain.com
VITE_APP_NAME=YourApp
VITE_APP_URL=https://your-domain.com
EMAIL_PROVIDER=resend       # or plunk / ses / none

Validation fails the boot loudly with the exact missing or invalid variable. If you run SQLite in production, mount a persistent volume and point DATABASE_URL at it, or the database resets on every deploy.

CI pipeline

.github/workflows/ci.yml is path-aware: documentation-only pushes skip the pipeline entirely, app changes run lint, format, build, typecheck, and tests, and test suites run scoped to the areas a change touched (escalating to the full suite for high-blast-radius changes like dependencies or schema).

Review handoff: agent branch → PR → preview → merge

This is the default way work — agent-authored or human-authored — gets reviewed before it merges:

  1. Branch — work happens on its own branch off master, never directly on master.
  2. Pull request — pushing the branch and opening a PR triggers .github/workflows/ci.yml, which lints, builds, typechecks, and tests it.
  3. "What changed" — the PR body follows .github/PULL_REQUEST_TEMPLATE.md: what changed, what to review/test, preview notes, the verification run, and follow-up notes. This is what tells a reviewer — human or otherwise — exactly where to look instead of re-reading the whole diff.
  4. Coolify preview link — once PR previews are configured (see below), Coolify deploys a live preview scoped to that PR and the link goes in the PR's "Preview notes" section.
  5. Human review — a person reviews the PR body, the preview (or, if none is configured, runs it locally), and requests changes or approves.
  6. Merge — merging to master triggers the webhook-driven deploy below.

Coolify PR previews are the default way a reviewer looks at a change without pulling the branch. A local tunnel (bun run tunnel) is a fallback only — for before a preview exists, a quick look, or an inbound webhook/OAuth callback that needs to reach localhost — not a substitute for review-by-preview.

Enabling Coolify PR previews

Previews are Coolify's own feature and need no CI wiring and no extra repository secrets. Coolify's GitHub App webhook creates a preview when a PR opens and rebuilds it when new commits land on that PR.

The short version for a downstream project:

  1. Add a wildcard DNS record for the preview hostname, pointing at the server. Behind Cloudflare it must be DNS-only: Universal SSL covers one label deep, and a preview host is two, so a proxied record has no certificate and the TLS handshake fails.
  2. Enable Preview Deployments on the Coolify application.
  3. Add persistent storage (<project>-data at /data) and point the preview DATABASE_URL at SQLite on it.
  4. Populate the entire preview environment. Coolify preview environments do not inherit production — is_preview splits the variables into two disjoint sets with no fallback, so an unset variable is absent rather than inherited.

Do not add a CI job to redeploy previews. An earlier version of this guide recommended one, on the strength of coollabsio/coolify#6752 reporting that Coolify only builds on opened/reopened. That is fixed: a CI job doing it as well produces two concurrent builds racing for the same -pr-N container, and they tend to fail each other.

docs/preview-deployments.md has the full setup and, more usefully, the failure modes — they are near-identical from outside (green deploy, running container, Traefik 404) and all resolve at docker logs <uuid>-pr-N.

Webhook-driven deploys

The deploy job triggers your platform only after CI passes on the main branch, via two repository secrets:

  • COOLIFY_WEBHOOK: the deploy webhook URL
  • COOLIFY_TOKEN: an API token

Disable your platform's auto-deploy-on-push so the green pipeline is the only path to production. Without the secrets configured, the deploy job skips gracefully.

Health checks

Point your platform's health check at /api/health: it returns 200 with a live database ping and 503 when the database is unreachable, catching both dead processes and lost volumes.