What are init containers and sidecar containers -- when to use each pattern?
Quick Answer
Init containers run sequentially before any app containers start and must complete successfully; they handle setup tasks like database migrations or config fetching. Sidecar containers (native in Kubernetes 1.28+) run alongside the main container for the Pod's entire lifecycle, handling cross-cutting concerns like logging, proxying, or secret rotation.
Detailed Answer
Think of opening a restaurant for dinner service. Init containers are like the prep cooks who arrive hours early to chop vegetables, make sauces, and set up stations -- they must finish completely before the doors open. Sidecar containers are like the busboys and sommelier who work alongside the chef throughout the entire service -- they support the main operation continuously but are not the main event. You need both: prep work that happens once upfront, and support roles that persist throughout.
In Kubernetes, init containers are defined in spec.initContainers and run one at a time in the order listed. Each init container must exit with code 0 before the next one starts. If an init container fails, the kubelet restarts it according to the Pod's restartPolicy (with backoff). Only after all init containers succeed do the regular containers in spec.containers begin. Init containers can use different images from app containers, have access to the same volumes, and can run privileged setup tasks that the app container should not have permissions for. Common use cases include waiting for a dependent service to be ready, running database schema migrations, fetching configuration from a vault, or populating a shared volume with static assets.
Sidecar containers, formalized as a native concept in Kubernetes 1.28 with the restartPolicy: Always field on init containers, run for the entire lifetime of the Pod. Before this native support, sidecars were simply regular containers in the same Pod, but they had a critical problem: during shutdown, all containers received SIGTERM simultaneously, so a logging sidecar might terminate before the app container finished flushing its final logs. Native sidecar containers (defined as init containers with restartPolicy: Always) start before regular containers and shut down after them, solving the ordering problem. The most common sidecar patterns include service mesh proxies (Envoy in Istio), log collectors (Fluent Bit forwarding to Elasticsearch), monitoring agents (Prometheus exporters), and secret rotation agents (Vault Agent injecting refreshed credentials).
Internally, the kubelet manages init containers and sidecars differently in its container runtime lifecycle. Init containers are tracked in a sequential state machine: the kubelet will not start initContainer[n+1] until initContainer[n] has terminated successfully. For native sidecars (restartPolicy: Always init containers), the kubelet starts them in order like init containers but does not wait for them to terminate -- it waits only for them to reach the Running state (or pass their startup probe) before proceeding. During Pod termination, the kubelet sends SIGTERM to regular containers first, waits for them to exit, and only then terminates sidecar containers in reverse order. This guarantees that sidecars outlive the workload they support.
A non-obvious gotcha with init containers is that they count toward the Pod's resource calculation differently. The effective resource request for a Pod is the maximum of: the highest single init container request, or the sum of all regular container requests. This is because init containers run sequentially (not concurrently), so only the largest one matters for scheduling. However, sidecar containers with restartPolicy: Always have their resources added to the sum of regular containers since they run concurrently. Another trap: init containers do not support lifecycle hooks, liveness probes, or readiness probes -- they rely solely on exit codes. If your init container hangs indefinitely (e.g., waiting for a service that never comes up), the Pod stays in Init state forever unless you set activeDeadlineSeconds on the Pod spec.