Kubernetes

Migrate Docker Compose Application to Kubernetes With Kompose

You have a docker-compose.yaml that runs your stack on a laptop, and now it needs to live on Kubernetes. Rewriting every service into Deployments, Services and PersistentVolumeClaims by hand is tedious and easy to get wrong. Kompose (the name blends Kubernetes and Compose) reads a Compose file and emits the equivalent Kubernetes objects, so you get a working starting point in seconds instead of an afternoon of YAML.

Original content from computingforgeeks.com - post 52821

This guide converts Docker Compose to Kubernetes with Kompose, end to end, using a real two service application. We take a WordPress and MariaDB stack, run it under Docker Compose, convert it with kompose convert, fix the one thing the conversion always misses, then deploy the generated manifests and load the running site. Along the way we cover Kompose labels, Helm chart output, the useful conversion flags, and the parts of a Compose file that Kompose cannot translate.

Everything below was run in June 2026 with Kompose 1.38 against a single node k3s cluster on Ubuntu 24.04.

What Kompose actually does

Kompose parses a Compose file and maps each piece to a Kubernetes resource. A service becomes a Deployment, a published port becomes a Service, a named volume becomes a PersistentVolumeClaim, and environment variables carry across into the container spec. It does not run a cluster or manage state. It is a translator that hands you manifests.

One thing trips up readers following older tutorials. Kompose used to ship kompose up and kompose down to deploy straight to a cluster. Those subcommands were removed. The current tool has exactly one job that matters, convert, and you deploy the output yourself with kubectl apply. Here is the full command list on the version tested:

kompose --help

Only four commands exist, and convert is the one you will use:

Available Commands:
  completion  Output shell completion code
  convert     Convert a Compose file
  help        Help about any command
  version     Print the version of Kompose

If a guide tells you to run kompose up, it predates this change. The convert plus apply flow below is the supported path.

Install Kompose on Linux, macOS, and Windows

Kompose is a single static binary with no runtime dependencies. On Linux, pull the latest release straight from the GitHub API so the command keeps working when a new version ships:

curl -s https://api.github.com/repos/kubernetes/kompose/releases/latest \
  | grep browser_download_url | grep linux-amd64 \
  | cut -d '"' -f 4 | wget -qi -
chmod +x kompose-linux-amd64
sudo mv kompose-linux-amd64 /usr/local/bin/kompose

On macOS, Homebrew is the cleanest route:

brew install kompose

On Windows, use Chocolatey or Scoop:

choco install kubernetes-kompose
# or
scoop install kompose

Confirm the binary is on your path before moving on:

kompose version

It prints the installed version and commit hash:

1.38.0 (a8f5d1cbd)

You also need a Kubernetes cluster and kubectl pointing at it. Any cluster works. For a quick lab the fastest options are a single node k3s install or Minikube. For a multi node setup, follow the kubeadm cluster guide. The walkthrough here uses k3s because it ships a default StorageClass and a built in load balancer, which means the PersistentVolumeClaims and the exposed Service just work without extra setup.

The Docker Compose application we will migrate

A toy single container demo would hide the interesting parts. Instead we migrate a stack that exercises everything you hit in real life: two services that talk to each other, environment driven configuration, and persistent storage. WordPress in front, MariaDB behind it. Save this as docker-compose.yaml:

services:
  db:
    image: mariadb:11.4
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: SuperSecretRoot
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: wordpressPass
    volumes:
      - db_data:/var/lib/mysql
    labels:
      kompose.volume.size: 1Gi

  wordpress:
    depends_on:
      - db
    image: wordpress:php8.3-apache
    restart: always
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db:3306
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: wordpressPass
      WORDPRESS_DB_NAME: wordpress
    volumes:
      - wp_data:/var/www/html
    labels:
      kompose.service.type: loadbalancer
      kompose.volume.size: 2Gi

volumes:
  db_data:
  wp_data:

The kompose.* labels are hints Kompose reads during conversion. They are ignored by Docker Compose, so the same file runs in both worlds. We will come back to them. First, prove the stack works under Compose so we have a known good baseline:

docker compose up -d

Check that both containers are running:

docker compose ps

WordPress answers on port 8080 and serves its installation screen:

NAME                        IMAGE                     STATUS         PORTS
wordpress-app-db-1          mariadb:11.4              Up 11 seconds   3306/tcp
wordpress-app-wordpress-1   wordpress:php8.3-apache   Up 11 seconds   0.0.0.0:8080->80/tcp

With the baseline confirmed, tear the Compose stack down. We rebuild it on Kubernetes next, and leaving it running would clash with the port:

docker compose down

If you do not have Docker Compose installed, the Docker Compose setup guide covers it. The baseline run is optional, but it saves you from blaming Kubernetes for a bug that was in the Compose file all along.

Convert Docker Compose to Kubernetes manifests

