Containers

Lesson 1 of 5

Why Containers?

"A container is a promise: whatever ran here in your test will run there in production — same bits, same behavior, no surprises." — a platform engineer's rule of thumb

Part I taught you the culture of DevOps and how a change travels from a keystroke to production. This Part is about the single most important invention that made that journey reliable: the container. If DevOps is the assembly line, containers are the standardized shipping crate that made global logistics possible — a uniform box that every crane, truck, and ship knows how to handle, regardless of what's inside.

Before containers, "deploying" meant taking software that worked in one carefully-tended environment and hoping it behaved the same in another. It rarely did. The gap between "works on my laptop" and "works in production" swallowed weekends, careers, and launch dates. Containers close that gap by shipping the application and its entire world together, so the environment stops being a variable.

By the end of this chapter you'll be able to explain — to a skeptical colleague or a sharp interviewer — exactly what a container is, how it differs from a virtual machine at the kernel level, which Linux primitives make the magic work, and why the industry chose the container as the fundamental unit of deployment. This is the conceptual bedrock; the next chapters build Docker images and orchestrate them on Kubernetes on top of it.

The big question

Why did an entire industry agree to package every application as a container — and what is a container actually made of, that it can run identically on a laptop, a CI runner, and a thousand production servers?

The "works on my machine" problem

Every engineer has lived this. The code runs perfectly on your laptop. You push it; it breaks in staging. You fix staging; it breaks in production. The infamous shrug — "well, it works on my machine" — is not an excuse, it's a symptom of a real disease: the environment is different everywhere the code runs, and software is exquisitely sensitive to its environment.

That difference has three classic flavors:

  • Environment drift. Your laptop has Node 20.11; the server has 18.4. You have libssl 3; the CI runner has libssl 1.1. A colleague apt install-ed something last Tuesday and nobody wrote it down. Over time, no two machines are truly identical — they drift. Each hand-configured server becomes a unique snowflake that can never be reproduced.
  • Dependency hell. App A needs Python 3.9 and requests 2.28; App B on the same host needs Python 3.11 and requests 2.31. They fight over shared system libraries. Installing one breaks the other. Multiply across a fleet and you get an unmaintainable web of conflicting versions.
  • "It ran in staging but not prod." The most expensive words in operations. Staging had a different OS patch level, a different environment variable, a different file that happened to exist. The code is identical; the context isn't — and context is what actually runs.

Analogy Imagine mailing someone a cake by shipping them a recipe and hoping their kitchen matches yours — same oven, same altitude, same brand of flour, same humidity. That's traditional deployment: you ship instructions and pray the target environment is identical. A container ships the finished cake in a sealed, climate-controlled box. It doesn't matter what the recipient's kitchen looks like — the cake arrives exactly as it left. The environment stops being a variable because you brought your own.

The root cause is that traditional deployment separates the application from its environment. You install the app into a pre-existing machine and inherit whatever that machine happens to be. Containers invert this: they bundle the application with a pinned, declared slice of its environment — the right runtime, libraries, and files — into one immutable artifact. Ship the artifact, and you ship the environment with it.

What a container actually is

A container is a lightweight, isolated, portable package that bundles an application together with everything it needs to run — runtime, system libraries, dependencies, configuration, and files — so that it runs the same way everywhere, independent of the host it lands on.

Three words carry the weight:

  • Isolated — a container has its own view of the filesystem, its own process tree, and its own network interface. From inside, it looks like the process has a whole machine to itself. It can't see or trample the host's other processes.
  • Lightweight — unlike a virtual machine, a container does not carry a full operating system. It shares the host's kernel and packages only what's above it. A container image can be tens of megabytes; it starts in milliseconds.
  • Portable — because the container carries its own dependencies, the same image runs unchanged on your laptop, a teammate's laptop, a CI runner, staging, and production. Build once, run anywhere.

A crucial distinction: an image vs a container. An image is the immutable, on-disk template — a stack of read-only filesystem layers plus metadata (which process to start, which ports to expose). A container is a running instance of an image — the image plus a thin writable layer and the isolation primitives that make it feel like its own machine. The image is the class; the container is the object. You build an image once and run many identical containers from it.

