Skip to main content

Kompose Only Gets You 70-80% of the Way From docker-compose.yml to Kubernetes: Here's the Exact List of What's Missing

9 min readDora NodaDora Noda
Share
On this page

Seventy-one percent of developers use Docker Compose. Forty-two percent use Kubernetes. That gap is the entire reason Kompose exists — point it at a docker-compose.yml and get Kubernetes manifests back, no YAML written by hand. The project's own numbers put that automatic conversion at 70-80% of a working deployment. The other 20-30% is the part that actually decides whether your app runs on the cluster or falls over the first time real traffic hits it.

Here's exactly what's in that gap, field by field, as of Kompose v1.38 (January 2026) — and what it means for the much more specific claim this list keeps coming back to: that a git-push PaaS should be able to treat an existing docker-compose.yml as a first-class deploy target, not just a one-time migration input.


What Kompose Actually Converts

Run kompose convert -f docker-compose.yml and it reads your Compose file and produces Kubernetes YAML — Deployments, Services, PersistentVolumeClaims, ConfigMaps. That part works well and has for years. The question worth answering precisely is which Compose fields make that trip cleanly, which only make it if you've already added Kompose-specific configuration, and which don't make it at all.

Compose fieldWhat happens
image, build, build.context, build.dockerfileConverts directly. (build.args is not yet implemented as of v1.38.)
command, entrypoint, environment, hostnameConverts directly into the Pod spec.
ports, exposeConverts directly into a Service.
volumesConverts into a PersistentVolumeClaim per named volume.
deploy.replicas, deploy.resources, deploy.placementConverts directly — but only if your Compose file already has a deploy: block. These are Swarm-era fields; almost no Compose file written for local docker compose up has ever populated them, since they only mattered for Swarm's multi-node scheduler. In practice this bucket converts on paper and does nothing on the overwhelming majority of real files.
healthcheck (native Compose field)Not read at all. Kompose ignores your healthcheck: block entirely, in every Compose spec version. Probes require separate, Kompose-only labels — covered below.
IngressNot generated by default. Requires a kompose.service.expose label — also Kompose-only syntax, not a Compose field.
configs, env_file, endpoint_modePartial: configs short-syntax only creates a ConfigMap in Compose Spec v3; env_file isn't converted in v1/v2 files; endpoint_mode only does anything when set to VIP.
network_mode, links, logging, devices, dns, ulimits, custom networks: topologiesNo Kubernetes equivalent. Silently dropped.

Two rows are doing almost all of the damage on a typical app: deploy.resources (converts fine, is almost never present) and healthcheck (doesn't convert at all, regardless of what's present). Both land you in the same place — a Deployment with no resource requests, no limits, and no liveness or readiness probes — but they get there for different reasons, and the fix for each is different.


The Healthcheck Gotcha: Your Compose File's Healthcheck Is Invisible to Kompose

Say your docker-compose.yml already does the responsible thing:

yaml
services:
  web:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 3

Run kompose convert against that file and the generated Deployment has no livenessProbe, no readinessProbe — nothing. Kompose doesn't parse the healthcheck: block on any Compose spec version. It never has; a 2021 pull request added liveness-probe support, but only through a parallel, Kompose-proprietary label syntax that has nothing to do with the healthcheck: field you already wrote:

yaml
services:
  web:
    image: myapp:latest
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
      interval: 10s
      timeout: 3s
      retries: 3
    labels:
      kompose.service.healthcheck.liveness.http_get_path: /health
      kompose.service.healthcheck.liveness.http_get_port: "8080"
      kompose.service.healthcheck.readiness.http_get_path: /health
      kompose.service.healthcheck.readiness.interval: 10s
      kompose.service.healthcheck.readiness.timeout: 3s
      kompose.service.healthcheck.readiness.retries: "3"

Only the second version, with the labels, produces livenessProbe/readinessProbe in the output. Every value has to be restated — the healthcheck you wrote for docker compose up and the healthcheck Kompose will act on are two separate declarations, in two separate places, and nothing warns you if they drift apart. Ingress works the same way: nothing gets generated from ports: alone, you need a kompose.service.expose: "example.com" label (plus, optionally, kompose.service.expose.ingress-class-name and .tls-secret) added to the source file before conversion produces a route at all.

The practical consequence: getting a production-shaped Deployment out of Kompose means editing your Compose file to add Kubernetes-only annotations first. That's not "convert my existing file" — it's "rewrite my existing file so it converts better," which is a materially smaller shortcut than the 70-80% headline number implies.


Why the Resource-Limits Gap Is Worse on a Shared Fleet Than on a Laptop

