depends_on has no Kubernetes equivalent, and your converter will not mention it
30 July 2026
There is a tool for this on the siteThis Compose file works:
services:
db:
image: postgres:16
api:
image: my/api
depends_on:
- db
Converted to Kubernetes it produces two Deployments and two Services, both valid, and the API crashes on startup roughly half the time.
Kubernetes has no ordering primitive, on purpose
depends_on tells Compose to start db before api. There is nothing in a
Deployment that expresses this. The scheduler places both pods when they are
created and they start concurrently on whichever nodes were free.
This is a design decision rather than a gap. Kubernetes assumes components reconcile toward a working state rather than being sequenced into one, because in a cluster the dependency can also disappear at any time. Ordering at startup does not help when the database pod is rescheduled to another node at three in the morning; the API has to survive its dependency going away regardless. Compose’s guarantee only covers the first thirty seconds of the system’s life.
Worth knowing before you mourn it: depends_on in its plain form does not do
what most people think either. It waits for the container to have started,
not for the service inside it to be ready. Postgres in a starting container is
not accepting connections. Compose added condition: service_healthy to close
that, and it requires a healthcheck on the dependency, and most files that use
depends_on do not have one.
So the guarantee being lost in the conversion is usually weaker than people believe it was. That is a much better thing to discover before a migration than during one.
What to do instead, in order of preference
1. Retry in the application. This is the real answer and the only one that also handles the restart case. A connection pool with backoff, and a process that does not exit when the first connection attempt fails. Most database drivers and HTTP clients already support this and have it switched off.
2. A readiness probe on the dependency. It does not delay the API’s start,
but it keeps the db Service from routing to a pod that is not yet accepting
connections, which converts a connection refusal into a connection that waits.
This is the piece people skip, and it is doing more work than depends_on ever
did.
3. An init container, if the application genuinely cannot be made to retry:
initContainers:
- name: wait-for-db
image: busybox:1.36
command: ['sh', '-c', 'until nc -z db 5432; do echo waiting; sleep 2; done']
This blocks the pod until the port answers. It is a faithful translation of
depends_on and it is the last resort, because it reproduces Compose’s
first-boot-only guarantee and does nothing for a mid-life restart. It also adds
a container image and a failure mode of its own.
| Approach | Handles startup | Handles restart | Cost |
|---|---|---|---|
| Application retry | yes | yes | Code change |
| Readiness probe on dependency | yes | yes | Manifest only, and you want it anyway |
| Init container | yes | no | An extra image and startup latency |
| Nothing | no | no | Crash loops that resolve themselves, noisily |
The last row deserves a mention: with restartPolicy: Always, a pod that
crashes because its dependency was not ready will eventually start, once the
dependency is up. It is ugly, it fills your logs and alerts with
CrashLoopBackOff, and it does converge. For a non-critical service in a dev
cluster that is a defensible place to stop.
The other things that do not survive
depends_on is the one people notice. These are the ones they do not, and
several are worse.
volumes: ./data:/var/lib/data is a bind mount to the host filesystem.
There is no cluster-wide equivalent because there is no single host. A
PersistentVolumeClaim is the nearest thing and it is a different model: storage
that follows the pod rather than the machine, provisioned by a StorageClass you
must have, with an access mode that determines whether more than one pod can
mount it. A converter emits a PVC and cannot know whether ReadWriteOnce is
enough, which decides whether you can ever scale past one replica.
env_file becomes a ConfigMap, and any secret in it becomes a Secret only
if whoever ran the conversion noticed. Compose does not distinguish; Kubernetes
does. This is where a database password lands in a ConfigMap and stays there,
readable by anything with list access to the namespace.
ports: "8080:80" is a host port mapping. The Kubernetes equivalent depends
entirely on intent: ClusterIP for internal traffic, NodePort for a host-level
port, LoadBalancer for external, or an Ingress. A converter has to guess and
usually picks ClusterIP, so the port you were reaching on localhost is no longer
reachable at all. That is the single most common “the conversion worked and
nothing responds” outcome.
restart: on-failure maps to a Job, not a Deployment. A Deployment’s
restartPolicy is always Always and cannot be changed. If the workload is
genuinely a task that completes, converting it to a Deployment gives you an
exit-code-0 CrashLoopBackOff, which is its own confusing rabbit hole.
links is legacy Compose networking and correctly produces nothing:
Kubernetes gives every Service a DNS name. What is not obvious is that the DNS
name is the Service name, which may not match the Compose service name if
the converter renamed it, and cross-namespace lookups need the full
service.namespace.svc.cluster.local form.
deploy.resources is Swarm syntax that Compose ignores outside Swarm mode.
People often assume it carried over. Kubernetes requests and limits are a
different model with different semantics, and requests in particular decide
scheduling and effectively decide what you pay.
The wrong instinct: convert everything at once
The reflex with a converter is to run it over the whole file and fix what breaks. It produces a large diff of generated YAML that nobody reviews properly, and the losses above are individually small and collectively significant.
Convert one service at a time, and read the list of what did not translate before you read the manifests. The manifests are mechanical. The dropped directives are the actual migration, and they are where the decisions live: which storage model, which service type, which of these values is a secret.
A second reason: a converted manifest is a starting point, not an output. It will not have resource requests, a PodDisruptionBudget, a readiness probe worth the name, or a security context. Treating the conversion as done is how you end up with a cluster full of pods with no requests, which is the most common reason a Kubernetes migration costs more than the thing it replaced.
What changed recently
Sidecar containers are now init containers with restartPolicy: Always.
This is directly relevant: it gives you a proper ordering primitive for the
sidecar case specifically. A sidecar declared this way starts before the main
container and is not required to complete, which is exactly the guarantee people
were trying to fake with init containers and shell loops.
kubectl debug and ephemeral containers are stable, which changes how you
diagnose the resulting crash loops, particularly on distroless images where
there is no shell to exec into.
Compose has depends_on: condition: service_healthy, which is the guarantee
people assumed they had all along. If you are still on the plain list form, it
is worth adopting before migrating, because it forces you to write the
healthchecks that become your Kubernetes readiness probes.
Migrating a system already in production
1. Add healthchecks in Compose first. They become readiness probes, and writing them while the system is still running under Compose is much easier than inventing them mid-migration.
2. Make the application retry. Do it before the platform change, verify it under Compose by restarting the dependency, and land it as its own deploy. This is the change that makes the ordering question disappear rather than move.
3. Convert one service, deploy it alongside Compose. Point it at the existing dependencies. You learn the service-type and storage decisions on one service rather than all of them.
4. Read the not-translated list before the YAML. Every time.
5. Add what the converter cannot know: resource requests, probes, a security context, a PodDisruptionBudget. Requests first: a pod with none is invisible to the scheduler and is the main cause of an over-provisioned, under-utilised cluster.
When not to migrate at all
Worth saying, because the conversion tooling makes it look like a foregone conclusion.
A single-host application with no scaling requirement runs fine under Compose, and Kubernetes adds a control plane, a networking model, a storage abstraction and an operational burden in exchange for capabilities you are not using.
A workload with genuine host dependencies, GPU pass-through with specific drivers, host device access, a bind mount to something machine-specific, is fighting the model rather than using it.
A team of two. The break-even on Kubernetes is not technical, it is organisational, and it arrives somewhere around “more services than people”.
If depends_on is the feature you would miss most, that is a signal worth
listening to: it suggests the system is sequenced rather than reconciling, and
sequencing is what Kubernetes does not do.
The short version
- Kubernetes has no ordering primitive. That is deliberate: a dependency can vanish at any time, not only at boot.
- Plain
depends_onwaits for started, not ready, so the guarantee was weaker than you think. - Fix it in the application with retries. Init containers are the last resort and only cover first boot.
- The quiet losses are bind mounts,
env_filesecrets, port mappings andrestart: on-failure. - Read the not-translated list before the generated YAML. That list is the migration.
- Add resource requests. The converter cannot, and their absence is the usual reason the cluster costs too much.
The Compose to Kubernetes converter emits the Deployments, Services and PVCs, and names every directive that has no equivalent rather than dropping it silently.