# 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 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 (finding H6). `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 # 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 (finding H7): 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 pids_limit: 512 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 - DATABASE_TYPE=${DATABASE_TYPE:-sqlite} - DATABASE_NAME=${DATABASE_NAME:-/app/data/openwa.sqlite} - DATABASE_HOST=${DATABASE_HOST:-postgres} - DATABASE_PORT=${DATABASE_PORT:-5432} - DATABASE_USERNAME=${DATABASE_USERNAME:-openwa} # No committed default secret (M16). Empty for the default sqlite setup; the # postgres service requires it (:?) when that profile is active. - DATABASE_PASSWORD=${DATABASE_PASSWORD:-} - DATABASE_SYNCHRONIZE=${DATABASE_SYNCHRONIZE:-false} # Engine - ENGINE_TYPE=${ENGINE_TYPE:-whatsapp-web.js} - SESSION_DATA_PATH=/app/data/sessions - PUPPETEER_HEADLESS=${PUPPETEER_HEADLESS:-true} - PUPPETEER_ARGS=${PUPPETEER_ARGS:---no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu} # Pin the WhatsApp Web version if a session hangs at "authenticating" after scanning the QR # (whatsapp-web.js 1.34.x can stall on an incompatible auto-selected WA-Web version, #251/#273). # Empty = auto-select (default). Set WWEBJS_WEB_VERSION in .env to a known-good version — see # docs/12-troubleshooting-faq.md. (Without this line the var in .env never reaches the container.) - WWEBJS_WEB_VERSION=${WWEBJS_WEB_VERSION:-} - WWEBJS_WEB_VERSION_REMOTE_PATH=${WWEBJS_WEB_VERSION_REMOTE_PATH:-} # Storage - STORAGE_TYPE=${STORAGE_TYPE:-local} - STORAGE_LOCAL_PATH=/app/data/media - S3_ENDPOINT=${S3_ENDPOINT:-http://minio:9000} - S3_ACCESS_KEY=${S3_ACCESS_KEY:-} - S3_SECRET_KEY=${S3_SECRET_KEY:-} - S3_BUCKET=${S3_BUCKET:-openwa} # Redis - REDIS_ENABLED=${REDIS_ENABLED:-false} - REDIS_HOST=${REDIS_HOST:-redis} - REDIS_PORT=${REDIS_PORT:-6379} # 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=/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} volumes: - postgres-data:/var/lib/postgresql/data 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 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. MINIO_ROOT_USER: ${S3_ACCESS_KEY:-} MINIO_ROOT_PASSWORD: ${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: openwa-data: driver: local postgres-data: driver: local redis-data: driver: local 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