Docker Compose to Kubernetes

Paste a docker-compose.yml and get a Deployment and a Service for each service, with PersistentVolumeClaims and ConfigMaps where they are warranted. Everything that does not translate, starting with depends_on and bind mounts, is reported rather than quietly dropped.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Convert. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

privileged: true

A container with full host access, which has no safe Kubernetes equivalent

services:
  web:
    image: nginx:latest
    privileged: true
    ports:
      - "80:80"

A password in environment

A value that must become a Secret rather than being copied into the manifest

services:
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: hunter2

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Expecting privileged: true to translate

    There is no safe equivalent. The nearest Kubernetes setting is securityContext.privileged, which grants the same full host access and most clusters block it by policy.

    Instead:Work out which capability was actually needed and grant only that.

  2. Copying environment values straight into a manifest

    A password in Compose becomes a password in a Deployment, in version control, visible to anyone with get on the namespace.

    Instead:Move it to a Secret and reference it with secretKeyRef.

  3. Assuming depends_on becomes ordering

    Kubernetes has no equivalent. Pods start in parallel and a dependency that is not ready produces crash loops until it is.

    Instead:Use readiness probes and let the application retry, or an init container that waits.

What it catches

kompose is a CLI, and nothing equivalent runs in a browser. Most of a Compose file maps across without comment. These are the parts where the literal translation is accepted by the cluster and behaves differently, or where there is nothing to translate to at all.

depends_on does not order pod startup

There is no field, no annotation and no object in Kubernetes that makes one pod wait for another. Compose starts the dependency first and, with condition: service_healthy, blocks until its healthcheck passes. Kubernetes creates every pod at the same moment and the deciding factor becomes image pull time. Init containers order containers inside one pod, not pods against each other. This is reported rather than dropped, because a converter that drops it hands you a stack that comes up in the wrong order and blames your application.

A bind mount does not become anything

A named volume becomes a PersistentVolumeClaim. ./html:/usr/share/nginx/html does not: cluster nodes have no copy of your working directory. No hostPath is emitted for it. hostPath would apply cleanly, work on a single-node cluster, and break the first time the pod was rescheduled, which is worse than not translating it, and it is a standard privilege escalation route that most admission policies reject anyway.

command and entrypoint swap places

Compose command overrides the image CMD; Kubernetes command overrides the ENTRYPOINT. So Compose command maps to Kubernetes args, and Compose entrypoint maps to Kubernetes command. Nearly every converter for this gets it backwards, and the result is a working image that exits immediately or cannot find a binary that is definitely present.

mem_limit: 512m is not memory: 512m

Compose byte suffixes are 1024-based, so m is mebibytes. In a Kubernetes quantity m is milli, one thousandth. Copied across as written, the manifest asks for half a byte, the API server accepts it without comment, and the container is OOMKilled on its first allocation. The value is converted here and the finding is critical, because nothing on the path from the file to the crash mentions units.

There is no host port to publish to

ports: "8080:80" maps a port on the machine running Compose, and a cluster has no such machine. A Service is a different object with a different job, and a NodePort cannot be 8080 because the range starts at 30000. The finding says kubectl port-forward, which is the true equivalent, rather than emitting a NodePort that will not apply. The long syntax, ranges, /udp and IP-qualified forms are all read.

Networks were the isolation, and they are gone

Kubernetes has one flat pod network: every pod in every namespace can reach every other pod until a NetworkPolicy says otherwise. A user-defined Compose network did two jobs, DNS and isolation, and only the DNS half needs no translation. If a network was a boundary, it has to be rewritten as a NetworkPolicy deliberately.

Where Compose and Kubernetes stop lining up

Compose describes containers on one machine, started in an order you chose, sharing that machine's filesystem and network. Kubernetes describes workloads a scheduler places, replicates, restarts and moves, in no order at all. Most fields have a direct equivalent. The ones that do not are where an apparently faithful conversion changes behaviour, and they are what this page is for.

depends_on, and what actually replaces it

This is the single most misunderstood part of the migration. Compose waits; Kubernetes does not, and there is no setting that makes it. The replacement is not an ordering mechanism at all: it is a readiness probe on the dependency, so it receives no traffic until it can serve, and a client that retries its first connection with a backoff. That combination also survives the dependency restarting at three in the morning, which ordered startup never covered.

compose
  depends_on:
    db:
      condition: service_healthy

kubernetes
  db gets a readinessProbe, so its Service has
  no endpoints until it answers

  api retries its first connection and logs
  the wait instead of exiting 1

what does NOT work
  initContainers          orders containers in ONE pod
  a sleep in the command  moves the race, keeps it
  restartPolicy tuning    there is nothing to tune

Which volumes translate, and which cannot

A named volume is a claim on storage, so it becomes a PersistentVolumeClaim. A bind mount is a path on the machine you typed the command on, and there is no cluster equivalent. Two more details bite afterwards: a Compose volume has no size and a PVC must request one, and a named volume mounted by two services needs ReadWriteMany, which the default storage class in every managed cluster does not offer.

- pgdata:/var/lib/postgresql/data
    PVC. Follows the pod, which is what the
    named volume did.

- ./html:/usr/share/nginx/html
    Nothing. Config becomes a ConfigMap, code
    becomes part of the image, data becomes
    a PVC. Live-reload has no equivalent.

- /var/lib/app          anonymous, so emptyDir.
                        Deleted with the pod, and
                        a Deployment replaces pods.

