Docker Compose to Kubernetes migration is the process of moving a Compose-defined application into Kubernetes while preserving its data, configuration, traffic behavior, health signals, and recovery path.
The important part is not converting compose.yaml into Kubernetes YAML. Kubernetes documentation shows that Kompose can translate a Compose file into Kubernetes resources, which is useful for creating a starting point. Production migration still requires architectural decisions that a converter cannot make for you: where durable state lives, which services can run multiple replicas, how readiness is detected, which ports are public, how secrets are handled, and how traffic returns to the old environment if the cutover fails.
This guide assumes your team has already decided that Kubernetes is justified. If that decision is still open, use Kubernetes vs Docker Compose for Small Teams first. For the broader container path, see Container Infrastructure: Docker, K3s, and Kubernetes. The checklist below focuses on preparing a production Compose workload for managed Kubernetes without turning the migration into an all-at-once rewrite.
Docker Compose migration starts with a workload inventory
A Compose file describes services, networks, volumes, environment values, health checks, dependencies, and ports. Kubernetes can represent the same broad concerns, but it does not preserve every Compose assumption automatically.
Start by inventorying what the current stack actually depends on.
| Compose concern | Record before migration | Kubernetes destination or decision |
|---|---|---|
| Service | Image, command, replicas, dependencies | Deployment, StatefulSet, Job, or external service |
| Published port | Public vs internal purpose | Service plus ingress or another traffic-entry method |
| Named volume | Data owner, size, backup method | PersistentVolumeClaim, external storage, or managed service |
| Bind mount | Why host filesystem access exists | ConfigMap, Secret, image content, PVC, or redesign |
| Environment value | Public config vs secret | ConfigMap, Secret, or external secret workflow |
| Health check | What healthy actually means | Readiness, liveness, and sometimes startup probe |
depends_on | Startup dependency | Application retry logic, readiness, service discovery |
| Database/cache | Stateful lifecycle and recovery | Managed service, dedicated stateful workload, or external VM |
| Scheduled task | Frequency and concurrency rules | CronJob |
| One-off task | Trigger and retry behavior | Job |
The inventory should include dependencies outside Compose too: DNS records, certificates, object storage, email providers, queues, database allowlists, webhook origins, monitoring, backup jobs, and CI/CD credentials.
A Compose stack often contains hidden host assumptions. A certificate may be mounted from /etc/letsencrypt, an upload directory may live under /srv/app, or a backup script may run from cron on the VM. Kubernetes will not discover those dependencies for you.
The first migration deliverable should therefore be a map of what must move, what should stay external, and what can be replaced.
State must be separated before Pods become replaceable
Kubernetes works best when application Pods can be replaced without losing business data. That makes state the highest-risk part of most Compose migrations.
Classify every writable path into one of four groups:
- Disposable runtime data — caches or temporary files that can disappear.
- Application configuration — should move into controlled configuration rather than mutable container storage.
- Durable files — uploads, generated assets, or artifacts that need persistent or object storage.
- Database or service state — needs application-aware backup, restore, and storage planning.
Do not treat a Docker named volume as if it maps directly to a Kubernetes volume with no further design work. Kubernetes PersistentVolumes and PersistentVolumeClaims provide a different lifecycle and scheduling model. Storage classes can dynamically provision persistent storage, but the application still needs a recovery plan that works independently of one Pod.
For many small teams, the cleanest migration removes some state from the application cluster before cutover:
Before Docker Compose host ├── web ├── worker ├── database volume └── upload volume After Managed Kubernetes ├── web Pods └── worker Pods ↓ External or managed database ↓ Durable storage / object storage
This is not a rule that every database must leave Kubernetes. It is a risk-reduction strategy. If your team wants to run a database in Kubernetes, its persistent storage, backup consistency, restore order, failover behavior, upgrade process, and resource guarantees should be intentional before production traffic moves.
Also test replica safety. Local sessions, local upload paths, in-process scheduled jobs, and filesystem locks can make an application appear containerized while still depending on one host. A workload is not ready for horizontal scheduling until those assumptions are identified.
Raff's migration rule is to move the workload model before the traffic: externalize or deliberately map state, prove health checks and rollback, then switch production traffic only after Kubernetes passes the same acceptance checks as the Compose stack.
Health checks must become Kubernetes probes
Compose health checks and Kubernetes probes are related, but Kubernetes separates different kinds of health.
A readiness probe answers whether a Pod should receive traffic. A liveness probe answers whether a container is stuck badly enough to restart. A startup probe protects slow-starting applications by delaying liveness and readiness evaluation until startup succeeds.
That separation matters during migration.
A single /health endpoint that always returns 200 may be insufficient. For example:
- readiness may need to fail while the app cannot serve required requests;
- liveness should not fail merely because a temporary database dependency is unavailable;
- startup may need a longer allowance for migrations, cache warm-up, or application initialization.
Kubernetes documents that failed readiness removes a Pod from Service endpoints, while failed liveness can restart the container. Those are very different consequences.
Review each service and decide:
| Signal | Migration question |
|---|---|
| Startup | How long can this service legitimately take to initialize? |
| Readiness | What must be true before production traffic reaches it? |
| Liveness | What condition proves the process cannot recover itself? |
| Dependency retry | Can the app retry database, cache, or API connections without restart loops? |
| Graceful shutdown | Does the app stop accepting work before termination? |
Do not copy a Compose health command blindly into all three probes. Define each probe from the behavior Kubernetes needs to control.
This is also where depends_on assumptions should be removed. In a distributed environment, services can restart independently at any time. Applications should tolerate dependencies being temporarily unavailable rather than relying on one startup order.
Configuration, secrets, and networking need explicit ownership
Compose commonly puts configuration close to the service definition. Kubernetes gives configuration, secrets, Services, and ingress separate lifecycles, which is useful but requires clearer ownership.
Configuration should be separated from images
Identify which values belong in:
- image defaults;
- ConfigMaps;
- Secrets;
- environment-specific deployment configuration;
- external managed services.
A ConfigMap is suitable for non-confidential configuration. Kubernetes Secrets are intended for sensitive values, but teams still need access control, rotation, and a policy for how secrets enter the cluster. Do not copy a production .env file into source-controlled manifests.
Service discovery replaces Compose service assumptions
Compose lets services find one another by service name on a Docker network. Kubernetes Services provide stable network endpoints for changing sets of Pods.
For each connection, decide whether it is:
- internal service-to-service traffic;
- external dependency traffic;
- public ingress;
- administrative access.
Only public workloads should receive a public traffic path. Databases, caches, internal workers, and administrative endpoints should remain private unless a specific requirement says otherwise.
Public traffic needs a deliberate entry model
In Compose, a reverse proxy may publish ports 80 and 443 on one VM. In Kubernetes, public traffic normally reaches a Service and, for HTTP/HTTPS routing, commonly an ingress layer or equivalent managed traffic entry.
Do not move every published Compose port into a publicly reachable Kubernetes Service. Reclassify each port by purpose.
A clean target usually looks like:
Internet ↓ Public HTTP/TLS entry ↓ Application Service ↓ Application Pods ↓ Private Services / external dependencies
DNS TTL, certificate ownership, WebSockets, client IP handling, redirects, upload limits, timeouts, and health endpoints should be checked before cutover because these can change at the traffic layer even when containers themselves run correctly.
The migration checklist defines the production cutover gate
The migration should have an explicit acceptance gate rather than a vague point where the new cluster "looks ready."
Use this checklist before production DNS or traffic is switched.
| Gate | Production acceptance condition |
|---|---|
| Images | Every workload uses a versioned, reproducible image |
| State | Every durable path has a Kubernetes or external storage owner |
| Database | Backup and restore path is known; schema migration compatibility is understood |
| Replicas | Web workloads can run more than one replica when required without local-session or local-file assumptions |
| Health | Startup, readiness, and liveness behavior has been validated |
| Resources | CPU and memory requests are set from known workload needs, with limits used deliberately |
| Configuration | Environment-specific config is separated from images |
| Secrets | Production secrets are not embedded in images or source-controlled manifests |
| Networking | Internal and public services are clearly separated |
| DNS/TLS | Hostnames, certificates, redirects, and traffic entry are validated |
| Jobs | Background workers, scheduled jobs, and one-off migrations have explicit ownership |
| Observability | Logs, metrics, Kubernetes events, and application errors are visible |
| Rollout | A new image can be deployed without rebuilding the cluster |
| Rollback | Previous application version and previous traffic path are recoverable |
| Data validation | Writes made during migration cannot be silently lost or split between two sources of truth |
| Cutover | Owner, window, validation checks, and abort criteria are written down |
A useful rule is that every line needs a named owner and an observable pass/fail condition. "We think networking is fine" is not an acceptance test. "The public hostname returns the expected application, internal database ports are not public, and readiness removes unhealthy replicas from traffic" is testable.