Deep dive
Options from small to large
| Option | What you manage | Cost shape |
| Docker Compose on one VM | Host, deploys | VM price only |
| Nomad / Docker Swarm / Kamal | Hosts, a small control plane | VM price only |
| Cloud Run, Container Apps, App Runner, Fly.io | Images only | Per request/second, scales to zero |
| ECS on Fargate | Task definitions | Per vCPU-s and GB-s |
| Managed Kubernetes (EKS, GKE, AKS) | Nodes, add-ons, YAML | Control plane fee + nodes |
| GKE Autopilot / EKS Auto Mode | YAML only | Per pod request, premium over nodes |
| Self-run k8s (k3s, kubeadm, Talos) on Hetzner | Everything | Cheapest hardware, most work |
Decision table
| Situation | Choice | Why |
| 1-3 services, small team | Compose on a VM, or Cloud Run / Container Apps | Zero cluster overhead |
| Spiky HTTP service, idle at night | Cloud Run / Container Apps / Lambda containers | Scale to zero |
| 10+ services, several teams | Managed Kubernetes | Shared deploy model, RBAC, ecosystem (Helm, operators) |
| Stateful databases | VMs or a managed DB, not k8s, unless you have operator experience | Pod rescheduling and network storage fight databases |
| Game servers per match | Kubernetes + Agones, or plain VMs with a fleet manager | Per-match allocation, UDP ports |
| Cheapest steady fleet | k3s/Talos on Hetzner bare metal | 5-10x lower node cost |
| GPU batch jobs | k8s with GPU operator, or Slurm | Gang scheduling, device plugins |
Code
Minimal production-shaped Deployment: requests, limits, probes, graceful shutdown, non-root.
apiVersion: apps/v1
kind: Deployment
metadata: { name: api }
spec:
replicas: 3
strategy: { rollingUpdate: { maxUnavailable: 0, maxSurge: 1 } }
selector: { matchLabels: { app: api } }
template:
metadata: { labels: { app: api } }
spec:
terminationGracePeriodSeconds: 30
topologySpreadConstraints:
- { maxSkew: 1, topologyKey: topology.kubernetes.io/zone, whenUnsatisfiable: ScheduleAnyway,
labelSelector: { matchLabels: { app: api } } }
securityContext: { runAsNonRoot: true, runAsUser: 10001 }
containers:
- name: api
image: registry.example.com/api@sha256:3f1c... # pin by digest
ports: [{ containerPort: 8080 }]
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { memory: "512Mi" } # no CPU limit: avoids CFS throttling
readinessProbe: { httpGet: { path: /ready, port: 8080 }, periodSeconds: 5 }
livenessProbe: { httpGet: { path: /live, port: 8080 }, periodSeconds: 10, failureThreshold: 6 }
lifecycle: { preStop: { exec: { command: ["sleep", "5"] } } } # let LB drain first
securityContext: { readOnlyRootFilesystem: true, allowPrivilegeEscalation: false }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: api }
spec: { minAvailable: 2, selector: { matchLabels: { app: api } } }
Small multi-stage image:
FROM rust:1.82 AS build
WORKDIR /src
COPY . .
RUN cargo build --release --locked
FROM gcr.io/distroless/cc-debian12:nonroot
COPY --from=build /src/target/release/api /api
ENTRYPOINT ["/api"]
Efficiency
- Cost: a container start is a process start plus an image pull, bound by registry bandwidth and layer size; the kubelet skips the pull when the image is already on the node and the policy is
IfNotPresent (Kubernetes images docs). A managed control plane is a fixed fee per cluster-hour, nodes bill per hour. A CPU limit is enforced by throttling when the container approaches it (Kubernetes resource docs).
- Levers:
- small multi-stage images (distroless) — less to pull, target 10-200 MB (order of) — every rollout
- pin by digest, keep
IfNotPresent — node-cached images skip the pull (Kubernetes images docs) — nodes that already ran the image
- CPU request without a CPU limit — avoids limit-based throttling (Kubernetes resource docs) — latency-sensitive services
- accurate requests — nodes are bin-packed on requests, so fewer nodes (order of) — shared clusters
- scale to zero on Cloud Run-style platforms — no cost while idle (Key facts) — spiky HTTP
- Naive → standard → state of the art:
latest full-OS images, no requests · slow pulls, idle nodes
- distroless image by digest, requests set, rolling update with probes · small pulls, packed nodes
- node autoscaler adding right-sized nodes, scale to zero for spiky services · pay for used capacity (order of)
Implementation notes
- Set memory request = limit (no overcommit) and no CPU limit, only a CPU request; CPU limits cause CFS throttling and tail latency.
- Readiness ≠ liveness: readiness gates traffic; liveness restarts. A liveness probe that checks the DB restarts every pod when the DB blips.
- Handle SIGTERM: stop accepting, finish in-flight requests, exit. The preStop sleep covers the delay before endpoints are removed from load balancers.
- Use a container-native load balancer or Gateway API; avoid one cloud LB per Service (each costs order of $16-20/month on AWS).
- Pin images by digest, scan them, sign them; see Supply chain security.
Pitfalls
- Cross-AZ traffic between pods is billed on AWS/Azure/GCP; chatty services spread over 3 zones pay per GB.
latest tags and mutable images make rollbacks lie.
- JVM/Go/.NET runtimes reading host CPU count instead of the cgroup quota (old runtimes) spawn too many threads.
- DNS (CoreDNS) and conntrack table exhaustion under high connection churn; use NodeLocal DNSCache and keep-alive pools.
- Upgrading the cluster every ~4 months is mandatory (k8s supports ~14 months per minor); extended support costs extra on EKS.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
| k3s | Go | k3s-io/k3s | `curl -sfL https://get.k3s.io | sh -` | — | — | — | — |
| Talos Linux | Go | siderolabs/talos | brew install talosctl (client) | — | — | — | — | immutable OS for Kubernetes nodes |
| Nomad | Go | hashicorp/nomad | brew tap hashicorp/tap && brew install hashicorp/tap/nomad | — | — | — | — | small orchestrator for containers and VMs |
| Docker Compose | Go | docker/compose | brew install docker-compose | — | — | — | — | multi-container apps on one host |
| Kamal | Ruby | basecamp/kamal | gem install kamal | — | — | — | — | deploy containers to plain servers |
Sources
- Brendan Burns et al., Borg, Omega, and Kubernetes, ACM Queue, 2016. paper
- Abhishek Verma et al., Large-scale cluster management at Google with Borg, EuroSys 2015. paper
- Kubernetes documentation, Pod Lifecycle docs and Resource Management for Pods and Containers docs