For AI agents: the complete documentation index is available at /bull-board/llms.txt, the full documentation bundle is available at /bull-board/llms-full.txt, and this page is available as Markdown at /bull-board/guide/docker.md.

Run with Docker

ghcr.io/felixmosh/bull-board 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.

docker run --rm -p 127.0.0.1:3000:3000 \
  -e BULL_BOARD_USER=admin -e BULL_BOARD_PASSWORD=secret \
  ghcr.io/felixmosh/bull-board --redis redis://host.docker.internal:6379

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 @bull-board/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 BULL_BOARD_* 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:

VariableImage defaultWhy
BULL_BOARD_HOST0.0.0.0The CLI default 127.0.0.1 only accepts connections from inside the container
BULL_BOARD_OPENfalseThere's no browser in there to open

Both are ordinary environment variables, so --host or your own -e BULL_BOARD_HOST still wins.

There's also a HEALTHCHECK polling the dashboard on its own port, so depends_on: { bull-board: { 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

TagPoints at
latestThe newest release
9The newest release in that major
9.5.0That exact release, forever

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:

services:
  redis:
    image: redis:latest
    ports:
      - '6379:6379'

  bull-board:
    image: ghcr.io/felixmosh/bull-board:9
    command: --redis redis://redis:6379
    environment:
      BULL_BOARD_USER: ${BULL_BOARD_USER}
      BULL_BOARD_PASSWORD: ${BULL_BOARD_PASSWORD}
    ports:
      - '127.0.0.1:3000:3000'
    depends_on:
      - redis

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, BULL_BOARD_* 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:

docker run --rm -p 127.0.0.1:3000:3000 \
  -e BULL_BOARD_REDIS_URL=redis://redis:6379 \
  -e BULL_BOARD_PREFIX=bull,tenant-a \
  -e BULL_BOARD_READ_ONLY=true \
  -e BULL_BOARD_BOARD_TITLE='Ops Dashboard' \
  ghcr.io/felixmosh/bull-board

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:

    volumes:
      - ./bull-board.config.js:/app/bull-board.config.js:ro

Serving the dashboard under a path prefix, which is what a reverse proxy routing on the path needs, is --base-path:

docker run --rm -p 127.0.0.1:3000:3000 \
  ghcr.io/felixmosh/bull-board --redis redis://redis:6379 --base-path /queues

Keeping it private

The container listens on every interface inside itself, so BULL_BOARD_USER and BULL_BOARD_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:

docker build --build-arg BULL_BOARD_VERSION=9.5.0 -t bull-board .

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:

  bull-board:
    image: node:22-alpine
    command: npx -y @bull-board/cli --redis redis://redis:6379 --host 0.0.0.0 --no-open
    environment:
      BULL_BOARD_USER: ${BULL_BOARD_USER}
      BULL_BOARD_PASSWORD: ${BULL_BOARD_PASSWORD}
    ports:
      - '127.0.0.1:3000:3000'
    depends_on:
      - redis

--host 0.0.0.0 and --no-open are spelled out here, since only the bull-board image presets them.