CONTAINERS

62 lines of Kubernetes YAML vs 16 lines of Docker Compose

April 10, 2026 · 5 min read

Mikel Martin

CTO, Keni Engineering

Every conference talk and vendor pitch will tell you that Kubernetes is the standard for running containers in production. But what does that actually look like in practice? Let us put the same workloads side by side and count the lines.

Three scenarios. Same applications. Same result. Wildly different amounts of configuration.

Scenario 1: Web app + database

The simplest possible production setup. One app, one database.

Kubernetes62 lines, 4 files
deployment.yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: webapp
5 labels:
6 app: webapp
7spec:
8 replicas: 1
9 selector:
10 matchLabels:
11 app: webapp
12 template:
13 metadata:
14 labels:
15 app: webapp
16 spec:
17 containers:
18 - name: webapp
19 image: myapp:latest
20 ports:
21 - containerPort: 3000
22 env:
23 - name: DATABASE_URL
24 valueFrom:
25 secretKeyRef:
26 name: db-secret
27 key: url
service.yaml
28apiVersion: v1
29kind: Service
30metadata:
31 name: webapp
32spec:
33 selector:
34 app: webapp
35 ports:
36 - port: 80
37 targetPort: 3000
postgres-statefulset.yaml
38apiVersion: apps/v1
39kind: StatefulSet
40metadata:
41 name: postgres
42spec:
43 serviceName: postgres
44 replicas: 1
45 selector:
46 matchLabels:
47 app: postgres
48 template:
49 metadata:
50 labels:
51 app: postgres
52 spec:
53 containers:
54 - name: postgres
55 image: postgres:16
56 ports:
57 - containerPort: 5432
58 volumeMounts:
59 - name: pg-data
60 mountPath: /var/lib/postgresql/data
postgres-service.yaml
61apiVersion: v1
62kind: Service
63metadata:
64 name: postgres
65spec:
66 selector:
67 app: postgres
68 ports:
69 - port: 5432
70 targetPort: 5432
secret.yaml
71apiVersion: v1
72kind: Secret
73metadata:
74 name: db-secret
75type: Opaque
76stringData:
77 url: postgres://postgres:secret@postgres:5432/app
Docker Compose16 lines, 1 file
docker-compose.yml
1services:
2 webapp:
3 image: myapp:latest
4 ports:
5 - "80:3000"
6 environment:
7 - DATABASE_URL=postgres://postgres:secret@db/app
8 depends_on:
9 - db
10 
11 db:
12 image: postgres:16
13 volumes:
14 - pg-data:/var/lib/postgresql/data
15 
16volumes:
17 pg-data:

The simplest case and the gap is already 4x. Kubernetes needs a Deployment, a Service, a StatefulSet, another Service, and a Secret. Five separate resources, each with its own apiVersion, kind, metadata, and spec. Docker Compose: one file, 16 lines, done.

Scenario 2: App + database + Redis + worker

A typical production stack. Background jobs, caching, relational storage.

