- Locks no longer fail open: an unreachable backend throws LockUnavailableError and withLock skips the run (or opts into running unlocked, as template seeding does). The lnbits payment poller keeps polling through an outage, made safe by only sending confirmation emails when a row actually transitioned. - Rate limiter INCR+PEXPIRE now runs as one Lua script, self-healing counters stranded without a TTL. - Per-email login lockout moves to a Redis-backed store shared across replicas (in-memory fallback preserved), case-insensitive on email. - Graceful shutdown on SIGTERM/SIGINT: stop periodic jobs, close the server, force-close lingering SSE sockets, close Redis. - Active PING probe backs the health flag; /health reports last ping. SSE payment streams also poll the DB as a pub/sub-gap fallback. - Scale compose gains requirepass, maxmemory 256mb with noeviction, and an authenticated healthcheck; .env.example documents passwords, TLS (rediss://), and DB-index selection. - First tests in the repo: vitest + ioredis-mock covering the lock, rate limiter, and login lockout stores (27 tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
95 lines
2.9 KiB
YAML
95 lines
2.9 KiB
YAML
# Example docker-compose for running the Spanglish API as multiple replicas
|
|
# behind nginx, with Redis for shared state and Postgres as the database.
|
|
#
|
|
# This is a starting point, not a turnkey production setup. It expects a
|
|
# Dockerfile at backend/Dockerfile that builds the API and runs it on PORT.
|
|
#
|
|
# Bring it up with N API replicas:
|
|
# docker compose -f deploy/docker-compose.scale.yml up --build --scale api=3
|
|
#
|
|
# No em dashes are used in this file by design.
|
|
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
environment:
|
|
POSTGRES_USER: spanglish
|
|
POSTGRES_PASSWORD: spanglish
|
|
POSTGRES_DB: spanglish
|
|
# Raise max_connections if DB_POOL_MAX * replicas approaches the default 100.
|
|
command: ["postgres", "-c", "max_connections=200"]
|
|
volumes:
|
|
- pgdata:/var/lib/postgresql/data
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U spanglish"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
redis:
|
|
image: redis:7-alpine
|
|
# noeviction is deliberate: rate-limit, lock, and lockout keys must never
|
|
# be evicted for correctness. The cache workload is a single short-TTL key,
|
|
# so memory pressure is negligible; if caching ever grows, revisit this or
|
|
# move the cache to its own DB index.
|
|
command:
|
|
[
|
|
"redis-server",
|
|
"--appendonly", "yes",
|
|
"--requirepass", "${REDIS_PASSWORD:-change-me-redis-password}",
|
|
"--maxmemory", "256mb",
|
|
"--maxmemory-policy", "noeviction",
|
|
]
|
|
environment:
|
|
REDISCLI_AUTH: "${REDIS_PASSWORD:-change-me-redis-password}"
|
|
volumes:
|
|
- redisdata:/data
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 5s
|
|
timeout: 3s
|
|
retries: 10
|
|
|
|
api:
|
|
build:
|
|
context: ../backend
|
|
environment:
|
|
NODE_ENV: production
|
|
PORT: "3001"
|
|
DB_TYPE: postgres
|
|
DATABASE_URL: postgresql://spanglish:spanglish@postgres:5432/spanglish
|
|
DB_POOL_MAX: "15"
|
|
REDIS_URL: "redis://:${REDIS_PASSWORD:-change-me-redis-password}@redis:6379"
|
|
JWT_SECRET: change-me-to-a-strong-secret
|
|
FRONTEND_URL: http://localhost:8080
|
|
# Optional S3-compatible storage so uploads are shared across replicas.
|
|
# If you omit these, mount a shared volume at /app/uploads on every replica.
|
|
# S3_ENDPOINT: http://garage:3900
|
|
# S3_REGION: garage
|
|
# S3_BUCKET: spanglish-media
|
|
# S3_ACCESS_KEY_ID: ""
|
|
# S3_SECRET_ACCESS_KEY: ""
|
|
# S3_PUBLIC_URL: http://localhost:8080/media
|
|
expose:
|
|
- "3001"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
|
|
# Load balancer across the scaled api replicas. nginx resolves the "api"
|
|
# service name via Docker's embedded DNS, which round-robins across replicas.
|
|
lb:
|
|
image: nginx:alpine
|
|
ports:
|
|
- "8080:80"
|
|
volumes:
|
|
- ./nginx.scale.conf:/etc/nginx/conf.d/default.conf:ro
|
|
depends_on:
|
|
- api
|
|
|
|
volumes:
|
|
pgdata:
|
|
redisdata:
|