Ctrl K
ComunidadGuías…

docker-compose.prod.yml

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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: