Skip to content

13.6.1 — Why Containers Exist

A deployment fails at 11 p.m. The application ran perfectly on the developer's laptop, ran perfectly in the test environment, and now refuses to start on the production server. The error is one line:

ImportError: libssl.so.1.1: cannot open shared object file: No such file or directory

The production server was patched last week. The patch moved OpenSSL from version 1.1 to version 3. A library the application depends on was compiled against the old one. Nobody changed a line of the application. Nobody thought they had changed anything at all.

This is the problem containers were built to end, and it is worth understanding in full before you touch a single Docker command, because every design decision in Docker and later in Kubernetes traces back to it.

1. The world before: one application, one machine

For most of computing's commercial history, running a program in production meant buying a physical server and putting the program on it.

You bought a machine per application, and you sized it for the worst day of the year. A shopping site that handles 200 orders an hour normally and 4,000 on the day of a sale needs the 4,000-order machine, all year. Which means that on an ordinary Tuesday it runs at roughly five percent of what you paid for.

Industry surveys through the 1990s and 2000s put average server utilisation somewhere between 6 and 15 percent. That is not a rounding error, it is most of the money.

And you bought a machine per application for a reason. Putting two applications on one server meant sharing everything: the same filesystem, the same installed libraries, the same versions of the language runtime, the same open ports. If one application needed Python 3.8 and the other needed Python 3.11, one of them lost. If one of them crashed the machine, both went down. If one needed the OpenSSL patch and the other broke under it, somebody had to choose.

So the industry chose isolation by hardware, and paid for it in racks of mostly idle metal.

Then there was the second cost, which was worse and less visible: the servers stopped being alike. A machine set up in March by one engineer and a machine set up in September by another are configured by hand, by two different people, following a wiki page that changed in between. Six months later nobody can say exactly what is installed on either. Fixing one means logging in and changing it, which makes it different again.

The industry name for this is a snowflake server — a machine that has been hand-tuned so many times that it is unique, undocumented, and impossible to rebuild. The test for whether you have one is simple: if the machine died right now, could you recreate it exactly? For most organisations in 2005 the honest answer was no, and the disaster recovery plan was a prayer.

The gap between machines has a name too: configuration drift. Two servers that started identical slowly diverge as patches, hotfixes and emergency changes land on one and not the other. Drift is why "it works on server 3 but not server 7" is one of the oldest sentences in operations.

2. Virtual machines fixed half of it

The first real fix arrived in production form with VMware in the early 2000s, and Chapter 2.9 covers the mechanism. The short version:

A hypervisor is software that pretends to be hardware. It presents each guest operating system with what looks like a complete machine — a CPU, memory, disks, network cards — while actually sharing one physical machine between many guests. The guest OS believes it owns a computer. It does not.

Virtual machinesContainersApp AApp BApp CGuest OS~2 GBGuest OS~2 GBGuest OS~2 GBHypervisorHost OS + kernelPhysical hardware3 kernels running. Boot: ~60 s.Isolation: hardware-enforced.App AApp BApp Clibs only~50 MBlibs only~50 MBlibs only~50 MBContainer runtimeONE shared host kernelPhysical hardware1 kernel running. Start: ~50 ms.Isolation: kernel-enforced.
Figure 1 — The one structural difference. A virtual machine carries its own kernel; a container borrows the host's. Everything else about containers — the speed, the small size, the density, and the weaker isolation — falls out of that single line in the middle of the picture.

What virtual machines genuinely solved: utilisation went from around ten percent to sixty or seventy, because you could pack many guests onto one machine. Provisioning went from weeks of purchasing to minutes of clicking. Whole machines became files you could copy, snapshot and move. This was an enormous advance and it is the technology the entire public cloud is still built on.

What virtual machines did not solve, and this is the part that matters for our story:

A VM is a whole computer, so it costs what a whole computer costs. Two gigabytes of disk before your application is installed. A full boot sequence, taking thirty to ninety seconds. Its own memory reserved for its own kernel, its own copy of every system library, its own package updates to apply, its own security patching. Run forty small services and you are running forty operating systems to run forty programs.

And the dependency problem simply moved inside the VM. The engineer still logs in, still installs Python by hand, still applies a patch that moves OpenSSL from 1.1 to 3. The snowflake did not disappear — it just became a virtual snowflake, and now there are two hundred of them instead of twenty.