A Deployment with no CPU/memory requests or limits is a cosmetic problem on a single-tenant Docker Compose host — one app, one box, no neighbors to starve. It's a different problem on a Cluster API-provisioned fleet, where a pool of shared Hetzner nodes is bin-packing many tenants' pods onto the same machines by design. The Kubernetes scheduler treats a pod with no resource requests as effectively invisible for capacity planning purposes — it gets placed wherever there's room and can then consume whatever the node has free, with no cgroup-enforced ceiling stopping it from taking memory another tenant's pod was counting on.

Nothing about kompose convert warns you this happened. The command exits 0. The YAML applies cleanly. The first sign of trouble is a neighboring tenant's pod getting OOM-killed on a node that looked fine an hour earlier, and tracing that back to "the Compose file's deploy: block was empty, so nothing ever got converted into resources:" is not where most people start debugging a random eviction.

This is the specific place where "self-host on owned hardware" and "just run kompose convert" pull in opposite directions. Owning the Hetzner fleet only pays off if the scheduler is actually told how to share it; a converter that quietly ships zero-request Deployments onto a multi-tenant node pool undoes exactly the isolation guarantee the fleet was supposed to provide.


Ingress on a Fleet Means More Than One Label

Even once you've added kompose.service.expose to get an Ingress resource at all, Kompose's model assumes one cluster, one team, one domain — reasonable for the single-tenant migration scenario Kompose was built for. A Cluster API-managed fleet running many tenants' apps needs per-tenant subdomain routing and TLS cert issuance through one shared ingress controller serving all of them simultaneously — a routing and certificate-management problem that doesn't show up anywhere in a single Compose file, because it isn't a property of any one tenant's app. It's a property of the fleet.

Compose's networks: block has a related, quieter failure mode. A Compose file that isolates a database network from a public-facing web network expresses a real security boundary the author intended. Kubernetes has no direct equivalent to Compose's per-stack network isolation — pod networking is flat by default — so that boundary doesn't get flagged as unsupported, it just disappears. The converted manifests apply without error and every pod can reach every other pod, silently wider open than the source file specified.


What a Git-Push PaaS's Compose Importer Needs to Do Differently

Docker's own "Compose for Kubernetes" path takes a different approach entirely: point docker compose at a Kubernetes context and it runs, with no intermediate manifest file for a human to generate, inspect, or hand-patch. That's a different promise than Kompose's — Kompose hands you YAML you now own and must fix; Docker's path skips the YAML-as-deliverable step altogether. A git-push PaaS treating docker-compose.yml as a build-target input is closer in spirit to the second model than the first: the point of "push your existing Compose stack and it just runs" is that nothing about the source file should need to change first.

That means a Compose importer built into a platform's build step has to auto-fill exactly the gaps above, instead of leaving them as a post-conversion checklist:

  • Default resource sizing. Apply a conservative CPU/memory request and limit to every service by default when deploy.resources is absent, sized off the buildpack's language/runtime detection (a Node API service and a Postgres container don't need the same defaults) — not a bare, unbounded Deployment that happens to schedule successfully today and starves a neighbor next week.
  • A generated health check from what's already there. If a service exposes a port, synthesize a basic TCP or HTTP readiness check against it even when no healthcheck: block exists — and when one does exist, read it directly instead of requiring a second, parallel declaration in Kompose-only label syntax.
  • Per-tenant ingress and TLS without a manual label. Every exposed service gets a route and a certificate automatically, issued against the fleet's shared ingress layer, the same way a normal git push already gets a URL on the platform today — not gated behind a kompose.service.expose annotation the user has to know to add.
  • networks: as isolation intent, not a discard pile. Translate custom network topology into Kubernetes NetworkPolicies that preserve the boundary the Compose author drew, instead of dropping it and leaving every pod flatly reachable from every other pod.

None of this is exotic — every item on that list is a default a buildpack-driven build step is already in the business of choosing (resource sizing, health checks, routing, TLS) for repos that never had a Compose file at all. Compose-as-input just means deriving those same defaults from a different source of truth, instead of asking the person deploying to first learn a label syntax that only exists to compensate for what the converter can't infer.


The Honest Read on "70-80%"

Kompose is a solid tool for exactly the job it was built for: a one-time migration, run once, output inspected and hand-edited by someone who already knows Kubernetes, then set aside. Treating that same 70-80% number as an onboarding path is a different claim, because closing the remaining gap means editing the source Compose file with Kubernetes- and Kompose-specific labels before conversion — which quietly breaks the actual promise of "your existing Compose stack just runs," since the stack you're running by the end isn't the one you started with.

Bex.co is the open-source, AI-native Render alternative — push a git repo (Dockerfile, buildpack-detected source, or a docker-compose.yml) and get a running HTTPS service on machines you own, with resource sizing, health checks, and per-tenant TLS handled by the platform instead of a label syntax you have to learn first. Star the repo on GitHub or deploy your first app today.


Sources

Related articles

Run this on infrastructure you own

bex is the open-source, AI-native Render alternative — push a git repo and get a running HTTPS service on your own machines.

Get started with bex