compose.yaml
Rama main · réplica de lectura del repositorio original.
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: