Deployment
Docker image
The image is built in three stages: the Vue SPA (frontend/), the compiled backend (dist/), and a runtime layer holding production dependencies, the built backend and frontend, the Drizzle migrations, the built-in page and e-mail templates, and the LiteLLM gateway configuration. It is based on node:22-alpine and exposes port 3001.
Run the prebuilt image
Images are published to GitHub Container Registry by the release and nightly workflows:
ghcr.io/circle-rd/auth-service:latest— latest tagged releaseghcr.io/circle-rd/auth-service:nightly— built fromdevelopby the manually triggered nightly workflow (the Compose default)ghcr.io/circle-rd/auth-service:X.Y/:X.Y.Z— pinned releases
Set AUTH_IMAGE in .env to pin a tag, then:
cp .env.example .env # then edit, see below
docker compose pull
docker compose up -d
To build from source instead, run docker compose up -d --build.
The auth-service container waits for PostgreSQL to be healthy. Migrations run automatically at startup whenever NODE_ENV is not development (the Compose stack forces production): no manual step is needed.
docker compose down -v. After the first production release, migrations become immutable history.Production settings
DOMAIN=example.com
PUBLIC_SCHEME=https
BETTER_AUTH_URL=https://auth.example.com
BETTER_AUTH_SECRET=<openssl rand -base64 32>
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=<at least 12 characters>
POSTGRES_PASSWORD=<strong password>
CORS_ORIGINS=https://app.example.com
TRUST_PROXY_HOPS=1
REQUIRE_EMAIL_VERIFICATION=true
SMTP_HOST=smtp.example.com
SMTP_USER=<user>
SMTP_PASS=<password>
SMTP_FROM=Example <no-reply@example.com>
Points to check:
- SMTP — the application requires e-mail verification by default in production, but the Compose file passes
REQUIRE_EMAIL_VERIFICATION=falseunless.envsets it. SetREQUIRE_EMAIL_VERIFICATION=truetogether withSMTP_HOSTto verify addresses: a failed SMTP connection check at startup is fatal in production, and the process refuses to start when an e-mail flow is on withoutSMTP_HOST(see Email setup). TRUST_PROXY_HOPS—1behind a single reverse proxy. With the default0, every client shares the proxy's address for rate limiting.BETTER_AUTH_URL— the public URL, used as the token issuer. Changing it invalidates the issuer trusted by your applications.POSTGRES_PASSWORD— the Compose default isauth; do not keep it.
The complete list is in the environment variables reference.
Optional services
The Compose file ships optional services behind profiles. Enable them with --profile <name> or COMPOSE_PROFILES=llm,redis in .env.
| Profile | Service | Purpose |
|---|---|---|
redis | redis | Shares rate-limit buckets and the admin real-time events between several auth-service instances. Also set REDIS_URL=redis://redis:6379 |
mail | mailpit | Local SMTP catcher (SMTP_HOST=mailpit, SMTP_PORT=1025, web UI on port 8025). Development only |
llm | litellm-setup, litellm-provision, litellm | LLM and MCP gateway authenticated with auth-service tokens, see LiteLLM gateway |
Reverse proxy
Terminate TLS in front of the service. A minimal Traefik configuration:
labels:
- traefik.enable=true
- traefik.http.routers.auth-service.rule=Host(`auth.example.com`)
- traefik.http.routers.auth-service.entrypoints=websecure
- traefik.http.routers.auth-service.tls.certresolver=letsencrypt
- traefik.http.services.auth-service.loadbalancer.server.port=3001
BETTER_AUTH_URL must match the public host name: BetterAuth uses it as the issuer of the discovery document and of the tokens.
Health check
curl https://auth.example.com/health
# → {"status":"ok","version":"<x.y.z>"}
The Compose file already declares a health check on /health for the auth-service and postgres services. For another orchestrator, probe the same endpoint.
Running migrations separately
To migrate outside the application start (for example in a Kubernetes init container), run from a source checkout with DATABASE_URL set:
pnpm db:migrate