Back to Blog
Infrastructure

Docker and Kubernetes: What Each One Is Actually Doing

Namespaces and cgroups, images and layers, then the control loop that makes a cluster converge — plus the defaults that will page you at 3am.

Published
Reading time
12 min read
Author
Yakhya

A container is not a lightweight virtual machine. It is an ordinary Linux process that has been lied to about what it can see. Two kernel features do the work: namespaces restrict visibility (its own PID tree, network stack, mount table, hostname), and cgroups restrict consumption (CPU shares, memory limits, I/O). There is no guest kernel and no hypervisor — which is why containers start in milliseconds, and also why kernel-level isolation is weaker than a VM's.

Images, layers, and why your build is slow

An image is a stack of read-only filesystem layers plus metadata. Each Dockerfile instruction produces a layer, and layers are cached and shared. That single fact dictates how to write a Dockerfile: put what changes rarely at the top, what changes constantly at the bottom. Copying your whole source tree before installing dependencies invalidates the dependency layer on every code change, which is the most common reason a build that should take fifteen seconds takes four minutes.

dockerfile
# ---- build stage ----
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci                      # cached until dependencies change
COPY . .
RUN npm run build

# ---- runtime stage ----
FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node                       # never run as root
EXPOSE 3000
CMD ["node", "dist/server.js"]
  • Multi-stage builds keep compilers and dev dependencies out of the shipped image — smaller surface, faster pulls.
  • Pin base images by digest, not by a floating tag, if you want reproducible builds.
  • One process per container. If you need a sidecar, run a sidecar; do not install supervisord.
  • Configuration comes from the environment; secrets never go in the image, in a layer, or in a build arg.
  • Handle SIGTERM. A container that ignores it gets killed hard after the grace period, mid-request.

Kubernetes is a control loop

Everything else follows from one idea: you declare desired state, and controllers continuously compare it to actual state and act to close the gap. You do not tell Kubernetes to start a container; you tell it that three replicas should exist, and a controller notices when there are two. Understanding this makes the whole API feel less arbitrary — and it explains why deleting a pod by hand does not accomplish anything.

  • Pod — one or more containers sharing a network namespace and volumes. The unit of scheduling, and disposable by design.
  • Deployment — manages a ReplicaSet, which manages pods, and gives you rolling updates and rollbacks.
  • Service — a stable virtual IP and DNS name that load-balances to a changing set of pods.
  • Ingress / Gateway API — HTTP routing from outside the cluster, TLS termination, host and path rules.
  • ConfigMap and Secret — configuration and credentials injected as env vars or files (Secrets are base64, not encrypted, unless you enable encryption at rest).
  • StatefulSet — stable identities and persistent volumes for things like databases, which mostly you should not run yourself.
  • Namespace, ResourceQuota, NetworkPolicy — the tenancy and blast-radius boundaries.

The settings that cause real outages

Requests and limits are the first. Requests determine scheduling; limits determine enforcement. A missing memory limit lets one pod evict its neighbours; a too-low CPU limit throttles you invisibly, and CPU throttling looks exactly like a slow dependency in your dashboards. Set memory limits equal to requests for predictable QoS, and be cautious with aggressive CPU limits.

Probes are the second. Liveness restarts a container that is stuck; readiness removes it from the Service until it can serve. Pointing liveness at a check that touches the database is a classic self-inflicted outage — the database blips, every pod fails liveness, every pod restarts, and now the database has a thundering herd on top of its original problem. Liveness should test the process, readiness should test the dependencies.

  1. 01Set requests and limits on every container, informed by real usage data rather than a round number.
  2. 02Add a PodDisruptionBudget so a node drain cannot take the whole service with it.
  3. 03Configure the rolling update surge and unavailability explicitly; the defaults are not always what you want.
  4. 04Handle SIGTERM: stop accepting new work, finish in-flight requests, then exit — inside terminationGracePeriodSeconds.
  5. 05Use topology spread constraints so replicas do not all land on one node or in one zone.
  6. 06Add a HorizontalPodAutoscaler on a metric that reflects load, and check that your pod start time is fast enough for it to matter.
  7. 07Keep manifests in Git and apply them through a GitOps controller (Argo CD, Flux) rather than kubectl from laptops.
Kubernetes does not make a fragile application reliable. It makes a reliable application easier to run in many copies — and a fragile one fail in more interesting ways.

Which is the honest closing point: if you run three services, a managed platform will serve you better than a cluster and the year of operational learning it demands. Adopt Kubernetes when you have enough services, enough teams, and enough deployment frequency that a common substrate is cheaper than bespoke setups. Below that line, it is complexity you are paying for and not yet using.

Tags
DockerKubernetesContainersDevOpsLinux
Keep readingAll Posts