Home › Cloud services › Compute
guide low

Containers and Kubernetes

Problem A team running several services must choose how to package and schedule them across machines. This guide answers whether Kubernetes is worth its operational weight or a simpler way to run containers suffices.

Containers package a service with its dependencies; Kubernetes schedules them across machines. Use the smallest orchestrator that does the job.

dockerkuberneteseksgkeaksfargatecloud-run
Overview

A container is a process with its own filesystem image, namespaces and cgroup limits; it starts in milliseconds and runs the same on a laptop and a server. An orchestrator decides which machine runs which container, restarts crashes, rolls out new versions and routes traffic. Kubernetes is the standard orchestrator and also a large operational surface; many teams need only "run this image N times behind a load balancer".

Key facts

Use when

  • Several services, frequent deploys, need for rolling updates, autoscaling and bin-packing.

Avoid when

  • One or two services: use a VM with systemd + Docker Compose, or a container PaaS (Cloud Run, Fly.io, Container Apps).

Deep dive

Options from small to large

OptionWhat you manageCost shape
Docker Compose on one VMHost, deploysVM price only
Nomad / Docker Swarm / KamalHosts, a small control planeVM price only
Cloud Run, Container Apps, App Runner, Fly.ioImages onlyPer request/second, scales to zero
ECS on FargateTask definitionsPer vCPU-s and GB-s
Managed Kubernetes (EKS, GKE, AKS)Nodes, add-ons, YAMLControl plane fee + nodes
GKE Autopilot / EKS Auto ModeYAML onlyPer pod request, premium over nodes
Self-run k8s (k3s, kubeadm, Talos) on HetznerEverythingCheapest hardware, most work

Decision table

SituationChoiceWhy
1-3 services, small teamCompose on a VM, or Cloud Run / Container AppsZero cluster overhead
Spiky HTTP service, idle at nightCloud Run / Container Apps / Lambda containersScale to zero
10+ services, several teamsManaged KubernetesShared deploy model, RBAC, ecosystem (Helm, operators)
Stateful databasesVMs or a managed DB, not k8s, unless you have operator experiencePod rescheduling and network storage fight databases
Game servers per matchKubernetes + Agones, or plain VMs with a fleet managerPer-match allocation, UDP ports
Cheapest steady fleetk3s/Talos on Hetzner bare metal5-10x lower node cost
GPU batch jobsk8s with GPU operator, or SlurmGang 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

namelangrepoinstallverdictlicencehealthas_ofnotes
k3sGok3s-io/k3s`curl -sfL https://get.k3s.iosh -`————
Talos LinuxGosiderolabs/talosbrew install talosctl (client)————immutable OS for Kubernetes nodes
NomadGohashicorp/nomadbrew tap hashicorp/tap && brew install hashicorp/tap/nomad————small orchestrator for containers and VMs
Docker ComposeGodocker/composebrew install docker-compose————multi-container apps on one host
KamalRubybasecamp/kamalgem 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

Builds on

Used by

Systems (7):

Alternatives and related

VMs and bare metalRunning long-lived services, databases or game servers raises the question of renting elastic cloud VMs or whole dedicated servers. This guide answers which to pick for cost, latency and control.Serverless functionsTeams with spiky or low-traffic workloads (webhooks, cron jobs, event handlers) want to avoid running idle servers. This guide answers when functions-on-demand fit and when they cost more or fall short.Infrastructure as codeTeams that create servers and cloud resources by clicking in a console cannot reproduce environments, review changes or recover quickly from disaster. This guide covers managing infrastructure as reviewed files and its risks.ObservabilityOperators of a live system need to know when it is broken and why. This guide answers which signals to collect (metrics, logs, traces), how much, and where to store them.Least privilege and isolationTeams deploying software that touches user data, money or other systems must decide how much access each component, service and person gets, so one compromised part does not expose everything. This guide covers that decision.Software supply chain securityTeams shipping software with third-party dependencies and a deploying CI system must decide how to control what code enters the build and prove what comes out, against malicious packages and compromised build tooling.Self-host vs managedFor each component (database, queue, cache, search, Kubernetes), a team must decide whether to run it themselves or pay a provider to. This guide answers whether the premium is worth the engineer time and risk removed.