Run the conversion and send the output to a directory so the manifests stay tidy:

mkdir -p k8s
kompose convert -f docker-compose.yaml -o k8s/

Kompose prints each file as it writes it and lists the generated manifests:

kompose convert command turning docker-compose.yaml into Kubernetes deployment, service and PVC manifests

Each Compose concept landed on the matching Kubernetes object. The two services became Deployments, the published WordPress port became a Service, and the two named volumes became PersistentVolumeClaims. Open the WordPress Deployment to see how the translation reads:

cat k8s/wordpress-deployment.yaml

The environment variables, the container port, and the volume mount all carried over, and the volume is wired to the PVC Kompose created:

    spec:
      containers:
        - env:
            - name: WORDPRESS_DB_HOST
              value: db:3306
            - name: WORDPRESS_DB_NAME
              value: wordpress
            - name: WORDPRESS_DB_PASSWORD
              value: wordpressPass
            - name: WORDPRESS_DB_USER
              value: wordpress
          image: wordpress:php8.3-apache
          name: wordpress
          ports:
            - containerPort: 80
          volumeMounts:
            - mountPath: /var/www/html
              name: wp-data
      volumes:
        - name: wp-data
          persistentVolumeClaim:
            claimName: wp-data

Fix the missing database Service

Look closely at the conversion output and you will spot a warning that bites almost every migration:

WARN Service "db" won't be created because 'ports' is not specified

This is the single most common reason a migrated app cannot reach its database. Under Docker Compose, every service on the same network resolves the others by name even with no published ports, so db is reachable automatically. Kubernetes does not work that way. Name resolution between pods needs a Service object, and Kompose only creates one when the Compose service declares a port. The db service had none, so WordPress would start, look up db:3306, and find nothing.

The fix is to tell Compose the database listens on a port without publishing it to the host. Add an expose stanza to the db service:

  db:
    image: mariadb:11.4
    restart: always
    environment:
      MARIADB_ROOT_PASSWORD: SuperSecretRoot
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: wordpressPass
    expose:
      - "3306"
    volumes:
      - db_data:/var/lib/mysql
    labels:
      kompose.volume.size: 1Gi

Convert again, and this time a ClusterIP Service for the database appears in the output:

rm -rf k8s && mkdir k8s
kompose convert -f docker-compose.yaml -o k8s/
ls k8s/

The new db-service.yaml is what lets WordPress find MariaDB by name inside the cluster:

db-data-persistentvolumeclaim.yaml  wordpress-deployment.yaml
db-deployment.yaml                  wordpress-tcp-service.yaml
db-service.yaml                     wp-data-persistentvolumeclaim.yaml

Deploy the converted manifests to Kubernetes

Apply the whole directory at once. kubectl reads every manifest and creates the objects:

kubectl apply -f k8s/

Wait for both Deployments to finish rolling out:

kubectl rollout status deployment/db
kubectl rollout status deployment/wordpress

Now look at the full picture. Pods running, the database Service resolving, the WordPress Service holding an external IP, and both PersistentVolumeClaims bound to real storage:

kubectl get deploy,svc,pods,pvc

The migrated stack is live, with every object in the state you want:

kubectl get output showing migrated WordPress and MariaDB deployments running, LoadBalancer service and bound PersistentVolumeClaims on Kubernetes

The wordpress-tcp Service shows up as type LoadBalancer with an external IP because the Compose file carried the kompose.service.type: loadbalancer label, and k3s assigns the node address through its built in load balancer. On a cloud cluster you would get a real cloud load balancer IP here instead. Both PVCs reached the Bound state on the default local-path StorageClass, which is the proof that the volume translation worked.

Open the external IP on the Service port and the migrated site loads. After the one time WordPress setup, the default theme renders from the Kubernetes pod, served by MariaDB running in its own pod with persistent storage behind it:

WordPress front page Kompose Migration Demo served from a Kubernetes cluster after converting docker-compose with kompose

That round trip, from a Compose file to a live site on Kubernetes, is the whole point of Kompose. The PersistentVolumeClaims only bind because the cluster has a StorageClass. k3s ships one. On a bare kubeadm cluster you must install a storage provisioner first, otherwise the claims sit in Pending and the pods never start. The Kubernetes storage guide walks through adding one.

Tune the output with Kompose labels

By default Kompose makes safe, conservative choices. Every service becomes a ClusterIP Service, volumes become PVCs of a default size, and one replica is created. Compose labels let you steer the conversion without hand editing the generated YAML. We already used two of them. The kompose.service.type label turned the WordPress Service into a LoadBalancer, and kompose.volume.size set each PVC request. These are the labels worth knowing:

