From docker-compose to Kubernetes
Appendix A. It collects the seven differences that trip up readers who have reached Docker / docker-compose and are moving to Kubernetes. It maps each `docker-compose.yml` key to the corresponding Kubernetes resource, walks through one migration cycle from a small web + db compose file to Kubernetes manifests, and explains the limits of kompose and what comes next. It is the book's last chapter, but for readers who started from Docker, it also becomes a starting point.
This is Appendix A. For readers who have followed this book through Chapters 1 ~ 31, it is an appendix. For readers who came as far as Docker / docker-compose and opened this book for the first time, it is a starting point. The mapping table introduced at the end of Chapter 1, What Kubernetes Is as something you will return to often is the center of this appendix.
The goals of this appendix are two.
- Which Kubernetes resource each key of
docker-compose.ymlcorresponds to — the big picture in one table. - The seven cases where that mapping is not a simple 1 : 1 — different shapes of the same intent.
Once these two are clear, Chapters 1 ~ 6 of the Kubernetes main text become much easier to read.
The difference in mental model — one host, one network vs multiple nodes, multiple controllers #
In one line, docker-compose up does the following.
On one host:
- one network is created (compose-default)
- and N containers are up inside it
- services on the same network can call each other by service name
- data is mounted to the host's disk
- restart: always auto-restarts on a downtimeKubernetes breaks this model into multiple nodes, multiple controllers, and multiple network layers. The same idea looks like this in Kubernetes.
On multiple nodes:
- multiple namespaces (the isolation unit)
- Pods inside them are scattered across multiple nodes
- a Service provides the Pod's virtual IP and DNS name
- a PersistentVolume is a persistent disk separated from the node
- a Deployment controller handles restarts and rolling updates
- the control plane (API Server, scheduler, controller-manager, etcd) orchestrates all of the aboveThe difference between one host’s simplicity and many nodes’ distribution is the core of the mental model. In compose, every container writes logs to the same host’s stdout and lives on the same network. In Kubernetes, by contrast, the node a Pod lands on depends on the manifest, and logs are distributed across nodes as well.
This difference is the starting point of all seven cases.
Resource mapping — the big picture #
The table below shows where the commonly used keys of docker-compose.yml map in Kubernetes.
| docker-compose | Kubernetes | Chapter of this book |
|---|---|---|
services | Deployment + Service | Chapter 4, Chapter 5 |
image | the Pod spec’s containers[].image | Chapter 3 |
command / entrypoint | the Pod spec’s command / args | Chapter 3 |
ports: "8080:8080" | Service (ClusterIP / NodePort / LoadBalancer) + Ingress | Chapter 5, Chapter 10 |
volumes (named) | PersistentVolumeClaim + StorageClass | Chapter 9 |
volumes (bind mount) | hostPath (for development) or ConfigMap (config files) | Chapter 9, Chapter 6 |
environment / env_file | ConfigMap + Secret + envFrom | Chapter 6 |
restart: always | the default of a Deployment (ReplicaSet) | Chapter 4 |
depends_on | initContainer / Job / Helm hook (no exact 1 : 1) | Chapter 8, Chapter 23 |
healthcheck | readinessProbe + livenessProbe + startupProbe | Chapter 12 |
networks | NetworkPolicy + Service DNS + CNI | Chapter 5, Chapter 14, Chapter 15 |
deploy.replicas | the Deployment’s replicas | Chapter 4 |
deploy.resources | the Pod spec’s resources.requests/limits | Chapter 11 |
deploy.update_config | the Deployment’s strategy.rollingUpdate | Chapter 4 |
deploy.placement.constraints | nodeSelector / affinity / taints | Chapter 22 |
secrets: (Compose) | Secret + (sealed-secrets / external-secrets) | Chapter 6, Chapter 29 |
configs: (Compose) | ConfigMap (volumeMount or envFrom) | Chapter 6 |
The table alone already shows the key difference. One services entry in compose becomes two objects in Kubernetes: Deployment + Service. The workload itself (Deployment) and its entry point (Service) are separate concerns. Compose bundled those two jobs into one concept; Kubernetes splits them into two objects and manages each lifecycle independently.
One more thing: there is no exact Kubernetes equivalent for depends_on. The closest pattern is initContainer (preparatory work before the Pod starts), but a meaning like “web starts after db comes up as another service” does not fit Kubernetes’s asynchronous model. Kubernetes does not force startup order across workloads; it handles that with readiness and retry. The details appear in difference 5 below.
The seven differences you commonly trip on #
1. Networking — service name DNS vs ClusterIP + Service #
The compose example:
services:
web:
image: nginx
db:
image: postgresInside the web container, psql -h db ... just works. In the default network created by compose, the service name resolves automatically through DNS.
The Kubernetes version:
apiVersion: v1
kind: Service
metadata:
name: db
namespace: default
spec:
selector:
app: db
ports:
- port: 5432In Kubernetes, you must create a Service object explicitly to call db (or db.default.svc.cluster.local) from inside the web Pod. Going through a Service instead of calling Pod to Pod directly is the standard pattern.
The reason is explained in Chapter 5, Service: a Pod’s IP changes as it dies and comes back, but a Service guarantees a stable virtual IP and DNS name. Compose’s automatic service-name resolution comes from Docker’s embedded DNS tracking the containers on one network inside one host, while Kubernetes separates the stable entry point that crosses multiple nodes into its own Service object.
2. Service discovery — same-network auto-discovery vs Service + endpoints #
This is the same point from a different angle. In compose, discovery is implicit. In Kubernetes, it happens only when you define a Service explicitly.
kubectl get endpoints dbEndpoints is the list of IPs of Pods that match the Service’s selector. If the selector and endpoints are empty or out of sync, the call does not go through — exactly the three-stage chain of selector → endpoints → port in Chapter 27, kubectl Debugging Patterns §“When a Service / Ingress won’t reach.”
Compose’s auto-discovery is simple but works only on one host. Kubernetes’s explicit model takes more work, but it is what allows consistent behavior across the whole cluster.
3. Persistent volume — host-path auto-mount vs PVC request + StorageClass #
The compose example:
services:
db:
image: postgres
volumes:
- db-data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
volumes:
db-data:A named volume is created automatically, and a bind mount mounts the host file path directly. It is a simple model where one host’s disk is visible directly.
The Kubernetes version:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
spec:
storageClassName: gp3
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gi
---
# the Pod's volumeMount + volumes
spec:
containers:
- name: db
image: postgres
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: db-dataThis is the model from Chapter 9, PV / PVC / StorageClass. It splits into three objects: PVC is the request, PV is the actual disk, and StorageClass is the provisioning policy.
The reason is simple: you cannot know in advance which node a Pod will land on, so you must not depend on the node’s host disk. You request a persistent disk that is independent of the node, such as EBS / EFS / GCP PD, with a PVC, and the StorageClass provisions and mounts it for the Pod.
The pattern corresponding to compose’s bind mount is not part of the book’s standard path — hostPath mounts in an operational environment are not recommended for security or portability reasons. A development-environment hostPath is possible, but it is not covered in the main text.
4. Secrets — env_file vs Secret + operational lifecycle #
The compose example:
services:
api:
image: myapi
env_file:
- .envWrite passwords and API keys in a single .env file, add it to .gitignore, and you’re done. It is simple.
The Kubernetes version:
apiVersion: v1
kind: Secret
metadata:
name: api-secrets
type: Opaque
stringData:
DATABASE_PASSWORD: "..."
---
# the Pod's envFrom
spec:
containers:
- name: api
envFrom:
- secretRef:
name: api-secretsThis is the model from Chapter 6, ConfigMap and Secret. A Secret’s data field is only base64-encoded, not encrypted, so committing the manifest to git unchanged would expose the secret.
The tools that solve this problem in practice are the three options from Chapter 29, Secret Operations (sealed-secrets / external-secrets / SOPS). Compose’s env_file + .gitignore model becomes, in Kubernetes, a more elaborate operational lifecycle — storage, rotation, injection, and audit are the real concerns, and simplicity has a cost.
5. Healthcheck — one stage vs three stages #
The compose experience:
services:
api:
image: myapi
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30sIt judges the container’s “healthiness” with one healthcheck. When unhealthy accumulates a certain number of times (combined with restart: on-failure), it restarts.
The Kubernetes version:
spec:
containers:
- name: api
readinessProbe:
httpGet: { path: /health/ready, port: http }
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet: { path: /health/live, port: http }
initialDelaySeconds: 30
periodSeconds: 10
startupProbe:
httpGet: { path: /health/live, port: http }
failureThreshold: 30
periodSeconds: 10It is the model covered in Chapter 12, Health Checks. It splits into three checks: readiness / liveness / startup.
- readiness — is it ready to receive traffic? On fail it’s excluded from the Service endpoints.
- liveness — is the container alive? When fail accumulates, the kubelet restarts the container.
- startup — is it initializing? A grace period for workloads with long initialization time.
The reason one compose healthcheck becomes three checks in Kubernetes is simple: “can receive traffic,” “is alive,” and “is initializing” are different intents. Even if the same endpoint fails, the meaning differs. Bundling them into one stage causes the incident where liveness fails during initialization and the container restarts forever.
The depends_on equivalent is also resolved here. Compose’s “start api after db becomes healthy” becomes a pattern where api’s readinessProbe checks db’s response — it does not force startup order and instead resolves naturally: if db does not respond, readiness is false, the Pod is not added to the Service endpoints, and traffic does not arrive. If something more explicit is needed, the pattern is to wait for db with an initContainer.
6. Scaling — the one-host limit of --scale vs HPA + Cluster Autoscaler
#
The compose experience:
docker-compose up --scale api=3You can grow up to one host’s limit, but once you reach that host’s CPU / memory limit, that’s the end.
The K8s model:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
kind: Deployment
name: api
minReplicas: 2
maxReplicas: 50
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60The two-stage combination of the HPA of Chapter 13, Autoscaling and Cluster Autoscaler / Karpenter is Kubernetes’s answer. The shape where nodes are added automatically as Pods increase is one of the essentials of container orchestration, and it is something compose does not have.
7. Logs — one-host stdout vs per-node distribution + agent #
The compose experience:
docker-compose logs -f apiAll the container stdout from one host appears in one place. Simple.
The K8s model:
kubectl logs <pod-name>
kubectl logs -l app=api --tail=100 # per labelPods are scattered across multiple nodes, so each node’s kubelet holds that node’s container logs. kubectl logs fetches each node’s logs through the API Server.
In an operational environment, a per-node log agent (Promtail of Chapter 19, Observability §“Loki — the lightweight log stack,” or Fluent Bit of Chapter 25, Monitoring · Alerts) is added to gather all logs into a central store (Loki / CloudWatch). Compose’s one-line command turns in Kubernetes into three components: a collection agent + a central store + a search interface.
One migration cycle — the example of a small web + db #
We move the most common docker-compose bundle to K8s manifests.
version: "3"
services:
web:
image: nginx:1.27-alpine
ports:
- "80:80"
depends_on:
- api
environment:
API_URL: http://api:8000
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/"]
interval: 30s
api:
image: myapi:1.0
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:secret@db:5432/myapp
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
POSTGRES_DB: myapp
volumes:
- db-data:/var/lib/postgresql/data
volumes:
db-data:This one file carries over into about 200 lines of K8s manifests. We point only at the core objects.
1. Namespace #
apiVersion: v1
kind: Namespace
metadata:
name: myappA pattern compose does not have. Kubernetes’s isolation unit — the first object of Chapter 7, Namespace and Labels.
2. Secret (the DB password) #
apiVersion: v1
kind: Secret
metadata:
name: db
namespace: myapp
type: Opaque
stringData:
POSTGRES_PASSWORD: secret
POSTGRES_USER: app
POSTGRES_DB: myappcompose’s plaintext environment splits into K8s’s Secret. In operations you seal it with one of the three options of Chapter 29, but in a learning manifest we start with plaintext stringData for now.
3. PVC (the DB data) #
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: db-data
namespace: myapp
spec:
storageClassName: gp3 # or minikube's standard
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 10Gicompose’s named volume is unfolded into PVC + StorageClass.
4. DB — Deployment + Service #
apiVersion: apps/v1
kind: Deployment
metadata:
name: db
namespace: myapp
spec:
replicas: 1
selector:
matchLabels:
app: db
template:
metadata:
labels:
app: db
spec:
containers:
- name: postgres
image: postgres:16
envFrom:
- secretRef:
name: db
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
readinessProbe:
exec:
command: ["pg_isready", "-U", "app"]
periodSeconds: 5
volumes:
- name: data
persistentVolumeClaim:
claimName: db-data
---
apiVersion: v1
kind: Service
metadata:
name: db
namespace: myapp
spec:
selector:
app: db
ports:
- port: 5432
targetPort: 5432Operational PostgreSQL usually goes to the StatefulSet of Chapter 8, StatefulSet · DaemonSet · Job, but for a single-instance learning scenario Deployment + a single PVC is enough. The operational standard is the managed RDS of Chapter 23, DB Integration — you don’t bring up a DB directly inside K8s.
5. API — Deployment + Service #
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
namespace: myapp
spec:
replicas: 2
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: myapi:1.0
env:
- name: DATABASE_URL
value: "postgresql://app:$(POSTGRES_PASSWORD)@db.myapp.svc.cluster.local:5432/myapp"
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: db
key: POSTGRES_PASSWORD
readinessProbe:
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
httpGet: { path: /health, port: 8000 }
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: api
namespace: myapp
spec:
selector:
app: api
ports:
- port: 8000
targetPort: 8000Compose’s depends_on: [db] is resolved in Kubernetes through the readinessProbe — while api’s readiness is false, it is excluded from the Service endpoints so web cannot see api. Once db comes up and api can connect to db, readiness becomes true and traffic flows.
6. Web — Deployment + Service + Ingress #
apiVersion: apps/v1
kind: Deployment
metadata:
name: web
namespace: myapp
spec:
replicas: 2
selector:
matchLabels:
app: web
template:
metadata:
labels:
app: web
spec:
containers:
- name: nginx
image: nginx:1.27-alpine
env:
- name: API_URL
value: "http://api.myapp.svc.cluster.local:8000"
readinessProbe:
httpGet: { path: /, port: 80 }
---
apiVersion: v1
kind: Service
metadata:
name: web
namespace: myapp
spec:
selector:
app: web
ports:
- port: 80
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: web
namespace: myapp
spec:
rules:
- host: myapp.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 80compose’s one line ports: "80:80" splits in K8s into Service (the cluster-internal virtual IP) + Ingress (the external entry point). The decision to expose externally is in the Ingress, not the Service — the separation model of Chapter 5, Service + Chapter 10, Ingress.
Applying #
kubectl apply -f namespace.yaml
kubectl apply -f secret.yaml
kubectl apply -f pvc.yaml
kubectl apply -f db.yaml
kubectl apply -f api.yaml
kubectl apply -f web.yamlOnce these 6 steps finish, the same system as compose’s one command (docker-compose up) is up on Kubernetes. The difference is that each object is explicit and its lifecycle is separate. You can update one object, debug one object, and grant permissions for one object independently.
The total line count of the manifests is about 200 lines — more than 6 times compose’s 30 lines. This difference is the starting point of Kubernetes’s learning curve, but it is also the cost of gaining that much expressiveness and real tooling for operational work.
The limits of the kompose tool #
kompose is a tool that auto-converts docker-compose.yml into K8s manifests.
kompose convert -f docker-compose.yml -o k8s-manifests/This command makes the K8s manifests of the above 6 steps in one go — useful as a starting point. But auto-conversion has limits.
1. depends_on -> initContainer mapping is simple, but it's not the intended readiness pattern
2. healthcheck -> probe conversion becomes only one stage (liveness) — the split into three checks is done by hand
3. bind mount -> converted to hostPath — unsuitable in operations
4. networks isolation -> not converted to NetworkPolicy
5. the sealing option of secrets / configs -> converted only to a plaintext Secret manifest
6. Ingress manifest -> kompose doesn't make it. Always added by a human
7. the StorageClass decision of a PersistentVolume -> differs per cluster, so chosen by a humankompose is a starting point, not the destination. Putting auto-converted manifests straight into operations means ignoring nearly all seven differences in this book. To get to operational manifests, you have to apply the patterns from Chapters 1 ~ 14 of this book and refine them by hand.
As an alternative, writing a Helm chart from scratch is more natural. Starting from one compose bundle with helm create myapp and applying this book’s patterns one manifest at a time is the standard entry path to operational K8s.
The next step of Appendix A — where to go #
At the point of finishing this appendix, the entry path of this book recommended to a reader who had come as far as Docker / docker-compose is the following.
[Appendix A finished]
|
v
[Chapter 1, What Kubernetes Is] -- at the point of finishing this appendix, the big picture is already in place
[Chapter 2, Local Environments] -- bring up kind or minikube
[Chapter 3, kubectl and Your First Pod] -- the first manifest of the migration cycle above
[Chapter 4, Deployment] -- the Deployment model in practice
[Chapter 5, Service] -- ClusterIP / NodePort / LoadBalancer + DNS
[Chapter 6, ConfigMap · Secret] -- the envFrom pattern in practice
[Chapter 7, Namespace and Labels] -- isolation and classification
|
v
Part 1 complete -- a state where all of compose's concerns are inside one Kubernetes bundleChapter 1, What Kubernetes Is is the first chapter of the main text, and if you keep this appendix’s mapping table beside you, the question “which compose key was this?” will connect naturally in every chapter.
If you follow Part 1 (Chapters 1 ~ 7) of this book, this appendix’s migration manifest will look familiar. From there you move on to Part 2 (Chapters 8 ~ 14) and, after covering StatefulSet · PV · Ingress · resource management · health checks · autoscaling · RBAC, you gain the vision to operate a variety of workloads on a small cluster. The depth of Part 3 (Chapters 15 ~ 20), the EKS practice of Part 4 (Chapters 21 ~ 26), the operations · debugging · cost of Part 5 (Chapters 27 ~ 30), and the capstone of Part 6 (Chapter 31) form the big picture of the whole book.
Exercises #
- Pick a small
docker-compose.ymlyou operate or are learning with, and move it to K8s manifests following the 6 steps of this appendix’s §“One migration cycle.” Compare the line counts of the manifests before and after conversion, and in one paragraph describe what the added lines are for (isolation · lifecycle separation · explicit objects). - Compare the result of auto-converting the same
docker-compose.ymlwithkompose convertagainst the result you moved by hand. Classify the differences by which of the 7 items in §“The limits of kompose” they correspond to, and map which chapter of the main text you should use to fill the gaps that auto-conversion could not cover. - Pick the one of §“The seven differences you commonly trip on” that resonates most with your experience, and sketch in advance, on one page, how your workload’s docker-compose model will change in Kubernetes. Keep this memo beside you while you read the book, and when you reach the relevant chapter, verify how that pattern is actually resolved.
In one line: docker-compose’s one-host / one-network / simple service-name DNS model becomes, in Kubernetes, the explicit model of multiple nodes / multiple namespaces / Service + endpoints. One line of
servicessplits into the two objects Deployment + Service,volumesinto PVC + StorageClass,env_fileinto Secret + operational lifecycle, the one-stagehealthcheckinto the three stages readiness + liveness + startup, and the one-host limit of--scaleinto the two-stage automatic response of HPA + Cluster Autoscaler. There is no exact equivalent fordepends_on; it is handled by the readiness probe pattern. kompose automates the starting point but is not the destination — you have to apply the patterns from Chapters 1 ~ 14 of this book by hand to get operational manifests. If you enter Chapter 1 after finishing this appendix, the mapping table will keep unfolding naturally in each chapter.
The end of the book #
With this appendix, all 32 chapters of this book (Chapters 1 ~ 31 + Appendix A) are complete. The retrospective table of Chapter 31, Deploying a Fullstack App on EKS, the last chapter of the main text, is the one-line summary of this book, and this appendix’s mapping table is the starting point. The structure of this book is that the starting point and destination are bound inside one volume.
Start the main text from Chapter 1, What Kubernetes Is, and we hope Appendix A serves as the reference you return to whenever the mapping gets fuzzy.