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 undoper 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
- Pin image tags in CI. The cluster should reference
example/api:1.4.0, not a floatinglatestand not abuild:block. - Split configuration. Non-secrets go to the ConfigMap. The password goes to a Secret created outside git on any shared cluster.
- Turn each Compose healthcheck into a readiness probe. Keep a liveness probe only for a check that means “restart this container.”
- Create the PVC and copy data with your own backup.
docker compose down -vplus a fresh claim drops the database. - Apply
shop.yamlon kind. Fix Pending pods and image pulls there. - Add requests and limits, and watch the API under load before you copy the numbers.
- Put an Ingress in front of web. Leave
kubectl port-forwardfor debugging. - Decide where Postgres lives before users move: a managed database, or a StatefulSet with backups.
- 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