shared by two services  needs ReadWriteMany. EBS,
                        GCE PD and Azure Disk are
                        ReadWriteOnce: one node.

ENTRYPOINT, CMD, command and args

Four names for two slots, and the two systems use the same word for different slots. Kubernetes command overrides the image's ENTRYPOINT and args overrides its CMD, each independently. Compose command overrides CMD only, leaving the ENTRYPOINT in place, which is why it belongs in args. Putting it in command instead discards the entrypoint the image was built around.

compose
  entrypoint: /app/bin/server
  command: ["--port", "9000"]

kubernetes
  command: ["/app/bin/server"]   <- entrypoint
  args: ["--port", "9000"]       <- command

get it backwards and the image's entrypoint
never runs. The failure is exit 0 on start,
or "executable file not found" for a binary
that is definitely in the image.

Ports, and the object that is not a port

A published port is a mapping on a host. A Service is a name and a virtual address inside the cluster, which is a different thing that happens to be spelled with two port numbers. Nothing in a cluster can give you 8080 on your laptop except port-forward, and nothing can give the outside world port 80 except an Ingress or a LoadBalancer. A NodePort exists and is not the answer: the range starts at 30000.

ports: "8080:80"

Service
  port: 80          what clients ask for
  targetPort: 80    where the container listens

kubectl port-forward svc/web 8080:80
  the real equivalent of the compose mapping

expose: "9000"
  needed no Service in compose, needs one here.
  A service with no ports gets no Service object
  and no DNS name, so dependents get NXDOMAIN.

One healthcheck becomes three probes

Compose has one healthcheck and Kubernetes has three probes answering different questions: readiness decides whether traffic is routed here, liveness decides whether to restart, and startup holds both off while a slow process boots. The Compose test is reused for readiness and liveness, which is the safe default and not always right. A liveness probe that checks a downstream dependency restarts this pod when the dependency is what is down.

test: ["CMD", "curl", "-f", "http://x/health"]
  exec runs argv directly. No shell.

test: curl -f http://x/health || exit 1
  the shell form. Wrapped in sh -c, which
  needs a shell in the image: distroless and
  scratch have none, and the probe fails 127.

start_period: 40s
  a startupProbe, not initialDelaySeconds.
  initialDelaySeconds delays the first check
  and nothing else, so a slow start still
  trips liveness on the second one.

A HEALTHCHECK in the Dockerfile is ignored
entirely. The kubelet does not read it.

restart, deploy, and the fields that are Swarm

restart: always is the Kubernetes default and needs no field, because Always is the only restartPolicy a Deployment allows. restart: no cannot be expressed at all: a container meant to run once and stop is a Job. Of the deploy block, only replicas and resources carry over; the rest describes Swarm scheduling and its Kubernetes relatives are separate fields with different semantics.

restart: always          nothing to write
restart: unless-stopped  nothing to write, and
                         there is no stopped
                         state. Scale to 0.
restart: "no"            this is a Job
restart: on-failure      Job, restartPolicy
                         OnFailure

deploy.replicas          spec.replicas
deploy.resources         requests and limits
deploy.mode: global      DaemonSet, not a
                         Deployment
deploy.placement         nodeSelector, affinity,
                         topology spread: pick one
deploy.update_config     strategy.rollingUpdate,
                         different fields

More docker tools

Docker Container Image Reference Parser Is that a registry or a Docker Hub user? Dockerfile Linter What this image will do to you later docker run to Kubernetes YAML And the flags that have no equivalent Docker Logging Driver Generator json-file never rotates Dockerfile Non-Root USER Generator Numeric, or Kubernetes rejects it Docker OCI Image Labels Generator One of them actually does something Docker Hub Pull Rate Limit Calculator Counted per IP, not per person Docker Registry Storage Calculator Deleting a tag frees nothing Docker Build Context Analyzer All of it uploads before anything runs Docker Compose to docker run Converter Names stop resolving Docker Compose Version Migrator version: is obsolete now Docker Compose Network and Port Viewer Everything reaches everything Docker Command Explainer Which flags hand over the host Docker BuildKit Cache Mount Generator apt deletes the cache you just mounted Docker BuildKit Secret Mount Generator ARG is in docker history forever Docker .dockerignore Pattern Tester *.log does not match logs/app.log Docker Image Manifest Viewer unknown/unknown is not broken Docker Image Config Viewer Anyone who can pull can read your Env Docker Registry Token Decoder Check the scope before the credentials Docker HEALTHCHECK Generator Docker does nothing when it fails Docker Network Subnet Calculator 31 networks, then it stops Docker History Analyzer The rm did not save anything Docker Run to Compose Converter Some flags have no equivalent Docker CMD and ENTRYPOINT Tester Why docker stop takes ten seconds Docker Port Mapping Tester -p reaches past your firewall Docker -v to --mount Converter -v invents missing directories Docker Restart Policy Simulator The backoff resets after ten seconds Docker Exit Code Explainer 137 is two different problems Docker CPU Limit Converter --cpu-shares limits nothing Docker Memory Limit Calculator -m 512m allows a gigabyte Docker Compose Interpolation Tester An unset variable is an empty string Docker Environment Variable Precedence Tester Your shell does not reach the container Docker Compose Override Merge Previewer An override can add a port, never remove one Dockerfile Multi-Stage Build Analyzer Which stages reach the image Dockerignore Validator It is not .gitignore

Elsewhere on the site