GenerateSaaS

Self-hosting with Docker

Build and run the node deploy target as a long-running container from the multi-stage Dockerfile, with your own database and cache.

The node deploy target ships a multi-stage Dockerfile per deployable app, built for any long-running container host - Render, Fly.io, Railway, Coolify, Dokploy, or a plain VPS. They all run the same image; the boilerplate has no platform-specific code.

The deploy artifact is the Dockerfile - in production you supply your own database and cache. infra/docker-compose.yml is the local development stack only (see below).

The image

The Dockerfile builds on node:24-alpine in four stages - prune, install, build, run - and the runner inherits FROM builder so drizzle-kit and the workspace tree are present at boot.

  • CMD is pnpm start.
  • The CLI (packages/cli/src/generators/deploy-scripts.ts) prepends the schema step onto the owner app's start script, so it runs on every container restart.
  • For buyer builds the step is additive pnpm -F @repo/database migrate; the --demo flag swaps it for a destructive reset && push --force.
  • The schema owner is the frontend app for fullstack (its Dockerfile) or apps/backend for separate (apps/backend/Dockerfile, EXPOSE 3010).

Never pass --demo for a real deployment - the schema step becomes a destructive reset && push --force that wipes the database on every boot.

Deploy the image

Build the image. Build the owner app's Dockerfile - the frontend app's for fullstack, apps/backend/Dockerfile for separate.

Point at your own database + cache. Production uses a database and cache you provision yourself - self-hosted Postgres/Redis or a managed provider. Set their connection env on the platform. See Database and Caching.

Set runtime env. Provide DATABASE_URL (required by the CMD-time schema step), the cache connection, BETTER_AUTH_SECRET, and the rest. See Environment variables.

Run the container. CMD pnpm start runs pnpm -F @repo/database migrate, then boots the server on its port (frontend 3000, backend API_PORT default 3010).

Local dependency stack (development only)

pnpm infra runs infra/docker-compose.yml to bring up local Postgres, Redis, Mailpit, and Inngest on your machine for development - it is not a production deployment step (pnpm infra:stop tears it down). init omits any service a managed provider already covers (Postgres for Neon/Supabase, Redis for Upstash) and includes Mailpit only when the email provider is smtp (omitted for resend/ses); if every service is managed, no compose file is generated at all.

ServiceImageDefault portPurpose
postgrespostgres:18-alpine5432 (POSTGRES_PORT)Local database; in production use your own - see Database
redisredis:8-alpine6379 (REDIS_PORT)Local cache + rate limiting; in production use your own - see Caching
mailpitaxllent/mailpit1025 SMTP / 8025 UILocal email capture; see Email
inngestinngest/inngest:v1.17.48288 (INNGEST_PORT)Background job dev server; see Background jobs

Postgres credentials default to postgres / postgres, database saas. Postgres and Redis persist to named volumes (postgres_data, redis_data).

Health check

Every image declares a HEALTHCHECK that probes the listening port, with a start-period long enough to cover the CMD-time schema step - so orchestrators like Dokploy or Traefik hold traffic until the server is actually ready. The probe differs by owner type:

ImageProbePasses onstart-period
Frontend (fullstack)wget against PORT (default 3000)2xx only30s
Backend (separate)node http.get against API_PORT (default 3010)any HTTP response (incl. 404), fails on connection refused90s

The backend's longer start-period covers the slower reset && push --force of --demo builds on a cold database.

On this page