Run with Docker
ghcr.io/naldomadeira/worker-manager is the official image: the standalone CLI with a Node runtime wrapped around it. The dashboard runs as its own container next to your Redis, so there's nothing to install on the host and no app to mount an adapter into. That's usually what you want when the workers live in a repo you aren't editing, or when nothing in the stack is Node in the first place.
That serves the dashboard on http://127.0.0.1:3000 with every Bull and BullMQ queue it finds under the bull key prefix. host.docker.internal is how a container reaches a Redis running on the host: Docker Desktop provides it, and on plain Docker Engine you add --add-host host.docker.internal:host-gateway.
What's in the image
node:22-alpine and the published @worker-manager/cli, nothing else. About 63 MB, built for linux/amd64 and linux/arm64, running as the unprivileged node user.
The entrypoint is the CLI itself, so anything after the image name is a flag exactly as the CLI guide documents it, and every WORKER_MANAGER_* variable behaves the same way. There's no image-specific configuration to learn. Two CLI defaults come preset, because they're the two that make no sense in a container:
Both are ordinary environment variables, so --host or your own -e WORKER_MANAGER_HOST still wins.
There's also a HEALTHCHECK polling the dashboard on its own port, so depends_on: { worker-manager: { condition: service_healthy } } works for anything that should start behind it. Basic auth doesn't get in its way, since a 401 still proves the server is answering.
Tags
Pin the exact version if you'd rather nothing moved under you, or the major for patches without surprises. The package page lists every tag that exists.
Docker Compose
Next to a Redis of your own:
None of that is Compose specific. It's an ordinary container listening on 3000, so any orchestrator runs it the same way.
Configuring it
Flags after the image name, WORKER_MANAGER_* variables and a config file all work, and they resolve in that order. The CLI guide has the full table; these are the ones that come up in a container:
For anything longer, mount a config file. The working directory is /app, which is where the CLI looks, so a file mounted there is picked up without a --config flag as long as uid 1000 can read it:
Historical metrics work here too, since the image carries @worker-manager/metrics as part of the CLI. --history, or WORKER_MANAGER_HISTORY=true, registers the history provider and starts recording throughput and latency into your Redis once a minute:
The container is a normal recorder, so it keeps writing for as long as it runs and stops when you stop it. Two of them against one Redis is safe, and --read-only keeps the charts while writing nothing. The CLI guide covers the config file keys and what leaves the charts empty.
Redis Sentinel needs no URL, which is the point: the master address is not fixed, so the container is given the sentinel nodes and the master group name instead.
Leave WORKER_MANAGER_REDIS_URL unset there. Setting both is an error, so an old URL left behind in a compose file or an env file stops the container rather than quietly winning.
A pg-boss board takes a PostgreSQL URL. The image runs Node.js 22, which is what pg-boss needs, and bundles its own pg-boss 12. With nothing else set it is the only board:
Next to a Redis, BullMQ keeps the root and pg-boss moves to /pg-boss/ (or WORKER_MANAGER_PGBOSS_PATH), behind the same login and linked from each board's header:
The container never migrates or creates anything in the pg-boss schema. If your app is on a pg-boss whose schema version differs from the one in the image, the board stays readable, turns writes off and logs why at startup.
Serving the dashboard under a path prefix, which is what a reverse proxy routing on the path needs, is --base-path:
Keeping it private
The container listens on every interface inside itself, so WORKER_MANAGER_USER and WORKER_MANAGER_PASSWORD (or --user and --password) aren't optional here, and the port mapping above publishes to 127.0.0.1 on the host rather than everywhere. The CLI warns at startup when it's bound to a non-loopback host with no auth set, since that's an unauthenticated dashboard with delete-job and obliterate-queue on it, reachable from anywhere that can route to the host. If all you need is visibility, --read-only turns off every destructive action.
Basic auth over plain HTTP still sends the credentials in the clear. Publishing the port beyond the host, whether that's a routable address, a cloud security group or a proxy without TLS, wants an SSH tunnel or TLS termination in front of it either way.
Building it yourself
The Dockerfile at the root of the repo is the one that produces the published image, and the CLI version is a build argument:
Worth doing if you need a different base image or an internal registry.
Without an image
The CLI also runs from npm inside a stock Node container. That re-resolves the package on every start, so it pins nothing and needs egress to the registry:
--host 0.0.0.0 and --no-open are spelled out here, since only the Worker Manager image presets them.