Deployment
Two deployment paths are officially supported: Docker, and a plain Node process manager (PM2) on a VPS. Both run the exact same build — pick whichever fits your infrastructure.
Environment variables
At minimum, set BETTER_AUTH_SECRET and BETTER_AUTH_URL (see .env.example for the
full list). The app validates required variables at startup and logs a clear error if
any are missing.
Option 1: Docker
docker compose up --build
The included Dockerfile/docker-compose.yml build the app and persist the SQLite
database file to a named volume, so it survives container restarts. The image installs
python3/make/g++ before pnpm install — better-sqlite3 compiles a native addon
on install, and the base node:22-slim image doesn't ship a C toolchain on its own.
Option 2: PM2 on a plain VPS
For a clean VPS with just Node installed (no Docker):
npm install -g pm2
pnpm install
pnpm run db:migrate
pnpm run build
pnpm run pm2:start
pm2 save
pm2 startup # prints a command to run once, so PM2 restarts the app on server reboot
ecosystem.config.cjs at the repo root is the PM2 process file. It loads .env via
Node's native --env-file flag — the built server run directly with node (unlike
pnpm run dev) doesn't auto-load .env on its own, so without this the app fails
its own startup env validation immediately. pnpm run pm2:restart/pm2:stop/
pm2:logs are shortcuts for the equivalent pm2 commands once the app is running.
To deploy an update: pull your changes, pnpm install (if dependencies changed),
pnpm run db:migrate, pnpm run build, then pnpm run pm2:restart.
Database
SQLite is the default for zero-infra local development and small deployments. Swap
the Drizzle adapter for Postgres/MySQL if you need multi-instance scaling — the schema
and queries in server/database/schema.ts use plain Drizzle, not SQLite-specific
features, so the migration is mostly a config change in server/utils/db.ts.