# OpenWA - Local smoke / quick-start compose # # This builds and runs the PRODUCTION image (same Dockerfile) against a local SQLite DB # with a bind-mounted ./data — it is a single-container local smoke test, NOT a # hot-reload development environment (there is no source mount or `start:dev`). # DATABASE_SYNCHRONIZE=true keeps the SQLite schema zero-config for local use; the # production compose (docker-compose.yml) never forces synchronize (it defaults to false). # # Quick Start: docker compose -f docker-compose.dev.yml up -d services: # API Backend openwa: build: context: . dockerfile: Dockerfile container_name: openwa-api # Container hardening — same posture as production (same Dockerfile/entrypoint). security_opt: - 'no-new-privileges:true' cap_drop: - ALL cap_add: - CHOWN - DAC_OVERRIDE - FOWNER - KILL # dumb-init: forward signals to the openwa process after gosu drops uid - SETGID - SETUID read_only: true tmpfs: - /tmp # Per-container PID ceiling (cgroup pids.max). 2048 fits several whatsapp-web.js sessions, which # each run a multi-process Chromium; Baileys (no Chromium) uses far fewer. A fork-bomb guard, not # an allocation — raising it is free for light containers. Do NOT use -1 (drops the guard). #636 pids_limit: ${OPENWA_PIDS_LIMIT:-2048} mem_limit: ${OPENWA_MEM_LIMIT:-2g} ports: # Bind to localhost by default; set BIND_HOST=0.0.0.0 in .env to reach it from another host, and # API_PORT to publish on a host port other than 2785. - '${BIND_HOST:-127.0.0.1}:${API_PORT:-2785}:2785' # User-facing settings read from the environment (or .env) with a sane default fallback # (${VAR:-default}); override any of them without editing this file. The data paths default # under /app/data (the ./data bind mount) so they persist out of the box — override only if you # also mount that target. Truly container-internal values (HOME, XDG_*, PORT) stay fixed: they # must match the image/entrypoint. Same convention as the production docker-compose.yml. environment: - NODE_ENV=${NODE_ENV:-development} # Explicit for the same reason as the production compose file: the data is UTC either way, this # only sets the zone the logs and the "today" stats window read in. - TZ=${TZ:-UTC} - PORT=2785 - HOME=/tmp # Chromium reads its home from the passwd entry (no /home/openwa), so it needs writable, existing # config/cache dirs on the tmpfs or it hard-crashes at launch; the entrypoint pre-creates them. (#254) - XDG_CONFIG_HOME=/tmp/.config - XDG_CACHE_HOME=/tmp/.cache - LOG_LEVEL=${LOG_LEVEL:-info} # Restart-resilience: auto-start previously authenticated sessions on boot. The liveness # watchdog (#798) only covers a RUNNING container — a `compose down/up` or rebuild kills every # engine, and without this flag sessions sit at `disconnected` until started by hand. Default # ON for the dev compose (single-operator box); the app-level default stays OFF. - AUTO_START_SESSIONS=${AUTO_START_SESSIONS:-true} - DATABASE_TYPE=${DATABASE_TYPE:-sqlite} - DATABASE_NAME=${DATABASE_NAME:-} - DATABASE_SYNCHRONIZE=${DATABASE_SYNCHRONIZE:-true} # Engine. Forwarded empty by default so the dashboard (Infrastructure > Engine) selects the # active engine via data/.env.generated (default 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:-} - SESSION_DATA_PATH=${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} - PUPPETEER_PROTOCOL_TIMEOUT_MS=${PUPPETEER_PROTOCOL_TIMEOUT_MS:-} # Optional WhatsApp Web version override. DEFAULT (empty, "latest", or "auto"): OpenWA auto-resolves a # settled build from the third-party wppconnect-team/wa-version registry and pins its remote HTML # (fetched into the web.whatsapp.com origin without an integrity check). Set "off" to disable pinning # and use the first-party build served by WhatsApp, or an exact version to pin a specific build. - 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. Empty = default. - WWEBJS_AUTH_TIMEOUT_MS=${WWEBJS_AUTH_TIMEOUT_MS:-} # Forward the plain-HTTP dashboard CSP override; unset preserves the application default. - CSP_UPGRADE_INSECURE_REQUESTS=${CSP_UPGRADE_INSECURE_REQUESTS:-} - STORAGE_TYPE=${STORAGE_TYPE:-local} - STORAGE_LOCAL_PATH=${STORAGE_LOCAL_PATH:-/app/data/media} # Install plugins into the writable, persistent data volume (the root FS is read-only). Matches # docker-compose.yml and the application default; set explicitly so the path stays correct even # if the volume moves. - PLUGINS_DIR=${PLUGINS_DIR:-/app/data/plugins} - PLUGIN_STATE_DIR=${PLUGIN_STATE_DIR:-} # Plugin install-from-URL redirect policy (ssrf-guard): off unless the exact string 'true'. - PLUGIN_DOWNLOAD_ALLOW_INSECURE_REDIRECTS=${PLUGIN_DOWNLOAD_ALLOW_INSECURE_REDIRECTS:-false} - 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} # Mirrors docker-compose.yml: flags with no dashboard route, which a .env value could not # otherwise reach the container at all. - WEBHOOK_CONTACT_DETAILS=${WEBHOOK_CONTACT_DETAILS:-} - BAILEYS_MARK_ONLINE_ON_CONNECT=${BAILEYS_MARK_ONLINE_ON_CONNECT:-} - BAILEYS_SYNC_FULL_HISTORY=${BAILEYS_SYNC_FULL_HISTORY:-} - BAILEYS_WA_VERSION=${BAILEYS_WA_VERSION:-} - ALLOW_UNSIGNED_INGRESS=${ALLOW_UNSIGNED_INGRESS:-} - STORE_EPHEMERAL_MESSAGES=${STORE_EPHEMERAL_MESSAGES:-} - RESOLVE_LID_TO_PHONE=${RESOLVE_LID_TO_PHONE:-} - SIMULATE_TYPING=${SIMULATE_TYPING:-} - MCP_ENABLED=${MCP_ENABLED:-} - SEARCH_ENABLED=${SEARCH_ENABLED:-} - SERVE_DASHBOARD=${SERVE_DASHBOARD:-} - CACHE_ENABLED=${CACHE_ENABLED:-} - DATABASE_LOGGING=${DATABASE_LOGGING:-} - MAIN_DATABASE_SYNCHRONIZE=${MAIN_DATABASE_SYNCHRONIZE:-} - 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:-} - TRUSTED_PROXIES=${TRUSTED_PROXIES:-} # Opt-in feature flags. Forwarded explicitly for the same reason as docker-compose.yml: this # file has no env_file, so a variable absent from this list never reaches the container and # the feature it gates stays off however the operator's .env is written. - CHAT_MEDIA_ARCHIVE_ENABLED=${CHAT_MEDIA_ARCHIVE_ENABLED:-} - CHAT_MEDIA_ARCHIVE_OUTBOUND=${CHAT_MEDIA_ARCHIVE_OUTBOUND:-} - CHAT_MEDIA_ARCHIVE_MAX_BYTES=${CHAT_MEDIA_ARCHIVE_MAX_BYTES:-} - CHAT_MEDIA_ARCHIVE_TTL_DAYS=${CHAT_MEDIA_ARCHIVE_TTL_DAYS:-} - MESSAGE_INLINE_MEDIA=${MESSAGE_INLINE_MEDIA:-} - CHAT_MEDIA_ORPHAN_SWEEP_INTERVAL_MS=${CHAT_MEDIA_ORPHAN_SWEEP_INTERVAL_MS:-} - CHAT_MEDIA_ORPHAN_GRACE_MS=${CHAT_MEDIA_ORPHAN_GRACE_MS:-} - SEND_PACING_ENABLED=${SEND_PACING_ENABLED:-} - SEND_PACING_WARMUP_SCHEDULE=${SEND_PACING_WARMUP_SCHEDULE:-} - SEND_PACING_COLD_DAILY_CAP=${SEND_PACING_COLD_DAILY_CAP:-} - SEND_PACING_BREAKER_THRESHOLD=${SEND_PACING_BREAKER_THRESHOLD:-} - SEND_PACING_BREAKER_COOLDOWN_MS=${SEND_PACING_BREAKER_COOLDOWN_MS:-} - MESSAGE_LIST_INLINE_MEDIA_BUDGET_BYTES=${MESSAGE_LIST_INLINE_MEDIA_BUDGET_BYTES:-} - MEDIA_DOWNLOAD_ENABLED=${MEDIA_DOWNLOAD_ENABLED:-} - MEDIA_DOWNLOAD_MAX_BYTES=${MEDIA_DOWNLOAD_MAX_BYTES:-} - MEDIA_DOWNLOAD_TIMEOUT_MS=${MEDIA_DOWNLOAD_TIMEOUT_MS:-} - INBOUND_MEDIA_CONCURRENCY=${INBOUND_MEDIA_CONCURRENCY:-} - INBOUND_MEDIA_GLOBAL_CONCURRENCY=${INBOUND_MEDIA_GLOBAL_CONCURRENCY:-} - SESSION_PROXY_URL_FETCH=${SESSION_PROXY_URL_FETCH:-} - MEDIA_CONVERSION_ENABLED=${MEDIA_CONVERSION_ENABLED:-} - FFMPEG_PATH=${FFMPEG_PATH:-} - MEDIA_CONVERSION_TIMEOUT_MS=${MEDIA_CONVERSION_TIMEOUT_MS:-} - MEDIA_CONVERSION_MAX_OUTPUT_BYTES=${MEDIA_CONVERSION_MAX_OUTPUT_BYTES:-} - MEDIA_CONVERSION_CONCURRENCY=${MEDIA_CONVERSION_CONCURRENCY:-} - NODE_ID=${NODE_ID:-} - NODE_URL=${NODE_URL:-} - SESSION_LEASE_TTL_MS=${SESSION_LEASE_TTL_MS:-} - SESSION_LEASE_HEARTBEAT_MS=${SESSION_LEASE_HEARTBEAT_MS:-} - SESSION_TAKEOVER_SWEEP_MS=${SESSION_TAKEOVER_SWEEP_MS:-} - SESSION_PROXY_TIMEOUT_MS=${SESSION_PROXY_TIMEOUT_MS:-} # Security / boot knobs the app reads. Like the blocks above, this list is the only path into # the container; compose-parity.spec.ts derives the required set from .env.example. - API_MASTER_KEY=${API_MASTER_KEY:-} - ALLOW_DEV_API_KEY=${ALLOW_DEV_API_KEY:-} - API_KEY_PEPPER=${API_KEY_PEPPER:-} - BOOTSTRAP_KEY_FILE=${BOOTSTRAP_KEY_FILE:-} - ENABLE_SWAGGER=${ENABLE_SWAGGER:-} - VALIDATION_ERROR_DETAIL=${VALIDATION_ERROR_DETAIL:-} - MCP_READONLY=${MCP_READONLY:-} - MCP_RATE_LIMIT_MAX=${MCP_RATE_LIMIT_MAX:-} - MCP_RATE_LIMIT_WINDOW_MS=${MCP_RATE_LIMIT_WINDOW_MS:-} - MCP_IP_RATE_LIMIT_MAX=${MCP_IP_RATE_LIMIT_MAX:-} - MCP_IP_RATE_LIMIT_WINDOW_MS=${MCP_IP_RATE_LIMIT_WINDOW_MS:-} - METRICS_TOKEN=${METRICS_TOKEN:-} - CORS_ORIGINS=${CORS_ORIGINS:-} - SSRF_ALLOWED_HOSTS=${SSRF_ALLOWED_HOSTS:-} - SSRF_DNS_TIMEOUT_MS=${SSRF_DNS_TIMEOUT_MS:-} - WEBHOOK_SSRF_REDIRECTS=${WEBHOOK_SSRF_REDIRECTS:-} # Postgres / S3 details (selection keys are forwarded above; blank lets a real host value pin). - DATABASE_HOST=${DATABASE_HOST:-} - DATABASE_PORT=${DATABASE_PORT:-} - DATABASE_USERNAME=${DATABASE_USERNAME:-} - DATABASE_PASSWORD=${DATABASE_PASSWORD:-} - POSTGRES_SCHEMA=${POSTGRES_SCHEMA:-} - DATABASE_SSL=${DATABASE_SSL:-} - DATABASE_SSL_REJECT_UNAUTHORIZED=${DATABASE_SSL_REJECT_UNAUTHORIZED:-} - S3_ENDPOINT=${S3_ENDPOINT:-} - S3_BUCKET=${S3_BUCKET:-} - S3_KEY_PREFIX=${S3_KEY_PREFIX:-} - S3_REGION=${S3_REGION:-} - S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-} - S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-} - REDIS_ENABLED=${REDIS_ENABLED:-} - REDIS_HOST=${REDIS_HOST:-} - REDIS_PORT=${REDIS_PORT:-} - REDIS_USERNAME=${REDIS_USERNAME:-} - REDIS_PASSWORD=${REDIS_PASSWORD:-} - REDIS_CONNECT_TIMEOUT_MS=${REDIS_CONNECT_TIMEOUT_MS:-} - REDIS_TLS=${REDIS_TLS:-} - REDIS_CACHE_DB=${REDIS_CACHE_DB:-} - QUEUE_PREFIX=${QUEUE_PREFIX:-} # Fetch statuses on ready (off unless the exact string 'true', matching docker-compose.yml). - STATUS_SEED_ON_READY=${STATUS_SEED_ON_READY:-false} - AUTOMATION_MAX_PER_SESSION=${AUTOMATION_MAX_PER_SESSION:-} # HTTP server tuning. - REQUEST_TIMEOUT_MS=${REQUEST_TIMEOUT_MS:-} - HEADERS_TIMEOUT_MS=${HEADERS_TIMEOUT_MS:-} - KEEPALIVE_TIMEOUT_MS=${KEEPALIVE_TIMEOUT_MS:-} - INFLIGHT_BODY_BUDGET_BYTES=${INFLIGHT_BODY_BUDGET_BYTES:-} # Webhook delivery tuning and retention. - WEBHOOK_SHUTDOWN_DRAIN_MS=${WEBHOOK_SHUTDOWN_DRAIN_MS:-} - WEBHOOK_MEDIA_INLINE_MAX_BYTES=${WEBHOOK_MEDIA_INLINE_MAX_BYTES:-} - WEBHOOK_MAX_PAYLOAD_BYTES=${WEBHOOK_MAX_PAYLOAD_BYTES:-} - WEBHOOK_MAX_PER_SESSION=${WEBHOOK_MAX_PER_SESSION:-} - WEBHOOK_FAILURE_RETENTION_DAYS=${WEBHOOK_FAILURE_RETENTION_DAYS:-} - WEBHOOK_FAILURE_PAYLOAD_RETENTION_HOURS=${WEBHOOK_FAILURE_PAYLOAD_RETENTION_HOURS:-} - WEBHOOK_WORKER_CONCURRENCY=${WEBHOOK_WORKER_CONCURRENCY:-} - WEBHOOK_DEGRADED_SESSION_CONCURRENCY=${WEBHOOK_DEGRADED_SESSION_CONCURRENCY:-} # Integration Fabric ingress: fairness, retries, retention, reconciler, workers. - INGRESS_INSTANCE_LIMIT=${INGRESS_INSTANCE_LIMIT:-} - INGRESS_INSTANCE_TTL=${INGRESS_INSTANCE_TTL:-} - INGRESS_IP_LIMIT=${INGRESS_IP_LIMIT:-} - INGRESS_MAX_ATTEMPTS=${INGRESS_MAX_ATTEMPTS:-} - INGRESS_RETRY_DELAY_MS=${INGRESS_RETRY_DELAY_MS:-} - INGRESS_DEDUP_RETENTION_DAYS=${INGRESS_DEDUP_RETENTION_DAYS:-} - INGRESS_RETENTION_DAYS=${INGRESS_RETENTION_DAYS:-} - INGRESS_TIMESTAMP_TOLERANCE_SEC=${INGRESS_TIMESTAMP_TOLERANCE_SEC:-} - INGRESS_RECONCILE_INTERVAL_MS=${INGRESS_RECONCILE_INTERVAL_MS:-} - INGRESS_RECONCILE_GRACE_MS=${INGRESS_RECONCILE_GRACE_MS:-} - INGRESS_RECONCILE_BATCH_SIZE=${INGRESS_RECONCILE_BATCH_SIZE:-} - INGRESS_RECONCILE_MAX_ATTEMPTS=${INGRESS_RECONCILE_MAX_ATTEMPTS:-} - WEBHOOK_RECONCILE_INTERVAL_MS=${WEBHOOK_RECONCILE_INTERVAL_MS:-} - WEBHOOK_RECONCILE_GRACE_MS=${WEBHOOK_RECONCILE_GRACE_MS:-} - WEBHOOK_RECONCILE_BATCH_SIZE=${WEBHOOK_RECONCILE_BATCH_SIZE:-} - WEBHOOK_RECONCILE_MAX_ATTEMPTS=${WEBHOOK_RECONCILE_MAX_ATTEMPTS:-} - WEBHOOK_OUTBOX_RETENTION_DAYS=${WEBHOOK_OUTBOX_RETENTION_DAYS:-} - INGRESS_WORKER_CONCURRENCY=${INGRESS_WORKER_CONCURRENCY:-} # WebSocket gateway limits. - WS_RATE_LIMIT_FRAME_PER_SECOND=${WS_RATE_LIMIT_FRAME_PER_SECOND:-} - WS_RATE_LIMIT_FRAME_BURST=${WS_RATE_LIMIT_FRAME_BURST:-} - WS_RATE_LIMIT_HANDSHAKE_MAX=${WS_RATE_LIMIT_HANDSHAKE_MAX:-} - WS_RATE_LIMIT_HANDSHAKE_WINDOW_MS=${WS_RATE_LIMIT_HANDSHAKE_WINDOW_MS:-} - WS_MAX_SOCKETS_PER_KEY=${WS_MAX_SOCKETS_PER_KEY:-} # Database connection tuning. - DATABASE_POOL_SIZE=${DATABASE_POOL_SIZE:-} - DATABASE_STATEMENT_TIMEOUT_MS=${DATABASE_STATEMENT_TIMEOUT_MS:-} - DATABASE_IDLE_TIMEOUT_MS=${DATABASE_IDLE_TIMEOUT_MS:-} - DATABASE_CONNECTION_TIMEOUT_MS=${DATABASE_CONNECTION_TIMEOUT_MS:-} - MAIN_DATABASE_NAME=${MAIN_DATABASE_NAME:-} # Engine / session tuning. - MAX_CONCURRENT_SESSIONS=${MAX_CONCURRENT_SESSIONS:-} - BAILEYS_AUTH_DIR=${BAILEYS_AUTH_DIR:-} - BAILEYS_BROWSER_NAME=${BAILEYS_BROWSER_NAME:-} - BAILEYS_LOG_LEVEL=${BAILEYS_LOG_LEVEL:-} - BAILEYS_MESSAGE_STORE_LIMIT=${BAILEYS_MESSAGE_STORE_LIMIT:-} - BAILEYS_SESSION_STORE_MAX_ENTRIES=${BAILEYS_SESSION_STORE_MAX_ENTRIES:-} - BAILEYS_CHAT_STATE_CACHE_MAX=${BAILEYS_CHAT_STATE_CACHE_MAX:-} - BAILEYS_GROUP_METADATA_CACHE_TTL_MS=${BAILEYS_GROUP_METADATA_CACHE_TTL_MS:-} - WWEBJS_ONBOARDING_CONTINUE_LABELS=${WWEBJS_ONBOARDING_CONTINUE_LABELS:-} # Default matches the image's ENV (Dockerfile); a blank forward would shadow it and make # puppeteer fall back to a bundled Chromium the image does not ship. compose-parity.spec.ts # asserts the default stays in sync with the Dockerfile value. - PUPPETEER_EXECUTABLE_PATH=${PUPPETEER_EXECUTABLE_PATH:-/usr/local/bin/puppeteer-chrome} - SHUTDOWN_DELAY_MS=${SHUTDOWN_DELAY_MS:-} - SIMULATE_TYPING_MAX_MS=${SIMULATE_TYPING_MAX_MS:-} - LID_MAPPING_CACHE_MAX=${LID_MAPPING_CACHE_MAX:-} # Media / storage budgets and sweeps. - EXPORT_INLINE_MEDIA_BUDGET_BYTES=${EXPORT_INLINE_MEDIA_BUDGET_BYTES:-} - CHAT_HISTORY_MEDIA_BUDGET_BYTES=${CHAT_HISTORY_MEDIA_BUDGET_BYTES:-} - STATUS_MEDIA_MAX_BYTES=${STATUS_MEDIA_MAX_BYTES:-} - STATUS_ORPHAN_SWEEP_INTERVAL_MS=${STATUS_ORPHAN_SWEEP_INTERVAL_MS:-} - STATUS_ORPHAN_GRACE_MS=${STATUS_ORPHAN_GRACE_MS:-} - STORAGE_IMPORT_MAX_BYTES=${STORAGE_IMPORT_MAX_BYTES:-} - STORAGE_IMPORT_MAX_ENTRIES=${STORAGE_IMPORT_MAX_ENTRIES:-} - STORAGE_IMPORT_MAX_TOTAL_BYTES=${STORAGE_IMPORT_MAX_TOTAL_BYTES:-} - STORAGE_EXPORT_TTL_MS=${STORAGE_EXPORT_TTL_MS:-} - STORAGE_EXPORT_SWEEP_MAX_AGE_MS=${STORAGE_EXPORT_SWEEP_MAX_AGE_MS:-} - S3_REPROBE_INTERVAL_MS=${S3_REPROBE_INTERVAL_MS:-} - MESSAGE_REAPER_INTERVAL_MS=${MESSAGE_REAPER_INTERVAL_MS:-} - MESSAGE_REAPER_GRACE_MS=${MESSAGE_REAPER_GRACE_MS:-} - MESSAGE_REAPER_BATCH_SIZE=${MESSAGE_REAPER_BATCH_SIZE:-} # Plugin sandbox limits and the install trust knobs. - PLUGIN_DOWNLOAD_MAX_BYTES=${PLUGIN_DOWNLOAD_MAX_BYTES:-} - PLUGIN_INSTALL_REQUIRE_PIN=${PLUGIN_INSTALL_REQUIRE_PIN:-} - PLUGIN_CATALOG_URL=${PLUGIN_CATALOG_URL:-} - PLUGIN_CAP_TIMEOUT_MS=${PLUGIN_CAP_TIMEOUT_MS:-} - PLUGIN_STORAGE_MAX_BYTES=${PLUGIN_STORAGE_MAX_BYTES:-} # The dashboard's update notice asks GitHub for the latest release; false turns that request off. - UPDATE_CHECK_ENABLED=${UPDATE_CHECK_ENABLED:-} # Misc application tuning. - AUDIT_RETENTION_DAYS=${AUDIT_RETENTION_DAYS:-} - MESSAGE_RETENTION_DAYS=${MESSAGE_RETENTION_DAYS:-} - BULK_MAX_CONCURRENT_BATCHES=${BULK_MAX_CONCURRENT_BATCHES:-} - TEMPLATE_RENDER_MAX_CHARS=${TEMPLATE_RENDER_MAX_CHARS:-} - STATS_CACHE_TTL_MS=${STATS_CACHE_TTL_MS:-} - SEARCH_PROVIDER=${SEARCH_PROVIDER:-} - SEARCH_LIMIT_MAX=${SEARCH_LIMIT_MAX:-} - LOG_FORMAT=${LOG_FORMAT:-} - BASE_URL=${BASE_URL:-} - DASHBOARD_URL=${DASHBOARD_URL:-} volumes: - ./data:/app/data restart: unless-stopped # Same teardown budget as docker-compose.yml, which has the arithmetic: Docker's 10s default would # SIGKILL the container while an engine is still stopping. stop_grace_period: 75s healthcheck: test: ['CMD', 'curl', '-f', 'http://localhost:2785/api/health/ready'] interval: 30s timeout: 10s retries: 3 start_period: 30s # The dashboard SPA is bundled into the image and served by NestJS on the same port: # open http://localhost:2785 — there is no separate dashboard container. networks: default: name: openwa-network