Docker
Running Flux server + workers + Postgres in containers — Dockerfile, docker-compose, env wiring, and workflow registration from outside the container.
Containerizing Flux is straightforward once you accept the constraints: durable Postgres, a bootstrap token wired into both server and worker via environment, and — if you ever scale the server past one replica — sticky routing for worker connections (see High availability).
The image
Flux publishes an official Docker image, built from the Dockerfile in the Flux repository and tagged with semver version tags (0.56.0 and 0.56) on every release. One hardened image serves every role, selected by FLUX_MODE:
- server — the default (
docker run <image>) - worker —
docker run -e FLUX_MODE=worker <image> - MCP server —
docker run -e FLUX_MODE=mcp <image> - ad-hoc CLI — an explicit command always wins over
FLUX_MODE:docker run <image> flux workflow list
The image runs as the non-root flux user (UID 1000) with tini as PID 1 (so SIGTERM-based worker drain and subprocess reaping work without docker run --init), ships a mode-aware HEALTHCHECK (in server mode it probes GET /health; in worker/MCP mode the flux process is PID 1, so container liveness is process liveness), and has Python bytecode precompiled. Pin a version tag that matches your flux-core version — avoid :latest, which moves under you on every release.
If you’d rather build your own (for example to bake in workflow dependencies), start from the official image or the upstream Dockerfile:
# Dockerfile
FROM your-registry/flux:0.56.0
RUN pip install --no-cache-dir pandas numpy && \
python -m compileall -q "$(python -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
The compileall step matters if you use the docker runner: containers are ephemeral, so without baked .pyc files every execution recompiles imports from source.
docker-compose.yml
The Flux repository ships a docker-compose.yml at the root, but it is development-only — it carries well-known credentials (Keycloak admin/admin, PostgreSQL flux/flux) and a default bootstrap token and encryption key. For anything beyond local development, author your own with real secrets.
The repository also ships a production-shaped starting point: examples/docker/docker-compose.full.yml. It runs every role from the single official image — PostgreSQL, a server with event dispatch, auth, retention, and observability enabled, two general workers, a docker-runner worker, a GPU-labeled worker for affinity routing, and an MCP server, plus opt-in compose profiles for an AI agent (agents) and an OTel collector + Prometheus pair (observability). The services are hardened (cap_drop: [ALL], no-new-privileges), and it refuses to start without real secrets exported in the environment (PG_PASSWORD, BOOTSTRAP_TOKEN, EXEC_TOKEN_SECRET, ENCRYPTION_KEY) — there are no baked-in defaults to forget to change. Pin its image tag to the flux-core version you run. It’s the fastest way to see the full topology on one host; read it alongside this page rather than instead of it.
A minimal working three-service compose: Postgres, one Flux server, two worker replicas.
# docker-compose.yml
services:
postgres:
image: postgres:16
environment:
POSTGRES_DB: flux
POSTGRES_USER: flux
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U flux -d flux"]
interval: 5s
timeout: 3s
retries: 10
server:
image: your-registry/flux:0.56.0
depends_on:
postgres:
condition: service_healthy
environment:
FLUX_DATABASE_URL: "postgresql://flux:${POSTGRES_PASSWORD}@postgres:5432/flux"
FLUX_WORKERS__BOOTSTRAP_TOKEN: ${FLUX_BOOTSTRAP_TOKEN}
FLUX_SECURITY__ENCRYPTION__ENCRYPTION_KEY: ${FLUX_ENCRYPTION_KEY}
FLUX_SECURITY__AUTH__API_KEYS__ENABLED: "true"
ports:
- "8000:8000"
volumes:
- flux-home:/app/.flux
worker:
image: your-registry/flux:0.56.0
depends_on:
- server
command: ["flux", "start", "worker", "worker-${HOSTNAME:-1}"]
environment:
FLUX_WORKERS__SERVER_URL: "http://server:8000"
FLUX_WORKERS__BOOTSTRAP_TOKEN: ${FLUX_BOOTSTRAP_TOKEN}
deploy:
replicas: 2
volumes:
pgdata:
flux-home:
Companion .env:
# .env (do not commit)
POSTGRES_PASSWORD=change-me-strong
FLUX_BOOTSTRAP_TOKEN=replace-with-64-hex-chars
FLUX_ENCRYPTION_KEY=replace-with-64-hex-chars
Generate the secrets once:
python -c 'import secrets; print(secrets.token_hex(32))' # bootstrap token
python -c 'import secrets; print(secrets.token_hex(32))' # encryption key (distinct value)
Then bring it up:
docker compose up -d
docker compose logs -f server
Worker replicas and naming
The compose snippet above gives each replica a name based on HOSTNAME. In practice you’ll either:
- Run a single
workerservice withreplicas: Nand accept that each replica registers under a generated name (the worker name has to be unique across registrations). - Define separate services —
worker-a,worker-b— when you need named workers for label-based routing.
For routing by label (GPU pool, region, paid-tier), see Running workers.
Server replicas vs scheduler
The compose snippet pins the server at a single instance, and on a single host that is the right shape. Multiple server replicas are supported — they coordinate through PostgreSQL (advisory-lock scheduler singleton, SKIP LOCKED dispatch), so a scaled server service won’t double-fire crons — but workers connecting through Docker’s round-robin service DNS would land on random replicas, and a worker’s SSE stream must stick to one. If you outgrow one server container, move to a topology with a real load balancer and connection affinity: Kubernetes or High availability.
If you’re load-testing the API surface, scale worker and leave server alone. The server bottleneck is rarely the HTTP path under realistic Flux workloads (it’s the database, or the worker pool); horizontal-scaling it doesn’t usually help.
Workflow registration
The server runs inside a container; your workflow source lives on the host. Two patterns work.
Register from outside
Install flux-core on the host, point it at the containerized server, register:
pip install flux-core==0.56.0
export FLUX_API_KEY=<admin-key> # if API-key auth is enabled
flux workflow register ./workflows/orders.py --server-url http://localhost:8000
The CLI POSTs the workflow source to POST /workflows. The server stores it in the catalog and the workers pick it up over SSE.
This is the cleaner pattern for CI — install Flux on the build agent, register against the deployed server, no container indirection.
Mount a directory
If you’d rather not install Flux on the host, mount a directory into the server container and docker compose exec the registration:
server:
# ...
volumes:
- flux-home:/var/lib/flux
- ./workflows:/workflows:ro
docker compose exec server flux workflow register /workflows/orders.py
This is the cleaner pattern for local development — the workflow source is editable on the host, registration is one command, no network dance.
Logs and debugging
Server and worker logs are stdlib logging output to stdout/stderr. docker compose logs -f server follows in real time. The line layout is controlled by FLUX_LOG_FORMAT, a stdlib logging format string (default %(asctime)s - %(name)s - %(levelname)s - %(message)s) — there is no built-in JSON formatter, so structured-log pipelines should parse the plain-text layout or wrap the collector’s own parser around it.
To shell into a running container for debugging:
docker compose exec server bash
flux --help
The flux CLI is available because flux-core is installed in the image. CLI subcommands target http://$FLUX_SERVER_HOST:$FLUX_SERVER_PORT by default, or accept an explicit --server-url — pass one of those to issue admin operations against the running server.
What can go wrong
Server crash-loops at startup
Symptom. server container restarts. Logs show OperationalError: could not connect to server: Connection refused.
Cause. Server started before Postgres was ready. depends_on alone doesn’t wait for the database to be accepting connections; you need the healthcheck condition: service_healthy (already wired in the compose above).
Fix. Confirm the healthcheck is present, then docker compose down && docker compose up -d. If you authored your own compose without the healthcheck, add it.
Workers stay OFFLINE
Symptom. flux worker list shows workers as OFFLINE, or the server logs 401 Unauthorized on POST /workers/register.
Cause. Bootstrap token mismatch between server and worker. Common when .env was edited but containers weren’t recreated, or two compose stacks share a volume but diverge on FLUX_BOOTSTRAP_TOKEN.
Fix. docker compose config | grep BOOTSTRAP to confirm both services see the same value, then docker compose up -d --force-recreate worker.
Network name mismatch
Symptom. Worker logs show httpx.ConnectError: [Errno -3] Temporary failure in name resolution when connecting to the server.
Cause. FLUX_WORKERS__SERVER_URL points at localhost or an old hostname rather than the compose service name server.
Fix. Inside the docker-compose network, services reach each other by service name. Set FLUX_WORKERS__SERVER_URL=http://server:8000 for workers, not http://localhost:8000.
Volume permission errors
Symptom. Server logs PermissionError: [Errno 13] Permission denied: '/var/lib/flux/bootstrap-token'.
Cause. The official image runs as the non-root flux user (UID 1000). The named volume was created by a previous container running as a different UID (older Flux images ran as root), or you mounted a host directory owned by a non-matching user.
Fix. For a fresh start: docker compose down -v && docker compose up -d (destroys data — only run if you have backups or in dev). For a real environment: chown -R 1000:1000 the volume contents (from a one-off root container with --entrypoint chown), or align the host directory’s owner with UID 1000.
Next
- Kubernetes when one host stops being enough.
- Production checklist before pointing real users at the stack.
- Storage backends for tuning Postgres.