Docker Compose
$ docker compose up -d # server + 1 worker
$ docker compose up -d --scale worker=4 # server + 4 workers
Open http://localhost:4317. The server logs a one-time admin password on first boot —
see Authentication.
The file
services:
flowwright:
image: flowwrightdev/flowwright:${FLOWWRIGHT_VERSION:-latest}
ports:
- "4317:4317"
volumes:
- data:/data
environment:
FW_WORKERS: "0"
command: ["/app/apps/server/dist/main.js"]
restart: unless-stopped
worker:
image: flowwrightdev/flowwright:${FLOWWRIGHT_VERSION:-latest}
depends_on:
- flowwright
volumes:
- data:/data
environment:
FW_CONCURRENCY: "2"
healthcheck:
disable: true
command: ["/app/apps/worker/dist/main.js"]
restart: unless-stopped
volumes:
data:
Four things in there are load-bearing.
FW_WORKERS: "0" makes the server enqueue-only. Without it the server would also run
runs in-process, and you'd have two things competing for the same queue. Delegating is
what lets you scale workers independently.
Both services mount the same data volume. On SQLite that volume is the queue —
the worker claims runs by writing to the same database. It also has to write run logs
where the server will read them.
healthcheck: disable: true on the worker. The image's healthcheck probes /healthz,
and the worker serves no HTTP. Without this it reports unhealthy forever.
restart: unless-stopped hides crash loops. A worker that dies on startup and
restarts still shows as running in docker compose ps. Check the logs for a workerId
line to know it actually came up.
Pinning a version
$ FLOWWRIGHT_VERSION=0.1.0 docker compose up -d
latest is fine for evaluating and a bad idea for anything you depend on — an image
update lands on your next docker compose pull.
Upgrading to the non-root image
The container runs as uid 1000, not root. A volume created by an earlier image is root-owned, and the server will refuse to start:
Error: unable to open database file
That's loud on purpose. One command fixes it:
$ docker compose down
$ docker volume ls # find the real name
$ docker run --rm -v flowwright_data:/data alpine chown -R 1000:1000 /data
$ docker compose up -d
Compose prefixes volume names with the project — the directory name — so it's
flowwright_data, not data. Run docker volume ls first: pointing the command at a
name that doesn't exist silently creates an empty volume and chowns that, and you'll
be looking at the same error afterwards.
A new volume needs nothing. Docker initialises an empty volume from the image's mount point, ownership included.
For a bind mount, ownership comes from the host — sudo chown -R 1000:1000 ./data.
If you need to defer the migration, user: "0:0" on the service restores the old
behaviour. Set it on the server and every worker: a root worker writing run
directories a non-root server can't unlink means garbage collection quietly stops working.
Scaling workers
$ docker compose up -d --scale worker=4
Each worker runs FW_CONCURRENCY stages at once, so four workers at 2 is eight
concurrent stages. Scale on the box you have; on SQLite they all need the same volume, so
scaling past one host means moving to
PostgreSQL and S3.
Secrets
Set FW_SECRET_KEY explicitly for anything real — 32 bytes as 64 hex characters or
base64. Left unset, the server generates one into the data volume, which means your
encryption key lives next to the data it encrypts.
The same value must be set on the server and every worker. They both decrypt credentials.
environment:
FW_SECRET_KEY: "${FW_SECRET_KEY:?set FW_SECRET_KEY}"
The :? form fails the up with a message instead of silently generating a key.
What compose doesn't give you
No TLS — put it behind a reverse proxy and set infra.externalBaseUrl so webhook and
OAuth callback URLs are right. No backups; see
Operations. And no container-stage execution — the image
ships no docker binary, so container stages fall back to the host. That's on
Worker.