Why containers exist
The deployment problem containers were built to solve, told through what came before them.
"It works on my machine"
Every developer has shipped code that worked perfectly on their laptop and broke the moment it touched another machine. The reasons are almost always boring: a different version of Python, a missing environment variable, a library that got upgraded, an OS that handles file paths differently. None of these are bugs in your code — they're differences in the environment your code runs in.
Docker and Kubernetes exist to make that sentence stop being true. This whole course is really about one idea: package an application together with everything it needs to run, so it behaves the same way everywhere — your laptop, a teammate's laptop, a test server, and production.
Before we touch a single Docker command, it's worth understanding what people did before containers existed, because every design decision in Docker is a direct reaction to the pain of what came before.
Attempt 1: "Just install everything on the server"
The earliest approach was to run applications directly on a server (physical or a single big VM), install the language runtime, the dependencies, and the app, and run it. Simple, and it's still how plenty of tiny projects run today.
The problem shows up the moment you run more than one application on that server. Say you have:
- App A needs Python 3.8 and an old version of a library.
- App B needs Python 3.11 and a newer, incompatible version of the same library.
They can't both be installed system-wide at once without conflict. This is called dependency hell — and it gets worse as the number of apps on a shared machine grows. Every deployment risks breaking something else that's already running there.
There's a second problem too: no isolation. If App A crashes and eats all the server's memory, App B goes down with it. One noisy neighbor can take out everything else on the box.
Attempt 2: Virtual machines
The next fix was virtualization. A hypervisor (like VMware or VirtualBox) lets one physical machine pretend to be several independent machines, each with its own full operating system.
Physical Server
├── Hypervisor
│ ├── VM 1 — Full OS + App A + its dependencies
│ ├── VM 2 — Full OS + App B + its dependencies
│ └── VM 3 — Full OS + App C + its dependencies
This genuinely solved the isolation and dependency problems. Each VM has its own kernel, its own filesystem, its own everything — App A and App B can now disagree about Python versions with zero conflict, and if one VM crashes, the others are unaffected.
But VMs are heavy. Each one boots a complete operating system — its own kernel, its own copy of system libraries, its own memory footprint just to exist, often hundreds of MBs to a few GBs, and it takes tens of seconds to minutes to boot. If your laptop can comfortably run 3-4 VMs before it starts crawling, that's the ceiling on how many isolated environments you can have.
This matters more than it sounds like. If a company wants to run 200 small services, spinning up 200 full VMs is enormously wasteful — most of that memory and CPU is spent running duplicate copies of an operating system that does nothing app-specific at all.
Attempt 3: containers
Containers ask a different question: what if we isolate the application, without duplicating the entire operating system underneath it?
A container packages an application with its dependencies (libraries, runtime, config files) but shares the host machine's kernel instead of running its own. It uses OS-level features (which we'll look at in the next lesson) to make each container believe it has an isolated filesystem, network, and process space — while actually running as a regular, lightweight process on the shared kernel.
Physical Server (one shared OS kernel)
├── Container Engine (e.g. Docker)
│ ├── Container 1 — App A + its dependencies
│ ├── Container 2 — App B + its dependencies
│ └── Container 3 — App C + its dependencies
The practical difference from VMs is dramatic:
| Virtual Machine | Container | |
|---|---|---|
| What it packages | Full OS + app | App + dependencies only |
| Boot time | Seconds to minutes | Milliseconds to ~1 second |
| Typical size | Hundreds of MB – GBs | Tens to low hundreds of MB |
| Isolation | Full (separate kernel) | Process-level (shared kernel) |
| Density per machine | Low (a handful) | High (dozens to hundreds) |
Containers don't make VMs obsolete — VMs still give stronger isolation, which matters when you don't trust what's running inside (e.g. multi-tenant cloud infrastructure, which is itself usually VMs running container engines inside them). But for the common case — "I want to package my app and its dependencies and run many of them efficiently" — containers are a much better fit, and that's why they became the default unit of deployment across the industry.
What "container" actually means
Concretely, a container is just:
- A set of files — your application code plus every library, binary, and config file it needs to run. This bundle is called an image (covered properly in the next module).
- Running as an isolated process on the host, using kernel features so it can't see or interfere with other containers, other processes, or the host's own files — even though it's really just a process, like any other, from the host's point of view.
That's genuinely most of the concept. Everything else in Docker — building images, networking containers together, persisting data — is machinery built around that one core idea: package once, isolate cheaply, run anywhere the same way.
What's next
The next lesson looks at the actual Linux kernel features — namespaces and cgroups — that make container isolation possible without a hypervisor. Understanding those two primitives demystifies almost everything that seems "magic" about containers later in this course.