Free, Open Source, Self-Hosted WhatsApp API Gateway
docker pull rmyndharis/openwa
docker run -d --name openwa -p 2785:2785 --restart always rmyndharis/openwa:latest
podman run -d --name openwa -p 2785:2785 --restart always rmyndharis/openwa:latest
skopeo inspect docker://docker.io/rmyndharis/openwa:latest
oras pull rmyndharis/openwa:latest
docker compose up -d
# 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