Compose and Kubernetes can run the same three processes: a web UI, an API, and Postgres. Compose is a project on one engine. Kubernetes is a set of API objects a control plane can reschedule. Below is one compose.yaml, the manifests that match it, and the point where a team should change tools.

Short answer

Pick Compose when losing the machine means the app is down, and that is acceptable. Pick Kubernetes when a node can disappear and the Pod should start somewhere else, or when you already operate a cluster. Both tools start the web, API, and database containers. Only Kubernetes has a scheduler, a Service that tracks ready Pods, and a rollout history.

Compose service names are DNS names on the project network. Kubernetes Service names are DNS names inside the namespace (api and api.demo.svc.cluster.local). http://api:3000 works in both files if the Service is named api and listens on port 3000.

Side by side

Concern compose.yaml Kubernetes
Web, API, database process services.web, services.api, services.postgres Deployment + Pod template
Published UI port ports: ["8080:80"] on web ClusterIP Service, then kubectl port-forward or an Ingress
API address http://api:3000 Service api port 3000
Non-secret settings environment ConfigMap app-config
Password .env variable Secret app-secret
Database disk named volume pgdata PersistentVolumeClaim pgdata
Start order depends_on with service_healthy readinessProbe; clients retry
Health check healthcheck inside the container readinessProbe and livenessProbe, run by the kubelet
Restart restart: unless-stopped Deployment recreates the Pod (restartPolicy: Always)
Extra copies of the API docker compose up --scale api=3 spec.replicas
Build docker build, then an image tag same image reference; the cluster does not build

The Compose file

change-me stands in for a password. Put it in a .env file next to the Compose file and leave that file out of git. Compose reads .env automatically. Command details live in the Docker Compose cheat sheet.

services:
  web:
    image: example/web:1.4.0
    ports:
      - "8080:80"
    environment:
      API_URL: http://api:3000
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
  api:
    image: example/api:1.4.0
    environment:
      DATABASE_URL: postgres://app:${POSTGRES_PASSWORD}@postgres:5432/app
    depends_on:
      postgres:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 3s
      retries: 10
    restart: unless-stopped
volumes:
  pgdata:

That wget check runs inside the image, so the binary has to exist there. Build tags with the Dockerfile cheat sheet.

# --wait returns non-zero if a healthcheck never passes
docker compose up -d --wait
docker compose ps
docker compose down -v

down -v deletes the pgdata volume. Use it in CI. Do not use it against a database you meant to keep.

The Kubernetes manifests

Save this as shop.yaml and apply it with kubectl. The Secret value must match POSTGRES_PASSWORD or the API cannot log in. On a shared cluster, create that Secret from outside the git repo and delete the stringData block.

apiVersion: v1
kind: Namespace
metadata:
  name: demo
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: app-config
  namespace: demo
data:
  API_URL: "http://api:3000"
  POSTGRES_USER: app
  POSTGRES_DB: app
---
apiVersion: v1
kind: Secret
metadata:
  name: app-secret
  namespace: demo
type: Opaque
stringData:
  POSTGRES_PASSWORD: change-me
  DATABASE_URL: postgres://app:change-me@postgres:5432/app
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: pgdata
  namespace: demo
spec:
  accessModes: ["ReadWriteOnce"]
  resources:
    requests:
      storage: 5Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: postgres
  template:
    metadata:
      labels:
        app: postgres
    spec:
      securityContext:
        fsGroup: 999
      containers:
        - name: postgres
          image: postgres:16
          ports:
            - containerPort: 5432
          env:
            - name: POSTGRES_USER
              valueFrom:
                configMapKeyRef: {name: app-config, key: POSTGRES_USER}
            - name: POSTGRES_DB
              valueFrom:
                configMapKeyRef: {name: app-config, key: POSTGRES_DB}
            - name: POSTGRES_PASSWORD
              valueFrom:
                secretKeyRef: {name: app-secret, key: POSTGRES_PASSWORD}
          readinessProbe:
            exec:
              command: ["pg_isready", "-U", "app", "-d", "app"]
            periodSeconds: 5
          livenessProbe:
            exec:
              command: ["pg_isready", "-U", "app", "-d", "app"]
            initialDelaySeconds: 15
            periodSeconds: 10
          volumeMounts:
            - name: data
              mountPath: /var/lib/postgresql/data
      volumes:
        - name: data
          persistentVolumeClaim:
            claimName: pgdata
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: demo
spec:
  selector:
    app: postgres
  ports:
    - port: 5432
      targetPort: 5432
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api
  template:
    metadata:
      labels:
        app: api
    spec:
      containers:
        - name: api
          image: example/api:1.4.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 3000
          env:
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef: {name: app-secret, key: DATABASE_URL}
          readinessProbe:
            httpGet: {path: /health, port: 3000}
            periodSeconds: 10
          resources:
            requests: {cpu: "100m", memory: 128Mi}
            limits: {memory: 256Mi}
