Spaces:
Runtime error
Runtime error
| # OpenWA - Docker Compose Configuration | |
| # Smart Orchestration with Profiles | |
| services: | |
| # ===== CORE: Docker Socket Proxy (sole container with /var/run/docker.sock access) ===== | |
| docker-proxy: | |
| image: tecnativa/docker-socket-proxy:v0.4.2 | |
| container_name: openwa-docker-proxy | |
| restart: unless-stopped | |
| # Only on the isolated internal network — reachable solely by openwa-api, NOT the | |
| # dashboard or any other peer. `internal: true` also denies the proxy | |
| # outbound access; it only needs the locally-mounted docker socket. | |
| networks: | |
| - internal-docker | |
| volumes: | |
| - /var/run/docker.sock:/var/run/docker.sock:ro | |
| environment: | |
| CONTAINERS: 1 | |
| IMAGES: 1 | |
| VOLUMES: 1 | |
| INFO: 1 | |
| PING: 1 | |
| POST: 1 | |
| DELETE: 1 | |
| # Everything else is denied by default (AUTH, SECRETS, NETWORKS, PLUGINS, SWARM, TASKS, SERVICES, CONFIGS, NODES, DISTRIBUTIONS) | |
| labels: | |
| - 'com.openwa.service=docker-proxy' | |
| - 'com.openwa.core=true' | |
| # ===== CORE: OpenWA Backend API ===== | |
| openwa-api: | |
| build: | |
| context: . | |
| dockerfile: Dockerfile | |
| container_name: openwa-api | |
| restart: unless-stopped | |
| # Give the graceful drain room to complete before Docker SIGKILLs the container: the shutdown | |
| # grace (SHUTDOWN_DELAY_MS, default 3s) plus the worst-case per-engine teardown bound (~10s) can | |
| # exceed Docker's 10s default, which would otherwise kill Chromium mid-teardown and orphan a | |
| # session profile. Tune down if you raise neither SHUTDOWN_DELAY_MS nor run many sessions. | |
| stop_grace_period: 45s | |
| # App network for datastores + the isolated network to reach | |
| # docker-proxy (DOCKER_HOST). createService() in docker.service.ts attaches | |
| # orchestrated containers to the literal `openwa-network`, so its name is fixed. | |
| networks: | |
| - openwa-network | |
| - internal-docker | |
| # Container hardening: Chromium runs with --no-sandbox, so the | |
| # container itself is the confinement boundary. cap_drop ALL + a minimal re-add | |
| # ONLY for the root entrypoint's chown + gosu privilege-drop; once gosu setuids to | |
| # the openwa user the node/Chromium process keeps NO effective capabilities. | |
| # read_only rootfs (Chromium's profile lives on the writable /app/data volume; | |
| # HOME=/tmp + tmpfs absorb stray writes). no-new-privileges blocks setuid escalation. | |
| # NOTE: requires a live single-session smoke (Chromium must launch) before merge. | |
| security_opt: | |
| - 'no-new-privileges:true' | |
| cap_drop: | |
| - ALL | |
| cap_add: | |
| - CHOWN # entrypoint: chown -R openwa /app/data on the named volume | |
| - DAC_OVERRIDE # entrypoint: chown across pre-existing files | |
| - FOWNER | |
| - SETGID # gosu: drop to the openwa group | |
| - SETUID # gosu: drop to the openwa user | |
| read_only: true | |
| tmpfs: | |
| - /tmp | |
| # Per-container PID ceiling (writes the cgroup pids.max). A fork-bomb guard, NOT an allocation — | |
| # the kernel only rejects new forks once the count is reached, so a higher limit is free for | |
| # light containers. The old default (512, from the #243 hardening pass) was picked without | |
| # accounting for Chromium's multi-process model: whatsapp-web.js runs a full Chromium instance | |
| # per session (browser + renderer + GPU + zygote + utilities), and WhatsApp Web is process-heavy | |
| # (service workers/iframes), so ~4 concurrent sessions already approach 512 and a new session's | |
| # Chromium gets killed mid-spawn — surfacing in the API as `Code: null`. 2048 fits ~8-10 wwjs | |
| # sessions with startup-spike headroom; Baileys is single-process (no Chromium) and uses far | |
| # less, so the higher ceiling is a no-op there. Raise via OPENWA_PIDS_LIMIT for larger fleets; | |
| # do NOT set -1 (unlimited) — that drops the fork-bomb guard. See #636. | |
| pids_limit: ${OPENWA_PIDS_LIMIT:-2048} | |
| mem_limit: ${OPENWA_MEM_LIMIT:-2g} # tune up for many concurrent sessions | |
| ports: | |
| - '127.0.0.1:${API_PORT:-2785}:2785' | |
| expose: | |
| - '2785' | |
| environment: | |
| # Core | |
| - NODE_ENV=${NODE_ENV:-production} | |
| # Writable HOME on tmpfs so Chromium's HOME-relative writes don't hit the read_only rootfs | |
| - HOME=/tmp | |
| # Chromium resolves its home from the passwd entry (no /home/openwa), ignoring $HOME, so without | |
| # writable, existing config/cache dirs it hard-crashes at launch on the read_only rootfs. Pin XDG | |
| # to the tmpfs; the entrypoint pre-creates these owned by openwa. (#254) | |
| - XDG_CONFIG_HOME=/tmp/.config | |
| - XDG_CACHE_HOME=/tmp/.cache | |
| - PORT=2785 | |
| - LOG_LEVEL=${LOG_LEVEL:-info} | |
| # Database. Forwarded blank by default (`${VAR:-}`) so a dashboard switch saved to | |
| # data/.env.generated actually applies at runtime — main.ts clears the blank (BLANK_SHADOWED_ENV_KEYS) | |
| # and the file wins. Set any of these in your .env/host to pin it (a real value keeps top precedence | |
| # and the dashboard control then shows "managed by environment"). First-run defaults (sqlite) are | |
| # written to data/.env.generated by the app, so a blank stack still boots SQLite. | |
| # NOTE: if you run the built-in Postgres MANUALLY (`docker compose --profile postgres up`) instead | |
| # of via the dashboard, set DATABASE_TYPE=postgres, DATABASE_HOST=postgres, DATABASE_PORT=5432, | |
| # DATABASE_USERNAME=openwa and DATABASE_PASSWORD in your .env/host — these are no longer defaulted here. | |
| - DATABASE_TYPE=${DATABASE_TYPE:-} | |
| - DATABASE_NAME=${DATABASE_NAME:-} | |
| - DATABASE_HOST=${DATABASE_HOST:-} | |
| - DATABASE_PORT=${DATABASE_PORT:-} | |
| - DATABASE_USERNAME=${DATABASE_USERNAME:-} | |
| # No committed default secret; blank unless the operator sets it (built-in Postgres is provisioned | |
| # with its own credentials by the orchestrator, not via this line). | |
| - DATABASE_PASSWORD=${DATABASE_PASSWORD:-} | |
| # PostgreSQL schema (blank-forwarded like the other DATABASE_* keys so a dashboard-saved | |
| # POSTGRES_SCHEMA in data/.env.generated applies; a real host value pins). Ignored for sqlite. | |
| - POSTGRES_SCHEMA=${POSTGRES_SCHEMA:-} | |
| - DATABASE_SYNCHRONIZE=${DATABASE_SYNCHRONIZE:-false} | |
| # Engine. Forwarded empty by default so the dashboard (Infrastructure > Engine) selects the | |
| # active engine via data/.env.generated (defaults to whatsapp-web.js); main.ts treats a blank | |
| # ENGINE_TYPE as unset, so .env.generated wins. Set ENGINE_TYPE in your .env/host to pin an | |
| # engine (e.g. baileys) — a real value flows through here and keeps top precedence. | |
| - ENGINE_TYPE=${ENGINE_TYPE:-} | |
| # Engine launch options (session data path, headless, browser args). Forwarded EMPTY by default | |
| # so the dashboard (Infrastructure > Engine) selections saved to data/.env.generated apply — like | |
| # ENGINE_TYPE above. main.ts treats a blank value as unset (.env.generated wins), and the app | |
| # layer (configuration.ts) supplies the sane container default (headless, ./data/sessions, the | |
| # sandbox + no-dev-shm flag set) when nothing is set anywhere. Set one on the host/.env to pin it. | |
| - SESSION_DATA_PATH=${SESSION_DATA_PATH:-} | |
| - PUPPETEER_HEADLESS=${PUPPETEER_HEADLESS:-} | |
| - PUPPETEER_ARGS=${PUPPETEER_ARGS:-} | |
| # Optional WhatsApp Web version override. Leave empty for whatsapp-web.js auto-selection. | |
| # If a session hangs at "authenticating", set WWEBJS_WEB_VERSION to a known-good cached build | |
| # from wppconnect-team/wa-version; latest|auto|off also forces auto-selection. | |
| - WWEBJS_WEB_VERSION=${WWEBJS_WEB_VERSION:-} | |
| - WWEBJS_WEB_VERSION_REMOTE_PATH=${WWEBJS_WEB_VERSION_REMOTE_PATH:-} | |
| # Raise whatsapp-web.js's first-boot init wait (default 30000ms) on slow boots — e.g. WSL2 or | |
| # low-resource hosts where the QR can time out before WA Web loads. Empty = default; see | |
| # docs/12-troubleshooting-faq.md. (Without this line the var in .env never reaches the container.) | |
| - WWEBJS_AUTH_TIMEOUT_MS=${WWEBJS_AUTH_TIMEOUT_MS:-} | |
| # Storage. Blank-forwarded (see Database note) so a dashboard local↔S3 switch applies; a real | |
| # host value pins. Credentials use the canonical S3_ACCESS_KEY_ID / S3_SECRET_ACCESS_KEY names the | |
| # app and dashboard write; the legacy S3_ACCESS_KEY / S3_SECRET_KEY are ALSO forwarded (and read | |
| # as a fallback by the storage layer) so existing .env files keep working unchanged. S3_REGION is | |
| # forwarded so external-S3 works. | |
| - STORAGE_TYPE=${STORAGE_TYPE:-} | |
| - STORAGE_LOCAL_PATH=${STORAGE_LOCAL_PATH:-} | |
| - S3_ENDPOINT=${S3_ENDPOINT:-} | |
| - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-} | |
| - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-} | |
| - S3_ACCESS_KEY=${S3_ACCESS_KEY:-} | |
| - S3_SECRET_KEY=${S3_SECRET_KEY:-} | |
| - S3_REGION=${S3_REGION:-} | |
| - S3_BUCKET=${S3_BUCKET:-} | |
| # Redis. Blank-forwarded (see Database note) so a dashboard enable/disable applies; a real host | |
| # value pins. | |
| - REDIS_ENABLED=${REDIS_ENABLED:-} | |
| - REDIS_HOST=${REDIS_HOST:-} | |
| - REDIS_PORT=${REDIS_PORT:-} | |
| # Webhook | |
| - WEBHOOK_TIMEOUT=${WEBHOOK_TIMEOUT:-10000} | |
| - WEBHOOK_RETRY_DELAY=${WEBHOOK_RETRY_DELAY:-5000} | |
| - WEBHOOK_DISPATCH_CONCURRENCY=${WEBHOOK_DISPATCH_CONCURRENCY:-16} | |
| - WEBHOOK_DISPATCH_MAX_QUEUED=${WEBHOOK_DISPATCH_MAX_QUEUED:-1000} | |
| # Rate limits. Blank values are cleared before lower-priority generated config is loaded. | |
| - RATE_LIMIT_SHORT_TTL=${RATE_LIMIT_SHORT_TTL:-} | |
| - RATE_LIMIT_SHORT_LIMIT=${RATE_LIMIT_SHORT_LIMIT:-} | |
| - RATE_LIMIT_MEDIUM_TTL=${RATE_LIMIT_MEDIUM_TTL:-} | |
| - RATE_LIMIT_MEDIUM_LIMIT=${RATE_LIMIT_MEDIUM_LIMIT:-} | |
| - RATE_LIMIT_LONG_TTL=${RATE_LIMIT_LONG_TTL:-} | |
| - RATE_LIMIT_LONG_LIMIT=${RATE_LIMIT_LONG_LIMIT:-} | |
| # Max request body size. Base64 media sends ride in the JSON body, so the default is generous; | |
| # raise this for large documents (e.g. BODY_SIZE_LIMIT=50mb). Blank keeps the app default of 25mb. | |
| - BODY_SIZE_LIMIT=${BODY_SIZE_LIMIT:-} | |
| # Plugins | |
| - PLUGINS_DIR=${PLUGINS_DIR:-/app/data/plugins} | |
| # Security | |
| - API_MASTER_KEY=${API_MASTER_KEY:-} | |
| - TRUSTED_PROXIES=${TRUSTED_PROXIES:-} | |
| # Forward the plain-HTTP dashboard CSP override; unset preserves the application default. | |
| - CSP_UPGRADE_INSECURE_REQUESTS=${CSP_UPGRADE_INSECURE_REQUESTS:-} | |
| # Docker socket proxy (openwa-api never touches /var/run/docker.sock directly) | |
| - DOCKER_HOST=tcp://docker-proxy:2375 | |
| volumes: | |
| - openwa-data:/app/data | |
| - ./docker-compose.yml:/app/docker-compose.yml:ro | |
| depends_on: | |
| docker-proxy: | |
| condition: service_started | |
| postgres: | |
| condition: service_healthy | |
| required: false | |
| redis: | |
| condition: service_healthy | |
| required: false | |
| healthcheck: | |
| test: | |
| [ | |
| 'CMD', | |
| 'node', | |
| '-e', | |
| "require('http').get('http://localhost:2785/api/health/ready', (r) => process.exit(r.statusCode === 200 ? 0 : 1))", | |
| ] | |
| interval: 30s | |
| timeout: 10s | |
| retries: 3 | |
| start_period: 30s | |
| labels: | |
| - 'com.openwa.service=api' | |
| - 'com.openwa.core=true' | |
| # The dashboard SPA is now bundled into the openwa-api image and served by NestJS on the | |
| # same port (2785) — there is no separate dashboard container. Reach it at the openwa-api | |
| # port directly; put your own TLS reverse proxy (nginx/Caddy/cloud LB) in front if needed. | |
| # ===== OPTIONAL: Built-in PostgreSQL ===== | |
| postgres: | |
| image: postgres:16-alpine | |
| container_name: openwa-postgres | |
| profiles: ['postgres', 'full'] | |
| restart: unless-stopped | |
| networks: | |
| - openwa-network | |
| security_opt: | |
| - 'no-new-privileges:true' | |
| environment: | |
| POSTGRES_USER: ${DATABASE_USERNAME:-openwa} | |
| # M16: no committed default secret. Empty unless the operator sets it; the postgres | |
| # image refuses to initialize with an empty password (clear fail-fast), and the app's | |
| # production boot guard rejects empty/placeholder secrets before startup. | |
| POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-} | |
| POSTGRES_DB: ${DATABASE_NAME:-openwa} | |
| # PostgreSQL schema for OpenWA's tables + migration ledger. Default 'public'. The init | |
| # script below creates the schema (if non-public) and sets the database default search_path, | |
| # so the built-in container works zero-config with a custom schema. Ignored when 'public'. | |
| POSTGRES_SCHEMA: ${POSTGRES_SCHEMA:-public} | |
| volumes: | |
| - postgres-data:/var/lib/postgresql/data | |
| # Runs only on first init (postgres image docker-entrypoint-initdb.d semantics): creates the | |
| # configured schema and sets the DB default search_path. No-op for 'public'; does not re-run | |
| # on an already-initialized volume (see the note in scripts/postgres-init-schema.sh). | |
| - ./scripts/postgres-init-schema.sh:/docker-entrypoint-initdb.d/01-create-schema.sh:ro | |
| healthcheck: | |
| test: ['CMD-SHELL', 'pg_isready -U ${DATABASE_USERNAME:-openwa}'] | |
| interval: 5s | |
| timeout: 3s | |
| retries: 5 | |
| labels: | |
| - 'com.openwa.service=database' | |
| - 'com.openwa.builtin=true' | |
| # ===== OPTIONAL: Built-in Redis ===== | |
| redis: | |
| image: redis:7-alpine | |
| container_name: openwa-redis | |
| profiles: ['redis', 'full'] | |
| restart: unless-stopped | |
| networks: | |
| - openwa-network | |
| security_opt: | |
| - 'no-new-privileges:true' | |
| command: redis-server --appendonly yes | |
| volumes: | |
| - redis-data:/data | |
| healthcheck: | |
| test: ['CMD', 'redis-cli', 'ping'] | |
| interval: 5s | |
| timeout: 3s | |
| retries: 5 | |
| labels: | |
| - 'com.openwa.service=cache' | |
| - 'com.openwa.builtin=true' | |
| # ===== OPTIONAL: Built-in MinIO (S3-compatible) ===== | |
| minio: | |
| image: minio/minio:RELEASE.2025-09-07T16-13-09Z | |
| container_name: openwa-minio | |
| profiles: ['minio', 'full'] | |
| restart: unless-stopped | |
| networks: | |
| - openwa-network | |
| security_opt: | |
| - 'no-new-privileges:true' | |
| command: server /data --console-address ":9001" | |
| environment: | |
| # M16: no committed default creds. MinIO refuses to start with empty root creds | |
| # (clear fail-fast); the app's production boot guard rejects empty/placeholder secrets. | |
| # Canonical names, matching what the app/dashboard read (legacy S3_ACCESS_KEY/S3_SECRET_KEY | |
| # still accepted as a fallback so older .env files keep working). | |
| MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID:-${S3_ACCESS_KEY:-}} | |
| MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY:-${S3_SECRET_KEY:-}} | |
| volumes: | |
| - minio-data:/data | |
| ports: | |
| - '127.0.0.1:9000:9000' | |
| - '127.0.0.1:9001:9001' | |
| healthcheck: | |
| test: ['CMD', 'curl', '-f', 'http://localhost:9000/minio/health/live'] | |
| interval: 10s | |
| timeout: 5s | |
| retries: 3 | |
| labels: | |
| - 'com.openwa.service=storage' | |
| - 'com.openwa.builtin=true' | |
| volumes: | |
| # Pin explicit volume names so the compose path and the Docker-API orchestration path (which uses | |
| # the literal `openwa_<svc>-data` names in docker.service.ts) bind the SAME volume regardless of the | |
| # compose project name — otherwise re-enabling a built-in service could mount a fresh empty volume. | |
| openwa-data: | |
| name: openwa_openwa-data | |
| driver: local | |
| postgres-data: | |
| name: openwa_postgres-data | |
| driver: local | |
| redis-data: | |
| name: openwa_redis-data | |
| driver: local | |
| minio-data: | |
| name: openwa_minio-data | |
| driver: local | |
| networks: | |
| # Application network — datastores and openwa-api. Keep the name `openwa-network`: | |
| # docker.service.ts attaches orchestrated containers to it by literal name. | |
| openwa-network: | |
| name: openwa-network | |
| # Isolated network for the Docker socket proxy. `internal: true` means no external | |
| # connectivity; only openwa-api joins it, so nothing else can reach docker-proxy:2375. | |
| internal-docker: | |
| name: openwa-internal-docker | |
| internal: true | |