-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Expand file tree
/
Copy pathdocker-compose.yml
More file actions
414 lines (406 loc) · 21.9 KB
/
Copy pathdocker-compose.yml
File metadata and controls
414 lines (406 loc) · 21.9 KB
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
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
# 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:
# Minimal set that keeps DockerService (src/modules/docker/docker.service.ts) working:
# PING (health), INFO (getSystemInfo), CONTAINERS (list/inspect/create/start/stop),
# IMAGES (pull), VOLUMES (createVolume). POST is the proxy's single all-or-nothing
# method gate for non-GET requests (haproxy.cfg: `deny unless METH_GET || env(POST)`).
# Two consequences of the pinned v0.4.2 config, accepted and documented in SECURITY.md:
# - its `DELETE` env flag is dead config (never read), so it is not set here; DELETE is
# admitted to enabled paths as a side effect of POST — OpenWA itself never issues
# deletes (profile teardown is stop-only, see DockerService.stopManagedService).
# - the proxy cannot scope container-create payloads, so a compromised API container
# could create containers with host bind-mounts. If you don't use the built-in
# datastore orchestration (Dashboard > Infrastructure built-in toggles), disable
# this service entirely, e.g. via a docker-compose.override.yml:
# services:
# docker-proxy:
# profiles: ['disabled']
# DockerService then reports Docker unavailable and orchestration degrades gracefully.
CONTAINERS: 1
IMAGES: 1
VOLUMES: 1
INFO: 1
PING: 1
POST: 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. 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 — 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:-}
# Re-start previously authenticated sessions at boot. Opt-in: the app reads the exact string
# "true", so a blank forward (nothing set) keeps the default off. Without this line the value in
# .env never reaches the container and the flag silently does nothing.
- AUTO_START_SESSIONS=${AUTO_START_SESSIONS:-}
# Fetching status@broadcast immediately after a fresh pairing can make affected accounts lose
# the companion at WhatsApp Web's first scheduled reload. Disabled by default; live statuses
# still arrive normally. Opt in only after validating the account.
- STATUS_SEED_ON_READY=${STATUS_SEED_ON_READY:-false}
# Session ownership / multi-node routing (see the "Session ownership" section of .env.example
# and docs/13). NODE_ID must be STABLE across restarts: the app default (the container
# hostname) changes on every recreate, making the new container a "new node" that has to wait
# out its previous identity's lease before re-adopting sessions.
- 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:-}
# Autoreply rules per session (0 = unlimited). Blank forwards to the app default (32); an
# unforwarded key could not be set from .env at all, which is the whole point of this list.
- AUTOMATION_MAX_PER_SESSION=${AUTOMATION_MAX_PER_SESSION:-}
# 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:-}
# Chat-media archiving (opt-in). Forwarded explicitly: this file has no env_file, so a
# variable absent from this list simply never reaches the container and the feature stays off
# no matter what the operator's .env says.
- 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 (opt-in anti-ban governor). Forwarded explicitly for the same reason as the
# chat-media block above: without these lines SEND_PACING_ENABLED=true in the operator's .env
# never reaches the container and every send stays unpaced with no signal anywhere.
- 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:-}
# Server-side media conversion (opt-in). The image ships ffmpeg, but without these forwards
# the feature could not be switched on from .env at all.
- 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:-}
# 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}
# Flags the app reads with a bare `=== 'true'` / `!== 'false'` and that have no dashboard
# route, so without these forwards setting them in .env does nothing under compose.
# BAILEYS_MARK_ONLINE_ON_CONNECT is the one that bites: its default silences push
# notifications on the paired phone for as long as the gateway is connected (#871).
- WEBHOOK_CONTACT_DETAILS=${WEBHOOK_CONTACT_DETAILS:-}
- BAILEYS_MARK_ONLINE_ON_CONNECT=${BAILEYS_MARK_ONLINE_ON_CONNECT:-}
- BAILEYS_SYNC_FULL_HISTORY=${BAILEYS_SYNC_FULL_HISTORY:-}
- 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 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}
# 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}
# 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
# Not required: lets operators disable the proxy (see the docker-proxy env comment)
# without breaking `docker compose up`. DockerService degrades gracefully without it.
required: false
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'
# noeviction is required by BullMQ: under any other policy Redis may evict queue keys once
# maxmemory is reached, silently dropping queued jobs.
command: redis-server --appendonly yes --maxmemory-policy noeviction
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