---
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: demo
spec:
  selector:
    app: api
  ports:
    - port: 3000
      targetPort: 3000
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  namespace: demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: example/web:1.4.0
          imagePullPolicy: IfNotPresent
          ports:
            - containerPort: 80
          env:
            - name: API_URL
              valueFrom:
                configMapKeyRef: {name: app-config, key: API_URL}
          readinessProbe:
            httpGet: {path: /, port: 80}
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: web
  namespace: demo
spec:
  selector:
    app: web
  ports:
    - port: 80
      targetPort: 80

httpGet probes run on the kubelet, so those images do not need wget. fsGroup: 999 lets the official Postgres image write its volume. imagePullPolicy: IfNotPresent fits a locally loaded tag; :latest still defaults to Always and contacts the registry.

kubectl apply -f shop.yaml
kubectl rollout status deployment/api -n demo
kubectl port-forward -n demo svc/web 8080:80

port-forward replaces Compose’s 8080:80 on your laptop. Other machines should use an Ingress.

A PVC with no StorageClass stays Pending (Pod Pending / FailedScheduling). A limit that is too small is OOMKilled. A bad DATABASE_URL is CrashLoopBackOff. A missing image is ImagePullBackOff.

Leave Postgres at one replica. ReadWriteOnce will not mount the same disk on two nodes. Use a StatefulSet and volumeClaimTemplates later if you need a stable name and one volume per replica.

When to pick Compose

Pick Compose when all of these are true:

  • The stack fits on one machine, including disk for Postgres.
  • A restart policy is enough when a process exits.
  • Developers and CI already run docker compose up.
  • A Pod does not have to move to another node.

Keep Compose for the inner loop even after production is a cluster. A file change and docker compose up is shorter than a push and a rollout.

When to pick Kubernetes

Pick Kubernetes when one of these is true:

  • You need more than one node, or a node can be drained while the API keeps serving.
  • You want a recorded rollout and kubectl rollout undo per Deployment.
  • Several environments should share the same manifests, with small differences handled by Helm or Kustomize.
  • The platform team already runs the cluster and expects requests, limits, and probes.

A second replica is not enough of a reason. --scale already starts extra API containers on one engine. Move when placement, upgrades, or policy matter.

Middle ground

Compose in CI. Boot the stack, fail the job if healthchecks do not pass, then delete volumes:

docker compose up -d --wait
docker compose ps
docker compose down -v

kind for a local control plane. Load the web and API images, then apply shop.yaml. The node can still pull postgres:16. Which local cluster to use is covered in kind vs minikube vs k3d.

kind create cluster --name demo
kind load docker-image example/web:1.4.0 --name demo
kind load docker-image example/api:1.4.0 --name demo
kubectl apply -f shop.yaml

Kompose as a draft, not a pipeline. From the directory that contains compose.yaml:

kompose convert -f compose.yaml

Kompose writes one YAML file per object in the current directory. Review probes, the password, imagePullPolicy, and Service type before anyone applies them. Kubernetes does not run compose.yaml itself.

Migration path

  1. Pin image tags in CI. The cluster should reference example/api:1.4.0, not a floating latest and not a build: block.
  2. Split configuration. Non-secrets go to the ConfigMap. The password goes to a Secret created outside git on any shared cluster.
  3. Turn each Compose healthcheck into a readiness probe. Keep a liveness probe only for a check that means “restart this container.”
  4. Create the PVC and copy data with your own backup. docker compose down -v plus a fresh claim drops the database.
  5. Apply shop.yaml on kind. Fix Pending pods and image pulls there.
  6. Add requests and limits, and watch the API under load before you copy the numbers.
  7. Put an Ingress in front of web. Leave kubectl port-forward for debugging.
  8. Decide where Postgres lives before users move: a managed database, or a StatefulSet with backups.
  9. Apply on the shared cluster, run kubectl rollout status, then switch DNS.

FAQ

Is Docker Compose the same as Kubernetes?

No. Compose starts containers on one Docker engine and wires them with a project network. Kubernetes stores Deployments, Services, and volumes in an API and can place Pods on other nodes. The image can be identical. The object model is not.

When should I switch from Docker Compose to Kubernetes?

Switch when the process has to survive a node failure, roll out without downtime, or share a cluster other teams already run. Stay on Compose when one machine, a restart policy, and a CI job are enough. A slide that says production means Kubernetes is not a requirement.

Does Kompose produce production-ready manifests?

It produces a first draft. kompose convert -f compose.yaml writes a Deployment and a Service per Compose service, and a PersistentVolumeClaim for named volumes. You still edit probes, Secrets, resource limits, and the Service type before anyone else applies the files.

What replaces depends_on in Kubernetes?

Nothing waits once at startup and then steps aside. A readiness probe removes a Pod from Service endpoints until it can serve, and it keeps doing that for the life of the Pod. Clients should retry. An init container can block the app container, but it does not replace the probe.

How this guide was verified

Written by hand from the official documentation, every command re-checked, and reviewed a second time before publishing. Last reviewed on .

Official sources read for this article

Share.

Haythem Rehouma builds and teaches DevOps and AI tooling: Kubernetes, Docker, Terraform, Claude Code, ChatGPT and n8n. Every KubPoint guide is written from the official docs and re-tested before it goes live.

Comments are closed.