Docker
A pipeline is one static-ish binary plus one YAML file. The repository ships
a flagship image at examples/docker/Dockerfile that packages the
first-pipeline tutorial's
binary (kafka_avro_to_clickhouse) — treat it as the template for any
pipeline you build on the framework.
The flagship image
Build from the repository root:
docker build -f examples/docker/Dockerfile -t etl-pipeline .
docker run -e KAFKA_BROKERS=broker:9092 -e CLICKHOUSE_URL=http://ch:8123 \
-p 9090:9090 etl-pipeline
The Dockerfile is two stages (lifted from examples/docker/Dockerfile):
FROM rust:1.96-bookworm AS builder
WORKDIR /build
COPY . .
RUN cargo build --release -p etl --example kafka_avro_to_clickhouse --features full \
&& cp target/release/examples/kafka_avro_to_clickhouse /build/pipeline
# Distroless: glibc + libgcc only, no shell, runs as nonroot (65532).
FROM gcr.io/distroless/cc-debian12:nonroot
COPY /build/pipeline /usr/local/bin/pipeline
COPY /build/crates/etl/examples/kafka_avro_to_clickhouse.yaml /etc/etl/pipeline.yaml
ENV ETL_CONFIG=/etc/etl/pipeline.yaml
EXPOSE 9090
ENTRYPOINT ["/usr/local/bin/pipeline"]
Why this shape:
- Distroless runtime (
gcr.io/distroless/cc-debian12:nonroot): the binary needs only glibc and libgcc. No shell, no package manager, runs as UID 65532 — the attack surface is the pipeline itself. - librdkafka is compiled from vendored sources in the builder stage, so no system packages are needed beyond the Rust image's toolchain. Expect the first build to take several minutes; for CI, layer a cargo-chef stage in front to cache dependencies.
- One image, one pipeline. The framework's operational unit is one process running one pipeline; scale by replicas, not by packing pipelines into a pod.
Configuration: ETL_CONFIG and mounted YAML
The binary reads a single YAML file from the path in ETL_CONFIG (the image
defaults it to /etc/etl/pipeline.yaml and bakes in the example config).
Every ${VAR:-default} in the file interpolates from the process
environment at startup — see
Configuring pipelines for the exact
interpolation semantics. The Kubernetes pattern:
- ConfigMap mounted over
/etc/etl/pipeline.yaml(or anywhere, withETL_CONFIGpointing at it) owns the structure — connectors, tuning, topology. - Environment variables (plain or Secret-backed) own credentials and
endpoints:
KAFKA_BROKERS,CLICKHOUSE_PASSWORD, and friends land in the YAML through interpolation, so secrets never live in the ConfigMap.
[!NOTE] There is no config hot-reload, by design. Reconfigure with a rolling restart: annotate the pod template with a checksum of the ConfigMap so config edits trigger a rollout.
Probes and the admin server
The admin server binds metrics.listen (default 0.0.0.0:9090) and serves
three endpoints:
GET /readyz— 200 once the source has received an assignment (lanes are live) and the sink's replicas answer connectivity probes. Wire this toreadinessProbeso a pod takes traffic-shaped duties (its consumer-group membership) only when it can actually write.GET /healthz— 200 while every pipeline thread's poll loop heartbeat is fresh and the checkpoint watermark is not stuck while data flows. Wire this tolivenessProbe: a wedged pipeline gets restarted instead of idling forever.GET /metrics— Prometheus exposition. See Monitoring and the canonical taxonomy in docs/METRICS.md.
readinessProbe: { httpGet: { path: /readyz, port: 9090 } }
livenessProbe: { httpGet: { path: /healthz, port: 9090 }, periodSeconds: 10 }
SIGTERM and the grace period
On SIGTERM the pipeline runs the same drain choreography as a consumer
rebalance: stop the lanes, flush the operator chains, give in-flight sink
batches checkpoint.drain_timeout (default 25s) to complete, commit
offsets synchronously, exit. The details live in
Graceful shutdown.
[!IMPORTANT] Set
terminationGracePeriodSecondscomfortably abovecheckpoint.drain_timeout— e.g. 30s for the default 25s. If Kubernetes kills the pod mid-drain nothing is lost (at-least-once: uncommitted offsets replay on the next member), but every SIGKILL turns a clean commit into replayed duplicates. Give the drain the time it was configured to take.
If a sink is down when the drain deadline hits, unflushed batches are
abandoned loudly (etl_sink_abandoned_batches_total plus a log line) and
replay after restart — delivery stays at-least-once either way.
Resources and scaling
pipeline.threadsdefaults toavailable_parallelismminus the I/O reserve, andavailable_parallelismhonours cgroup CPU limits. Set a CPU limit or setpipeline.threadsexplicitly — a pod without limits sees the node's cores and spawns a thread per core. See Tuning.- One pod = one consumer-group member. Scaling the Deployment triggers a rebalance; revoked partitions drain and commit before handover, so a scale event costs redelivery of only the uncommitted tail.
- Memory is dominated by
backpressure.max_inflight_bytesplus librdkafka's prefetch caps; sizeresources.limits.memoryabove their sum with headroom. The sizing rule is in Tuning.
The full Kubernetes walkthrough (including the scaling and rebalance notes)
lives next to the Dockerfile in examples/docker/README.md.