Kubernetes131 lines, 9 files
webapp-deployment.yaml
1apiVersion: apps/v1
2kind: Deployment
3metadata:
4 name: webapp
5spec:
6 replicas: 1
7 selector:
8 matchLabels:
9 app: webapp
10 template:
11 metadata:
12 labels:
13 app: webapp
14 spec:
15 containers:
16 - name: webapp
17 image: myapp:latest
18 ports:
19 - containerPort: 3000
20 envFrom:
21 - configMapRef:
22 name: app-config
23 - secretRef:
24 name: app-secrets
webapp-service.yaml
25apiVersion: v1
26kind: Service
27metadata:
28 name: webapp
29spec:
30 selector:
31 app: webapp
32 ports:
33 - port: 80
34 targetPort: 3000
worker-deployment.yaml
35apiVersion: apps/v1
36kind: Deployment
37metadata:
38 name: worker
39spec:
40 replicas: 2
41 selector:
42 matchLabels:
43 app: worker
44 template:
45 metadata:
46 labels:
47 app: worker
48 spec:
49 containers:
50 - name: worker
51 image: myapp:latest
52 command: ["node", "worker.js"]
53 envFrom:
54 - configMapRef:
55 name: app-config
56 - secretRef:
57 name: app-secrets
redis-deployment.yaml
58apiVersion: apps/v1
59kind: Deployment
60metadata:
61 name: redis
62spec:
63 selector:
64 matchLabels:
65 app: redis
66 template:
67 metadata:
68 labels:
69 app: redis
70 spec:
71 containers:
72 - name: redis
73 image: redis:7-alpine
74 ports:
75 - containerPort: 6379
redis-service.yaml
76apiVersion: v1
77kind: Service
78metadata:
79 name: redis
80spec:
81 selector:
82 app: redis
83 ports:
84 - port: 6379
85 targetPort: 6379
postgres-statefulset.yaml
86apiVersion: apps/v1
87kind: StatefulSet
88metadata:
89 name: postgres
90spec:
91 serviceName: postgres
92 replicas: 1
93 selector:
94 matchLabels:
95 app: postgres
96 template:
97 metadata:
98 labels:
99 app: postgres
100 spec:
101 containers:
102 - name: postgres
103 image: postgres:16
104 ports:
105 - containerPort: 5432
106 envFrom:
107 - secretRef:
108 name: db-secrets
109 volumeMounts:
110 - name: pg-data
111 mountPath: /var/lib/postgresql/data
postgres-service.yaml
112apiVersion: v1
113kind: Service
114metadata:
115 name: postgres
116spec:
117 selector:
118 app: postgres
119 ports:
120 - port: 5432
121 targetPort: 5432
configmap.yaml
122apiVersion: v1
123kind: ConfigMap
124metadata:
125 name: app-config
126data:
127 REDIS_URL: redis://redis:6379
128 NODE_ENV: production
secret.yaml
129apiVersion: v1
130kind: Secret
131metadata:
132 name: app-secrets
133type: Opaque
134stringData:
135 DATABASE_URL: postgres://postgres:secret@postgres:5432/app
Docker Compose30 lines, 1 file
docker-compose.yml
1services:
2 webapp:
3 image: myapp:latest
4 ports:
5 - "80:3000"
6 environment:
7 DATABASE_URL: postgres://postgres:secret@db/app
8 REDIS_URL: redis://redis:6379
9 depends_on:
10 - db
11 - redis
12 
13 worker:
14 image: myapp:latest
15 command: node worker.js
16 environment:
17 DATABASE_URL: postgres://postgres:secret@db/app
18 REDIS_URL: redis://redis:6379
19 depends_on:
20 - db
21 - redis
22 
23 db:
24 image: postgres:16
25 volumes:
26 - pg-data:/var/lib/postgresql/data
27 
28 redis:
29 image: redis:7-alpine
30 
31volumes:
32 pg-data:

Add a cache and a background worker and Kubernetes jumps to 9 files with 135 lines. Every new component requires a Deployment and a Service at minimum, plus shared ConfigMaps and Secrets. Docker Compose adds another block in the same file. 135 vs 32 lines. 4.2x more YAML for the same result.

Scenario 3: Full platform with monitoring

App, database, Redis, Nginx, Prometheus, Grafana. A real production stack.

