Getting started

Deployment

Run auth-service in production with Docker Compose and a reverse proxy.

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 release
  • ghcr.io/circle-rd/auth-service:nightly — built from develop by 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.

Until the first production release, the schema baseline is regenerated. Upgrading a database initialised with an older baseline stops with an explicit error; recreate it with 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=false unless .env sets it. Set REQUIRE_EMAIL_VERIFICATION=true together with SMTP_HOST to 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 without SMTP_HOST (see Email setup).
  • TRUST_PROXY_HOPS — 1 behind a single reverse proxy. With the default 0, 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 is auth; 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.

ProfileServicePurpose
redisredisShares rate-limit buckets and the admin real-time events between several auth-service instances. Also set REDIS_URL=redis://redis:6379
mailmailpitLocal SMTP catcher (SMTP_HOST=mailpit, SMTP_PORT=1025, web UI on port 8025). Development only
llmlitellm-setup, litellm-provision, litellmLLM 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
Copyright © 2026