depends_on has no Kubernetes equivalent, and your converter will not mention it

30 July 2026

There is a tool for this on the site

This 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.

ApproachHandles startupHandles restartCost
Application retryyesyesCode change
Readiness probe on dependencyyesyesManifest only, and you want it anyway
Init containeryesnoAn extra image and startup latency
NothingnonoCrash 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_on waits 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_file secrets, port mappings and restart: 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.