This guide covers the Docker setup for QAuth: production (prod build, no watch) and development (dev image, Compose Watch + nx serve --watch).
QAuth uses Docker Compose to orchestrate the following services:
| Service | Image | Purpose |
|---|---|---|
| postgres | postgres:18-alpine |
Primary database (PostgreSQL 18 with uuidv7() support) |
| redis | redis:7-alpine |
Session cache and rate limiting |
| migration-runner | Custom | Runs database migrations via Nx |
| auth-server | Custom | Main authentication API server |
| Use case | Compose file(s) | Auth-server image | Watch |
|---|---|---|---|
| Production | docker-compose.yml |
Dockerfile (multi-stage prod build) |
No |
| Development | docker-compose.yml + docker-compose.dev.yml |
Dockerfile.dev (deps + source, nx serve) |
Yes (sync + rebuild) |
- Docker 20.10+ and Docker Compose 2.0+
- Docker Compose 2.22+ for development watch (
docker-compose.dev.yml+--watch) - OpenSSL (for generating JWT keys)
QAuth uses EdDSA (Ed25519) for JWT signing. Generate a key pair:
# Generate private key
openssl genpkey -algorithm Ed25519 -out private.pem
# Extract public key
openssl pkey -in private.pem -pubout -out public.pem# Copy the example environment file
cp .env.docker.example .env
# Edit .env and add your JWT keys
# The keys should include the BEGIN/END linesExample .env content:
DB_PASSWORD=your_secure_password
JWT_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIKx...
-----END PRIVATE KEY-----"
JWT_PUBLIC_KEY="-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEA...
-----END PUBLIC KEY-----"
JWT_ISSUER=http://localhost:3000Production (prod build, no watch):
docker compose up -d
docker compose logs -fDevelopment (dev image, Compose Watch, nx serve --watch; requires Docker Compose 2.22+):
# In .env: NODE_ENV=development, LOG_LEVEL=debug
docker compose -f docker-compose.yml -f docker-compose.dev.yml up --watch- Sync: changes in
apps/auth-server/andlibs/are synced into the container;nx serve --watchrebuilds and restarts. - Rebuild: changes to
package.json, lockfile,nx.json, etc. trigger a full image rebuild. - See Compose file watch.
# Check all services are healthy
docker-compose ps
# Test health endpoint
curl http://localhost:3000/healthExpected response:
{
"status": "ok",
"timestamp": "2026-01-15T05:15:47.887Z",
"services": {
"database": "connected",
"redis": "connected"
}
}┌─────────────────────────────────────────────────────────────┐
│ Docker Network │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ postgres │ │ redis │ │ migration-runner │ │
│ │ :5432 │ │ :6379 │ │ (runs once) │ │
│ └──────┬───────┘ └──────┬───────┘ └────────┬─────────┘ │
│ │ │ │ │
│ │ health check │ │ waits for │
│ │ dependency │ │ postgres │
│ ▼ ▼ ▼ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ auth-server │ │
│ │ :3000 │ │
│ │ Waits for: postgres (healthy), redis (healthy), │ │
│ │ migration-runner (completed) │ │
│ └────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
- Image:
postgres:18-alpine - Port: 5432 (mapped to host)
- Database:
qauth - User:
qauth - Features:
uuidv7()native support (PostgreSQL 18+)
Connect via psql:
docker exec -it qauth-postgres psql -U qauth -d qauth- Image:
redis:7-alpine - Port: 6379 (mapped to host)
Connect via redis-cli:
docker exec -it qauth-redis redis-cliA dedicated service that runs database migrations before auth-server starts.
- Executes
pnpm nx run infra-db:db:migrate - Exits after completion (restart: "no")
- Auth-server waits for this to complete successfully
Run migrations manually:
docker-compose run --rm migration-runnerThe main authentication API server.
- Port: 3000 (mapped to host)
- Health Check:
GET /health - Production:
Dockerfile→docker-entrypoint.shrunsnode main.js. - Development:
Dockerfile.dev→pnpm nx serve auth-server --watch; use withdocker-compose.dev.ymland--watch.
Production:
docker compose up -d --build
# Or rebuild only auth-server
docker compose build auth-server && docker compose up -d auth-serverDevelopment: use docker compose -f docker-compose.yml -f docker-compose.dev.yml up --watch; sync/rebuild is automatic. For dependency or config changes, the dev setup will rebuild the auth-server image when those files change.
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f auth-server
# Last 100 lines
docker-compose logs --tail=100 auth-server# Stop all services (keeps data)
docker-compose stop
# Stop and remove containers (keeps volumes)
docker-compose down
# Stop and remove everything including volumes
docker-compose down -v# Remove postgres volume and restart
docker-compose down -v
docker-compose up -d# Auth server
docker exec -it qauth-auth-server sh
# PostgreSQL
docker exec -it qauth-postgres sh
# Redis
docker exec -it qauth-redis shSee .env.docker.example for all available variables. Key variables:
| Variable | Required | Description |
|---|---|---|
DB_PASSWORD |
Yes | PostgreSQL password |
JWT_PRIVATE_KEY |
Yes | EdDSA private key (PEM format) |
JWT_PUBLIC_KEY |
Yes | EdDSA public key (PEM format) |
JWT_ISSUER |
No | JWT issuer URL (default: http://localhost:3000) |
EMAIL_PROVIDER |
No | Email provider: mock, resend, smtp (default: mock) |
NODE_ENV |
No | production (default) or development for dev |
LOG_LEVEL |
No | info (default); use debug for development |
For development with docker-compose.dev.yml, set NODE_ENV=development and LOG_LEVEL=debug in .env.
If ports 3000, 5432, or 6379 are in use:
# Check what's using the port
lsof -i :3000
# Modify port mappings in docker-compose.yml
ports:
- '3001:3000' # Map to different host port# Check migration-runner logs
docker-compose logs migration-runner
# Check postgres is ready
docker-compose ps postgres
# Re-run migrations
docker-compose run --rm migration-runnerEnsure your JWT keys in .env:
- Include the
-----BEGIN/END-----lines - Are properly quoted with double quotes
- Have no extra whitespace
# Clean Docker build cache
docker builder prune
# Rebuild without cache
docker-compose build --no-cacheWhen running docker compose ... up --watch, Compose uses inotify. The error often means inotify limits (not disk space):
- Close other watchers (Nx graph, IDE, etc.):
pkill -f 'nx graph.*watch' - Increase inotify instance limit:
sudo sysctl fs.inotify.max_user_instances=4096 echo "fs.inotify.max_user_instances=4096" | sudo tee -a /etc/sysctl.d/99-inotify.conf
- Use watch-free dev if it still fails:
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build # After code changes: docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build auth-server
# Check container logs
docker logs qauth-auth-server
# Check container status
docker inspect qauth-auth-server | jq '.[0].State'This Docker setup is designed for local development. For production:
- Use secrets management (Vault, AWS Secrets Manager) instead of
.envfiles - Use managed databases (RDS, Cloud SQL) instead of containerized PostgreSQL
- Use managed Redis (ElastiCache, Memorystore) for high availability
- Add reverse proxy (nginx, Traefik) with TLS termination
- Configure resource limits in Docker/Kubernetes
- Set up monitoring (Prometheus, Grafana)
- Enable logging aggregation (ELK, Loki)
See ADR-001: JWT Key Management for production key management strategy.
A comprehensive test script is available:
./scripts/test-docker.shThis verifies:
- Environment configuration
- Docker image builds
- Service startup and health
- Database migrations
- API endpoint functionality
- Data persistence