Files
rmyndharis--openwa/docker-compose.dev.yml
Yudhi Armyndharis 47399d9d81 fix(config): let the plugin registry be redirected off the shared data dir
Closing an application disables every enabled plugin, which writes into the
plugin registry. In the e2e lane that registry was the one piece of state a
suite could not redirect, so each suite booted against whatever the previous
suite's shutdown left there, and the same write landed in the developer's own
`./data/plugins/registry.json`. Measured directly: set the whatsapp-web.js
engine to `enabled`, run one AppModule-booting suite, and it comes back
`disabled`.

`configuration.ts` argued against a DATA_DIR knob because none of the other data
paths would follow it, so it would move part of the state while looking like it
moved all of it. That objection is answered rather than repeated here: this
value reaches exactly one consumer, PluginStorageService, so the knob is named
for the registry and per-plugin storage it actually moves.

The e2e lane points it at a throwaway directory, alongside the databases and the
bootstrap key file it already redirects. The developer's registry is now
untouched by a run, verified by flipping it to `enabled` and confirming the
write lands in the temp tree instead.

This does NOT make the lane deterministic on its own. Measured after the change:
one failure in six runs, down a different path (a serve-static asset answering
401), which is clean in four consecutive runs of that suite alone. The note in
setup-e2e.ts no longer claims serial execution closes the hole.
2026-08-19 17:09:24 +07:00

293 lines
17 KiB
YAML

# 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
- 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.
- '${BIND_HOST:-127.0.0.1}: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}
- 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}
# 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:-}
- QUEUE_ENABLED=${QUEUE_ENABLED:-false}
# 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:-}
- 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:-}
- 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:-}
- S3_ENDPOINT=${S3_ENDPOINT:-}
- S3_BUCKET=${S3_BUCKET:-}
- 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:-}
# 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_WORKER_CONCURRENCY=${WEBHOOK_WORKER_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:-}
- 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_LIST_MAX_FILES=${STORAGE_LIST_MAX_FILES:-}
- STORAGE_IMPORT_MAX_BYTES=${STORAGE_IMPORT_MAX_BYTES:-}
- STORAGE_IMPORT_MAX_ENTRIES=${STORAGE_IMPORT_MAX_ENTRIES:-}
- 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:-}
# Misc application tuning.
- AUDIT_RETENTION_DAYS=${AUDIT_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
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