Install & deploy

Four ways to run j-broker: Docker Compose for a local 3-broker cluster, Helm for Kubernetes, the published release artifacts, or a from-source build. This page covers all four, then the full broker configuration reference.

Prerequisites

Install with Docker Compose

From a repository checkout:

docker compose up

This builds both images and starts three brokers plus the admin UI. The admin container waits for each broker's gRPC port to accept connections before starting, so the whole stack settles on its own.

ComponentHost portNotes
Admin UI15672RabbitMQ-management port convention.
Broker 1 (gRPC)localhost:9092Container port 9092.
Broker 2 (gRPC)localhost:9093Container port 9092.
Broker 3 (gRPC)localhost:9094Container port 9092.
Chaos HTTP (opt-in)9100 / 9101 / 9102Bound only with JBROKER_CHAOS_ENABLED=true JBROKER_CHAOS_PORT=9100 JBROKER_CHAOS_TOKEN=<secret>.
Raft peer RPCPort 9192 stays inside the compose bridge network; it is never exposed to the host.

Each broker advertises localhost:909{2,3,4} to external clients via --advertised-listeners, so host-side producers and consumers can follow leader hints across the bridge network.

Admin UI overview

The admin UI overview after startup: topology, controller badge, per-broker roles.

Volumes and teardown

Broker data persists in named volumes broker1-data, broker2-data, broker3-data, mounted at /var/lib/jbroker in each container (Raft state plus partition segment files).

docker compose down       # stop; data volumes survive for the next up
docker compose down -v    # stop and wipe all broker data

Monitoring profile

docker compose -f docker-compose.yml -f docker-compose.monitoring.yml --profile monitoring up

Adds Prometheus at http://localhost:9091 and Grafana at http://localhost:3000 with two auto-provisioned dashboards (cluster overview, partitions). Prometheus scrapes the admin app's merged /actuator/prometheus endpoint — the admin app is the single scrape point; brokers have no metrics port of their own.

Install with Helm

The chart ships a 3-replica broker StatefulSet (stable network identity for the Raft voter config), a single-replica admin Deployment, an optional bundled Redis, and opt-in mTLS wiring. Chart source and full docs: deploy/helm/j-broker.

From the published chart

Release-artifact path — pulls the chart and images published by the v2.0.0-rc.1 release:

helm install jb oci://ghcr.io/jeremainecheong/charts/j-broker --version 2.0.0-rc.1 \
  --set broker.image.repository=ghcr.io/jeremainecheong/jbroker-broker \
  --set broker.image.tag=2.0.0-rc.1 \
  --set admin.image.repository=ghcr.io/jeremainecheong/jbroker-admin \
  --set admin.image.tag=2.0.0-rc.1

kubectl port-forward svc/jb-j-broker-admin 15672:15672
The chart's default image references are the local-build names (jbroker-broker:1.4.0), matching the from-source flow below — release installs must override repository and tag as shown.

From source (Kind / minikube)

# Build + load the images into your local cluster:
docker build -f Dockerfile.broker -t jbroker-broker:1.4.0 .
docker build -f Dockerfile.admin  -t jbroker-admin:1.4.0  .
kind load docker-image jbroker-broker:1.4.0 jbroker-admin:1.4.0

# Install with defaults (3-broker plaintext cluster, no TLS, no Redis):
helm install jb deploy/helm/j-broker
kubectl port-forward svc/jb-j-broker-admin 15672:15672

Against a live release, helm test jb runs a hook pod that TCP-connects to every broker pod and the admin service and fails if any port does not answer. deploy/helm/j-broker/tests/render-smoke.sh checks the rendered templates without any cluster.

Values highlights

values.yaml is the authoritative, fully commented list. The ones that shape a deployment:

KeyDefaultPurpose
broker.replicaCount3Raft-majority-friendly odd number. Changing it on a running release requires manual Raft voter reconfiguration.
broker.persistence.size10GiPer-broker PVC size, backing /var/lib/jbroker.
broker.podDisruptionBudget.enabledtrueCaps voluntary evictions at maxUnavailable: 1 so node drains never cost Raft its majority.
broker.podAntiAffinity.enabledtrueSoft anti-affinity spreading brokers across nodes; single-node clusters still schedule.
broker.rack.enabledfalseRack awareness — see below.
broker.advertisedHostTemplate""sprintf-style template (%d → 1-indexed broker id) building --advertised-listeners for external clients, e.g. broker%d.example.com. Empty skips the flag.
broker.auth.modenonenone or mtls. mTLS derives the principal from the client certificate CN and enforces default-deny ACLs. Requires tls.enabled.
broker.auth.superUsersbroker,adminPrincipals that bypass ACL checks; the inter-broker and admin-app certificate CNs must be here before flipping to mtls.
admin.replicaCount1More than one replica requires redis.enabled=true for cross-replica SSE fan-out.
admin.ingress.enabledfalseStand up an Ingress for the admin UI.
admin.auth.existingSecret""Secret whose users key holds name:bcrypt-hash pairs for operator login. Empty leaves the admin app open (it logs a startup warning).
tls.enabledfalsemTLS on every gRPC hop.
tls.secretNamejbroker-tlsSecret carrying ca.crt + tls.crt + tls.key PEM files.
redis.enabledfalseBundled Redis for quota buckets + SSE fan-out. The default install never dials Redis.
networkPolicy.enabledfalseIngress lockdown — see below.
metrics.serviceMonitor.enabledfalseprometheus-operator ServiceMonitor — see below.
metrics.prometheusRule.enabledfalseAlert pack — see below.

Enabling mTLS

scripts/tls/bootstrap-ca.sh .tls

kubectl create secret generic jbroker-tls \
    --from-file=tls.crt=.tls/broker1.crt \
    --from-file=tls.key=.tls/broker1.key \
    --from-file=ca.crt=.tls/ca.crt

helm upgrade --install jb deploy/helm/j-broker \
    --set tls.enabled=true \
    --set tls.secretName=jbroker-tls

With tls.enabled, every broker server and every inter-broker client speaks mTLS, and the admin app uses the same bundle to dial brokers (tls.adminClientSecretName optionally points it at a separate client cert). To also turn on authentication and ACL enforcement, set broker.auth.mode=mtls — and make sure the inter-broker and admin certificate CNs are in broker.auth.superUsers first, or the brokers will deny each other. The full principal/ACL model is on the Security page.

The dev script generates per-broker server certs (broker1.crt…), but the chart mounts one Secret on every broker pod. In a production deployment, issue one cert per StatefulSet pod — SAN-matching each pod's stable DNS name — with cert-manager or similar.

Rack awareness

broker.rack.enabled=true makes each pod read its rack from its own topology.kubernetes.io/zone pod label via the downward API (surfaced to the broker as JBROKER_RACK); topic placement then spreads each partition's replicas across the racks brokers report, so a whole-zone outage cannot take out every copy. The downward API cannot read node labels, so the pods themselves must carry the label — mirror it from the node with an admission webhook or stamp it through zone-pinned deployment tooling. A pod missing the label fails to start with CreateContainerConfigError. Leave this off on single-zone clusters.

NetworkPolicy

networkPolicy.enabled=true locks down ingress: the Raft port stays broker-only, chaos and Redis ports admin-only, the broker client port is scoped by the networkPolicy.clientFrom peer list and the admin UI/API port by networkPolicy.adminFrom (an empty list keeps that port open to any peer). Egress is left unrestricted. Enforcement needs a CNI that implements NetworkPolicy (Calico, Cilium, …) — Kind's stock kindnet accepts the objects but enforces nothing.

ServiceMonitor and alerts

metrics.serviceMonitor.enabled=true renders a prometheus-operator ServiceMonitor scraping the admin /actuator/prometheus endpoint (the merged per-broker view; the actuator path stays open even with admin auth enabled). Keep the scrape interval at or above the admin app's own 5 s broker-scrape cadence. When NetworkPolicy is on with a non-empty adminFrom, add the Prometheus pods to it or scrapes will be dropped.

