Two ways to get a broker: run the published container, or build the binary. Both end at the same place: one process holding a connection pool to PostgreSQL, with the schema and the dashboard already inside it.
The broker applies its own schema on every boot, so there is no ordering requirement beyond “PostgreSQL is reachable”. If it is not, boot fails loudly rather than starting in a half-working state.
Run the published image
The image is ghcr.io/queen-mq/queen. Its runtime stage is ubuntu:24.04,
its working directory is /app, and its command is ./bin/queen.
-
Give the broker a network and a PostgreSQL. Any reachable PostgreSQL works; the test harness and the benchmark rigs both use
postgres:16.docker network create queendocker run -d --name queen-pg --network queen -e POSTGRES_PASSWORD=postgres postgres:16 -
Start the broker.
PG_USERandPG_DATABASEboth default topostgres, so a minimal run only needs the host, the port and the password.docker run -d --name queen --network queen -p 6632:6632 -e PG_HOST=queen-pg -e PG_PORT=5432 -e PG_PASSWORD=postgres ghcr.io/queen-mq/queen:latest -
Give the disk spool a real home.
FILE_BUFFER_DIRdefaults to/var/lib/queen/buffers, which inside a container is ephemeral. Pushes accepted while PostgreSQL was unreachable live there until they are replayed, so mount it if you want that durability to survive the container.docker run -d --name queen --network queen -p 6632:6632 -e PG_HOST=queen-pg -e PG_PASSWORD=postgres -v queen-spool:/var/lib/queen/buffers ghcr.io/queen-mq/queen:latest
The environment variables that matter
Everything has a working default; in practice a deployment sets variables from this list and leaves the rest alone. The full generated table lives in Reference.
| Variable | Default | Why you would set it |
|---|---|---|
PG_HOST |
localhost |
Always, in a container. |
PG_PORT |
5432 |
Non-standard port or a pooler’s port. |
PG_USER |
postgres |
A dedicated role. |
PG_PASSWORD |
postgres |
Always. |
PG_DATABASE |
postgres |
A dedicated database. Resolves PG_DATABASE, then the legacy PG_DB, then postgres. |
DB_POOL_SIZE |
160 |
Must fit inside PostgreSQL’s max_connections across all replicas. |
PORT |
6632 |
A different HTTP port. |
QUEEN_APPLY_SCHEMA |
true |
Set 0 to run as a low-privilege role against a pre-applied schema. |
FILE_BUFFER_DIR |
/var/lib/queen/buffers |
Point it at a writable, persistent path. |
QUEEN_STMT_TIMEOUT_MS |
30000 |
Bound how long a single statement may run before the broker abandons and cancels it. |
PG_USE_SSL |
false |
A managed PostgreSQL that requires TLS. |
JWT_ENABLED |
false |
Turn authentication on. Off means every route is open. |
QUEEN_MESH_PEERS |
unset | Running more than one replica. |
QUEEN_SYNC_SECRET |
unset | Always, when the mesh is on. |
LOG_LEVEL |
info |
Accepts full EnvFilter syntax, e.g. info,queen::pop=debug. RUST_LOG overrides it. |
Ports
| Port | Protocol | Exposed by the image | Purpose |
|---|---|---|---|
| 6632 | HTTP | Yes (EXPOSE 6632) |
The API and the dashboard. PORT changes it. |
| 6633 | Framed TCP | No | The inter-replica mesh. QUEEN_MESH_PORT changes it; the legacy name QUEEN_UDP_NOTIFY_PORT still reads the same setting. |
The mesh port is not published by the image because a single broker with no peers never binds it. When you do run replicas, the mesh port must be reachable between them and reachable from nowhere else. See High availability.
What else is inside the image
The published image is more than the broker binary, and two of the extras are directly useful to an operator.
-
queenctlonPATH, at/usr/local/bin/queenctl, built fromclients/client-clias a static CGO-free Go binary. The image setsENV QUEEN_SERVER=http://localhost:6632, so an in-container invocation needs no flags:docker exec -it queen queenctl status -
PostgreSQL 18 client tools, installed from the PGDG repository:
psql,pg_dump,pg_restore. The broker’s whole state is in PostgreSQL, so backup and restore are plainpg_dumpandpg_restore, and the tools travel with the image rather than needing a second container. -
/app/webapp/dist, the same dashboard bytes the binary already embeds, written to disk for inspection. The binary does not read them. There is no static-directory override, and the image deliberately does not setQUEEN_STATIC_DIR, because no code reads it.
Build from source
The repository root Dockerfile builds the whole stack in four stages: the Vue
dashboard, the Rust broker, queenctl, and the runtime image. It requires
BuildKit, because the Rust stage mounts cache directories for the Cargo registry
and the target directory.
DOCKER_BUILDKIT=1 docker build -t queen-mq .build.sh is the publish path, for both images:
./build.sh all --push --multiarch --latestIt defaults to ghcr.io/queen-mq (override with --registry or $QUEEN_REGISTRY)
and builds queen from the root Dockerfile, queen-proxy from
proxy/Dockerfile. Without --multiarch it builds for the host
architecture only and loads the result locally instead of pushing; --multiarch
implies --push and needs a docker-container buildx builder, which the script
checks for before starting rather than failing halfway through a Rust build.
Each image takes its tag from its own manifest: the broker from
server/server.json, the proxy from proxy/Cargo.toml. The version in
server/server.json is therefore the single source of truth for the broker’s
image tag, for what /health reports, and for what queenctl version reports.
server/build.rs embeds it into the binary as QUEEN_VERSION, and build.sh
passes the same string as QUEENCTL_VERSION.
To build only the binary:
cargo build --release --manifest-path server/Cargo.tomlThe result is server/target/release/queen. Two build-time couplings are
worth knowing before you start.
There is also a broker-only image at server/Dockerfile, on a
debian:bookworm-slim runtime. It builds only the binary from server/, sets
PORT=6632, and carries no queenctl and no PostgreSQL client tools. Use it
when you want the smallest possible broker and have the operator tooling
elsewhere.
Verify it actually works
/health proves the broker can reach PostgreSQL. It does not prove that a
message can be stored and delivered. Run all four steps: the last one is the one
that catches a broker that accepts writes but cannot serve them.
-
Health. This is not a static answer: the handler takes a connection from the pool and issues a real query, so a 200 means the broker and PostgreSQL are both working.
curl -s http://localhost:6632/health{"status":"healthy","database":"connected","engine":"segments-rust","version":"1.0.0"}Unreachable PostgreSQL yields HTTP 503 and
{"status":"unhealthy","database":"disconnected",…}. -
Read the boot log. The broker prints the configuration it actually resolved, one line per subsystem, with secrets masked. These are the messages to look for, in order:
applied schema.sql + procedureson theschematarget, then theconfig: server/config: postgres/config: auth/config: sync/config: engine/config: flow/config: jobs/config: file_buffer/config: security/config: loggingblocks on theboottarget, thenlistening.docker logs queen 2>&1 | grep -E 'schema|config:|listening'If a setting appears not to have taken effect, this block is the answer: it shows the value the broker understood, not the value you thought you set.
-
Push a message. A queue and a partition are created by the first push that names them; there is nothing to provision first.
curl -s -X POST http://localhost:6632/api/v1/push -H 'Content-Type: application/json' -d '{"items":[{"queue":"deploy-check","partition":"p1","payload":{"hello":"world"}}]}'The response carries one result per item.
statusisqueuedwhen the message was stored. -
Pop it, and pop again.
autoAck=truecommits the consumer cursor inside the pop transaction, so the second call proves the cursor advanced rather than only proving that a read returned bytes.curl -s 'http://localhost:6632/api/v1/pop/queue/deploy-check?batch=10&autoAck=true'curl -si 'http://localhost:6632/api/v1/pop/queue/deploy-check?batch=10&autoAck=true'The second call must answer
HTTP/1.1 204 No Contentwith no body at all. A 204 from this broker never carries a body. That is deliberate, because announcing a content-length on an elided body poisoned Node and undici connections.
If step 4 keeps returning the same message, the cursor is not advancing and
something is wrong with the ack path, not with the network. If step 3 succeeds
but step 4 returns 204 on the first call, check whether pop maintenance mode is
on: a paused pop path also answers 204 with no body, so the response alone cannot
distinguish “paused” from “empty”. GET /api/v1/system/maintenance/pop reports
the flag.
Compose, for a two-broker stack
The repository’s test harness contains a working multi-broker Compose file at
test/compose/docker-compose.ha.yml: one PostgreSQL, two brokers with distinct
QUEEN_SERVER_ID, an identical QUEEN_SYNC_SECRET, QUEEN_MESH_PEERS pointing
at each other, and a separate FILE_BUFFER_DIR per broker. That last detail is
load-bearing: the spool is node-local and must never be shared between brokers.
High availability explains why.
Next
PostgreSQL
Privileges, pool sizing, why a direct connection is required, and the server settings that matter.
Configuration
The strict boolean parser, the fatal boot errors, and which variables are experiment-only.
Kubernetes
A complete reference manifest, including probes that do not restart brokers when the database blips.