Skip to content

This page tells you how to run QueueBox in production. It covers the image, the database, the configuration, the health probes and replicas.

QueueBox publishes a multi-architecture image for linux/amd64 and linux/arm64 to GitHub Container Registry.

Tag Meaning
ghcr.io/alternayte/queuebox:1.2.3 The exact release.
ghcr.io/alternayte/queuebox:1.2 The newest patch of that minor release.
ghcr.io/alternayte/queuebox:latest The newest release.

Pin the exact version in production. The registry also holds the software bill of materials and the build provenance of each image.

The image runs Java 21 as the non-root user queuebox. It listens on port 8080. Its built-in HEALTHCHECK calls http://localhost:8080/health.

Run the image against an existing database:

Terminal window
docker run --rm -p 8080:8080 \
-e QUEUEBOX_DATABASE_URL=jdbc:postgresql://db.internal:5432/queuebox \
-e QUEUEBOX_DATABASE_USERNAME=queuebox \
-e QUEUEBOX_DATABASE_PASSWORD=secret \
-v /etc/queuebox/queuebox.yml:/etc/queuebox/queuebox.yml:ro \
ghcr.io/alternayte/queuebox:1.2.3

QueueBox needs one of these databases:

  • PostgreSQL 14, 15 or 16.
  • SQL Server 2019 or 2022.

Set database.type to postgresql (the default) or sqlserver.

At start, QueueBox waits up to database.startupTimeoutMs (default 60000) for the database. It exits when no database answers in that time.

By default, QueueBox applies its bundled Flyway migrations at start. The database account then needs the right to create and alter tables, indexes and sequences.

Set database.migrate: false when the application account has no DDL rights. Apply the migration files by hand before each upgrade. The files are in postgres/src/main/resources/db/postgresql and sqlserver/src/main/resources/db/sqlserver.

A custom table name or column name requires database.migrate: false. QueueBox refuses to start with a renamed table or column and migrate: true, because the bundled files name the default schema. See Use custom tables.

At start, QueueBox also checks that the tables have the columns it needs. A missing required column stops the start with a message that gives the ALTER TABLE statement.

Each replica opens up to database.poolSize connections (default 10). The poller, the relay, the retention job and the HTTP routes share the pool. Keep database.poolSize larger than outbox.concurrency. Check that the database allows poolSize times the number of replicas, plus your other clients.

QueueBox reads its configuration from three places. The first place that holds a key wins.

  1. An environment variable that starts with QUEUEBOX_.
  2. The YAML file that QUEUEBOX_CONFIG_FILE names, or /etc/queuebox/queuebox.yml when the variable is not set.
  3. The YAML file packaged in the image.

An external file replaces the packaged file. It does not add to it. Write a complete configuration, and copy examples/queuebox.yml as the start point. QueueBox reads the packaged file only when you supply no external file and set no QUEUEBOX_ variable.

These three variables are the minimum:

Terminal window
QUEUEBOX_DATABASE_URL=jdbc:postgresql://db.internal:5432/queuebox
QUEUEBOX_DATABASE_USERNAME=queuebox
QUEUEBOX_DATABASE_PASSWORD=secret

The name after the prefix is the configuration path in upper case. One underscore separates two levels of the path. A leaf name of more than one word has no underscore inside it: QUEUEBOX_OUTBOX_POLLINTERVALMS sets outbox.pollIntervalMs. A variable with an extra underscore sets nothing, and the start reports no error. See Environment variables and Configuration.

A configuration change needs a restart. It does not need a new image.

Keep secrets out of the YAML file and out of plain environment values. See Security.

The repository holds a Compose stack with QueueBox, PostgreSQL and a test receiver:

Terminal window
git clone https://github.com/alternayte/queuebox.git
cd queuebox
docker compose -f docker-compose.yml --env-file .env.example up -d --build
  • -f docker-compose.yml selects the shipped stack. Without it, Compose also reads docker-compose.override.yml, which runs a development loop from the source tree.
  • The stack mounts examples/queuebox.yml at /etc/queuebox/queuebox.yml. Edit that file and restart the container.
  • --env-file .env.example supplies the database variables. Without it, the start fails with a message that names the variables to set.
  • docker compose --profile rabbitmq up -d also starts RabbitMQ.

The stack builds the image from the source. To run a released image with it, add docker-compose.release.yml:

Terminal window
RELEASE_IMAGE=ghcr.io/alternayte/queuebox:1.2.3 \
docker compose -f docker-compose.yml -f docker-compose.release.yml --env-file .env.example up -d

Use this stack to learn and to test. For production, run the image against a database that you back up and monitor.

