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).

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.