Kubernetes explainer
Sidecar vs Init Containers: Native Sidecar Setup
An init container runs to completion before the app starts. A sidecar runs next to the app for the life of the pod. For years Kubernetes had no first-class way to express the second kind, so teams put helpers in the containers list and hoped for the best. Native sidecars, on by default since Kubernetes 1.29, add a first-class form, and they change how start order and shutdown behave.
By Muhammad Soliman, Founder at SeaGit
·
Key takeaways
- A regular init container must exit successfully before the next init container and the app containers start. It is for setup work, not for long-running services.
- A native sidecar is an entry in initContainers with restartPolicy: Always. It starts before the next init container, keeps running with the app, and is restarted if it fails.
- The SidecarContainers feature gate is stable and locked on in current releases. It was first available in v1.28 and on by default from v1.29.
- Native sidecars stop after the main containers have stopped, which fixes the Job problem where a helper in containers keeps the pod running forever.
Init containers: run to completion, in order
Init containers are declared under spec.initContainers. The kubelet runs them one at a time, in the order listed. Each one must succeed before the next starts. When all of them have finished, the app containers start. If an init container fails, the kubelet restarts it according to the pod restartPolicy, and the app does not start.
apiVersion: v1
kind: Pod
metadata:
name: my-api
spec:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z postgres 5432; do sleep 2; done']
- name: run-migrations
image: my-api:1.4.2
command: ['./migrate', '--up']
containers:
- name: app
image: my-api:1.4.2
ports:
- containerPort: 8080Regular init containers do not support lifecycle hooks, liveness, readiness or startup probes. They are one-shot work: wait for a dependency, run a migration, fetch a file into a shared volume, or fix ownership on a mount.
Sidecar containers: the helper that stays
A sidecar runs alongside the main application for its whole life. A log shipper, a service proxy, a secrets refresher or a config reloader are typical examples. Before native support, the usual way to run one was to list it in spec.containers next to the app. That works, but it has three gaps.
- There is no start order. The sidecar and the app start at about the same time, so a proxy may not be listening when the app makes its first call.
- There is no shutdown order. The sidecar can be stopped before the app has finished flushing logs or draining connections.
- A helper in containers keeps a Job pod alive. The Job never completes because the sidecar never exits, even after the main work is done.
Native sidecars close those gaps. The kubelet starts them before the app and stops them after the app, and their lifecycle is independent of the regular init containers.
Native sidecars: restartPolicy Always in initContainers
To make a sidecar, put it under initContainers and set its restartPolicy to Always. The field is valid only on init containers, and it marks the entry as a sidecar rather than a one-shot step. The example below ships application logs from a shared volume through a sidecar, with a readiness probe on the sidecar.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-api
namespace: prod
spec:
replicas: 2
selector:
matchLabels:
app: my-api
template:
metadata:
labels:
app: my-api
spec:
initContainers:
- name: log-shipper
image: fluent/fluent-bit:3.1
restartPolicy: Always # makes this a native sidecar
readinessProbe:
httpGet:
path: /readiness # replace with the health endpoint your shipper exposes
port: 2020
periodSeconds: 10
volumeMounts:
- name: logs
mountPath: /var/log/app
containers:
- name: app
image: my-api:1.4.2
volumeMounts:
- name: logs
mountPath: /var/log/app
volumes:
- name: logs
emptyDir: {}Because the sidecar is in the init list, it starts before any init container that follows it. If you put a migration after the sidecar, the migration runs while the sidecar is already up. Put setup that the sidecar must not see before it in the list.
Ordering, probes and shutdown in one table
The three kinds of container differ in what they guarantee. The table below sums up the behavior described in the Kubernetes Sidecar Containers and Init Containers pages.
| Question | Regular init | Native sidecar | In containers list |
|---|---|---|---|
| Runs for the pod lifetime? | No, runs to completion | Yes | Yes |
| Starts before the app? | Yes | Yes, and before later init containers | No ordering guarantee |
| Readiness probe | Not supported | Supported, feeds pod Ready | Supported |
| Stopped after the app? | Not applicable | Yes, after main containers stop | Not guaranteed |
| Keeps a Job pod running? | No | No, the kubelet stops it | Yes, if it never exits |
Probes and the started status
A native sidecar counts as started when its process runs, or when its startupProbe succeeds, if one is set. The kubelet then moves on to the next init container. Use a startup probe for a sidecar that takes time to listen, so the app does not start before its proxy is up.
Versions and checking your cluster
Native sidecars were first available in Kubernetes v1.28, behind the SidecarContainers feature gate. The feature has been on by default since v1.29. The Kubernetes page for the feature now describes it as stable since v1.33, and the gate is locked, so setting it has no effect. Clusters on an older minor release may need the gate enabled explicitly, and the Kubernetes version policy covers the support window for each minor release.
# Server version of the cluster you are connected to
kubectl version -o json | grep -A2 serverVersion
# On EKS, the control plane version for a named cluster
aws eks describe-cluster --region us-east-1 --name prod-cluster \
--query cluster.version --output textIf you run an older release and cannot upgrade, keep the sidecar in containers and accept the start-order gap. Add a small wait loop in the app or an init container that checks the proxy before the app starts.
Choosing between an init container and a sidecar
Ask one question: does the helper need to finish, or does it need to keep running? If it must finish before the app can start, it is an init container. If it must run for as long as the app runs, it is a sidecar, and on a current cluster it should be a native sidecar.
- Use an init container to wait for a dependency, run a schema migration, or prepare files in a volume.
- Use a native sidecar for a log shipper, a service proxy, a certificate or secret refresher, or a config reloader that must run beside the app.
- Use a plain container in containers when the helper does not need ordering and the pod never needs to complete, such as a metrics exporter that you are happy to start in any order.
- Use a native sidecar, not a plain container, for any helper inside a Job or CronJob pod that must not block completion.
The same pod can mix all three kinds. Order the initContainers list as the start sequence you want, put the one-shot steps and the native sidecars together in that list, and keep the app in containers.