This section runs QueueBox from the source against a PostgreSQL database on the same host. It needs a Java Development Kit, version 21. CI runs these steps on every push to main.

  1. Create the database:

    CREATE DATABASE queuebox;

    QueueBox creates its tables at startup with the bundled migrations.

  2. Copy the example configuration. Do not edit config/src/main/resources/queuebox.yml, because the image packages that file.

    Terminal window
    sudo mkdir -p /etc/queuebox
    sudo cp examples/queuebox.yml /etc/queuebox/queuebox.yml
  3. Set the database variables. An environment variable wins over every file.

    Terminal window
    export QUEUEBOX_DATABASE_URL=jdbc:postgresql://localhost:5432/queuebox
    export QUEUEBOX_DATABASE_USERNAME=postgres
    export QUEUEBOX_DATABASE_PASSWORD=secret
  4. Build and run:

    Terminal window
    ./gradlew run
  5. Confirm that the instance is ready. GET /health answers 200 when every component is up.

    Terminal window
    curl http://localhost:8080/health
Endpoint Use Answer
GET /health/live Liveness probe Always 200 while the process runs. It touches no dependency.
GET /health/ready Readiness probe 200 when every component is up. 503 when one component is down.
GET /health Compatibility The same answer as /health/ready.

The readiness body names each component:

  • database,
  • outbox-poller,
  • inbox-relay, when inbox.relay.enabled is true,
  • rabbitmq.<source>, kafka.<source> and nats.<source> for each broker source,
  • retention-service, when retention.enabled is true,
  • outbox-capture, when capture is enabled. This component is advisory: a capture fault never makes the answer 503.

Each check has a 3-second limit. A check that does not answer in time counts as down, so a slow database gives 503 and not a probe timeout.

Use /health/live for liveness. A liveness probe on /health/ready restarts the pod when the database is slow, and a restart does not repair the database.

Set server.managementPort to move /health/*, /metrics and /admin to a separate port. The data port then answers 404 for those paths and carries only the inbox routes. Bind the management port to an internal network, because the metrics reveal traffic volumes and destination names.

QueueBox has no Helm chart. A plain Deployment and Service are enough.

apiVersion: apps/v1
kind: Deployment
metadata:
name: queuebox
spec:
replicas: 2
selector:
matchLabels:
app: queuebox
template:
metadata:
labels:
app: queuebox
spec:
terminationGracePeriodSeconds: 75
containers:
- name: queuebox
image: ghcr.io/alternayte/queuebox:1.2.3
ports:
- name: http
containerPort: 8080
- name: management
containerPort: 9090
env:
- name: QUEUEBOX_CONFIG_FILE
value: /etc/queuebox/queuebox.yml
- name: QUEUEBOX_DATABASE_URL
value: jdbc:postgresql://postgres.db.svc:5432/queuebox
- name: QUEUEBOX_DATABASE_USERNAME
value: queuebox
- name: QUEUEBOX_DATABASE_PASSWORD
value: file:/run/secrets/db-password
- name: QUEUEBOX_SERVER_MANAGEMENTPORT
value: "9090"
livenessProbe:
httpGet:
path: /health/live
port: management
readinessProbe:
httpGet:
path: /health/ready
port: management
volumeMounts:
- name: config
mountPath: /etc/queuebox
readOnly: true
- name: secrets
mountPath: /run/secrets
readOnly: true
volumes:
- name: config
configMap:
name: queuebox-config
- name: secrets
secret:
secretName: queuebox-secrets
  • Put the complete queuebox.yml in the ConfigMap.
  • Mount secrets as files and reference them with file:. See Security.
  • Publish only /inbox through the ingress, and terminate TLS there. See Security.
  • Scrape /metrics on the management port. See Monitoring.

On SIGTERM, QueueBox stops in this order:

  1. The HTTP server refuses new requests and waits up to 5 seconds for the requests in flight.
  2. The broker consumers, capture, the outbox poller and the inbox relay stop. The poller waits up to outbox.shutdownTimeoutMs (default 30000) for the messages in flight.
  3. The publishers and the database pool close.

A message still in flight after the timeout stays in state processing. Another replica reclaims it when its lease expires. Set terminationGracePeriodSeconds above 5 seconds plus twice outbox.shutdownTimeoutMs. With the defaults, that is above 65 seconds.

Run as many replicas as you need. Every replica runs the outbox poller and the inbox relay against the same tables. Claims, leases and the claim fence keep two replicas off the same message, and the order of a key or an aggregate holds across replicas. See Claims and leases.

  • Every replica needs the same configuration. A replica with a different route set routes the same topic differently.
  • Enable capture on one replica only. Set outbox.capture.enabled: false on the others. See Change data capture.
  • A broker source consumes on every replica. The broker spreads the messages, and the inbox unique index rejects a repeat.
  • Stop every old replica before a migration that the upgrade notes mark as breaking. See Upgrade QueueBox.