The libssl.so.1.1 error from the top of this page happens just as readily inside a virtual machine. Virtualisation solved hardware sharing. It never touched software packaging.

3. The dependency problem, stated exactly

Let us make the problem concrete, because "dependency hell" is a phrase people use without pinning down.

Your team runs two services on one Linux machine:

  • checkout — written three years ago. Needs Python 3.8, and a payment library that was compiled against OpenSSL 1.1.
  • reporting — written last month. Needs Python 3.11, and a data library that requires OpenSSL 3.

These cannot both be satisfied by one machine's filesystem. There is one /usr/lib. There is one /usr/bin/python3. Whichever version of OpenSSL sits at /usr/lib/x86_64-linux-gnu/libssl.so, exactly one of these two services is happy.

The workarounds people used, and why each one hurt:

Install both versions side by side and juggle paths. Python virtual environments, LD_LIBRARY_PATH, alternate install prefixes. This works until it does not: a subprocess spawned without the right environment picks up the wrong library, and the failure appears in a code path that runs once a month.

Give each service its own VM. Correct, and expensive — two operating systems, two patching schedules, two sets of monitoring, for two programs totalling forty megabytes.

Freeze the machine and never patch it. Extremely common, and it is how organisations end up running an OS that stopped receiving security fixes years ago.

The insight that ends this: the reason the two services conflict is that they are sharing the userland — the filesystem, the libraries, the binaries — when the only thing they actually need to share is the kernel.

A program does not care what version of libssl is at /usr/lib on the host. It cares what version is at /usr/lib when it looks. So give each program its own answer to that question.

That is the whole idea. A container is a program running with its own private view of the filesystem, its own process list, its own network stack and its own resource budget, while sharing the one kernel the machine already runs.

4. The lineage: forty-five years of the same idea

Docker did not invent containers. It is genuinely important to know this, because it tells you what Docker did invent, which is the reason it succeeded where earlier attempts stalled.

1979 — chroot. Version 7 Unix gains a system call that changes what a process considers to be /. Point a process at /var/sandbox and its entire filesystem world becomes that directory. This is the first ancestor, and it is still in use today for build environments and recovery shells.

Its limitation was severe: it changed only the filesystem view. The process could still see every other process on the machine, use the machine's network directly, and consume all the CPU it wanted. And a process with root privileges could escape a chroot with a few lines of C, which is why nobody treated it as security.

2000 — FreeBSD jails. Poul-Henning Kamp built jails for a hosting company that wanted to give customers root on a shared machine without giving them root on the machine. A jail isolated the filesystem and the process list and the network interface. This is the first thing that really looks like a container, and it predates Docker by thirteen years.

2004 — Solaris Zones. Sun's version, with resource controls attached, so a zone could be limited in CPU and memory as well as isolated.

2006–2008 — cgroups. Two Google engineers, Paul Menage and Rohit Seth, wrote "process containers" to solve a Google-scale problem: running many jobs from many teams on one machine without one job eating everything. Renamed control groups (cgroups) to avoid confusion with the word container, it merged into the Linux kernel in version 2.6.24 in January 2008. This is the accounting and limiting half of a container.

2002 onward — namespaces. Added to Linux piece by piece over a decade: mount namespaces in 2002, then UTS, IPC, PID, network, user. This is the isolation half of a container.

2008 — LXC. The first tool to wire namespaces and cgroups together into something you could actually call "Linux containers". It worked. Almost nobody outside a handful of infrastructure teams used it, because using it meant understanding namespaces, cgroups, networking and root filesystems, and then writing your own tooling to build and move the root filesystems around.

2013 — Docker. A small platform-as-a-service company called dotCloud had built internal tooling to run customer code in containers. In March 2013 its founder, Solomon Hykes, gave a five-minute lightning talk at PyCon titled "The future of Linux Containers", open-sourced the tool, and within two years the company had renamed itself after it.

2014 — Kubernetes is announced by Google, drawing on a decade of running containers internally (Chapter 13.6.8 tells that story).

2015 — the Open Container Initiative (OCI) is formed under the Linux Foundation, so that the image format and the runtime behaviour become a specification anyone can implement rather than one company's product.

5. So what did Docker actually invent?

Not the isolation. Namespaces and cgroups were in the kernel already, and Docker's first version was a wrapper around LXC.

