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