Ctrl K
ComunidadGuías…

compose.yaml

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

Archivo original ↗Preguntar a KiBot
name: openbooks

x-openbooks-runtime-environment: &openbooks-runtime-environment
  NODE_ENV: production
  HOSTNAME: 0.0.0.0
  PORT: "3000"
  OPENBOOKS_DB_URL: postgres://openbooks_app:${OPENBOOKS_DB_PASSWORD:?OPENBOOKS_DB_PASSWORD is required}@postgres:5432/openbooks
  # Cross-tenant work (installation-wide maintenance, sandbox promotion) runs
  # on this dedicated BYPASSRLS login, never on the runtime login: production
  # web/worker refuse at import without it (engine/src/platform/db.ts). The
  # one-shot bootstrap provisions the role automatically (see
  # docs/operations/communal-postgres.md for the posture it enforces);
  # password rotation is a re-run of the bootstrap service with the new value.
  OPENBOOKS_BYPASS_DB_URL: postgres://openbooks_bypass:${OPENBOOKS_BYPASS_DB_PASSWORD:?OPENBOOKS_BYPASS_DB_PASSWORD is required}@postgres:5432/openbooks
  OPENBOOKS_REDIS_URL: redis://:${REDIS_PASSWORD:?REDIS_PASSWORD is required}@redis:6379
  SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required}
  OPENBOOKS_DATA_KEY: ${OPENBOOKS_DATA_KEY:?OPENBOOKS_DATA_KEY is required}
  OPENBOOKS_INTERNAL_TOKEN: ${OPENBOOKS_INTERNAL_TOKEN:?OPENBOOKS_INTERNAL_TOKEN is required}
  OPENBOOKS_APP_URL: ${OPENBOOKS_APP_URL:-http://localhost:4780}
  APP_BASE_URL: ${OPENBOOKS_APP_URL:-http://localhost:4780}
  # OpenTelemetry (optional, see docs/operations/observability.md): set
  # an OTLP/HTTP collector endpoint and the web + worker processes export
  # traces/metrics; leaving all endpoints empty keeps them a free no-op.
  OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
  OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: ${OTEL_EXPORTER_OTLP_TRACES_ENDPOINT:-}
  OTEL_EXPORTER_OTLP_METRICS_ENDPOINT: ${OTEL_EXPORTER_OTLP_METRICS_ENDPOINT:-}
  OTEL_EXPORTER_OTLP_HEADERS: ${OTEL_EXPORTER_OTLP_HEADERS:-}
  OTEL_SERVICE_NAME: ${OTEL_SERVICE_NAME:-openbooks}
  OTEL_RESOURCE_ATTRIBUTES: ${OTEL_RESOURCE_ATTRIBUTES:-}
  OTEL_METRIC_EXPORT_INTERVAL: ${OTEL_METRIC_EXPORT_INTERVAL:-}
  OTEL_TRACES_EXPORTER: ${OTEL_TRACES_EXPORTER:-}
  OTEL_METRICS_EXPORTER: ${OTEL_METRICS_EXPORTER:-}
  # Keep proxy-derived client addresses disabled unless every inbound request
  # crosses a trusted proxy that overwrites forwarding headers.
  OPENBOOKS_TRUST_PROXY: ${OPENBOOKS_TRUST_PROXY:-0}
  OPENBOOKS_OIDC_ISSUER: ${OPENBOOKS_OIDC_ISSUER:-}
  OPENBOOKS_OIDC_CLIENT_ID: ${OPENBOOKS_OIDC_CLIENT_ID:-}
  OPENBOOKS_OIDC_CLIENT_SECRET: ${OPENBOOKS_OIDC_CLIENT_SECRET:-}
  OPENBOOKS_OIDC_LABEL: ${OPENBOOKS_OIDC_LABEL:-}
  S3_ENDPOINT: http://minio:9000
  S3_REGION: us-east-1
  # Least privilege: the application holds a dedicated MinIO user scoped to
  # the app bucket (created by minio-init below), never the root credentials.
  S3_ACCESS_KEY_ID: ${MINIO_APP_USER:-openbooks-app}
  S3_SECRET_ACCESS_KEY: ${MINIO_APP_PASSWORD:?MINIO_APP_PASSWORD is required}
  S3_BUCKET: openbooks

services:
  postgres:
    # Exact release tags make upgrades operator-controlled and reproducible.
    image: postgres:16.9-alpine3.22@sha256:7c688148e5e156d0e86df7ba8ae5a05a2386aaec1e2ad8e6d11bdf10504b1fb7
    environment:
      POSTGRES_DB: openbooks
      POSTGRES_USER: openbooks_owner
      POSTGRES_PASSWORD: ${POSTGRES_OWNER_PASSWORD:?POSTGRES_OWNER_PASSWORD is required}
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U openbooks_owner -d openbooks"]
      interval: 5s
      timeout: 5s
      retries: 20
    restart: unless-stopped

  redis:
    image: redis:7.4.5-alpine3.21@sha256:bb186d083732f669da90be8b0f975a37812b15e913465bb14d845db72a4e3e08
    command:
      - redis-server
      - --appendonly
      - "yes"
      - --requirepass
      - ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD-SHELL", "redis-cli -a \"$${REDIS_PASSWORD}\" ping | grep PONG"]
      interval: 5s
      timeout: 5s
      retries: 20
    environment:
      REDIS_PASSWORD: ${REDIS_PASSWORD}
    restart: unless-stopped

  minio:
    image: minio/minio:RELEASE.2025-04-22T22-12-26Z@sha256:a1ea29fa28355559ef137d71fc570e508a214ec84ff8083e39bc5428980b015e
    command: server /data --console-address ":9001"
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
    volumes:
      - minio-data:/data
    healthcheck:
      # curl left the minio image in late 2023 (ubi-micro base; see
      # minio/minio#18372), so a curl probe can never pass and minio-init —
      # gated on service_healthy — would never run. `mc` ships inside the
      # server image for exactly this: this is MinIO's documented probe.
      test: ["CMD", "mc", "ready", "local"]
      interval: 5s
      timeout: 5s
      retries: 20
    restart: unless-stopped

  minio-init:
    image: minio/mc:RELEASE.2025-04-16T18-13-26Z@sha256:aead63c77f9db9107f1696fb08ecb0faeda23729cde94b0f663edf4fe09728e3
    depends_on:
      minio:
        condition: service_healthy
    environment:
      MINIO_ROOT_USER: ${MINIO_ROOT_USER}
      MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
      MINIO_APP_USER: ${MINIO_APP_USER:-openbooks-app}
      MINIO_APP_PASSWORD: ${MINIO_APP_PASSWORD:?MINIO_APP_PASSWORD is required}
    entrypoint:
      - /bin/sh
      - -ec
      - |
        mc alias set local http://minio:9000 "$${MINIO_ROOT_USER}" "$${MINIO_ROOT_PASSWORD}"
        mc mb --ignore-existing local/openbooks
        # Dedicated least-privilege app identity: web/worker get these
        # credentials (never root) with a custom policy scoped to the app
        # bucket only. Every step is idempotent so re-running this one-shot
        # is safe; `policy set` re-attaching is a no-op success.
        # Rotation: the password is only applied at creation. To rotate,
        # remove the user and re-run this service:
        #   docker compose exec minio mc admin user remove local "$APP_USER"
        #   docker compose up minio-init --force-recreate
        APP_USER="$${MINIO_APP_USER:-openbooks-app}"
        mc admin user info local "$APP_USER" >/dev/null 2>&1 \
          || mc admin user add local "$APP_USER" "$${MINIO_APP_PASSWORD}"
        POLICY_FILE="$(mktemp)"
        cat > "$POLICY_FILE" <<'POLICY_JSON'
        {
          "Version": "2012-10-17",
          "Statement": [{
            "Effect": "Allow",
            "Action": [
              "s3:ListBucket",
              "s3:GetBucketLocation",
              "s3:GetObject",
              "s3:PutObject",
              "s3:DeleteObject",
              "s3:AbortMultipartUpload",
              "s3:ListMultipartUploadParts",
              "s3:ListBucketMultipartUploads"
            ],
            "Resource": ["arn:aws:s3:::openbooks", "arn:aws:s3:::openbooks/*"]
          }]
        }
        POLICY_JSON
        mc admin policy info local openbooks-app >/dev/null 2>&1 \
          || mc admin policy create local openbooks-app "$POLICY_FILE"
        rm -f "$POLICY_FILE"
        mc admin policy set local openbooks-app "user=$APP_USER"
    restart: "no"

  bootstrap:
    image: ${OPENBOOKS_IMAGE:?OPENBOOKS_IMAGE must be a post-clean, scanned image pinned by digest}
    pull_policy: missing
    command: ["node", "scripts/bootstrap.mjs"]
    environment:
      <<: *openbooks-runtime-environment
      OPENBOOKS_BOOTSTRAP: "1"
      # Changes with every immutable deployment so Compose recreates this
      # completed one-shot container and applies newly published migrations.
      OPENBOOKS_BOOTSTRAP_REV: ${OPENBOOKS_BOOTSTRAP_REV:-local}
      OPENBOOKS_MIGRATION_DB_URL: postgres://openbooks_owner:${POSTGRES_OWNER_PASSWORD:?POSTGRES_OWNER_PASSWORD is required}@postgres:5432/openbooks
      OPENBOOKS_RUNTIME_DB_URL: postgres://openbooks_app:${OPENBOOKS_DB_PASSWORD:?OPENBOOKS_DB_PASSWORD is required}@postgres:5432/openbooks
      ORG_NAME: ${ORG_NAME:-OpenBooks}
      ORG_CURRENCY: ${ORG_CURRENCY:?ORG_CURRENCY is required}
      ORG_COUNTRY: ${ORG_COUNTRY:?ORG_COUNTRY is required}
      ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@openbooks.local}
      ADMIN_NAME: ${ADMIN_NAME:-Administrator}
      ADMIN_PASSWORD: ${ADMIN_PASSWORD:?ADMIN_PASSWORD is required}
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"

  web:
    image: ${OPENBOOKS_IMAGE:?OPENBOOKS_IMAGE must be a post-clean, scanned image pinned by digest}
    pull_policy: missing
    command: ["node", "web/server.js"]
    environment:
      <<: *openbooks-runtime-environment
    depends_on:
      bootstrap:
        condition: service_completed_successfully
      redis:
        condition: service_healthy
      minio-init:
        condition: service_completed_successfully
    ports:
      - "${OPENBOOKS_BIND_ADDRESS:-127.0.0.1}:${OPENBOOKS_PORT:-4780}:3000"
    healthcheck:
      test:
        - CMD
        - node
        - -e
        - "fetch('http://127.0.0.1:3000/api/v1/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
      interval: 10s
      timeout: 5s
      retries: 20
      start_period: 90s
    restart: unless-stopped

  # Scheduled work runs HERE: the worker owns every scheduled tick (scripts,
  # feeds, billing, outbox, flows, close scans), claimed once across replicas
  # by the shared Postgres lock. The web service intentionally sets no
  # OPENBOOKS_RUN_SCHEDULER — set it to 1 on web only for single-process
  # installs; developer processes (`next dev`) never schedule.
  worker:
    image: ${OPENBOOKS_IMAGE:?OPENBOOKS_IMAGE must be a post-clean, scanned image pinned by digest}
    pull_policy: missing
    command: ["node", "scripts/worker.mjs"]
    # Graceful-drain ceiling sized to the longest job's window (migration
    # steps hold 5-minute BullMQ locks; close-delivery renders and backups
    # run for minutes on large installs). SIGTERM starts the drain in
    # engine/src/worker/index.ts — workers close first while Redis is alive,
    # then connections — so an ordinary stop exits well before this ceiling;
    # only a hung job waits it out before SIGKILL, and stalled locks recover
    # via lockDuration/stalledInterval. Mirror this on the worker service in
    # the Dokploy-managed swarm stack file, which is not checked in here.
    stop_grace_period: 5m
    environment:
      <<: *openbooks-runtime-environment
      OPENBOOKS_INTERNAL_URL: http://web:3000
    depends_on:
      web:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres-data:
  redis-data:
  minio-data: