Deployment & Ops
Docker, release builds, PGO, blue/green and canaries, feature flags, rollback, connection pooling, graceful shutdown. The day-to-day operations craft.
Multi-stage Docker builds
The standard pattern for Rust images: build in a heavy image, copy the binary into a minimal runtime image. Result: ~20MB images that boot in <1s.
# syntax=docker/dockerfile:1.6
FROM rust:1.78-slim AS build
WORKDIR /app
# Cache deps separately from source
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main(){}" > src/main.rs && \
cargo build --release && rm -rf src
COPY src ./src
RUN cargo build --release --bin gateway
FROM gcr.io/distroless/cc-debian12 AS runtime
COPY --from=build /app/target/release/gateway /usr/local/bin/gateway
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/gateway"]
Things to mention in interviews:
- Layer caching — copying
Cargo.toml/Cargo.lockfirst builds a deps-only layer that's reused across source changes. Huge CI win. - Distroless base — no shell, no package manager, smaller attack surface.
ccvariant has glibc. - Static binary alternative — build with musl (
--target x86_64-unknown-linux-musl) for a fully static binary that runs onscratch. Worth doing for the smallest production image. - cargo-chef — third-party tool that splits Rust builds for better Docker caching; worth knowing by name.
Release vs debug builds
Never deploy debug builds. Performance differences are 10-100x for compute-bound code. Always run perf tests against release.
[profile.release]
opt-level = 3
lto = "fat" # whole-program optimization
codegen-units = 1 # single CGU enables more inlining; slower compile
panic = "abort" # smaller binaries, can't unwind across FFI
strip = true # strip symbols
debug = 1 # keep line-number debug info for backtraces in prod
Tradeoffs:
lto = "fat"— big perf win; longer compile.codegen-units = 1— better inlining; slower compile; you keep it on for prod releases, not dev.panic = "abort"— saves binary size, no unwinding overhead, but you losecatch_unwind. Most services don't need it.debug = 1— keep just enough debug info for symbolic stack traces. Pair with a separate--strip=symbolsstep and external symbol upload (sentry, datadog).
PGO and LTO
Profile-Guided Optimization: compile instrumented binary, run representative load, recompile using the profile. Typical wins 5-15% on throughput. Tooling: cargo-pgo.
# 1. instrument
RUSTFLAGS="-Cprofile-generate=/tmp/pgo" cargo build --release
# 2. run representative workload
./target/release/gateway &
# ... load test for ~10 min ...
killall gateway
# 3. merge profile data
llvm-profdata merge -o /tmp/pgo.profdata /tmp/pgo
# 4. recompile using profile
RUSTFLAGS="-Cprofile-use=/tmp/pgo.profdata" cargo build --release
Worth doing for the inference gateway; not worth doing for everything. Like jemalloc: benchmark, then claim.
Blue/green deploys
Two production environments (blue, green). Traffic on blue. Deploy new version to green, smoke-test, flip the load balancer. Old blue stays warm; if issue, flip back.
- Zero-downtime cutover.
- Fast rollback (LB flip is seconds).
- Cost — 2x infra during the cutover period.
- State coordination — both environments writing to the same DB. Schema migrations must be compatible across both versions during the window.
For stateful services (orchestrators with active runs), blue/green is harder; canaries (next) tend to be better.
Canaries
Deploy the new version to a small fraction of capacity (e.g., 1% then 10% then 50% then 100%). Monitor SLOs at each step. Roll back on regression.
- Traffic split by header, by user-id hash, or by random sampling.
- Compare metrics between canary and baseline pods. Statistical comparison preferred (e.g., the canary's p99 vs baseline's p99 with confidence intervals).
- Tooling: Argo Rollouts, Flagger, Spinnaker. Or homegrown — a service mesh (Istio, Linkerd) can route by weight.
- Auto-rollback if canary metrics breach. Half-cooked canaries that need humans to notice problems aren't safer.
Feature flags
Decouple deploy from release. Ship code dark; flip flag to enable.
- Boolean flags — on/off per environment.
- Percentage rollout — enable for X% of users.
- Targeted rollout — enable for specific user IDs / tenants.
- Kill switches (covered in 09) are a subset.
Tooling: LaunchDarkly, Flagsmith, Unleash, or a homegrown service. In Rust, statsig-rust, launchdarkly-server-sdk, or your own config-watcher pulling from Postgres.
Flags accumulate. Every flag is conditional logic you'll maintain. Set a removal date when you add one. Audit quarterly.
Rollback strategy
The bar for production: any change can be rolled back in <5 minutes without coordination.
- Code: Redeploy previous image. Pre-pull images on the cluster so rollback is fast.
- Feature flag: Flip the flag. Seconds.
- Schema changes: Always backward-compatible. Expand-then-contract: add new fields/columns, deploy code that handles both old and new, only later remove the old.
- Stateful migrations: Migrations are forward-only; rolling back code while the DB has the new schema must work.
- Drill rollback. Once a quarter, deliberately roll back something in staging. Find the broken assumption while you're not on fire.
Connection pooling
Three pools matter most:
- Database (Postgres):
sqlx,deadpool-postgres,tokio-postgres. Size = expected concurrent DB ops per replica. 10-50 typical. - Redis:
fred,deadpool-redis. Size = concurrent Redis ops per replica. 20-100 typical for hot paths. - HTTP (to backends):
reqwest::Clientreuses HTTP/2 connections. OneClientper process;.pool_max_idle_per_host(N)tunes.
Anti-patterns:
- Creating a new client per request — TCP handshake / TLS per call. Disastrous for latency.
- Holding a DB connection across a long external HTTP call. Locks up the pool.
- Letting pool sizes exceed downstream capacity — your DB has a max-connections limit; sum of all replicas × pool size must fit.
Graceful shutdown and SIGTERM
k8s sends SIGTERM, then waits terminationGracePeriodSeconds (default 30s), then SIGKILL. Your service should:
- preStop sleep — sleep 5-10s in the k8s preStop hook so the load balancer notices the pod is going away before SIGTERM arrives.
- On SIGTERM: stop accepting new connections, mark unhealthy.
- Drain in-flight requests, with a deadline.
- For long-running runs (agents): persist state, mark resumable, exit.
- Close resources: flush logs/traces, close DB connections, flush kafka producers.
- Exit zero on clean shutdown.
See chapter 11 P10 for the Rust pattern.
Readiness vs liveness
| Probe | Meaning | On failure |
|---|---|---|
| Liveness | "Am I alive?" — process responsive. | k8s kills and restarts the pod. |
| Readiness | "Can I serve traffic?" — dependencies OK. | k8s removes from service endpoints; pod stays running. |
| Startup | "Have I finished initializing?" | Disables liveness checks until startup passes. |
Endpoints:
/livez— returns 200 if the process can answer HTTP. Don't include DB/Redis checks; you don't want k8s killing pods because Postgres flickered./readyz— returns 200 only if downstreams (DB, Redis, backends) are reachable and the service has loaded config./startupz— returns 200 once init complete.
Kubernetes essentials for this role
You don't need to be a k8s expert, but you should be fluent in:
- Deployments, Pods, Services, Endpoints. The day-to-day primitives.
- HPA (Horizontal Pod Autoscaler) — scale on CPU, memory, or custom metrics (queue depth, p99 latency).
- PDBs (Pod Disruption Budgets) — keep N pods up during voluntary disruption.
- Resource requests/limits. Requests = scheduling guarantee; limits = OOMkill threshold. Set both.
- Node affinity / taints. Pin the inference gateway near GPU nodes if that matters; or pin away from noisy neighbors.
- Secrets, ConfigMaps. Where credentials/config live.
- Service mesh. Istio or Linkerd, for mTLS between services, traffic routing for canaries, retries-at-mesh.
"My deploy story is: multi-stage Docker → small image → tagged with git SHA → canary rollout via Argo → automatic rollback on SLO regression. Postgres migrations are expand-then-contract, always backward-compatible during the deploy window. Feature flags decouple deploy from release. SIGTERM triggers a graceful drain with state persistence; preStop sleep handles the LB-aware window. Liveness is process-only; readiness includes dependencies."