metrics.prometheusRule.enabled=true ships seven alert rules: under-replicated partitions, sustained follower lag, stuck high watermark, metadata-Raft term flapping, unreachable broker, low disk headroom, and (with the ServiceMonitor) scrape-down. Two thresholds are tunable under metrics.prometheusRule.thresholds: replicationLagRecords (default 1000) and raftTermIncreasesPer10m (default 3). The expressions assume the operator-attached namespace label, so enable the rule pack together with the ServiceMonitor. Both objects need the monitoring.coreos.com CRDs.

Published artifacts

All commands in this section are release-artifact-dependent — they pull what the tag-triggered release pipeline published for v2.0.0-rc.1.

Container images

ghcr.io/jeremainecheong/jbroker-broker and ghcr.io/jeremainecheong/jbroker-admin, tagged with the release version. Both run as non-root (uid 1001) and are linux/amd64.

:latest tracks stable releases only. A prerelease such as 2.0.0-rc.1 never moves it — pin the explicit version tag.

A single-broker instance (the image's default command runs server --data-dir /var/lib/jbroker with itself as the only voter):

docker run -p 9092:9092 -v jbroker-data:/var/lib/jbroker \
  ghcr.io/jeremainecheong/jbroker-broker:2.0.0-rc.1

Named volumes inherit the image's ownership; a host bind-mount onto /var/lib/jbroker must be writable by uid 1001.

Helm chart

helm install jb oci://ghcr.io/jeremainecheong/charts/j-broker --version 2.0.0-rc.1

Remember the image overrides from the Helm section above. Each GitHub Release also carries the packaged chart .tgz as an attachment.

Client jars (GitHub Packages)

Five modules publish under io.github.jeremainecheong: proto, raft-core, raft-transport, broker-storage, broker-core. The client classes — ClusterClient, BatchingProducer, TransactionalProducer, Consumer — live in broker-core, which pulls the others transitively.

GitHub Packages requires authentication even for public packages. You need a GitHub personal access token with the read:packages scope — an anonymous repositories block will fail with 401. This is a GitHub constraint, not a choice this project made.
// build.gradle
repositories {
    mavenCentral()
    maven {
        url = uri("https://maven.pkg.github.com/jeremainecheong/j-broker")
        credentials {
            username = findProperty("gpr.user") ?: System.getenv("GITHUB_ACTOR")
            password = findProperty("gpr.token") ?: System.getenv("GITHUB_TOKEN")
        }
    }
}

dependencies {
    implementation "io.github.jeremainecheong:broker-core:2.0.0-rc.1"
}

Building from source

git clone https://github.com/jeremainecheong/j-broker.git
cd j-broker

./gradlew :broker-app:installDist    # broker + CLI
./gradlew build                      # full build with unit + fast integration tests

installDist produces the shipping binary at broker-app/build/install/broker-app/bin/broker-app — the same launcher the Docker image packages. It is both the broker entrypoint (broker-app server …) and the CLI (topics, produce, consume, console-consumer, admin); alias it to j-broker for your shell. Full CLI reference: broker-app/README.md. The admin app runs with ./gradlew :admin-app:bootRun (port 9090 bare, 15672 in the container), and the two Dockerfiles (Dockerfile.broker, Dockerfile.admin) build the images used by compose and the chart.

Configuration reference

Server settings resolve in layers, later layers winning: built-in defaults ← --config j-broker.yamlJBROKER_* environment variables ← command-line flags. The YAML file is a flat map of dotted keys; unknown keys in the file are startup errors (they are almost always typos), unrecognized JBROKER_* variables only warn.

broker-app server --validate-config

prints every resolved key with its value and source, reports all validation problems at once, and exits 0/2 without binding a port.

The table below mirrors the key table that drives validation (ServerConfig.KEYS), so the code is the ground truth for every row.

KeyDefaultEnvFlagDescription
node.id1JBROKER_NODE_ID--idBroker id. Must appear in the voter list.
data.dir./var/brokerJBROKER_DATA_DIR--data-dirData directory (Raft log + partition logs).
broker.port9092JBROKER_BROKER_PORT--broker-portClient and inter-broker gRPC port.
raft.port9192JBROKER_RAFT_PORT--raft-portRaft peer RPC port.
voters(empty)JBROKER_VOTERS--votersCluster voter list, ID@HOST:RAFT:BROKER,.... Empty runs a single-broker cluster with self as the only voter.
advertised.listeners(empty)JBROKER_ADVERTISED_LISTENERS--advertised-listenersClient-facing address overlay, ID=HOST:PORT,.... Ids absent from the overlay advertise their bind address.
rack(empty)JBROKER_RACK--rackRack / availability-zone label for this broker (e.g. the topology.kubernetes.io/zone value). When brokers span two or more racks, topic placement spreads replicas across them. Empty = no rack.
chaos.port-1JBROKER_CHAOS_PORT--chaos-portCooperative chaos HTTP port. -1 disables.
chaos.enabledfalseJBROKER_CHAOS_ENABLED--enable-chaosExplicit opt-in for the chaos control plane. chaos.port refuses to bind without it.
chaos.token(empty)JBROKER_CHAOS_TOKENBearer token every chaos HTTP request must present. Required when the chaos port is enabled.
consumer.offsets.partitions50JBROKER_CONSUMER_OFFSETS_PARTITIONS--consumer-offsets-partitionsPartition count for the internal __consumer_offsets topic. Fixed at first boot.
min.insync.replicas2JBROKER_MIN_INSYNC_REPLICAS--min-insync-replicasCluster default acks=all durability floor. Per-topic config overrides; RF-1 topics clamp down.
max.message.bytes1048576JBROKER_MAX_MESSAGE_BYTESCluster default for the largest serialized produce batch. Per-topic config overrides; hard cap 8 MiB (gRPC frame limits bound every hop).
log.segment.bytes134217728JBROKER_LOG_SEGMENT_BYTESCluster default segment roll threshold. Per-topic segment.bytes overrides.
log.retention.ms604800000JBROKER_LOG_RETENTION_MSCluster default time retention (7 days). -1 = unlimited. Per-topic retention.ms overrides.
log.retention.bytes-1JBROKER_LOG_RETENTION_BYTESCluster default size-retention budget per partition. -1 = unlimited. Per-topic retention.bytes overrides.
log.flush.messages-1JBROKER_LOG_FLUSH_MESSAGESCluster default flush count trigger. -1 = off (fsync on segment roll + replication). Per-topic flush.messages overrides.
log.flush.ms-1JBROKER_LOG_FLUSH_MSCluster default flush age trigger, ms. -1 = off. Per-topic flush.ms overrides.
log.cleaner.interval.ms300000JBROKER_LOG_CLEANER_INTERVAL_MSRetention/compaction cleaner tick interval, ms.
offsets.retention.ms604800000JBROKER_OFFSETS_RETENTION_MSCommitted offsets of groups with no live members expire once their newest commit is older than this (7 days). -1 disables expiry.
shutdown.timeout.ms30000JBROKER_SHUTDOWN_TIMEOUT_MSSIGTERM drain budget: how long the broker spends handing led partitions to other ISR members before closing. 0 skips the drain.
storage.headroom.bytes1073741824JBROKER_STORAGE_HEADROOM_BYTESDisk-headroom watermark. Below it, client produces get retriable STORAGE_FULL while fetch/replication/admin keep serving.
auth.modenoneJBROKER_AUTH_MODE--auth-modeClient authentication: none or mtls. mtls derives the principal from the client certificate CN, rejects principal-less RPCs, and requires tls.enabled.
super.users(empty)JBROKER_SUPER_USERSComma-separated principals that bypass ACL checks. Put inter-broker certificate CNs here before turning on auth.mode=mtls.
tls.enabledfalseJBROKER_TLS_ENABLED--tls-enabledEnable mTLS on every gRPC listener and inter-broker client. The flag is a switch (no value).
tls.cert(empty)JBROKER_TLS_CERT--tls-certPEM certificate chain. Required when tls.enabled.
tls.key(empty)JBROKER_TLS_KEY--tls-keyPEM private key. Required when tls.enabled.
tls.trust(empty)JBROKER_TLS_TRUST--tls-trustPEM trust bundle peers are verified against. Required when tls.enabled.

Per-topic keys (min.insync.replicas, max.message.bytes, retention.ms, retention.bytes, segment.bytes, flush.messages, flush.ms) are set at topic create/update through the admin API and override the cluster default for that topic.