Where this came from The ideas are older than Docker. Unix chroot (1979) first gave a process a fake root directory. FreeBSD jails (2000) and Solaris Zones (2004) added stronger isolation. Google was running everything in containers internally for years — its cgroups technology (originally "process containers," 2006) was contributed to the Linux kernel by Google engineers, and its internal Borg cluster manager scheduled billions of containers, directly inspiring Kubernetes. What Docker did in 2013 wasn't invent containers — it made them usable: a simple CLI, a human-readable Dockerfile, and a registry to share images. It packaged decades of kernel plumbing into a developer experience anyone could learn in an afternoon, and the industry changed within two years.

Containers vs virtual machines — the key comparison

This is the question interviewers reach for, because understanding it means you understand what a container really is. Both containers and VMs isolate workloads, but they virtualize at completely different levels.

A virtual machine virtualizes hardware. A hypervisor (VMware, KVM, Hyper-V) emulates a full machine — virtual CPU, memory, disk, network card — and on top of that you install a complete guest operating system, kernel and all. Every VM carries its own OS. That's powerful (you can run Windows on Linux, or a different kernel entirely) but heavy: gigabytes of disk, hundreds of megabytes of RAM just for the OS, and tens of seconds to boot.

A container virtualizes the operating system. There's no guest OS and no hypervisor. Containers share the host's kernel and are isolated from each other by kernel features (namespaces and cgroups, below). Each container packages only the application and its user-space dependencies — the layers above the kernel. That's why a container is megabytes not gigabytes, starts in milliseconds not seconds, and lets you pack far more workloads onto the same hardware.

Rendering diagram…

Notice the difference in the stacks: every VM repeats a whole Guest OS + Kernel; the containers share one kernel and repeat only the thin Bins/Libs they each need. That single structural difference is the source of every practical advantage below.

Dimension Virtual Machine Container
Virtualizes Hardware (via a hypervisor) The operating system (via the kernel)
Guest OS Full OS + kernel per VM None — shares the host kernel
Size Gigabytes Megabytes (often 10s–100s MB)
Startup time Seconds to minutes (boots an OS) Milliseconds to seconds (starts a process)
Density (per host) Tens Hundreds to thousands
Isolation Strong — hardware-level, separate kernels Process-level via namespaces/cgroups (weaker boundary)
Overhead High (each runs a full OS) Near-native (just process overhead)
Portability Heavy images, slower to move Lightweight, built for build-once-run-anywhere
Kernel choice Any (Windows guest on Linux host, etc.) Must match host kernel family (Linux containers need a Linux kernel)

It's not always either/or In the cloud, you almost always run containers inside VMs. Your Kubernetes worker nodes are VMs (EC2 instances, GCE VMs), and each node runs dozens of containers. You get the VM's hard security boundary between tenants and the container's density and speed within your own trust boundary. This is also the honest answer to the isolation trade-off: because containers share a kernel, a kernel exploit could cross the boundary, so for hostile multi-tenant workloads people add a stronger layer — gVisor (a user-space kernel) or Firecracker microVMs (what AWS Lambda and Fargate use) — to get VM-grade isolation with near-container speed. "Containers or VMs?" is often "containers and VMs."

The Linux primitives underneath

Containers can feel like magic until you see there's no magic — just two long-standing Linux kernel features doing the work. A container is really just an ordinary process on the host that has been given a restricted, private view of the system. Two mechanisms create that view.

Namespaces — isolation (what the process can see)

A namespace partitions a kernel resource so that a process inside it sees only its own slice and believes it's alone on the machine. Linux has several kinds, and a container typically gets its own set of each:

  • PID namespace — its own process-ID tree. Inside the container, your app is PID 1 and can't see (or kill) host processes. ps inside shows only the container's processes.
  • Mount (mnt) namespace — its own filesystem view. The container sees its image's root filesystem, not the host's /. This is chroot's powerful descendant.
  • Network (net) namespace — its own network stack: interfaces, IP address, routing table, ports. Two containers can each bind port 8080 without colliding, because each has a private network namespace.
  • UTS namespace — its own hostname.
  • IPC namespace — its own inter-process communication (shared memory, semaphores).
  • User namespace — its own user/group ID mapping, so root inside the container can map to an unprivileged user outside — a key security boundary.

