67. Running Rust in containers and Kubernetes
67.1 A small, cache-friendly image
# syntax=docker/dockerfile:1
FROM rust:1 AS build
WORKDIR /src
# Build dependencies in their own layer: rebuilt only when Cargo.toml/Cargo.lock change.
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main() {}' > src/main.rs && cargo build --release --locked && rm -rf src
COPY src ./src
RUN touch src/main.rs && cargo build --release --locked
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=build /src/target/release/api /api
USER nonroot:nonroot
EXPOSE 3000
ENTRYPOINT ["/api"]
The final image contains glibc, CA certificates, and your binary: about
30 MB, with no shell and no package manager to exploit. With a musl build,
FROM scratch also works (add CA certificates yourself if the program
makes outbound TLS calls through the system store. webpki-roots builds
them into the binary). cargo-chef is a more robust version of the
dependency-layer trick above.
67.2 Behaving well under orchestration
| Concern | What to do in Rust | Across the series |
|---|---|---|
| CPU limits | std::thread::available_parallelism() reads the cgroup CPU quota on Linux. Tokio sizes its worker pool from it. Go needed GOMAXPROCS fixes for years |
OS Ch 75 |
| Memory limits | no GC heap to tune; memory is what you allocate. Bound caches and queues, and watch RSS against memory.max. The OOM killer sends SIGKILL with no warning |
OS Ch 76 |
| SIGTERM | graceful shutdown (§31.4, §47); finish within terminationGracePeriodSeconds |
OS Ch 74 |
| PID 1 | handle signals explicitly or use tini (§40.3) |
same |
| Probes | /healthz (liveness: the process works) separate from /readyz (readiness: dependencies are reachable; returns 503 while draining) |
Go §67 |
| Logs | JSON to stdout: tracing_subscriber::fmt().json() (feature json) |
Go §65 |
| Metrics and traces | metrics + a Prometheus exporter, or OpenTelemetry via tracing-opentelemetry |
HTTPS Ch 23 |
| Security context | runAsNonRoot, readOnlyRootFilesystem, drop all capabilities. A static Rust binary needs none of them |
Security in Depth Ch 15 |
Real-world example. Teams that rewrite a Go or JVM service in Rust usually report two operational changes. Memory use becomes flat and predictable, because there is no GC heap growing to its limit. Tail latency (p99, p999) improves more than median latency, because there are no collector pauses. Discord's 2020 post about rewriting its "Read States" service from Go to Rust describes exactly this pattern of latency spikes from GC every two minutes, which disappeared after the rewrite. The counterpoint: Go's GC has improved since then, and a well-tuned Go service is often good enough. Measure before you rewrite.