Spaces:
Runtime error
Runtime error
File size: 15,956 Bytes
46252cd | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | # 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
|