Every other page in this section deploys one part. This one is the whole thing in a single file, against published images, so there is something to run before there is something to decide.
Three secrets, first
They have to exist before the brokers start, because two of them are broker environment. Generate
them once into .env beside the Compose file:
cat > .env <<EOF
CELL_JWT_SECRET=$(openssl rand -hex 32)
MESH_SECRET=$(openssl rand -hex 32)
SESSION_SECRET=$(openssl rand -hex 32)
EOFCELL_JWT_SECRET is the broker’s JWT_SECRET, and the same value signs the token the proxy
presents as a cell. MESH_SECRET authenticates the mesh and has to be byte-identical on both
brokers. SESSION_SECRET signs proxy sessions, so changing it later logs every human out.
The file
services:
pg:
image: postgres:16
environment:
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
- ./initdb:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 2s
timeout: 3s
retries: 40
queen-a:
image: ghcr.io/queen-mq/queen:latest
restart: on-failure
depends_on:
pg: { condition: service_healthy }
environment:
PG_HOST: pg
PG_PASSWORD: postgres
QUEEN_SERVER_ID: queen-a
QUEEN_MESH_PORT: "6633"
QUEEN_MESH_PEERS: "queen-b:6633"
QUEEN_SYNC_SECRET: ${MESH_SECRET:?set MESH_SECRET}
JWT_ENABLED: "true"
JWT_ALGORITHM: HS256
JWT_SECRET: ${CELL_JWT_SECRET:?set CELL_JWT_SECRET}
QUEEN_TENANCY_HEADER: "true"
QUEEN_KV_TRUSTED_PROXY: "true"
FILE_BUFFER_DIR: /var/lib/queen/buffers
volumes:
- spool-a:/var/lib/queen/buffers
ports: ["6632:6632"]
networks:
default:
aliases: [queen]
queen-b:
image: ghcr.io/queen-mq/queen:latest
restart: on-failure
depends_on:
pg: { condition: service_healthy }
environment:
PG_HOST: pg
PG_PASSWORD: postgres
QUEEN_SERVER_ID: queen-b
QUEEN_MESH_PORT: "6633"
QUEEN_MESH_PEERS: "queen-a:6633"
QUEEN_SYNC_SECRET: ${MESH_SECRET:?set MESH_SECRET}
JWT_ENABLED: "true"
JWT_ALGORITHM: HS256
JWT_SECRET: ${CELL_JWT_SECRET:?set CELL_JWT_SECRET}
QUEEN_TENANCY_HEADER: "true"
QUEEN_KV_TRUSTED_PROXY: "true"
FILE_BUFFER_DIR: /var/lib/queen/buffers
volumes:
- spool-b:/var/lib/queen/buffers
ports: ["6642:6632"]
networks:
default:
aliases: [queen]
proxy:
image: ghcr.io/queen-mq/queen-proxy:latest
restart: on-failure
depends_on:
pg: { condition: service_healthy }
environment:
PXDB_HOST: pg
PXDB_PASSWORD: postgres
PXDB_DB: queen_proxy
QUEEN_PROXY_JWT_SECRET: ${SESSION_SECRET:?set SESSION_SECRET}
volumes:
- proxy-spool:/app/spool
ports: ["6711:6711"]
volumes:
pgdata:
spool-a:
spool-b:
proxy-spool:The proxy wants its own database, which is one file next to the Compose file:
CREATE DATABASE queen_proxy;Four things in there are load-bearing. depends_on with condition: service_healthy is what keeps
the brokers from racing PostgreSQL’s first boot, which they lose. Each broker has a distinct
QUEEN_SERVER_ID and its own spool volume, because the spool is node-local and a shared one is
corruption. Both brokers answer to the queen network alias, which is how the proxy reaches the
pair. And ${VAR:?} fails the up with a readable message instead of starting a broker with an
empty secret.
docker compose up -dAll three answer within a few seconds of that returning:
curl -s http://localhost:6632/health && curl -s http://localhost:6642/health && curl -s http://localhost:6711/healthz{"status":"healthy","database":"connected","engine":"segments-rust","version":"1.0.2"}
{"status":"healthy","database":"connected","engine":"segments-rust","version":"1.0.2"}
{"enforce":false,"status":"ok","tenant_header":true}Bootstrap the control plane
The proxy is up and routes nothing, because routing is rows. Both brokers now refuse unauthenticated traffic too, so until this runs there is no way in. It is two SQL calls, and the multi-tenant page is the long version of what they do.
-
Mint the cell token. The broker holds the HMAC secret; the cell row holds a token signed with it. The
adminrole is required, because the proxy forwards deletions.set -a; . ./.env; set +a b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; } header=$(printf '{"alg":"HS256","typ":"JWT"}' | b64url) claims=$(printf '{"sub":"queen-proxy","role":"admin"}' | b64url) sig=$(printf '%s.%s' "$header" "$claims" | openssl dgst -binary -sha256 -hmac "$CELL_JWT_SECRET" | b64url) CELL_SECRET="$header.$claims.$sig" -
Register the cell, pointing
base_urlat the alias rather than at either broker.docker compose exec -T pg psql -U postgres -d queen_proxy -v ON_ERROR_STOP=1 -v cell_secret="$CELL_SECRET" <<'SQL' INSERT INTO queen_proxy.cells (slug, region, base_url, class, cell_secret) VALUES ('local', 'local', 'http://queen:6632', 'shared', :'cell_secret'); SQL -
Onboard a tenant. One call makes the tenant, the cluster, the admin user and the first API key, and unwinds completely if any part of it raises.
docker compose exec -T pg psql -U postgres -d queen_proxy -tA <<'SQL' SELECT queen_proxy.bootstrap_tenant( 'acme', 'Acme Inc', 'acme-prod', 'free', (SELECT id FROM queen_proxy.cells WHERE slug = 'local'), 'ops@acme.example', 'initial-password', 'default' ); SQL{"api_key": "qk_live_...", "user_id": "...", "tenant_id": "...", "cluster_id": "..."}That key is plaintext exactly once: only its sha256 hash is stored, and re-running on the same slugs returns the existing ids with a null key.
Through the front door
Two things get a request routed: the key as a bearer token, and a Host whose first DNS label
is the cluster slug. Locally that host is a header rather than a name that resolves.
export KEY=qk_live_...
curl -s -X POST http://localhost:6711/api/v1/push \
-H "Host: acme-prod.local.test" -H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"items":[{"queue":"orders","partition":"customer-42","payload":{"hello":"world"}}]}'curl -s "http://localhost:6711/api/v1/pop/queue/orders?batch=1&wait=true&consumerGroup=demo&subscriptionMode=all" \
-H "Host: acme-prod.local.test" -H "Authorization: Bearer $KEY"The popped message carries "producerSub": "queen-proxy", which is how you know it went through
the front door and not around it. Drop either header and the answer is 401, from the proxy
without the key and from the broker without a token.
What this stack does and does not survive
Stop either broker and the next request through the proxy succeeds against the other, with nothing
in between to reconfigure. That is the whole of what the second broker buys, and it is worth being
precise about how it works here: Docker’s DNS returns both containers for the queen alias, and
the proxy pins to whichever it resolved rather than spreading across the pair. This is standby, not
load spreading. A Kubernetes Service or a real load balancer in that position does spread, which is
Kubernetes.
What it does not survive is PostgreSQL, because there is one and it is the source of truth. Two brokers cover a broker dying, a rolling restart and one node’s network. They do not cover the database, and no arrangement of brokers does. That is High availability, and the database’s own story is PostgreSQL.
The mesh between the brokers is visible in their logs, including the five-second threshold at which a peer is called down:
docker compose logs queen-a | grep meshINFO mesh: started server_id=queen-a port=6633 peers=1 auth="hmac"
WARN mesh: peer down peer=queen-b threshold_ms=5000
INFO mesh: peer recovered peer=queen-bClean up
docker compose down -vHigh availability
What the mesh carries, what a push does while PostgreSQL is unreachable, and why the spool is not replication.
Proxy
Running the front door for real: pxdb, one replica, and the single-tenant shape.
Multi-tenant
Cells, tenants, clusters, API keys and quotas, at the length the subject deserves.