Bundle these together around a process and, from the inside, it inhabits what looks like a private machine — its own processes, filesystem, network, and hostname — while actually running right beside every other process on the shared host kernel.

cgroups — resource limits (what the process can use)

Namespaces control what a process can see; control groups (cgroups) control what it can use. A cgroup meters and caps a process group's consumption of:

  • CPU — e.g. "this container may use at most 0.5 of a core."
  • Memory — a hard ceiling; exceed it and the kernel's OOM killer stops the container (the dreaded OOMKilled).
  • Block I/O and network — throughput limits.
  • PIDs — a cap on how many processes it can spawn (fork-bomb protection).

Without cgroups, one runaway container could starve every other workload on the host — the "noisy neighbor" problem. With them, you can safely pack hundreds of containers onto one machine, each fenced into its declared share. This is also exactly the knob orchestrators turn: when you set resources.requests and resources.limits in a Kubernetes manifest (Part III), you are configuring cgroups.

Rendering diagram…

Analogy Think of one big office building (the host kernel). Namespaces are the walls, locked doors, and frosted glass — each tenant sees only their own office and believes they have the building to themselves. cgroups are the utility meters and circuit breakers — each office gets a metered allotment of power, water, and bandwidth, and can't hog the whole building's supply. Walls for isolation, meters for resources. A container is just a tenant fenced in by both, sharing the same physical structure as everyone else.

Immutability and portability — build once, run anywhere

Here is where containers connect back to the DevOps principles from Part I. A container image is immutable: once built, it never changes. You don't patch a running container in place — you build a new image and replace the old container entirely. This is the essence of immutable infrastructure, and it's what kills the snowflake server. There's no configuration drift because there's no configuration to drift; the artifact is frozen.

Immutability makes the build-once, promote-many promise real (recall the delivery lifecycle from Chapter 2). You build the image one time in CI, and the exact same bytes — verified by a content hash (digest) — flow through every stage:

Rendering diagram…

This is why "it ran in staging but not prod" essentially disappears: staging and prod run the identical image, byte for byte, verified by digest. If it passed in staging, the artifact that reaches prod is not like the tested one — it is the tested one. You never rebuild per environment; you rebuild only when the source changes. Environment-specific differences (database URLs, secrets) are injected at runtime as configuration, never baked in — a principle we'll formalize when we discuss config and secrets.

In production When an incident hits and you need to roll back, immutable images make it trivial and instant — you re-point traffic at the previous image digest, which still exists in the registry, unchanged. No "un-installing," no reverse migrations of the artifact, no hoping a server reverts cleanly. Compare that to the old world of mutating servers in place, where "rolling back" meant frantically reconstructing a prior state nobody had recorded. The ability to redeploy any prior version in seconds — because every version is a frozen, addressable artifact — is one of the quiet superpowers containers gave operations teams.

Why containers are the perfect deploy unit for DevOps

Pull the threads together and it's clear why the container became the unit of deployment — the thing pipelines produce, registries store, and orchestrators schedule:

  • Dev/prod parity. The same image runs on the laptop and in production, so the environment stops being a source of bugs. This directly serves DevOps' fast-feedback goal — what you test is what you ship.
  • Density and cost. Sharing the host kernel means hundreds of containers per machine instead of tens of VMs. Higher utilization, lower cost.
  • Fast start. Milliseconds to boot means you can scale out under load in seconds, and orchestrators can reschedule a failed workload almost instantly — the foundation of self-healing systems.
  • A uniform, schedulable unit. This is the deepest reason. Once everything is a container with the same shape — an image plus resource requests — a scheduler can treat your web app, your worker, and your database cache identically: "find a host with room, run it, restart it if it dies, move it if the host fails." That uniformity is precisely what Kubernetes exploits (Part III). Containers are what made cluster-scale orchestration tractable.
  • Portability across clouds. Because the container carries its environment, the same image runs on AWS, GCP, Azure, or bare metal — reducing lock-in and making migration a scheduling change, not a rewrite.

A container is, in short, the immutable, self-contained, resource-fenced, fast-starting artifact that every other layer of modern delivery is built to produce, move, and run. Master it and the rest of this volume — pipelines, orchestration, cloud — becomes machinery for handling this one well-designed box.

The ecosystem: Docker, OCI, containerd

