4ce4204b6c
CI / Lint (push) Failing after 2s
CI / Build (push) Has been skipped
SDK CI / PHP SDK (push) Failing after 1s
Split PHP SDK / PHP SDK tests (push) Failing after 1s
CI / Test (push) Failing after 1s
CI / Dashboard (push) Failing after 0s
SDK CI / JavaScript SDK (push) Failing after 0s
SDK CI / Python SDK (push) Failing after 2s
SDK CI / Java SDK (push) Failing after 1s
Split PHP SDK / Mirror sdk/php -> rmyndharis/openwa-php (push) Has been skipped
CI / Test (PostgreSQL migrations) (push) Failing after 7m47s
CI / Docker Build (push) Has been skipped
320 lines
15 KiB
YAML
320 lines
15 KiB
YAML
# 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_MAX_RETRIES=${WEBHOOK_MAX_RETRIES:-3}
|
|
- WEBHOOK_RETRY_DELAY=${WEBHOOK_RETRY_DELAY:-5000}
|
|
# Rate Limit
|
|
- RATE_LIMIT_TTL=${RATE_LIMIT_TTL:-60}
|
|
- RATE_LIMIT_MAX=${RATE_LIMIT_MAX:-100}
|
|
# Plugins
|
|
- PLUGINS_ENABLED=${PLUGINS_ENABLED:-true}
|
|
- PLUGINS_DIR=${PLUGINS_DIR:-/app/data/plugins}
|
|
# Security
|
|
- API_MASTER_KEY=${API_MASTER_KEY:-}
|
|
# 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
|