Antyxsoft Cloud Blog

From docker-compose to Kubernetes | Antyxsoft Cloud

Written by Antyxsoft Cloud | Aug 23, 2026, 5:54:02 PM

A docker-compose file that has been running in production for two years is not technical debt. It is a specification. It tells you exactly which services exist, how they talk, what state they hold and which environment variables matter. Treat it as the source of truth and the migration gets much less dramatic.

What follows is the staged path, plus the four things that break every single time.

Stage 0: classify your services

Go through the compose file and put every service in one of three buckets.

  • Stateless — web servers, API workers, queue consumers. These move first and easily.
  • Stateful with a managed alternative — Postgres, MySQL, Redis. These should usually not move into the cluster at all.
  • Stateful with no alternative — a search index, an upload directory, a legacy binary that writes to disk. These move last, deliberately, with a maintenance window.

The single biggest cause of miserable migrations is running the database in the cluster on day one because compose did. Compose ran it on the same box as everything else, which was fine when the box was pets. In a scheduler that can evict and reschedule your pod at any time, a database on ephemeral storage is a loaded gun.

Stage 1: translate, do not redesign

Resist the urge to adopt service meshes, GitOps, operators and Helm chart libraries in the same change. Get the workload running as boringly as possible first.

Each compose service becomes a Deployment plus a Service. Each ports: entry becomes a container port and a service port. Each environment: block becomes a ConfigMap, with anything secret pulled out into a Secret. depends_on becomes nothing at all — Kubernetes has no ordering, so services must tolerate their dependencies being briefly absent, which is a property you wanted anyway.

kubectl create configmap api-env --from-env-file=.env.production --dry-run=client -o yaml > api-config.yaml
kubectl create secret generic api-secrets --from-env-file=.env.secrets --dry-run=client -o yaml > api-secrets.yaml

Tools that auto-convert compose files will get you a first draft. Read every line of the output — they are consistently wrong about volumes, health checks and anything involving networking.

Stage 2: health checks, properly

In compose, a container that starts is a container that works. In Kubernetes, a container that starts gets traffic immediately, which means a service that takes eight seconds to warm its connection pool will serve eight seconds of 502s on every deploy.

Three probes, three different jobs:

startupProbe:    # "has it finished booting?" — generous
  httpGet: { path: /healthz, port: 8080 }
  failureThreshold: 30
  periodSeconds: 2
readinessProbe:  # "should it receive traffic right now?"
  httpGet: { path: /ready, port: 8080 }
  periodSeconds: 5
livenessProbe:   # "is it wedged and in need of a kill?" — conservative
  httpGet: { path: /healthz, port: 8080 }
  periodSeconds: 10
  failureThreshold: 6

The readiness endpoint should check the things the service needs to serve a request — database reachable, cache reachable. The liveness endpoint should check almost nothing. A liveness probe that fails when the database is down will restart every pod you have during a database blip, turning a degradation into an outage.

Stage 3: the four things that always break

Localhost. In compose, services reach each other by service name over a shared network, and containers in the same service share nothing. In Kubernetes, containers in the same pod share localhost, and everything else goes through a Service DNS name. Any hard-coded 127.0.0.1:5432 that worked because of a host network mode will now fail quietly.

File uploads. Something in your stack writes to a local directory and something else reads it. On one box that worked. Across pods it does not, and a shared ReadWriteMany volume is not the fix you want. Move uploads to object storage; it is a smaller change than it looks and removes the volume from the migration entirely.

Cron. Compose stacks grow a container that runs cron internally. Two replicas means every job runs twice. Convert them to CronJob objects, one per job, and add a concurrency policy.

apiVersion: batch/v1
kind: CronJob
spec:
  schedule: "*/15 * * * *"
  concurrencyPolicy: Forbid

Graceful shutdown. Kubernetes sends SIGTERM and waits 30 seconds. Applications that ignore SIGTERM get SIGKILL mid-request on every single rollout. If your framework does not handle it, this is the one code change worth making before you migrate.

Stage 4: cut over per service

Point DNS at the cluster for one low-risk service, leave the rest on the old host, and run both for a week. Kubernetes and compose can happily talk to the same database while you do this. Move the next service when the first one has survived a deploy, a restart and a traffic peak.

The temptation is a single big-bang cutover on a Friday, because that is how the last migration went. The staged version takes three weeks of low-stress evenings instead of one catastrophic weekend, and at every point you have a working system to roll back to.

What you should not port

Some things in the compose file exist only because it was compose: the reverse proxy container that terminates TLS (an Ingress does this), the container that waits for the database before starting the app (readiness probes do this), the entrypoint script that renders config from environment variables at boot (a ConfigMap does this). Deleting them is part of the migration, not a follow-up task.