A little vocabulary so the landscape isn't confusing:

  • Docker is the tool that popularized containers and is still the most common way developers build images and run them locally. Colloquially people say "Docker container," but Docker is one implementation, not the standard.
  • OCI (Open Container Initiative) is the vendor-neutral standard — a spec for the image format and the runtime — so an image built by Docker runs on any compliant runtime. This is why containers are portable across tools, not just across machines. Standardization is what turned containers from a Docker feature into an industry substrate.
  • containerd is the lightweight core runtime that actually pulls images and manages container lifecycles. It lives inside Docker, and Kubernetes talks to it directly (via the CRI). Below it, runc is the low-level tool that actually creates the namespaces and cgroups — the piece that turns an image into a running, isolated process.

So when Kubernetes "runs a container," the chain is roughly: your OCI image → pulled by containerd → handed to runc → which sets up namespaces + cgroups around your process. Docker is the friendly developer front-end over most of that same machinery.

Think about it If a container is "just a process with namespaces and cgroups," then a container isn't really a thing that exists — it's a configuration of an ordinary Linux process. There is no "container" object in the kernel. Sit with that: the most important deployment unit of the last decade is not a new kind of entity at all, but a clever recombination of features that were already lying around in the kernel. The revolution was in packaging and developer experience, not in physics.

Common Mistakes

  • Thinking a container is a lightweight VM. It's not a tiny machine — it's an isolated process sharing the host kernel. This changes everything: you don't run an init system and a fleet of services inside one container; you run one process per container ("one concern per container").
  • Assuming containers are a hard security boundary. They share the host kernel, so a kernel vulnerability can escape the box. For hostile multi-tenant workloads, add a VM-grade layer (gVisor, Firecracker) — don't rely on namespaces alone.
  • Baking config and secrets into the image. An image is immutable and often shared — hardcoding a database URL or an API key makes it un-portable and leaks secrets. Inject environment-specific config and secrets at runtime.
  • Expecting Linux containers to run on a Windows/Mac kernel natively. They need a Linux kernel; on a Mac or Windows laptop, Docker quietly runs a lightweight Linux VM to host them. The kernel must match the container's OS family.
  • Storing important data inside the container. The writable layer is ephemeral — when the container is replaced (which is constant), that data is gone. Persistent data belongs in a volume or an external database, never in the container's own filesystem.
  • Rebuilding a different image per environment. That breaks build-once-promote-many and reintroduces "works in staging, not prod." Build one image; vary only the injected config.

Thought experiment A teammate says, "Our app fails in production but works perfectly in staging — the container must be broken." You confirm both environments run the same image digest. Since the artifact is byte-for-byte identical, the container itself can't be the difference — so where do you look? (Walk the possibilities: injected environment variables and secrets, mounted volumes and their data, network reachability to the database, cgroup resource limits causing an OOMKilled under prod load, or the host kernel version differing. The value of immutability is exactly that it eliminates the artifact as a variable and forces you to the real culprit — the runtime context around the box.)

