What Happens When You Run a Container
Container runtimes like Docker use namespaces, cgroups, and union filesystems to isolate processes without full virtualization. This article breaks down the three-step pipeline: image layering, filesystem assembly, and process execution.
What Happens When You Run a Container? A Peek Under the Hood
You type docker run python:3.9 and a second later, you’re inside a Python shell. Feels like magic, right? But under the hood, something far more grounded—and clever—is happening. Let me walk you through what container runtimes actually do when they execute an image.
The Basic Sequence (Spoiler: It’s Not That Complicated)
Container runtimes like Docker, containerd, or CRI-O follow a predictable pipeline. It breaks down into three main steps:
- Image pulling and unpacking – Getting the blueprints
- Filesystem preparation – Setting up the isolated environment
- Process execution with isolation – Actually running your code
Let’s look at each one.
Step 1: The Image Isn’t a Single File
When PythonSkillset.com team pulls an image, they’re not downloading one giant blob. A container image is a collection of compressed layers. Each layer represents a set of changes to the filesystem—like adding Python libraries or setting environment variables.
The runtime fetches these layers from a registry (like Docker Hub), verifies their checksums (no tampering), and then extracts them into a graph where layers stack on top of each other. This is possible thanks to union filesystems like OverlayFS or aufs. Layers that already exist locally get reused—so pulling python:3.9 after pulling python:3.8 only fetches the differences.
Step 2: Building the Jigsaw (Filesystem Assembly)
Once all layers are present, the runtime creates a merged mount. Imagine stacking transparent sheets with different parts of a drawing—each sheet has unique content, but when you look from the top, you see the complete picture. The lower layers are read-only, and the top layer (the container’s writable layer) allows changes without altering the base images.
Here’s the key: all containers sharing the same base image (like Ubuntu) point to the same underlying layers on disk. That’s why running 50 python:3.9 containers doesn’t consume 50x disk space. The runtime uses copy-on-write—only when you modify a file inside the container does it copy that file to the writable layer.
Step 3: The Actual Execution (Where the real magic hides)
Now the runtime has a complete, isolated filesystem. But how does it run the process inside that filesystem, not on the host? This involves two Linux kernel features working together:
Namespaces – The Great Wall of Process Isolation
Every container gets its own set of namespaces. Think of them as partitions that hide certain system resources:
- PID namespace – The container sees only its own processes (PID 1 is the application, not the host’s init)
- Mount namespace – The container sees only its own filesystem hierarchy (the merged layers)
- Network namespace – The container gets its own network stack (virtual Ethernet, IP tables, etc.)
- User namespace – Inside the container, you can run as root, but outside you’re an unprivileged user
When the runtime calls clone() (a system call to create new processes), it passes flags like CLONE_NEWNS | CLONE_NEWPID | CLONE_NEWNET. The new process inherits these namespaces and can’t see outside them.
Cgroups – The Bouncer for Resources
Namespaces hide things; cgroups control limits. Using control groups, the runtime sets:
- CPU shares (e.g., this container gets 50% of one core)
- Memory limits (e.g., max 512MB, kill process if exceeded)
- Disk I/O weights
- Network bandwidth
The kernel enforces these limits without the container even knowing about them. If your Python script starts allocating 512MB of RAM and the limit is 500MB, the kernel OOM-kills the process—not your host’s other apps.
The Moment of Truth: exec() into the Container
Once namespaces, cgroups, and the filesystem are ready, the runtime performs an exec system call, replacing the new process’s memory with the entrypoint command (e.g., /bin/bash or python app.py). The process now runs inside the isolated environment, with its root directory pointed at the merged layer.
The runtime then detaches itself (or waits for exit, depending on flags). From the container’s perspective, it’s running on its own machine—unaware that it’s sharing a kernel with hundreds of others.
A Real-World Example from PythonSkillset
When PythonSkillset deploys a microservice, we use containerd directly (not Docker) for production. Developers push images to our registry, and the runtime handles the rest. The critical thing we learned: if a container runs as root inside its user namespace, but the host maps it to a non-privileged user, files created inside the container will be owned by that non-privileged user on the host. This prevents accidental permission escalation when volumes are shared.
So, What’s the Difference Between Runtimes?
Runtimes differ mainly in how they isolate processes:
- runc (used by Docker, containerd) – Standard OCI runtime, uses namespaces + cgroups natively
- crun (used by Podman) – Written in C, faster for large numbers of containers
- gVisor (by Google) – Adds a kernel-level sandbox between the container and host
- Kata Containers – Each container runs in its own lightweight VM
The first two are “safe enough” for most cases. The last two are for high-security environments where you want an extra layer.
Summary
Container runtimes aren’t magic. They:
- Pull and layer image filesystems efficiently using union mounts
- Create isolated namespaces to hide host resources
- Enforce resource limits via cgroups
execinto the isolated environment
Once you understand this, debugging container issues becomes easier. That “permission denied” error when writing to a mounted volume? Probably a user namespace mismatch. That container that keeps getting killed? Check cgroup memory limits.
Next time you run a container, remember—you’re not running a whole system. You’re running one process, surrounded by layers of careful deception crafted by the Linux kernel. And that’s more interesting than magic, because it’s real.
Comments
Questions, corrections, and tips stay visible for everyone reading this page.
Join the discussion
No comments yet
Be the first to leave a note — it helps the next reader.