The first time I thought seriously about moving a Docker Compose stack toward Kubernetes, I wanted the whole thing to behave like a format conversion.
Take one YAML file.
Run one tool.
Receive one better YAML file.
Move on.
That fantasy lasted right up until I understood what Kubernetes was actually asking me to describe.
Compose had always felt intimate to me. It described a small world I could hold in my head: these containers, these ports, these environment variables, these volumes, this dependency chain. Kubernetes did not want that same story copied over line by line. It wanted me to restate the application in its own grammar: workloads, services, storage claims, configuration objects, health checks, and exposure rules.
That was the moment the whole problem became easier to respect.
The good news is that you do not have to start from scratch. Kubernetes’ own documentation points to Kompose as a tool for helping Docker Compose users move toward Kubernetes, and the official task guide shows the simplest flow clearly: run kompose convert, then apply the resulting files with kubectl.
What changed for me was realizing that this is not the end of the migration story.
It is the beginning of the honest part.
- Why Compose-to-Kubernetes conversion feels simpler than it really is
- How Kompose gives you a practical starting point
- Which Compose ideas usually become different Kubernetes objects
- What you still need to review by hand after conversion
- How I would approach a small homelab migration today
For the technical baseline below, I use the official documentation at kubernetes.io and separate documented platform behavior from my own placement and operating recommendations.
A Compose file and a Kubernetes manifest are not trying to say the same thing
This was the first mental correction I needed.
A Compose file is wonderful at expressing a compact service arrangement. It is friendly to local development, small deployments, and the kind of operator who wants one readable place where service names, images, environment variables, volumes, and port mappings all sit together.
Kubernetes splits that intent apart on purpose.
The official Kubernetes concepts documentation makes that separation very clear:
- Deployments manage stateless application rollout and replacement behavior.
- StatefulSets exist for workloads that need stable identity and ordered handling.
- Services define stable network endpoints for reaching Pods.
- ConfigMaps and Secrets separate configuration from images.
- Persistent Volumes and PersistentVolumeClaims formalize durable storage.
- Ingress provides HTTP and HTTPS routing behavior at the cluster edge.
That is why “convert the file” is a slightly misleading phrase.
In practice, one Compose file usually becomes several manifests.
The file is not merely being translated.
It is being decomposed.
Kompose is the right first step when you want momentum
Kubernetes’ official task for translating a Docker Compose file shows the workflow directly: go to the directory with your Compose file, run kompose convert, and then apply the generated files with kubectl.
Kubernetes’ tools reference also describes Kompose in very practical terms: it is there to help Docker Compose users move to Kubernetes and translate Compose files into Kubernetes objects.
That framing matters.
Kompose is useful because it helps you stop staring at a blank directory.
It gives you structure.
It turns a migration into something you can inspect.
The official Kompose user guide shows the expected pattern:
kompose --file compose.yaml convert
kubectl apply -f .
I like that workflow because it respects both sides of the problem:
- the tool accelerates the first pass;
- the operator still reviews what was generated;
- Kubernetes remains the final source of truth;
- the manifests become something you can version, refine, and eventually trust.
That is a much healthier expectation than asking an automatic converter to understand everything your original Compose file meant emotionally, operationally, and architecturally.
The cleanest migration starts with an audit of the Compose file
Before I would convert anything now, I would read the Compose file like a checklist of hidden assumptions.
I want to know:
- which services are stateless;
- which services are actually stateful;
- which ports are internal versus truly external;
- which bind mounts are host-specific convenience rather than portable storage design;
- which values belong in normal configuration versus secrets;
- which startup expectations depend on Compose-era habits like
depends_on.
This is the same kind of cleanup instinct that shows up in cleaner Compose structure and in being more honest about which ports really need exposure.
If the Compose file is messy, Kubernetes will not magically make it clearer.
It will simply ask you to express the mess with more objects.
The mapping gets easier once you stop demanding one-to-one symmetry
What finally helped me was not memorizing every conversion detail.
It was accepting the broader shape of the mapping.
Stateless services usually point toward Deployments
Kubernetes Deployments are the natural landing place for many typical app containers: web frontends, APIs, workers, and small internal services that do not need stable network identity of their own.
If a service in Compose is basically “run this container, keep it healthy, and allow rolling updates later,” a Deployment is often the right destination.
Stateful services deserve a second thought
StatefulSets exist because some workloads need more than generic replaceable replicas. Stable identity, ordered deployment, and state-aware behavior matter more once the service is not merely disposable compute.
That means not every database, queue, or stateful backend should be treated like a standard Deployment just because it started life next to one in Compose.
This is especially important for database services, which is why I would review them alongside the storage plan and the exposure model rather than trusting an automatic default. That same caution shows up in isolating databases more intentionally.
Port mappings usually become Services, not just open sockets
In Compose, a published port often feels like a quick wiring decision.
In Kubernetes, a Service becomes the stable networking object that fronts Pods and gives them a discoverable endpoint.
That is a very different mindset.
You are no longer saying only “map this port.”
You are saying “this workload should be reachable in this stable way.”
And if HTTP or HTTPS traffic needs entry from outside the cluster, that is often where Ingress enters the picture rather than pretending every exposed service should behave the same way.
Environment variables often need to be split into ConfigMaps and Secrets
Kubernetes keeps ordinary configuration and sensitive data as separate object types for a reason.
If the Compose file currently mixes everything together, the migration is a good time to separate:
- ordinary runtime settings into ConfigMaps where appropriate;
- sensitive credentials into Secrets;
- image-level assumptions away from deployment-time configuration.
That separation makes the final manifests calmer to operate.
Volumes usually become a bigger architectural question
Compose lets storage feel deceptively simple.
A named volume works.
A bind mount works.
Everything looks durable enough until you try to move the application somewhere more structured.
Kubernetes forces a better question:
what is the persistent storage model here, and how is it provisioned?
The official storage documentation centers this around PersistentVolumes and PersistentVolumeClaims, which is much closer to infrastructure design than to a casual local bind mount. That is part of why storage decisions deserve more attention during migration than most people expect.
depends_on is where many first migrations stop feeling magical
Compose encourages a comforting fiction: if service A depends on service B, then startup order feels mostly handled.
Kubernetes thinks more in terms of readiness and health.
The official docs on probes explain the distinction clearly through liveness, readiness, and startup checks. The init container docs show another important idea: sometimes the right answer is to make one part of the Pod do preparatory work before the main container starts.
That means the Kubernetes equivalent of “wait until the database is there” is usually not a direct copy of depends_on.
It is more likely to become some combination of:
- readiness probes;
- startup probes;
- init containers;
- application retry behavior;
- better separation between boot order and service health.
This was one of the most useful migration lessons for me, because it pushed me out of startup choreography and into resilience thinking.
A small Compose-to-Kubernetes workflow I would trust today
If I were migrating a modest homelab stack now, I would do it like this:
- Clean the Compose file first.
- Remove credentials that should not stay inline.
- Separate truly external ports from internal-only service traffic.
- Run
kompose convert. - Review each generated object before applying it.
- Decide which workloads should stay as Deployments and which deserve state-aware handling.
- Replace convenience assumptions with Kubernetes-native ones: Services, ConfigMaps, Secrets, PersistentVolumeClaims, probes, and Ingress where needed.
- Apply to a test namespace before treating the result as real.
That may sound slower than a pure conversion story.
In practice, it is much faster than pretending the first generated YAML is already production logic.
The first converted manifests are usually scaffolding
This is the main idea I wish I had understood earlier.
The generated output is valuable precisely because it gives you scaffolding:
- names;
- object boundaries;
- baseline YAML shape;
- quick visibility into what the stack looks like as Kubernetes resources.
But the final quality comes from review.
You still need to think about:
- whether a service should be internal or internet-facing;
- whether storage is portable and durable enough;
- whether secrets are handled cleanly;
- whether the workload should scale;
- whether health checks reflect reality;
- whether the chosen resource type matches the service’s behavior.
That is not failure.
That is the migration finally becoming honest.
Use automation to generate structure, then use judgment to make the result Kubernetes-native. The converter saves time. The review is what makes the manifests trustworthy.
Conclusion
The most useful way to convert a Docker Compose file into Kubernetes manifests is to stop asking for a perfect translation and start asking for a strong first draft. Kompose is excellent for giving the migration momentum. Kubernetes then asks you to refine that draft into clear workloads, stable services, deliberate configuration, real storage design, and honest health checks. Once I stopped treating the conversion as a file-format trick, the process became much easier to reason about.
FAQ
Can one Docker Compose file become a single Kubernetes manifest?
Sometimes you can store multiple Kubernetes objects in one YAML file, but conceptually the Compose stack usually becomes several manifests or several Kubernetes objects such as Deployments, Services, ConfigMaps, Secrets, and storage-related resources.
Is Kompose enough for production-ready Kubernetes manifests?
Usually not by itself. It is best treated as a starting point that generates useful scaffolding which you then review and refine for storage, secrets, health checks, exposure, and workload type.
What is the biggest mistake when migrating from Compose to Kubernetes?
Expecting a one-to-one copy of Compose behavior. Kubernetes models workloads, networking, configuration, and storage more explicitly, so some Compose assumptions need to be redesigned rather than translated.
Should databases from Compose always become Deployments in Kubernetes?
Not automatically. Stateful services often need additional review and may fit better with state-aware patterns such as StatefulSets, depending on how the workload behaves and how storage is handled.
What should I review first after running kompose convert?
Review service exposure, storage, configuration versus secrets, health checks, and whether each generated workload type actually matches the behavior of the service.



