Production Deployment
Deploy answerLoops to a production server.
This assumes you've already worked through the self-host quickstart locally and know your setup works — production is the same app, just with real secrets, a real domain, and docker-compose.prod.yml instead of the dev compose file.
Recommended stack
- Server: Any Linux VPS (Ubuntu 22.04+)
- Reverse proxy: Caddy (auto SSL) or nginx
- Process: Docker Compose
- Database: Docker Postgres with named volume, or managed Postgres (Neon, Supabase, Railway)
Get the code onto the server
Clone the repo on your server the same way you did locally, then create a production .env (not .env.local — that's the dev-only file):
git clone https://github.com/answerLoops/answerLoops.git
cd answerLoops
cp .env.local.example .envFill in .env with your production values — the checklist below covers what has to be different from your dev setup.
Environment checklist
Before going live, verify these are set in your production .env:
-
AUTH_URLset to your production domain (e.g.https://app.yourdomain.com) -
AUTH_SECRET— unique 32-byte hex, not the same as dev -
ENCRYPTION_KEY— unique 32-byte hex, not the same as dev -
DATABASE_URLpointing to production database - OAuth callback URLs updated in GitHub/Google dashboards
-
OPENAI_API_KEYset
Caddy (recommended — auto SSL)
app.yourdomain.com {
reverse_proxy localhost:3000
}Run: caddy run --config Caddyfile
nginx
server {
listen 443 ssl;
server_name app.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}Start in production
Use docker-compose.prod.yml, not the plain docker-compose.yml from local dev — it builds the multi-stage production image and runs the bot in bot:start mode instead of the dev watch mode:
docker compose -f docker-compose.prod.yml up -d --buildDrizzle migrations run automatically against your production database on first start, same as local dev — no separate migration step.
The bot service must be running for knowledge-base syncs (Notion, GitHub) to complete — it's the worker that picks up queued sync jobs and drives them. It reaches the app at BOT_TARGET_URL (default http://localhost:3000) and authenticates with BOT_SECRET; set both if the two services aren't on the same host. The sync itself runs inside POST /api/kb/sync-jobs/run on the app, which does real work after responding to auth — deploy the app on a runtime without a short per-request timeout (the standard pnpm start Node server is fine; a serverless function platform may cut a large sync off).
Health check
curl https://app.yourdomain.com/api/health
# → {"ok":true}Alternative: Railway or another PaaS
If you'd rather not manage a VPS, reverse proxy, and TLS certs yourself, push the repo to Railway (or another PaaS) — Railway reads the committed railway.toml / railway.bot.toml, builds each service with Nixpacks, handles HTTPS automatically, and you skip the Caddy/nginx section above entirely. You'll still need to set the same environment variables, and a managed Postgres (Neon, Railway's own Postgres) instead of the Docker-volume one.