Docker invented the packaging, the distribution, and the experience. Four things, and each one is the answer to a question LXC never answered:

The image, as a stack of layers. A container's filesystem is built from read-only layers stacked on top of each other, each layer holding only the differences from the one below. Ten images built from the same Ubuntu base share that base on disk and over the network — you download it once. Chapter 13.6.4 takes this apart in full.

The Dockerfile. A plain text file, checked into your repository next to the code, that describes how to build the image from nothing, step by step. This is the moment server configuration became a reviewable artefact. The snowflake dies here: you no longer describe how a machine was set up, you describe how it is always set up, and running the file again produces the same result.

The registry. A server that stores images and hands them out. docker push and docker pull. Docker Hub launched with the tool, so from day one there was somewhere to put an image and somewhere to get one, and "send me your environment" became a one-line command.

The single-command experience. docker run -it ubuntu bash gave you a shell in a fresh Ubuntu in about a second, on any machine, with no configuration. That demo is why Docker spread. People had been arguing about containers for a decade; Hykes shipped something a developer could try in the thirty seconds between two meetings.

The lesson generalises well beyond containers: the underlying capability had existed for years and gone nowhere. What changed the industry was the packaging format, the distribution channel, and the fact that the first command worked.

6. What you actually get, one benefit at a time

One artefact, from laptop to production. The image you built and tested is byte-for-byte the image that runs in production. Not "the same versions". The same bytes, identified by a cryptographic hash. The libssl failure at the top of this page cannot happen, because the container carries its own libssl and never looks at the host's.

Start time in milliseconds. There is no boot. The kernel is already running. Starting a container means creating some namespaces, setting up a filesystem view, and executing your program. Typical container start is 20–100 milliseconds against 30–90 seconds for a VM — a factor of roughly a thousand. This is what makes autoscaling responsive and makes a crashed service recover before a human notices.

Density. No duplicated kernel, no duplicated system libraries, no reserved memory per guest. A machine that hosts 10 virtual machines comfortably hosts 100 containers. For small services this is the difference between one server and ten.

Immutability. You do not patch a running container. You build a new image and replace the container. Nothing accumulates, so there is no drift, and "what is installed on this thing?" has an exact answer that lives in Git.

Honest dependency isolation. Python 3.8 and Python 3.11 on one machine, each in its own container, each certain it owns /usr/bin/python3.

A local environment that matches production. Your service plus a real PostgreSQL plus a real Redis, at production versions, started with one command and thrown away afterwards (Chapter 13.6.7). Compare with installing PostgreSQL on your laptop, where you get whatever version your package manager offers and it stays forever.

A build that is a description, not a ritual. The Dockerfile is code. It is reviewed, versioned, and diffed. When somebody adds a system package, that shows up in a pull request instead of in a memory.

7. What containers do not give you

Every one of these is a real limit, and each is a place where people get hurt by assuming otherwise.

A container is not as strong an isolation boundary as a virtual machine. All containers on a host share one kernel. A flaw in that kernel is a flaw that every container on the machine sits directly against. A VM guest attacking the host has to get through the hypervisor, which exposes a much smaller surface. This is why cloud providers run different customers' workloads in separate VMs, and put containers inside them. If you must run genuinely untrusted code, put a VM boundary around it — that is what sandboxed runtimes like gVisor and Kata Containers exist for (Chapter 8.6.2).

Containers do not make a stateful system stateless. A database in a container is still a database with files that must survive, be backed up, and not be corrupted by two writers. The container adds packaging convenience and nothing else.

Containers do not fix a bad architecture. A tangled application, containerised, is a tangled application that starts faster.

Containers do not, by themselves, run across many machines. One Docker daemon runs containers on one machine. Nothing in Docker decides which of your fifty servers should run this workload, moves it when a server dies, or keeps three copies alive. That gap is exactly what Chapter 13.6.8 is about, and it is why Kubernetes exists.

Containers do not solve Windows-on-Linux or Linux-on-Windows. A container shares the host kernel, so a Linux container needs a Linux kernel. On a Mac or a Windows laptop, Docker quietly runs a small Linux virtual machine and puts your containers inside it (Chapter 13.6.3 shows this). It works, but it is not magic and it explains several performance surprises.

8. Where you are already using this without noticing

Every serverless platform. When you upload a function to a cloud provider, it runs in a container that the provider starts on demand and freezes when idle. The "cold start" people complain about is that container being created (Chapter 13.3).

