Infrastructure & Operations · Containers & orchestration

Containers & orchestration

A container packages an application with everything it needs to run, but unlike a VM it shares the host's kernel instead of virtualizing hardware underneath its own — which is why it starts in milliseconds instead of tens of seconds. This page covers what that shared-kernel model actually buys you, how a Dockerfile turns source code into a layered image, where that image lives between build and run, and why running more than a handful of containers forces you toward an orchestrator like Kubernetes. By the end you'll be able to say precisely what a Pod, a Deployment, and a Service each do, and why none of the three is optional.

☺ Explain it like I'm 10

A VM is a house: its own foundation, its own plumbing, its own electrical panel, built from scratch on a plot of land, and it takes real time to construct before anyone can move in. A container is an apartment: it shares the building's foundation, plumbing, and electrical service — the landlord's kernel — and moving a tenant in is just handing over a key to a unit that already has walls. You can build an apartment building far denser than a street of detached houses, and a tenant moves in the moment the key turns, not months later. The trade-off is the same one apartment living has in real life: a problem with the building's shared plumbing (the kernel) can affect every unit at once, in a way it never could across separate houses.

Containers vs. VMs: process isolation, not hardware virtualization

A virtual machine gets its isolation from a hypervisor (KVM, VMware ESXi, Hyper-V) that virtualizes hardware — CPU, memory, disk, network interfaces — and boots a complete guest operating system, kernel included, on top of that virtual hardware. That guest kernel takes real time to initialize (tens of seconds is typical) and consumes real memory and disk just to exist, before the application inside it does anything. Run ten VMs on one host and you're running ten kernels.

A container gets its isolation from the host kernel itself, using two Linux primitives: namespaces (separate views of process IDs, network interfaces, mounts, hostname, and inter-process communication, so a container can't see or touch what's outside it) and cgroups (hard limits on how much CPU and memory a group of processes can consume, so one container can't starve its neighbors). No second kernel boots — a container is just a process, or a small group of processes, launched by a container runtime (containerd, or the lower-level runc that implements the Open Container Initiative's runtime spec) with those namespaces and cgroups applied. Starting one is closer to a fork()+exec() than to booting a machine, which is why start times are measured in milliseconds, not seconds, and why a single host can run hundreds of containers instead of the dozen or so VMs the same hardware would support.

The isolation is real but it is weaker than a VM's: every container on a host shares that one kernel, so a kernel-level vulnerability or a kernel panic is a blast radius of "everything on this host," not "one VM." That's the actual trade a team is making by choosing containers over VMs for a given workload — density and startup speed against a thinner isolation boundary — not a claim that containers are simply VMs done better.

Image layers and a Dockerfile's anatomy

A container image is built as a stack of read-only layers, each one a filesystem diff from the layer below it, combined at run time by a union filesystem (typically OverlayFS) into what looks like one coherent filesystem. Layers are content-addressed and cached: if two images share an identical base layer, a Docker daemon or CI runner that already has that layer doesn't re-download or re-store it. A Dockerfile is the recipe that produces this stack, one instruction at a time:

The snippet below shows a minimal Dockerfile for a small service, followed by the Kubernetes object — covered in full further down this page — that actually runs many copies of the image it produces.

# Dockerfile
FROM node:20-slim
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]

# deployment.yaml — runs the image built above
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout-api
spec:
  replicas: 3
  selector:
    matchLabels: { app: checkout-api }
  template:
    metadata:
      labels: { app: checkout-api }
    spec:
      containers:
        - name: checkout-api
          image: registry.internal/checkout-api:1.4.2
          ports:
            - containerPort: 3000
⚠ Watch out

Every instruction in a Dockerfile after the first change invalidates the build cache for every layer below it, so ordering matters: copying package*.json and running npm ci before copying the rest of the source (as above) means editing application code doesn't force a full dependency reinstall on the next build. Getting this backwards — copying everything, then installing dependencies — turns a ten-second incremental build into a multi-minute one on every single commit.

Registries: where images live between build and run

A built image has to land somewhere a runtime can pull it from, and that somewhere is a container registry — Docker Hub, Amazon ECR, Google Artifact Registry, GitHub Container Registry (ghcr.io), or a self-hosted option like Harbor. A registry is artifact storage in the same sense covered in build and artifact management: the CI pipeline builds an image once, tags it, pushes it to the registry, and every later stage — staging, production, a rollback — pulls that exact same immutable artifact rather than rebuilding it.

An image reference has the shape registry/namespace/repository:tag, for example registry.internal/checkout-api:1.4.2 in the YAML above. A tag is a mutable pointer — someone can push a new image under the same tag tomorrow — while a digest (@sha256:...) pins to one specific, content-addressed image forever. Production manifests that reference a mutable tag like :latest can silently start running a different image than the one that was tested, which is why many teams pin production deployments to a digest, or at minimum a version tag that's never reused, and reserve :latest for local development only.

Why orchestration once you're past a handful of containers

Running one container on one host is a docker run command. Running fifty containers across a dozen hosts, staying up as traffic grows and hosts fail, is a different problem entirely, and it's the reason orchestrators exist. Four needs show up as soon as the container count outgrows what a person can track by hand:

Kubernetes is the dominant answer to all four, but it's not the only one — Docker Swarm and HashiCorp Nomad solve the same problem with a smaller surface area. What all of them share is a control loop: a scheduler that places workloads, a controller that continuously compares desired state to actual state and corrects drift, the same reconciliation model covered for infrastructure generally in infrastructure as code.

Kubernetes' core objects, conceptually

Kubernetes exposes that control loop through a small set of objects. Three matter most for a conceptual grasp of how workloads actually run:

Nothing about Kubernetes replaces the need to configure what runs inside a container image — it decides where and how many copies run, not what's installed on them. That's still the job of the image build itself, or of a configuration-management tool for anything Kubernetes doesn't own; see configuration management for where that line sits. The same pushed image a Deployment references is what a pipeline scanned and a registry stored upstream, and what gets watched once it's running, covered in monitoring and observability.

✓ Checkpoint

1. Why does a container start in milliseconds while a VM typically takes tens of seconds to boot? 2. Put these Dockerfile instructions in the order that keeps builds fast when only application code changes: COPY . ., RUN npm ci, COPY package*.json ./, FROM node:20-slim. 3. What's the difference between a mutable image tag and a digest, and why do production deployments often pin to the latter? 4. What specific problem does a Kubernetes Service solve that a Deployment on its own does not?

Check your answers
  1. A container shares the host's existing kernel and is isolated using namespaces and cgroups, so starting one is close to launching a process; a VM boots an entire separate guest operating system, kernel included, on virtualized hardware before anything inside it can run.
  2. FROM node:20-slim, then COPY package*.json ./, then RUN npm ci, then COPY . . — dependencies get installed and cached before the frequently-changing application code is copied in, so an edit to source code doesn't invalidate the dependency-install layer.
  3. A tag is a mutable pointer that can be repointed to a different image later (like :latest); a digest is a content-addressed hash that pins to one specific, unchangeable image forever. Production deployments pin to a digest, or a never-reused version tag, so what's running is guaranteed to be exactly what was tested, not whatever the tag happens to point to now.
  4. A Deployment keeps a set of Pod replicas running and healthy but each Pod still gets a new IP whenever it's rescheduled; a Service gives every other component in the cluster one stable address and DNS name to reach that set of Pods by, regardless of which individual Pods are currently behind it.