Interview Corner — the same idea at three levels. Answer yours; recognise the one above it.
Warm-up
What is a container, and how is it different from a virtual machine?
Model answer — A container is a lightweight, isolated, portable package of an application plus all its dependencies that runs the same everywhere. The key difference: a VM virtualizes hardware — each VM runs a full guest OS with its own kernel on top of a hypervisor, so it's heavy (gigabytes) and slow to boot (seconds). A container virtualizes the operating system — it shares the host's kernel and packages only the app and its libraries, so it's lightweight (megabytes) and starts in milliseconds. Containers give you far higher density; VMs give you a stronger isolation boundary.
Intermediate
What actually makes a container isolated? There's no VM — so how is it separated from the host?
Model answer — Two Linux kernel features. Namespaces partition what the process can see — PID, mount, network, UTS, IPC, and user namespaces give it a private process tree, filesystem, network stack, and hostname, so it thinks it's alone on the machine. cgroups (control groups) limit what it can use — capping CPU, memory, I/O, and process count so one container can't starve the others. A container is really just an ordinary host process wrapped in its own namespaces and fenced by cgroups; there's no separate kernel involved.
Advanced
Why did the container become the standard unit of deployment for cloud-native systems, and what's the trade-off you accept by choosing it?
Model answer — Because it's immutable, self-contained, fast-starting, and — crucially — uniform: once every workload has the same shape (an OCI image plus resource requests), an orchestrator like Kubernetes can schedule, restart, scale, and reschedule anything identically, which is what makes cluster-scale automation tractable. Immutability delivers real dev/prod parity (same image digest everywhere) and instant rollback (re-point to a prior digest). The trade-off is the isolation boundary: sharing the host kernel means a kernel exploit can cross between containers, so for untrusted multi-tenant workloads you add a stronger layer — gVisor or Firecracker microVMs — trading a little of the speed/density back for VM-grade separation.
Company lens — how the same topic gets asked
micro1the AI screen loves the crisp fundamentals here — expect "container vs VM" and "what makes a container isolated" as an opener before moving to Dockerfiles and Kubernetes; naming namespaces and cgroups specifically (not just "it's isolated") is the signal of someone who actually understands the tech.
Googlecontainers are in its DNA — Google engineers contributed cgroups to the Linux kernel, ran everything on the Borg cluster manager for years, and that heritage became Kubernetes; expect depth on how the kernel primitives and scheduling actually work.
Metaoperates enormous fleets with heavy internal container tooling (Twine/Tupperware); expect scale-and-efficiency framing — density, resource isolation, and packing workloads onto shared hosts.
Amazonthe cloud you'll be asked about — know the managed container landscape: ECS (AWS's own orchestrator), EKS (managed Kubernetes), and Fargate (serverless containers, isolated with Firecracker microVMs); be ready to compare them and to explain why Fargate uses microVMs for isolation.

Hands-on On paper (or a whiteboard), draw the two stacks side by side from the bare metal up: for VMs — Hardware → Hypervisor → [Guest OS + Kernel + Libs + App] × N; for containers — Hardware → Host OS + Kernel → Container Runtime → [Libs + App] × N. Label exactly where the kernel lives in each and circle what is duplicated per workload (a whole OS for VMs, just libs for containers). Then, next to a single container box, write the two Linux primitives and what each does: namespaces = isolation (what it can see: PID, mount, net, UTS, IPC, user) and cgroups = limits (what it can use: CPU, memory, I/O, PIDs). If you can reproduce this diagram and the two-primitive annotation from memory, you can answer the most common container interview question cold.

Summary

  • Containers solve the "works on my machine" problem — environment drift, dependency hell, and "ran in staging but not prod" — by shipping the application together with its environment as one immutable, portable artifact.
  • A container is a lightweight, isolated, portable package of an app plus all its dependencies that runs the same everywhere. An image is the immutable template; a container is a running instance of it.
  • The key contrast: VMs virtualize hardware (a full guest OS + kernel each — heavy, slow to boot, strong isolation); containers virtualize the OS (share the host kernel — lightweight, start in milliseconds, higher density, a softer boundary).
  • Under the hood a container is just a host process shaped by two Linux primitives: namespaces (isolate what it can see — PID, mount, network, etc.) and cgroups (limit what it can use — CPU, memory, I/O, PIDs).
  • Immutability + portability deliver build-once, promote-many: the same image digest flows through dev → staging → prod, giving real parity and instant rollback — the perfect, uniform, schedulable unit for DevOps and orchestrators.
  • The ecosystem is standardized by OCI, with Docker as the popular front-end and containerd/runc as the runtime that ultimately sets up the namespaces and cgroups.

Now that you know what a container is and why it won, the next chapter gets concrete: we'll write a real Dockerfile, understand image layers and the build cache, and turn an application into the immutable artifact this chapter described.

Editorial reflection The deepest lesson of the container isn't technical — it's philosophical. For decades we tried to make environments identical by discipline: careful runbooks, configuration management, hoping nobody apt install-ed something off the record. It never fully worked, because humans and time introduce drift. Containers succeeded by changing the strategy from discipline to architecture — instead of asking people to keep environments identical, they made the environment part of the artifact, so identical-ness is guaranteed by construction, not by vigilance. That move — turning a problem you have to remember to get right into one that's impossible to get wrong — is the signature of great engineering, and you'll see it again and again in this volume.

Q&A · 0

Enrol to ask questions and join the discussion.

No questions yet — be the first to ask.