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-appVITE_* 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.
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 / noneValidation 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.
.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).
The deploy job triggers your platform only after CI passes on the main branch, via two repository secrets:
COOLIFY_WEBHOOK: the deploy webhook URLCOOLIFY_TOKEN: an API tokenDisable 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.
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.