A Compose file is a description of one machine, and half its clauses have no cluster equivalent because the cluster solves those problems differently. Conversion tools handle the transcription; the work is the handful of clauses that encode single-host assumptions. This identifies each one, gives the replacement, and suggests an order that keeps rollback cheap.

Every Compose file encodes assumptions that Kubernetes does not share: one host, a shared bridge network, bind-mounted directories, and containers that find each other by service name on that host. The migration is mostly a matter of finding those assumptions and replacing each with its cluster equivalent.

What Maps Cleanly, and What Does Not

ComposeKubernetesDifficulty
servicesDeployment plus ServiceMechanical
image, command, environmentContainer spec fieldsMechanical
portsService, then Ingress for external accessNeeds a decision
depends_onNothing; use probes and retriesNeeds a code change
volumes, namedPersistentVolumeClaimNeeds a storage class
volumes, bind mountConfigMap, Secret, or an init containerCase by case
networksNothing; all pods can reach each otherReplace with NetworkPolicy
restart: alwaysDefault behaviourDelete it
deploy.replicasDeployment replicasMechanical, if stateless

The three rows in the middle are the migration. Everything else is transcription, and a tool can do it.

Start With a Generated Draft

kompose converts a Compose file into manifests. Its output is not production-ready and that is fine, because its value is as a starting point that gets the boring fields right.

kompose convert -f docker-compose.yml -o k8s/
ls k8s/

Then throw away roughly half of it. Expect to remove the emptyDir volumes it invents for bind mounts, replace its Services of type LoadBalancer with ClusterIP plus an Ingress, add resource requests and limits, which it does not generate, and add probes, which it cannot infer.

Treat the generated files as a first draft written by somebody who has not read your application. That is precisely what they are.

depends_on Does Not Exist, and Should Not

Compose can start the database before the application. Kubernetes deliberately does not, because in a cluster the database can also vanish and come back at any time, so ordering at startup solves a problem that recurs.

The fix is to make the application tolerate an absent dependency. Retry the connection with a backoff, and let the readiness probe report not-ready while it cannot serve. Kubernetes then keeps it out of the Service's endpoints until it is genuinely ready, which is better behaviour than Compose's ordering ever gave you.

readinessProbe:
  httpGet: { path: /healthz, port: 8080 }
  initialDelaySeconds: 5
  periodSeconds: 5
startupProbe:
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 5

Where changing the application is not an option, an init container that blocks until the dependency answers reproduces the old behaviour. It is a workaround rather than a design, and it does nothing for a dependency that fails later.

Configuration and Secrets

Compose usually reads an .env file and bind-mounts config files from the repository. Both need to become cluster objects, and the split matters for who can read them.

kubectl create configmap app-config --from-file=./config/app.yaml -n prod
kubectl create secret generic app-secrets --from-env-file=./.env.production -n prod

Two cautions. A Kubernetes Secret is base64-encoded, not encrypted, so anyone who can read Secrets in the namespace can read your credentials; restrict that with RBAC and enable encryption at rest for etcd. And do not commit the generated Secret manifest to Git, which is the mistake that turns a private repository into a credential leak. If you want secrets in Git, use a tool that encrypts them before they get there.

Mount config as a ConfigMap volume rather than baking it into the image. Changing configuration then does not require a rebuild, which was probably the reason the bind mount existed.

Storage Is Where Lift and Shift Stops

A named volume in Compose is a directory on one host, implicitly ReadWriteOnce and implicitly tied to that machine. In a cluster the pod can move, so the volume has to move with it or the data is gone.

Three things to settle before migrating a stateful service. Whether the workload needs one writer or many, because most block storage offers ReadWriteOnce and mounting a database on a shared filesystem corrupts it rather than scaling it. Whether it belongs in a StatefulSet rather than a Deployment, which is the case whenever each replica needs its own persistent identity and disk. And how the existing data gets in, which is a dump and restore rather than a volume copy for anything with a write-ahead log.

Our guide to persistent volumes, access modes and StatefulSets covers the access-mode decision, which is the one that determines the architecture.

Networking Loses a Boundary

Compose networks provide isolation: services on separate networks cannot reach each other. Kubernetes has no equivalent by default. Every pod can reach every other pod in the cluster, across namespaces, unless a NetworkPolicy says otherwise.

If your Compose file used multiple networks for segmentation, that segmentation is silently gone after migration. Restore it with NetworkPolicy, starting from a default-deny in the namespace and allowing only what is needed:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: prod
spec:
  podSelector: {}
  policyTypes: [ Ingress ]

This requires a CNI that enforces policy. Some do not, and they accept the objects without applying them, which is the worst possible outcome. Verify enforcement with an actual connection test rather than assuming.

The Order That Goes Smoothly

Migrate the stateless tier first and leave the database in place, reachable from the cluster. You get scheduling, rolling updates and ingress working while the risky component is untouched, and a rollback is a DNS change rather than a data recovery.

Then move stateful services one at a time, each with its own dump-and-restore rehearsal and a measured cutover window. Doing the whole Compose file in one change means every failure arrives simultaneously and you cannot tell which one caused the others.

Before any of this, check that the destination is worth it. A single Compose file on one host, deployed by one person, gains little from a cluster and inherits a permanent operational overhead. Our comparison of Docker Swarm and Kubernetes covers the case for staying put, which is stronger than the industry admits.

Where to Land It

The two things that make this migration go badly are storage that cannot follow a pod and a control plane that cannot survive a node.

MassiveGRID's managed Kubernetes starts from $0.03474 per hour, about $25.37 a month, with resources billed in cloudlets of 128 MiB RAM and 400 MHz CPU, so a first migration can be sized honestly rather than by picking an instance type. Underneath, Ceph storage replicates every block three times across independent NVMe drives and Proxmox high-availability clustering restarts a lost node's workloads automatically, which is what makes a PersistentVolumeClaim behave the way the manifests assume. If you would rather not run a cluster at all, Docker hosting on the PaaS takes a Compose-shaped application without the orchestration layer.

Either can be ordered across a partner footprint of more than 700 datacenters in 85 metros, 30 countries and six continents, with auto-provisioning in New York, London, Frankfurt and Singapore.

Further Reading