Files
windmill/docker-compose.yml
T
Ruben Fiszel c2a133c169 docs(security): make dind opt-in and warn about untrusted use
The repo-root docker-compose.yml quickstart ran a privileged root Docker
daemon (dind) on tcp://dind:2375 with no TLS on the worker network, and
pointed workers at it via DOCKER_HOST. Because user scripts share the
worker network namespace (no --net isolation by design), any user who can
run a script could reach the daemon and escalate to root on the dind host.

This is a docs/deployment hardening change only — no worker sandbox or
network-isolation behavior is changed:

- Gate the dind sidecar behind the "dind" compose profile so it no longer
  starts by default; make DOCKER_HOST and the dind depends_on opt-in.
- Add a prominent SECURITY WARNING comment on the dind service and a
  warning callout in the README self-host section: trusted single-tenant
  use only, never untrusted/multi-tenant.
- Document how to enable dind TLS + client-cert auth (DOCKER_TLS_CERTDIR,
  tcp://dind:2376, cert volumes), including the caveat that TLS does not
  protect against a malicious script running on the worker itself.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 10:46:53 +00:00

269 lines
9.3 KiB
YAML

version: "3.7"
x-logging: &default-logging
driver: "json-file"
options:
max-size: "${LOG_MAX_SIZE:-20m}"
max-file: "${LOG_MAX_FILE:-10}"
compress: "true"
services:
db:
deploy:
# To use an external database, set replicas to 0 and set DATABASE_URL to the external database url in the .env file
replicas: 1
image: postgres:16
shm_size: 1g
restart: unless-stopped
volumes:
- db_data:/var/lib/postgresql/data
expose:
- 5432
environment:
POSTGRES_PASSWORD: changeme
POSTGRES_DB: windmill
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
logging: *default-logging
windmill_server:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
restart: unless-stopped
expose:
- 8000
- 2525
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=server
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
logging: *default-logging
# Docker-in-Docker sidecar: a privileged, root Docker daemon for running
# containers from scripts. Any user who can run a script can reach it (workers
# share the script network namespace), so it is root-equivalent.
# => Trusted, single-tenant use only. Not for untrusted/multi-tenant workloads.
# Opt-in: uncomment DOCKER_HOST + depends_on in windmill_worker, then run
# `docker compose --profile dind up -d`. TLS hardening: see comments below.
dind:
image: docker:dind
# Rootless alternative: a docker-job escape lands as an unprivileged host
# user instead of root (recommended when docker-job users aren't fully
# host-trusted). The outer container stays privileged, but the daemon runs
# unprivileged. Needs cgroup v2 + a recent kernel, or /dev/fuse for
# fuse-overlayfs (else it falls back to the slow vfs driver). To use it, swap
# the image and the data volume path (and add the /dev/fuse device):
# image: docker:dind-rootless
# volumes: - dind-data:/home/rootless/.local/share/docker
# devices: ["/dev/fuse"]
profiles: ["dind"] # off by default
privileged: true
restart: unless-stopped
environment:
DOCKER_TLS_CERTDIR: "" # plaintext on 2375; set to "/certs" for TLS on 2376
volumes:
- dind-data:/var/lib/docker
# TLS: share the generated client certs with the worker
# - dind-certs-ca:/certs/ca
# - dind-certs-client:/certs/client
expose:
- 2375
# - 2376 # TLS port (when DOCKER_TLS_CERTDIR is set)
healthcheck:
test: ["CMD", "docker", "info"]
interval: 10s
timeout: 5s
retries: 5
logging: *default-logging
windmill_worker:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 3
resources:
limits:
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
# Uncomment to enable PID namespace isolation (recommended for security)
# Requires privileged mode for --mount-proc flag
# See: https://www.windmill.dev/docs/advanced/security_isolation
privileged: true
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=default
# If running with non-root/non-windmill UID (e.g., user: "1001:1001"),
# add: - HOME=/tmp
- FAVOR_UNSHARE_PID=true
# Run containers from scripts via the dind sidecar (opt-in, trusted only;
# see the dind service). TLS variant: use the 2376 lines instead.
# - DOCKER_HOST=tcp://dind:2375
# - DOCKER_HOST=tcp://dind:2376
# - DOCKER_TLS_VERIFY=1
# - DOCKER_CERT_PATH=/certs/client
depends_on:
db:
condition: service_healthy
# Uncomment when enabling dind (see DOCKER_HOST above):
# dind:
# condition: service_healthy
# to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
volumes:
- worker_dependency_cache:/tmp/windmill/cache
- worker_logs:/tmp/windmill/logs
# TLS dind: mount the client certs (read-only)
# - dind-certs-client:/certs/client:ro
## WARNING: mounting the host Docker socket grants user scripts full access to
## the host Docker daemon, enabling host filesystem access and privilege escalation.
## Only use this if you fully trust all users who can run scripts.
## To use it, remove the DOCKER_HOST env var and dind depends_on above,
## and uncomment the line below:
# - /var/run/docker.sock:/var/run/docker.sock
logging: *default-logging
## This worker is specialized for "native" jobs. Native jobs run in-process and thus are much more lightweight than other jobs
windmill_worker_native:
# Use ghcr.io/windmill-labs/windmill-ee:main for the ee
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 1
resources:
limits:
memory: 2048M
# for GB, use syntax '2Gi'
restart: unless-stopped
# Uncomment to enable PID namespace isolation (recommended for security)
# Requires privileged mode for --mount-proc flag
# See: https://www.windmill.dev/docs/advanced/security_isolation
environment:
- DATABASE_URL=${DATABASE_URL}
- MODE=worker
- WORKER_GROUP=native
- NATIVE_MODE=true
- SLEEP_QUEUE=200
depends_on:
db:
condition: service_healthy
volumes:
- worker_logs:/tmp/windmill/logs
logging: *default-logging
# This worker is specialized for reports or scraping jobs. It is assigned the "reports" worker group which has an init script that installs chromium and can be targeted by using the "chromium" worker tag.
# windmill_worker_reports:
# image: ${WM_IMAGE}
# pull_policy: always
# deploy:
# replicas: 1
# resources:
# limits:
# memory: 2048M
# # for GB, use syntax '2Gi'
# restart: unless-stopped
# # Uncomment to enable PID namespace isolation (recommended for security)
# # Requires privileged mode for --mount-proc flag
# # See: https://www.windmill.dev/docs/advanced/security_isolation
# privileged: true
# environment:
# - DATABASE_URL=${DATABASE_URL}
# - MODE=worker
# - WORKER_GROUP=reports
# - FAVOR_UNSHARE_PID=true
# depends_on:
# db:
# condition: service_healthy
# # to mount the worker folder to debug, KEEP_JOB_DIR=true and mount /tmp/windmill
# volumes:
# - worker_dependency_cache:/tmp/windmill/cache
# - worker_logs:/tmp/windmill/logs
# The indexer powers full-text job and log search, an EE feature.
windmill_indexer:
image: ${WM_IMAGE}
pull_policy: always
deploy:
replicas: 0 # set to 1 to enable full-text job and log search
restart: unless-stopped
expose:
- 8002
environment:
- PORT=8002
- DATABASE_URL=${DATABASE_URL}
- MODE=indexer
depends_on:
db:
condition: service_healthy
volumes:
- windmill_index:/tmp/windmill/search
- worker_logs:/tmp/windmill/logs
logging: *default-logging
# Combined extra services: LSP, Multiplayer, and Debugger
# Each service can be enabled/disabled via environment variables:
# - ENABLE_LSP=true (default) - Language Server Protocol for code intelligence
# - ENABLE_MULTIPLAYER=false - Real-time collaboration (Enterprise Edition)
# - ENABLE_DEBUGGER=false - Interactive debugging via DAP WebSocket
windmill_extra:
image: ghcr.io/windmill-labs/windmill-extra:latest
pull_policy: always
restart: unless-stopped
expose:
- 3001 # LSP
- 3002 # Multiplayer
- 3003 # Debugger
environment:
- ENABLE_LSP=true
- ENABLE_MULTIPLAYER=false # Set to true to enable multiplayer (Enterprise Edition)
- ENABLE_DEBUGGER=true # Set to true to enable debugger
- DEBUGGER_PORT=3003 # Debugger service port
- ENABLE_NSJAIL=false # Set to true for nsjail sandboxing (requires privileged: true)
- REQUIRE_SIGNED_DEBUG_REQUESTS=false # Set to true to require JWT tokens for debug sessions
- WINDMILL_BASE_URL=http://windmill_server:8000
volumes:
- lsp_cache:/pyls/.cache
logging: *default-logging
caddy:
image: ghcr.io/windmill-labs/caddy-l4:latest
restart: unless-stopped
# Configure the mounted Caddyfile and the exposed ports or use another reverse proxy if needed
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
# - ./certs:/certs # Provide custom certificate files like cert.pem and key.pem to enable HTTPS - See the corresponding section in the Caddyfile
ports:
# To change the exposed port, simply change 80:80 to <desired_port>:80. No other changes needed
- 80:80
- 25:25
# - 443:443 # Uncomment to enable HTTPS handling by Caddy
environment:
- BASE_URL=":80"
# - BASE_URL=":443" # uncomment and comment line above to enable HTTPS via custom certificate and key files
# - BASE_URL=mydomain.com # Uncomment and comment line above to enable HTTPS handling by Caddy
logging: *default-logging
volumes:
db_data: null
worker_dependency_cache: null
worker_logs: null
worker_memory: null
windmill_index: null
lsp_cache: null
caddy_data: null
dind-data: null
# Uncomment for dind TLS (see the dind service):
# dind-certs-ca: null
# dind-certs-client: null