PostgreSQL plus three active brokers.
A local topology that behaves like a broker fleet: private east-west traffic, unique broker identities, readiness gates, loopback-only host ports, and no bind mounts.
Complete Compose file
Section titled “Complete Compose file”This is the exact file used by the automated example gate.
x-broker-environment: &broker-environment DATA_PATH: '' BUNQUEUE_STORAGE_DRIVER: postgres BUNQUEUE_POSTGRES_URL: postgres://bunqueue:local-demo-only@postgres:5432/bunqueue BUNQUEUE_POSTGRES_NAMESPACE: realistic-example BUNQUEUE_POSTGRES_POOL_SIZE: '4' BUNQUEUE_POSTGRES_LEASE_DURATION_MS: '10000' BUNQUEUE_POSTGRES_POLL_INTERVAL_MS: '50' AUTH_TOKENS: demo-token METRICS_AUTH: 'true' TCP_PORT: '6789' HTTP_PORT: '6790' SHUTDOWN_TIMEOUT_MS: '15000'
x-broker: &broker build: context: ../.. dockerfile: Dockerfile depends_on: postgres: condition: service_healthy healthcheck: test: ['CMD-SHELL', 'wget --spider -q http://127.0.0.1:6790/ready'] interval: 2s timeout: 3s retries: 30 start_period: 5s restart: unless-stopped networks: [demo]
services: postgres: image: postgres:18.6-alpine environment: POSTGRES_DB: bunqueue POSTGRES_USER: bunqueue POSTGRES_PASSWORD: local-demo-only healthcheck: test: ['CMD-SHELL', 'pg_isready -U bunqueue -d bunqueue'] interval: 2s timeout: 3s retries: 30 volumes: - postgres-data:/var/lib/postgresql restart: unless-stopped networks: [demo]
broker-a: <<: *broker environment: <<: *broker-environment BUNQUEUE_BROKER_ID: broker-a ports: - '127.0.0.1:${BROKER_A_TCP_PORT:-16789}:6789' - '127.0.0.1:${BROKER_A_HTTP_PORT:-16790}:6790'
broker-b: <<: *broker environment: <<: *broker-environment BUNQUEUE_BROKER_ID: broker-b ports: - '127.0.0.1:${BROKER_B_TCP_PORT:-17789}:6789' - '127.0.0.1:${BROKER_B_HTTP_PORT:-17790}:6790'
broker-c: <<: *broker environment: <<: *broker-environment BUNQUEUE_BROKER_ID: broker-c ports: - '127.0.0.1:${BROKER_C_TCP_PORT:-18789}:6789' - '127.0.0.1:${BROKER_C_HTTP_PORT:-18790}:6790'
sdk-example: profiles: [tools] build: context: ../.. dockerfile: examples/postgres-multibroker/client.Dockerfile entrypoint: ['bun', 'run', 'examples/postgres-multibroker/run.ts'] command: ['all'] environment: BUNQUEUE_TOKEN: demo-token BROKER_A_HOST: broker-a BROKER_A_PORT: '6789' BROKER_A_HTTP_URL: http://broker-a:6790 BROKER_B_HOST: broker-b BROKER_B_PORT: '6789' BROKER_B_HTTP_URL: http://broker-b:6790 BROKER_C_HOST: broker-c BROKER_C_PORT: '6789' BROKER_C_HTTP_URL: http://broker-c:6790 BUNQUEUE_EXAMPLE_SCENARIO_TIMEOUT_MS: ${BUNQUEUE_EXAMPLE_SCENARIO_TIMEOUT_MS:-60000} depends_on: broker-a: condition: service_healthy broker-b: condition: service_healthy broker-c: condition: service_healthy networks: [demo]
volumes: postgres-data:
networks: demo: internal: trueWhy these details matter
Section titled “Why these details matter”| Detail | Reason |
|---|---|
postgres:18.6-alpine | The pinned and recommended PostgreSQL release |
DATA_PATH: '' | Prevents the image’s SQLite default from conflicting with PostgreSQL mode |
| One shared URL and namespace | Makes PostgreSQL the authoritative state for all three brokers |
Unique BUNQUEUE_BROKER_ID | Prevents session and lease-owner identity collisions |
/ready health check | Gates traffic on PostgreSQL and maintenance-loop health, not process life alone |
AUTH_TOKENS and METRICS_AUTH | Exercises authenticated TCP and Prometheus access |
internal: true network | Stops runtime containers from reaching the public internet |
| Loopback host bindings | Keeps local demonstration ports off external interfaces |
| Named PostgreSQL volume | Makes persistence explicit and teardown auditable |
| Startup and scenario deadlines | Turn a stalled health gate or runner into a failing, cleanable command |
Run it safely
Section titled “Run it safely”-
Start from the repository root
Docker must be running. No pre-existing bunqueue or PostgreSQL process is used.
-
Execute the verifier
Terminal window ./examples/postgres-multibroker/verify.sh -
Read one JSON result per scenario
{ "durationMs": 7, "scenario": "topology", "status": "PASS" }The duration is diagnostic functional timing, not a publishable benchmark.
-
Confirm teardown
The script independently attempts resource teardown and local-image removal, then preserves the original scenario failure status. A cleanup failure makes an otherwise successful run fail.
Complete verification script
Section titled “Complete verification script”The trap is registered before the first Docker resource is created.
#!/bin/shset -eu
EXAMPLE_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)PROJECT_NAME=${BUNQUEUE_EXAMPLE_PROJECT:-bunqueue-pg-example-$(date +%s)-$$}
case "$PROJECT_NAME" in bunqueue-pg-example-?*) ;; *) echo "BUNQUEUE_EXAMPLE_PROJECT must start with bunqueue-pg-example-" >&2 exit 2 ;;esaccase "$PROJECT_NAME" in *[!a-z0-9_-]*) echo "BUNQUEUE_EXAMPLE_PROJECT may contain only lowercase letters, digits, _ and -" >&2 exit 2 ;;esac
compose() { docker compose --project-name "$PROJECT_NAME" --file "$EXAMPLE_DIR/compose.yaml" "$@"}
cleanup() { original_status=$? trap - EXIT INT TERM set +e compose down --volumes --remove-orphans resources_status=$? compose down --volumes --remove-orphans --rmi local images_status=$?
if [ "$resources_status" -ne 0 ] || [ "$images_status" -ne 0 ]; then echo "Example cleanup failed" >&2 fi if [ "$original_status" -ne 0 ]; then exit "$original_status" fi if [ "$resources_status" -ne 0 ] || [ "$images_status" -ne 0 ]; then exit 1 fi exit 0}
trap cleanup EXITtrap 'exit 130' INTtrap 'exit 143' TERM
compose config --quietcompose --progress plain buildcompose up --detach --wait --wait-timeout 120 postgres broker-a broker-b broker-c
for scenario in topology multi-queue reliability flow; do compose run --rm --no-deps sdk-example "$scenario"done
compose psHost endpoints
Section titled “Host endpoints”The SDK runner uses service DNS on the internal network. For manual inspection from the host, the Compose file publishes these loopback-only endpoints:
| Broker | TCP | HTTP |
|---|---|---|
| A | 127.0.0.1:16789 | 127.0.0.1:16790 |
| B | 127.0.0.1:17789 | 127.0.0.1:17790 |
| C | 127.0.0.1:18789 | 127.0.0.1:18790 |
Override a host port with BROKER_A_TCP_PORT, BROKER_A_HTTP_PORT, and the
corresponding B or C variables. Internal container ports remain 6789 and
6790.
BUNQUEUE_EXAMPLE_SCENARIO_TIMEOUT_MS overrides the positive 60,000 ms runner
deadline. A custom BUNQUEUE_EXAMPLE_PROJECT must begin with
bunqueue-pg-example- and contain only lowercase letters, digits, _, and -.
The verifier deliberately destroys every resource owned by that selected
project, so never reuse the name of a project you intend to retain.
Readiness is not liveness
Section titled “Readiness is not liveness”/healthzanswers whether the broker process can serve HTTP./readyreturns503when persistent storage or a critical maintenance loop is degraded, so a load balancer should remove that broker./prometheusexposes operational metrics; this example requires the bearer token.
Clients should be routed only to ready brokers. Killing an unhealthy process just because readiness failed can make a transient database incident noisier; keep liveness and readiness probes separate.