Production Setup
Configure domains, TLS, storage, and critical environment variables for production.
Required Environment Variables
Set these in apps/server/.env:
DATABASE_URL
BETTER_AUTH_SECRET
BETTER_AUTH_URL
CORS_ORIGINS
BETTER_AUTH_COOKIE_DOMAIN (optional; set it if you want root-domain cookies)
ENABLE_PAYMENTS (false)
STORAGE_BUCKET
STORAGE_ACCESS_KEY_ID
STORAGE_SECRET_ACCESS_KEY
STORAGE_REGION (required when STORAGE_ENDPOINT is unset)
STORAGE_ENDPOINT (recommended for R2/custom S3 endpoints)
STORAGE_ADDRESSING_STYLE (optional; `auto`, `path`, or `virtual`)
STORAGE_PUBLIC_URL (optional)Set these in apps/web/.env:
NEXT_PUBLIC_APP_URL
NEXT_PUBLIC_SERVER_URL
NEXT_PUBLIC_GOOGLE_AUTH_ENABLED (optional)Optional Google OAuth:
GOOGLE_CLIENT_IDGOOGLE_CLIENT_SECRETNEXT_PUBLIC_GOOGLE_AUTH_ENABLED=true
For self-hosting, set
ENABLE_PAYMENTS=falseand ignorePOLAR_*.
Notes:
DATABASE_URLis the source of truth for the app.POSTGRES_*vars are only for configuring the bundled Compose Postgres service.- Root
.envis Docker-only (ports, bundled Postgres init, and optional Caddy settings).
Recommended Security
These settings are not required for self-hosting, but they are recommended:
CAPTURE_SUBMIT_TOKEN_SECRET
UPSTASH_REDIS_REST_URL
UPSTASH_REDIS_REST_TOKEN
TURNSTILE_SITE_KEY
TURNSTILE_SECRET_KEYWhat each one does:
CAPTURE_SUBMIT_TOKEN_SECRETenables signed short-lived capture submit tokens.- Upstash Redis enables rate limiting and one-time submit token replay protection.
- Turnstile enables an anti-bot challenge for capture submissions.
Without them, self-hosted instances still work, but capture submission protections are reduced.
CAPTURE_SUBMIT_TOKEN_SECRET can be set on its own. The setup wizard auto-generates it even if Redis and Turnstile are left disabled.
Built-In Caddy Production Setup
This repo includes a production reverse proxy layer at docker-compose.caddy.yml.
The repo includes a root Caddyfile that reads its site addresses and ACME email from .env and apps/web/.env.
Manual production layout:
- Public traffic -> Caddy (
80/443) - Caddy ->
web:3001for/ - Caddy ->
server:3000for/apiand/rpc
Start commands:
Prebuilt images:
docker compose -f docker-compose.yml -f docker-compose.caddy.yml up -dPrebuilt images with an external PostgreSQL database:
docker compose -f docker-compose.external-db.yml -f docker-compose.caddy.yml up -dContainer routing note:
- In the default compose file,
webandserverrun as two containers. websharesservernetworking, so server-side calls tolocalhost:3000work without extra internal URL variables.
Example Caddyfile for the supported single-domain deployment:
{
email {$CADDY_ACME_EMAIL}
}
{$NEXT_PUBLIC_APP_URL} {
encode gzip zstd
@api path /api/* /rpc/*
handle @api {
reverse_proxy server:3000
}
handle {
reverse_proxy server:3001
}
}Matching env for the supported single-domain deployment:
NEXT_PUBLIC_APP_URL=https://app.example.com
NEXT_PUBLIC_SERVER_URL=https://app.example.com
BETTER_AUTH_URL=https://app.example.com
CORS_ORIGINS=https://app.example.com
BETTER_AUTH_COOKIE_DOMAIN=app.example.comStorage
- Cloud storage is mandatory in all environments.
- Bug report artifacts upload directly from the browser to storage.
- Bucket CORS is required for browser uploads to work.
- Configure
STORAGE_BUCKET,STORAGE_ACCESS_KEY_ID, andSTORAGE_SECRET_ACCESS_KEY. - Use
STORAGE_REGIONfor AWS S3. - Use
STORAGE_ENDPOINTfor Cloudflare R2/custom S3-compatible storage. - Leave
STORAGE_ADDRESSING_STYLEunset to keep the defaultautobehavior. - In
auto, custom non-AWS endpoints use path-style URLs, which fits similar providers. - For AWS S3, prefer
STORAGE_REGIONwithoutSTORAGE_ENDPOINTunless you intentionally need a specific AWS endpoint hostname. - Set
STORAGE_ADDRESSING_STYLE=pathorvirtualonly if your provider needs an explicit override. - Set
STORAGE_PUBLIC_URLif you want stable public URLs instead of signed URLs.
Full setup guide:
Data Durability
Compose volume persistence:
- PostgreSQL data in
postgres_data
Also implement off-host backups and periodic restore tests.