LabelWhat it controls
kompose.service.typeService kind: nodeport, clusterip, loadbalancer, or headless
kompose.service.exposeCreate an Ingress and set its host
kompose.service.expose.tls-secretTLS secret name for the generated Ingress
kompose.volume.typeVolume kind: persistentVolumeClaim, emptyDir, hostPath, or configMap
kompose.volume.sizeStorage request for the generated PVC
kompose.controller.typeWorkload kind: deployment, daemonset, or statefulset
kompose.image-pull-policyContainer image pull policy
kompose.image-pull-secretSecret used to pull from a private registry
kompose.service.healthcheck.readiness.*Readiness probe settings
kompose.service.groupPack several Compose services into one pod

A common pattern is to expose the front end through an Ingress instead of a load balancer. Swap the WordPress label for the two below and Kompose generates an Ingress resource pointing at your host:

    labels:
      kompose.service.expose: "wordpress.example.com"
      kompose.service.expose.tls-secret: "wordpress-tls"

Because the database is stateful, kompose.controller.type: statefulset on the db service is often a better fit than the default Deployment. It gives the pod a stable identity and a more predictable volume lifecycle.

Generate a Helm chart from Docker Compose

If you deploy with Helm, Kompose can emit a chart skeleton instead of loose manifests. Pass the -c flag:

kompose convert -f docker-compose.yaml -c

It builds a directory named after the project with a Chart.yaml, a README.md, and every manifest under templates/:

docker-compose/Chart.yaml
docker-compose/README.md
docker-compose/templates/db-deployment.yaml
docker-compose/templates/db-service.yaml
docker-compose/templates/db-data-persistentvolumeclaim.yaml
docker-compose/templates/wordpress-deployment.yaml
docker-compose/templates/wordpress-tcp-service.yaml
docker-compose/templates/wp-data-persistentvolumeclaim.yaml

Treat it as a head start, not a finished chart. The templates are static copies of the manifests with no values wired up, so the next job is to lift hard coded settings into values.yaml. Once that is done you install it like any other chart:

helm install wordpress ./docker-compose

For a fuller Helm workflow on a real application, the Prometheus and Grafana on Kubernetes guide shows charts driven by proper values files.

Useful kompose convert flags

A handful of flags cover most of what you will reach for after the basic conversion:

FlagEffect
-o, --out FILEWrite everything to one file or a directory instead of many files
--stdoutPrint the manifests to the terminal so you can pipe straight into kubectl apply -f -
--replicas NSet the replica count on generated workloads (default 1)
--controller TYPEForce deployment, daemonSet, or replicationController
--volumes TYPEChoose persistentVolumeClaim, emptyDir, hostPath, or configMap
-n, --namespace NSStamp a target namespace onto the resources
--provider openshiftEmit OpenShift objects instead of plain Kubernetes

For a one liner that converts and applies in a single step, --stdout is the cleanest:

kompose convert -f docker-compose.yaml --stdout | kubectl apply -f -

What Kompose does not convert

Kompose gets you most of the way, but the output is a draft, not a production manifest. These are the gaps to close by hand after a conversion, drawn from running the migration above.

  • Secrets stay in plain text. The database password landed in the Deployment as an environment value, exactly as it sat in the Compose file. Move credentials into a Kubernetes Secret and reference them with valueFrom.secretKeyRef before this goes anywhere real.
  • depends_on does not enforce ordering. Kubernetes starts pods in parallel. WordPress will crash loop for a few seconds while MariaDB initializes, then recover on its own once the database answers. If you need strict ordering, add a readiness probe or an init container that waits for the database.
  • Services with no ports get no Service. The database gotcha from earlier. Any backend that other services reach by name needs a port declared with expose so Kompose generates a Service for it.
  • PVCs need a StorageClass. The generated claims assume the cluster can provision storage. Without a default StorageClass they stay Pending and the pods never schedule.
  • Local builds are not handled by default. A Compose service with a build section needs --build local plus a registry to push to, since Kubernetes pulls images rather than building them.

None of these are reasons to skip Kompose. They are the short list of edits that turn a fast, mechanical conversion into something you would actually run. Start with kompose convert, deploy it, then tighten the manifests. Keep the kubectl cheat sheet handy while you iterate on the result.

Keep reading

Claude Code Cheat Sheet – Commands, Shortcuts, Tips AI Claude Code Cheat Sheet – Commands, Shortcuts, Tips OpenAI Codex CLI Cheat Sheet – Commands, Shortcuts, Tips AI OpenAI Codex CLI Cheat Sheet – Commands, Shortcuts, Tips Best UI Applications for Managing Docker Containers Containers Best UI Applications for Managing Docker Containers Ansible with Kubernetes: Deploy and Manage a Cluster Ansible Ansible with Kubernetes: Deploy and Manage a Cluster Install Filestash: Self-Hosted File Manager for Any Storage Containers Install Filestash: Self-Hosted File Manager for Any Storage etcd Backup and Restore for Kubernetes Disaster Recovery Containers etcd Backup and Restore for Kubernetes Disaster Recovery

Leave a Comment

Press ESC to close