Kubernetes258 lines, 15 files
namespace.yaml
1apiVersion: v1
2kind: Namespace
3metadata:
4 name: production
webapp-deployment.yaml
5apiVersion: apps/v1
6kind: Deployment
7metadata:
8 name: webapp
9 namespace: production
10 labels:
11 app.kubernetes.io/name: webapp
12spec:
13 replicas: 1
14 selector:
15 matchLabels:
16 app.kubernetes.io/name: webapp
17 template:
18 metadata:
19 labels:
20 app.kubernetes.io/name: webapp
21 annotations:
22 prometheus.io/scrape: "true"
23 prometheus.io/port: "3000"
24 spec:
25 containers:
26 - name: webapp
27 image: myapp:latest
28 ports:
29 - containerPort: 3000
30 resources:
31 requests:
32 memory: "128Mi"
33 cpu: "100m"
34 limits:
35 memory: "256Mi"
36 cpu: "500m"
37 envFrom:
38 - configMapRef:
39 name: app-config
40 - secretRef:
41 name: app-secrets
42 readinessProbe:
43 httpGet:
44 path: /health
45 port: 3000
webapp-service.yaml
46apiVersion: v1
47kind: Service
48metadata:
49 name: webapp
50 namespace: production
51spec:
52 selector:
53 app.kubernetes.io/name: webapp
54 ports:
55 - port: 80
56 targetPort: 3000
postgres-statefulset.yaml
57apiVersion: apps/v1
58kind: StatefulSet
59metadata:
60 name: postgres
61 namespace: production
62spec:
63 serviceName: postgres
64 replicas: 1
65 selector:
66 matchLabels:
67 app: postgres
68 template:
69 metadata:
70 labels:
71 app: postgres
72 spec:
73 containers:
74 - name: postgres
75 image: postgres:16
76 ports:
77 - containerPort: 5432
78 envFrom:
79 - secretRef:
80 name: db-secrets
81 volumeMounts:
82 - name: pg-data
83 mountPath: /var/lib/postgresql/data
postgres-service.yaml
84apiVersion: v1
85kind: Service
86metadata:
87 name: postgres
88 namespace: production
89spec:
90 selector:
91 app: postgres
92 ports:
93 - port: 5432
94 targetPort: 5432
postgres-pvc.yaml
95apiVersion: v1
96kind: PersistentVolumeClaim
97metadata:
98 name: pg-data
99 namespace: production
100spec:
101 accessModes: [ReadWriteOnce]
102 resources:
103 requests:
104 storage: 10Gi
redis-deployment.yaml
105apiVersion: apps/v1
106kind: Deployment
107metadata:
108 name: redis
109 namespace: production
110spec:
111 selector:
112 matchLabels:
113 app: redis
114 template:
115 metadata:
116 labels:
117 app: redis
118 spec:
119 containers:
120 - name: redis
121 image: redis:7-alpine
122 ports:
123 - containerPort: 6379
redis-service.yaml
124apiVersion: v1
125kind: Service
126metadata:
127 name: redis
128 namespace: production
129spec:
130 selector:
131 app: redis
132 ports:
133 - port: 6379
134 targetPort: 6379
nginx-deployment.yaml
135apiVersion: apps/v1
136kind: Deployment
137metadata:
138 name: nginx
139 namespace: production
140spec:
141 selector:
142 matchLabels:
143 app: nginx
144 template:
145 metadata:
146 labels:
147 app: nginx
148 spec:
149 containers:
150 - name: nginx
151 image: nginx:alpine
152 ports:
153 - containerPort: 80
154 volumeMounts:
155 - name: nginx-conf
156 mountPath: /etc/nginx/nginx.conf
157 subPath: nginx.conf
158 volumes:
159 - name: nginx-conf
160 configMap:
161 name: nginx-config
nginx-service.yaml
162apiVersion: v1
163kind: Service
164metadata:
165 name: nginx
166 namespace: production
167spec:
168 type: LoadBalancer
169 selector:
170 app: nginx
171 ports:
172 - port: 80
173 targetPort: 80
prometheus-deployment.yaml
174apiVersion: apps/v1
175kind: Deployment
176metadata:
177 name: prometheus
178 namespace: production
179spec:
180 selector:
181 matchLabels:
182 app: prometheus
183 template:
184 metadata:
185 labels:
186 app: prometheus
187 spec:
188 containers:
189 - name: prometheus
190 image: prom/prometheus
191 ports:
192 - containerPort: 9090
193 volumeMounts:
194 - name: prom-config
195 mountPath: /etc/prometheus
196 - name: prom-data
197 mountPath: /prometheus
198 volumes:
199 - name: prom-config
200 configMap:
201 name: prometheus-config
202 - name: prom-data
203 persistentVolumeClaim:
204 claimName: prom-data
prometheus-service.yaml
205apiVersion: v1
206kind: Service
207metadata:
208 name: prometheus
209 namespace: production
210spec:
211 selector:
212 app: prometheus
213 ports:
214 - port: 9090
215 targetPort: 9090
grafana-deployment.yaml
216apiVersion: apps/v1
217kind: Deployment
218metadata:
219 name: grafana
220 namespace: production
221spec:
222 selector:
223 matchLabels:
224 app: grafana
225 template:
226 metadata:
227 labels:
228 app: grafana
229 spec:
230 containers:
231 - name: grafana
232 image: grafana/grafana
233 ports:
234 - containerPort: 3000
235 volumeMounts:
236 - name: grafana-data
237 mountPath: /var/lib/grafana
238 volumes:
239 - name: grafana-data
240 persistentVolumeClaim:
241 claimName: grafana-data
grafana-service.yaml
242apiVersion: v1
243kind: Service
244metadata:
245 name: grafana
246 namespace: production
247spec:
248 selector:
249 app: grafana
250 ports:
251 - port: 3000
252 targetPort: 3000
prom-pvc.yaml
253apiVersion: v1
254kind: PersistentVolumeClaim
255metadata:
256 name: prom-data
257 namespace: production
258spec:
259 accessModes: [ReadWriteOnce]
260 resources:
261 requests:
262 storage: 20Gi
grafana-pvc.yaml
263apiVersion: v1
264kind: PersistentVolumeClaim
265metadata:
266 name: grafana-data
267 namespace: production
268spec:
269 accessModes: [ReadWriteOnce]
270 resources:
271 requests:
272 storage: 5Gi
configmap.yaml
273apiVersion: v1
274kind: ConfigMap
275metadata:
276 name: app-config
277 namespace: production
278data:
279 REDIS_URL: redis://redis:6379
280 NODE_ENV: production
secret.yaml
281apiVersion: v1
282kind: Secret
283metadata:
284 name: app-secrets
285 namespace: production
286type: Opaque
287stringData:
288 DATABASE_URL: postgres://postgres:secret@postgres:5432/app
289 POSTGRES_PASSWORD: secret
Docker Compose52 lines, 1 file
docker-compose.yml
1services:
2 nginx:
3 image: nginx:alpine
4 ports:
5 - "80:80"
6 volumes:
7 - ./nginx.conf:/etc/nginx/nginx.conf
8 depends_on:
9 - webapp
10 
11 webapp:
12 image: myapp:latest
13 environment:
14 DATABASE_URL: postgres://postgres:secret@db/app
15 REDIS_URL: redis://redis:6379
16 depends_on:
17 - db
18 - redis
19 
20 db:
21 image: postgres:16
22 environment:
23 POSTGRES_DB: app
24 POSTGRES_PASSWORD: secret
25 volumes:
26 - pg-data:/var/lib/postgresql/data
27 
28 redis:
29 image: redis:7-alpine
30 
31 prometheus:
32 image: prom/prometheus
33 volumes:
34 - ./prometheus.yml:/etc/prometheus/prometheus.yml
35 - prom-data:/prometheus
36 ports:
37 - "9090:9090"
38 
39 grafana:
40 image: grafana/grafana
41 ports:
42 - "3001:3000"
43 volumes:
44 - grafana-data:/var/lib/grafana
45 depends_on:
46 - prometheus
47 
48volumes:
49 pg-data:
50 prom-data:
51 grafana-data:

A real production stack with monitoring. The Kubernetes side is 289 lines across 15 files. The Docker Compose file is 51 lines. Every single line of Kubernetes YAML is real configuration that someone on your team needs to understand, maintain, and debug. The empty space on the right tells the whole story.

The pattern is clear

ScenarioKubernetesComposeRatio
App + DB77 lines / 5 files17 lines / 1 file4.5x
App + DB + Redis + Worker135 lines / 9 files32 lines / 1 file4.2x
Full platform + monitoring289 lines / 15 files51 lines / 1 file5.7x

Kubernetes requires 4 to 6x more configuration than Docker Compose for the same workloads. And this comparison is still generous: it does not include the Ingress controller setup, cert-manager for TLS, RBAC policies, NetworkPolicies, nginx ConfigMap content, prometheus ConfigMap content, or the Helm charts most teams use to manage all of this.

More YAML is not the real problem

The line count is a proxy for something deeper: operational complexity. Every Kubernetes resource is something that can break, drift, or be misconfigured. Every file is something a new team member needs to understand before they can debug a production issue at 2 AM.

  • 15 files means 15 things that can go wrong. A typo in a selector label silently breaks service routing. A missing PVC binding leaves your database without storage. A wrong port number in a Service manifest returns 502s that are hard to trace back.
  • Onboarding takes weeks instead of hours. A new developer can read a 51-line docker-compose.yml and understand the entire stack. Understanding 15 Kubernetes manifests, plus how they interact with the cluster's networking, storage, and RBAC, takes serious time.
  • The control plane itself needs maintenance. Docker Compose runs on top of the Docker daemon. Kubernetes needs etcd, the API server, the scheduler, the controller manager, kubelets, and a CNI plugin. Each one needs monitoring, upgrades, and disaster recovery.

When does Kubernetes make sense?

Kubernetes earns its complexity when you actually need what it offers: auto-scaling across nodes, multi-region deployments, service mesh, or running 50+ microservices. If you have a dedicated platform team to absorb the operational cost, it can be the right choice. For teams with 2 to 30 developers running fewer than 20 services, the complexity is overhead, not value.

The bottom line

If your production stack looks like scenarios 1, 2, or 3 above, Docker Compose gives you the same result with a fraction of the configuration, a fraction of the failure modes, and a fraction of the learning curve. Start simple. Move to Kubernetes when the actual limits of Compose force you to, not when a conference talk makes you feel like you should.

Want a deeper comparison? Kubernetes vs Docker Compose: which one does your team actually need? covers cost, operational overhead, and the migration path in detail.

Already running Docker Compose? Read Docker Compose in Production for health checks, zero-downtime deploys, and monitoring.

Get the DevOps checklist for your stack

We send one practical guide per week. No spam, unsubscribe anytime.

Running Kubernetes and wondering if you should be?

Our infrastructure audit evaluates whether your orchestration matches your team size.

Get an audit