Every CI system. A GitHub Actions job, a GitLab pipeline stage, a Jenkins agent — each step runs in a fresh container, which is why your build starts from a clean machine every time and why you specify a base image at the top of the workflow file (Chapter 13.7).

Most managed databases and message brokers, inside the provider's own infrastructure.

And a growing amount of desktop software distribution on Linux, where Flatpak and Snap use the same kernel features to ship an application together with the libraries it needs.

What the interviewer will push on

"What is the difference between a container and a virtual machine?" The one-line answer is that a VM virtualises hardware and carries its own kernel, while a container virtualises the operating system and shares the host's kernel. What separates a good answer is naming the consequences rather than the definition: the shared kernel is why containers start in milliseconds and weigh megabytes, and it is also why the isolation is weaker, since a kernel flaw is shared by everything on the host. The common weak answer is "containers are lightweight VMs", which suggests the person has never had to reason about why a cloud provider still puts a VM boundary between customers.

"Why did Docker succeed when LXC existed?" Because the hard part was never the isolation, it was the packaging and distribution. Docker contributed the layered image format, the Dockerfile as a reviewable build description, a registry with push and pull, and a first command that worked in thirty seconds. The tell is that the candidate knows Docker's first release wrapped LXC — that is the fact that shows they have read the history rather than the marketing.

"What problem does a container actually solve for your team?" The strong answer is specific: the artefact tested is the artefact deployed, because the image carries its own userland. Then give the failure it prevents — a host patch moving a shared library out from under an application that was compiled against it. The weak answer lists adjectives: portable, scalable, lightweight.

"Are containers a security boundary?" They are a boundary, not an equivalent one. Shared kernel means shared attack surface, so container escape via a kernel flaw is a real category of vulnerability. For untrusted multi-tenant code, add a VM boundary. Saying "yes, containers are secure" is the answer that ends the conversation badly.

"If containers are so good, why does anyone still run VMs?" Because you need a kernel from somewhere, because different operating systems need different kernels, because hardware-level isolation is genuinely stronger, and because the entire cloud is virtual machines underneath — your Kubernetes nodes are VMs. The two are layers, not competitors.

One thing to volunteer: point out that virtualisation solved hardware sharing and containers solved software packaging, and that they are answers to two different questions. That framing is what makes every follow-up easy, and it is why the modern default is containers inside virtual machines rather than one replacing the other.

Recall

  • The problem is packaging, not hardware. One machine has one /usr/lib, so two applications needing different library versions cannot both be satisfied. VMs solved hardware sharing and left this untouched.
  • A snowflake server is a hand-configured machine nobody can rebuild; configuration drift is two identical machines slowly diverging. The Dockerfile kills both by making the build a reviewable file.
  • The one structural difference: a VM carries its own kernel, a container shares the host's. Everything else follows — milliseconds not minutes, megabytes not gigabytes, high density, and weaker isolation.
  • The lineage: chroot 1979 (filesystem view only) · FreeBSD jails 2000 · Solaris Zones 2004 · cgroups from Google, in the kernel 2008 (limits) · namespaces 2002 onward (isolation) · LXC 2008 · Docker 2013 · Kubernetes 2014 · OCI 2015.
  • Docker invented none of the isolation — its first version wrapped LXC. It invented layered images, the Dockerfile, the registry, and a first command that worked.
  • Real gains: identical artefact by hash from laptop to production · start in ~50 ms · roughly ten times the density of VMs · immutability, so nothing drifts · true dependency isolation · production-matching local environments.
  • Real limits: shared kernel means weaker isolation than a VM, so untrusted code still wants a VM boundary · state is still state · a bad architecture is unchanged · one Docker daemon runs one machine, which is why orchestration exists · Linux containers still need a Linux kernel, so a Mac runs a hidden VM.

Self-test: Why does giving each application its own VM fix the libssl conflict but cost too much? · Which half of a container do cgroups provide, and which half do namespaces? · Name the four things Docker added on top of technology that already existed. · Why do cloud providers still separate customers with VMs? · What does a container give you that a VM does not, and what does a VM give you that a container does not?

Next: 13.6.2 opens the box. A container is not a thing the kernel knows about — it is a process, plus a set of namespaces, plus a cgroup, plus a stacked filesystem, and you will build one by hand without Docker to prove it.