CONTAINERS
62 lines of Kubernetes YAML vs 16 lines of Docker Compose
April 10, 2026 · 5 min read
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.
deployment.yaml1apiVersion: apps/v12kind: Deployment3metadata:4 name: webapp5 labels:6 app: webapp7spec:8 replicas: 19 selector:10 matchLabels:11 app: webapp12 template:13 metadata:14 labels:15 app: webapp16 spec:17 containers:18 - name: webapp19 image: myapp:latest20 ports:21 - containerPort: 300022 env:23 - name: DATABASE_URL24 valueFrom:25 secretKeyRef:26 name: db-secret27 key: urlservice.yaml28apiVersion: v129kind: Service30metadata:31 name: webapp32spec:33 selector:34 app: webapp35 ports:36 - port: 8037 targetPort: 3000postgres-statefulset.yaml38apiVersion: apps/v139kind: StatefulSet40metadata:41 name: postgres42spec:43 serviceName: postgres44 replicas: 145 selector:46 matchLabels:47 app: postgres48 template:49 metadata:50 labels:51 app: postgres52 spec:53 containers:54 - name: postgres55 image: postgres:1656 ports:57 - containerPort: 543258 volumeMounts:59 - name: pg-data60 mountPath: /var/lib/postgresql/datapostgres-service.yaml61apiVersion: v162kind: Service63metadata:64 name: postgres65spec:66 selector:67 app: postgres68 ports:69 - port: 543270 targetPort: 5432secret.yaml71apiVersion: v172kind: Secret73metadata:74 name: db-secret75type: Opaque76stringData:77 url: postgres://postgres:secret@postgres:5432/app
docker-compose.yml1services:2 webapp:3 image: myapp:latest4 ports:5 - "80:3000"6 environment:7 - DATABASE_URL=postgres://postgres:secret@db/app8 depends_on:9 - db1011 db:12 image: postgres:1613 volumes:14 - pg-data:/var/lib/postgresql/data1516volumes: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.
webapp-deployment.yaml1apiVersion: apps/v12kind: Deployment3metadata:4 name: webapp5spec:6 replicas: 17 selector:8 matchLabels:9 app: webapp10 template:11 metadata:12 labels:13 app: webapp14 spec:15 containers:16 - name: webapp17 image: myapp:latest18 ports:19 - containerPort: 300020 envFrom:21 - configMapRef:22 name: app-config23 - secretRef:24 name: app-secretswebapp-service.yaml25apiVersion: v126kind: Service27metadata:28 name: webapp29spec:30 selector:31 app: webapp32 ports:33 - port: 8034 targetPort: 3000worker-deployment.yaml35apiVersion: apps/v136kind: Deployment37metadata:38 name: worker39spec:40 replicas: 241 selector:42 matchLabels:43 app: worker44 template:45 metadata:46 labels:47 app: worker48 spec:49 containers:50 - name: worker51 image: myapp:latest52 command: ["node", "worker.js"]53 envFrom:54 - configMapRef:55 name: app-config56 - secretRef:57 name: app-secretsredis-deployment.yaml58apiVersion: apps/v159kind: Deployment60metadata:61 name: redis62spec:63 selector:64 matchLabels:65 app: redis66 template:67 metadata:68 labels:69 app: redis70 spec:71 containers:72 - name: redis73 image: redis:7-alpine74 ports:75 - containerPort: 6379redis-service.yaml76apiVersion: v177kind: Service78metadata:79 name: redis80spec:81 selector:82 app: redis83 ports:84 - port: 637985 targetPort: 6379postgres-statefulset.yaml86apiVersion: apps/v187kind: StatefulSet88metadata:89 name: postgres90spec:91 serviceName: postgres92 replicas: 193 selector:94 matchLabels:95 app: postgres96 template:97 metadata:98 labels:99 app: postgres100 spec:101 containers:102 - name: postgres103 image: postgres:16104 ports:105 - containerPort: 5432106 envFrom:107 - secretRef:108 name: db-secrets109 volumeMounts:110 - name: pg-data111 mountPath: /var/lib/postgresql/datapostgres-service.yaml112apiVersion: v1113kind: Service114metadata:115 name: postgres116spec:117 selector:118 app: postgres119 ports:120 - port: 5432121 targetPort: 5432configmap.yaml122apiVersion: v1123kind: ConfigMap124metadata:125 name: app-config126data:127 REDIS_URL: redis://redis:6379128 NODE_ENV: productionsecret.yaml129apiVersion: v1130kind: Secret131metadata:132 name: app-secrets133type: Opaque134stringData:135 DATABASE_URL: postgres://postgres:secret@postgres:5432/app
docker-compose.yml1services:2 webapp:3 image: myapp:latest4 ports:5 - "80:3000"6 environment:7 DATABASE_URL: postgres://postgres:secret@db/app8 REDIS_URL: redis://redis:63799 depends_on:10 - db11 - redis1213 worker:14 image: myapp:latest15 command: node worker.js16 environment:17 DATABASE_URL: postgres://postgres:secret@db/app18 REDIS_URL: redis://redis:637919 depends_on:20 - db21 - redis2223 db:24 image: postgres:1625 volumes:26 - pg-data:/var/lib/postgresql/data2728 redis:29 image: redis:7-alpine3031volumes: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.
namespace.yaml1apiVersion: v12kind: Namespace3metadata:4 name: productionwebapp-deployment.yaml5apiVersion: apps/v16kind: Deployment7metadata:8 name: webapp9 namespace: production10 labels:11 app.kubernetes.io/name: webapp12spec:13 replicas: 114 selector:15 matchLabels:16 app.kubernetes.io/name: webapp17 template:18 metadata:19 labels:20 app.kubernetes.io/name: webapp21 annotations:22 prometheus.io/scrape: "true"23 prometheus.io/port: "3000"24 spec:25 containers:26 - name: webapp27 image: myapp:latest28 ports:29 - containerPort: 300030 resources:31 requests:32 memory: "128Mi"33 cpu: "100m"34 limits:35 memory: "256Mi"36 cpu: "500m"37 envFrom:38 - configMapRef:39 name: app-config40 - secretRef:41 name: app-secrets42 readinessProbe:43 httpGet:44 path: /health45 port: 3000webapp-service.yaml46apiVersion: v147kind: Service48metadata:49 name: webapp50 namespace: production51spec:52 selector:53 app.kubernetes.io/name: webapp54 ports:55 - port: 8056 targetPort: 3000postgres-statefulset.yaml57apiVersion: apps/v158kind: StatefulSet59metadata:60 name: postgres61 namespace: production62spec:63 serviceName: postgres64 replicas: 165 selector:66 matchLabels:67 app: postgres68 template:69 metadata:70 labels:71 app: postgres72 spec:73 containers:74 - name: postgres75 image: postgres:1676 ports:77 - containerPort: 543278 envFrom:79 - secretRef:80 name: db-secrets81 volumeMounts:82 - name: pg-data83 mountPath: /var/lib/postgresql/datapostgres-service.yaml84apiVersion: v185kind: Service86metadata:87 name: postgres88 namespace: production89spec:90 selector:91 app: postgres92 ports:93 - port: 543294 targetPort: 5432postgres-pvc.yaml95apiVersion: v196kind: PersistentVolumeClaim97metadata:98 name: pg-data99 namespace: production100spec:101 accessModes: [ReadWriteOnce]102 resources:103 requests:104 storage: 10Giredis-deployment.yaml105apiVersion: apps/v1106kind: Deployment107metadata:108 name: redis109 namespace: production110spec:111 selector:112 matchLabels:113 app: redis114 template:115 metadata:116 labels:117 app: redis118 spec:119 containers:120 - name: redis121 image: redis:7-alpine122 ports:123 - containerPort: 6379redis-service.yaml124apiVersion: v1125kind: Service126metadata:127 name: redis128 namespace: production129spec:130 selector:131 app: redis132 ports:133 - port: 6379134 targetPort: 6379nginx-deployment.yaml135apiVersion: apps/v1136kind: Deployment137metadata:138 name: nginx139 namespace: production140spec:141 selector:142 matchLabels:143 app: nginx144 template:145 metadata:146 labels:147 app: nginx148 spec:149 containers:150 - name: nginx151 image: nginx:alpine152 ports:153 - containerPort: 80154 volumeMounts:155 - name: nginx-conf156 mountPath: /etc/nginx/nginx.conf157 subPath: nginx.conf158 volumes:159 - name: nginx-conf160 configMap:161 name: nginx-confignginx-service.yaml162apiVersion: v1163kind: Service164metadata:165 name: nginx166 namespace: production167spec:168 type: LoadBalancer169 selector:170 app: nginx171 ports:172 - port: 80173 targetPort: 80prometheus-deployment.yaml174apiVersion: apps/v1175kind: Deployment176metadata:177 name: prometheus178 namespace: production179spec:180 selector:181 matchLabels:182 app: prometheus183 template:184 metadata:185 labels:186 app: prometheus187 spec:188 containers:189 - name: prometheus190 image: prom/prometheus191 ports:192 - containerPort: 9090193 volumeMounts:194 - name: prom-config195 mountPath: /etc/prometheus196 - name: prom-data197 mountPath: /prometheus198 volumes:199 - name: prom-config200 configMap:201 name: prometheus-config202 - name: prom-data203 persistentVolumeClaim:204 claimName: prom-dataprometheus-service.yaml205apiVersion: v1206kind: Service207metadata:208 name: prometheus209 namespace: production210spec:211 selector:212 app: prometheus213 ports:214 - port: 9090215 targetPort: 9090grafana-deployment.yaml216apiVersion: apps/v1217kind: Deployment218metadata:219 name: grafana220 namespace: production221spec:222 selector:223 matchLabels:224 app: grafana225 template:226 metadata:227 labels:228 app: grafana229 spec:230 containers:231 - name: grafana232 image: grafana/grafana233 ports:234 - containerPort: 3000235 volumeMounts:236 - name: grafana-data237 mountPath: /var/lib/grafana238 volumes:239 - name: grafana-data240 persistentVolumeClaim:241 claimName: grafana-datagrafana-service.yaml242apiVersion: v1243kind: Service244metadata:245 name: grafana246 namespace: production247spec:248 selector:249 app: grafana250 ports:251 - port: 3000252 targetPort: 3000prom-pvc.yaml253apiVersion: v1254kind: PersistentVolumeClaim255metadata:256 name: prom-data257 namespace: production258spec:259 accessModes: [ReadWriteOnce]260 resources:261 requests:262 storage: 20Gigrafana-pvc.yaml263apiVersion: v1264kind: PersistentVolumeClaim265metadata:266 name: grafana-data267 namespace: production268spec:269 accessModes: [ReadWriteOnce]270 resources:271 requests:272 storage: 5Giconfigmap.yaml273apiVersion: v1274kind: ConfigMap275metadata:276 name: app-config277 namespace: production278data:279 REDIS_URL: redis://redis:6379280 NODE_ENV: productionsecret.yaml281apiVersion: v1282kind: Secret283metadata:284 name: app-secrets285 namespace: production286type: Opaque287stringData:288 DATABASE_URL: postgres://postgres:secret@postgres:5432/app289 POSTGRES_PASSWORD: secret
docker-compose.yml1services:2 nginx:3 image: nginx:alpine4 ports:5 - "80:80"6 volumes:7 - ./nginx.conf:/etc/nginx/nginx.conf8 depends_on:9 - webapp1011 webapp:12 image: myapp:latest13 environment:14 DATABASE_URL: postgres://postgres:secret@db/app15 REDIS_URL: redis://redis:637916 depends_on:17 - db18 - redis1920 db:21 image: postgres:1622 environment:23 POSTGRES_DB: app24 POSTGRES_PASSWORD: secret25 volumes:26 - pg-data:/var/lib/postgresql/data2728 redis:29 image: redis:7-alpine3031 prometheus:32 image: prom/prometheus33 volumes:34 - ./prometheus.yml:/etc/prometheus/prometheus.yml35 - prom-data:/prometheus36 ports:37 - "9090:9090"3839 grafana:40 image: grafana/grafana41 ports:42 - "3001:3000"43 volumes:44 - grafana-data:/var/lib/grafana45 depends_on:46 - prometheus4748volumes: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
| Scenario | Kubernetes | Compose | Ratio |
|---|---|---|---|
| App + DB | 77 lines / 5 files | 17 lines / 1 file | 4.5x |
| App + DB + Redis + Worker | 135 lines / 9 files | 32 lines / 1 file | 4.2x |
| Full platform + monitoring | 289 lines / 15 files | 51 lines / 1 file | 5.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.