docker-compose.prod.yml
Rama master · réplica de lectura del repositorio original.
# DOMINUS — Production Docker Compose profile.
# Extends the base compose.yml for production deployments.
#
# Usage:
# docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# Override with your own .env file:
# docker compose --env-file .env -f docker-compose.yml -f docker-compose.prod.yml up -d
#
# What this profile does:
# - Uses GHCR images (published by the Deploy workflow) instead of builds
# - Applies resource limits and healthchecks to every service
# - Owns the Cloud data stores: postgres + redis are defined here (the
# base compose intentionally has no external stores — community runs
# on SQLite), so the documented command starts the full stack
# - Adds the monitoring stack: prometheus, alertmanager, grafana
#
# Image pinning (ADR-0046):
# The Deploy workflow publishes `master`, `sha-<commit>` and `vX.Y.Z`
# tags (plus -worker/-scheduler variants). DOMINUS_IMAGE_TAG is REQUIRED:
# the default `master` was removed so a rollout that forgets the pin
# fails fast instead of silently deploying a drifting tag. Pin it to a
# release tag or a sha tag for immutable rollouts:
# DOMINUS_IMAGE_TAG=v0.10.1
# DOMINUS_IMAGE_TAG=sha-6f4b3c2a1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a
#
# Backups:
# A dedicated `backups` volume keeps backups off the disk that holds
# dominus.db (same host, but isolated from DB write amplification and
# from accidental wipe of ./data). For true off-disk durability, mount
# the Hetzner Volume (or any external drive) instead — see
# docs/deployment/README.md. Never place dominus.db itself on a network
# filesystem — SQLite WAL mode is unsafe over NFS/SMB.
#
# Monitoring:
# Prometheus scrapes http://api:3000/api/v1/metrics/prometheus (no auth
# by default, reachable only on the Docker network — do not expose /metrics
# publicly through the reverse proxy). If the API sets METRICS_TOKEN, the
# Prometheus scrape must send the same token too (authorization block in
# deploy/prometheus/prometheus.yml) or it will be rejected with 401 while
# the API is protected. Alertmanager forwards alerts to the webhook
# URL set in deploy/prometheus/alertmanager.yml (set it before deploying;
# point it at the same endpoint as NOTIFIER_WEBHOOK_URL to keep a single
# alert inbox). Grafana binds 127.0.0.1:3001 by default — reach it via an
# SSH tunnel or set GRAFANA_BIND to a real address behind a proxy.
#
# Fail-fast defaults (no silent drift):
# - GRAFANA_ADMIN_PASSWORD is required: the image default admin/admin is
# never acceptable on a production host, so the overlay refuses to
# start without an explicit credential.
# - The API port binds to 127.0.0.1 (base compose): expose it through a
# reverse proxy, not the host network.
# - Postgres runs with ssl=on and hostssl-preferred pg_hba rows
# (deploy/postgres/init-ssl.sh); set sslmode=require in DATABASE_URL
# to enforce TLS on the app connection.
services:
api:
image: ghcr.io/alessiobrillo/dominus:${DOMINUS_IMAGE_TAG:?DOMINUS_IMAGE_TAG is required (ADR-0046) — set it to vX.Y.Z or sha-<commit>}
restart: always
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
reservations:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD", "node", "dist/app/healthcheck-cli.js"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
worker:
image: ghcr.io/alessiobrillo/dominus:${DOMINUS_IMAGE_TAG:?DOMINUS_IMAGE_TAG is required (ADR-0046) — set it to vX.Y.Z or sha-<commit>}-worker
restart: always
depends_on:
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.1'
memory: 64M
volumes:
- backups:/backups
environment:
- BACKUP_DIR=/backups
# The worker entrypoint exposes a loopback health server on 9090
# (/health -> 200 only while the worker is polling/processing).
healthcheck:
test: ["CMD", "node", "-e", "require('node:http').get('http://127.0.0.1:9090/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
scheduler:
image: ghcr.io/alessiobrillo/dominus:${DOMINUS_IMAGE_TAG:?DOMINUS_IMAGE_TAG is required (ADR-0046) — set it to vX.Y.Z or sha-<commit>}-scheduler
restart: always
depends_on:
redis:
condition: service_healthy
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
reservations:
cpus: '0.1'
memory: 64M
volumes:
- backups:/backups
environment:
- BACKUP_DIR=/backups
# The scheduler entrypoint exposes a loopback health server on 9091.
healthcheck:
test: ["CMD", "node", "-e", "require('node:http').get('http://127.0.0.1:9091/health',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))"]
interval: 30s
timeout: 5s
retries: 3
start_period: 15s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
# ── Cloud data stores (defined here, not in the base file: the community
# edition runs on SQLite and must never pull these containers) ─────────
postgres:
image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
restart: always
# Point-in-time recovery (ADR-0054): archive every 16MB segment into a
# subdir of PGDATA immediately after it is switched (archive_mode=on),
# or at least every 5 minutes of activity (archive_timeout). Standing
# inside PGDATA avoids volume-ownership pitfalls and keeps the archive
# on the same persistent disk as the database; base backups land in
# /backups via deploy/postgres/base-backup.sh (host cron). Trade-off:
# WAL archive shares the DB disk — off-VPS durability comes from
# shipping backups off-host (see docs/deployment/README.md).
command:
- postgres
- -c
- wal_level=replica
- -c
- archive_mode=on
- -c
- archive_command=mkdir -p /var/lib/postgresql/data/archive && test ! -f /var/lib/postgresql/data/archive/%f && cp %p /var/lib/postgresql/data/archive/%f
- -c
- archive_timeout=300
# TLS: certs are generated by deploy/postgres/init-ssl.sh on first
# initialisation (on the pgdata volume); pg_hba gains hostssl rows
# ahead of the image's host rows, so TLS-capable clients (including
# the app with sslmode=require in DATABASE_URL) always negotiate TLS
# while pre-existing non-SSL clients keep working.
- -c
- ssl=on
- -c
- ssl_cert_file=/var/lib/postgresql/ssl/server.crt
- -c
- ssl_key_file=/var/lib/postgresql/ssl/server.key
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
reservations:
cpus: '0.1'
memory: 128M
env_file: .env
environment:
POSTGRES_USER: ${POSTGRES_USER:-dominus}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set}
POSTGRES_DB: ${POSTGRES_DB:-dominus}
DOMINUS_APP_PASSWORD: ${DOMINUS_APP_PASSWORD:?DOMINUS_APP_PASSWORD must be set}
volumes:
- pgdata:/var/lib/postgresql/data
- ./deploy/postgres/init-ssl.sh:/docker-entrypoint-initdb.d/init-ssl.sh:ro
- ./deploy/postgres/init-app-role.sh:/docker-entrypoint-initdb.d/init-app-role.sh:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dominus}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
redis:
image: redis:7-alpine@sha256:e7723ff73d963f5cc6d9c4643ea3d989527a402a319239054e9472a7fb9219a2
restart: always
command: redis-server --appendonly yes --maxmemory 128mb --maxmemory-policy volatile-lru
deploy:
resources:
limits:
cpus: '0.25'
memory: 192M
reservations:
cpus: '0.05'
memory: 32M
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
# Redis metrics exporter — scraped by Prometheus so eviction pressure and
# memory usage are observable (RedisDown / RedisEvictionsHigh alerts).
redis-exporter:
image: oliver006/redis_exporter:v1.64.0@sha256:164814f4fb1455eb96a303a7fe4cebe0f6db62ae533c167adc97be6e04e94bc3
restart: always
command:
- --check-keys
- --redis.addr=redis://redis:6379
depends_on:
- redis
deploy:
resources:
limits:
cpus: '0.1'
memory: 64M
reservations:
cpus: '0.01'
memory: 16M
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
# ── Monitoring stack ──────────────────────────────────────────────────
prometheus:
image: prom/prometheus:v2.53.1@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
restart: always
command:
- --config.file=/etc/prometheus/prometheus.yml
- --storage.tsdb.path=/prometheus
- --storage.tsdb.retention.time=30d
- --web.enable-lifecycle
deploy:
resources:
limits:
cpus: '0.25'
memory: 256M
reservations:
cpus: '0.05'
memory: 64M
volumes:
- ./deploy/prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./deploy/prometheus/rules.yml:/etc/prometheus/rules.yml:ro
- prometheus-data:/prometheus
depends_on:
- api
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
alertmanager:
image: prom/alertmanager:v0.27.0@sha256:e13b6ed5cb929eeaee733479dce55e10eb3bc2e9c4586c705a4e8da41e5eacf5
restart: always
command:
- --config.file=/etc/alertmanager/alertmanager.yml
- --storage.path=/alertmanager
deploy:
resources:
limits:
cpus: '0.1'
memory: 64M
reservations:
cpus: '0.01'
memory: 16M
volumes:
- ./deploy/prometheus/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
- alertmanager-data:/alertmanager
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
grafana:
image: grafana/grafana-oss:11.2.0@sha256:408afb9726de5122b00a2576763a8a57a3c86d5b0eff5305bc994ceb3eb96c3f
restart: always
ports:
- "${GRAFANA_BIND:-127.0.0.1}:3001:3000"
environment:
- GF_SECURITY_ADMIN_USER=${GRAFANA_ADMIN_USER:-admin}
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD:?GRAFANA_ADMIN_PASSWORD must be set — no default admin/admin credentials in production}
- GF_USERS_ALLOW_SIGN_UP=false
- GF_SERVER_ROOT_URL=${GRAFANA_ROOT_URL:-http://localhost:3001}
deploy:
resources:
limits:
cpus: '0.25'
memory: 128M
reservations:
cpus: '0.05'
memory: 32M
volumes:
- ./deploy/grafana/provisioning:/etc/grafana/provisioning:ro
- grafana-data:/var/lib/grafana
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
compress: "true"
volumes:
backups:
pgdata:
redis-data:
prometheus-data:
alertmanager-data:
grafana-data: