---
title: "02 — Underlying Technologies: How the Kernel Actually Makes a Container"
uid: underlying-technologies
tags: ["kernel", "cgroups", "roadmap:docker", "docker", "namespaces", "unionfs", "linux"]
excerpt: "No container magic: namespaces (what a process sees), cgroups (what it can use), and a union filesystem (what its files are) — three old Linux features pointed at one process."
date: 2026-08-13T03:28:15+0000
source: https://www.aveshina.my.id/en/blog/underlying-technologies
---

"Container magic" was the black box I blamed whenever Docker behavior surprised me. Writing it down dissolved the magic into three ordinary Linux kernel features, and the model that clicked is almost disappointingly simple: **a container is an ordinary process that the kernel has wrapped with namespaces (what it can see), cgroups (what it can use), and a union filesystem (what its files are).** [1][2]

The framing worth holding onto is that Docker invented almost none of this. Namespaces, cgroups, and union filesystems all existed in Linux for years before Docker shipped in 2013 [2][3]. What Docker did was package them into one pleasant tool — docker run — so I never have to think about the kernel plumbing. But every now and then a container misbehaves in a way that only makes sense if I remember what's actually happening underneath, so I wrote the plumbing down.

## The three primitives at a glance

```figure
<svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="One process wrapped by three kernel features. At the center a box labelled container process. Above it, namespaces enclose it with a translucent wall labelled what it can see. To the right, cgroups attach a gauge labelled what it can use — CPU, memory, I/O. Below it, a union filesystem is drawn as stacked translucent layers labelled what its files are.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">

    <!-- Namespaces wall -->
    <rect x="160" y="30" width="400" height="100" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5" stroke-dasharray="5 4"/>
    <text x="180" y="52" font-size="12" font-weight="700" fill="#1e1b4b">namespaces — what it can see</text>
    <text x="180" y="68" font-size="10" fill="#475569">PID · NET · MNT · UTS · IPC · USER</text>

    <!-- Process -->
    <rect x="300" y="115" width="120" height="50" rx="8" fill="#1e1b4b" stroke="#6366f1" stroke-width="1.5"/>
    <text x="360" y="138" font-size="12" font-weight="700" fill="#e0e7ff" text-anchor="middle">container process</text>
    <text x="360" y="154" font-size="9.5" fill="#c7d2fe" text-anchor="middle">one ordinary PID on the host</text>

    <!-- Cgroups gauge -->
    <rect x="520" y="100" width="170" height="80" rx="8" fill="#fef9c3" stroke="#ca8a04" stroke-width="1.5"/>
    <text x="605" y="120" font-size="12" font-weight="700" fill="#422006" text-anchor="middle">cgroups — what it can use</text>
    <text x="605" y="140" font-size="10" fill="#475569" text-anchor="middle">CPU · memory</text>
    <text x="605" y="156" font-size="10" fill="#475569" text-anchor="middle">I/O · device access</text>

    <!-- Union FS layers -->
    <g>
      <rect x="200" y="200" width="320" height="16" rx="4" fill="#dcfce7" stroke="#16a34a" stroke-width="1.1"/>
      <rect x="200" y="220" width="320" height="16" rx="4" fill="#bbf7d0" stroke="#16a34a" stroke-width="1.1"/>
      <rect x="200" y="240" width="320" height="16" rx="4" fill="#86efac" stroke="#16a34a" stroke-width="1.1"/>
    </g>
    <text x="540" y="232" font-size="11" font-weight="700" fill="#052e16">union filesystem</text>
    <text x="540" y="248" font-size="10" fill="#475569">what its files are</text>
    <text x="180" y="232" font-size="10" fill="#475569" text-anchor="end">writable</text>
    <text x="180" y="252" font-size="10" fill="#475569" text-anchor="end">read-only layers</text>
  </g>
</svg>
```

Three concerns, three kernel features. Each one answers a different question the kernel has to resolve for an isolated process [1][2][5].

## Namespaces: what the process can see

A **namespace** wraps a global system resource so that a process inside it sees its own private instance of that resource [4]. The kernel maintains separate namespace "rooms," and a process placed in a set of them simply cannot see the resources in the host's or other containers' rooms. Docker leans on six of them:

- **PID** — the process sees itself as PID 1, blind to other processes on the host.
- **NET** — its own network stack: loopback, interfaces, routing table, ports.
- **MNT** — its own mount tree, so the filesystem it sees can differ entirely from the host's.
- **UTS** — its own hostname and domain name.
- **IPC** — isolated inter-process communication (shared memory, semaphores).
- **USER** — a UID/GID mapping, so the process can think it's root inside while mapping to an unprivileged user outside [4].

The part that clicked: namespaces are purely about *visibility*. They don't limit how much CPU or memory a process can burn — they only hide things. Throttling is the next primitive's job.

## cgroups: what the process can use

**Control groups (cgroups)** are the complementary feature — they limit and account for resources a group of processes can use [5]. Where namespaces hide, cgroups cap. Docker uses them to enforce the constraints I pass on the command line:

```
# cap this container at half a CPU and 512MB of RAM
docker run --cpus="0.5" --memory="512m" my-app
```

Those two flags translate into writes to cgroup control files the kernel reads on every scheduling decision. cgroups cover CPU, memory, block I/O, network bandwidth, and device access, and they're the reason a runaway container can't take the whole host down — I told the kernel, in advance, the ceiling [5].

Together namespaces and cgroups are the full isolation story. A container is a process placed in a fresh set of namespaces *and* attached to a cgroup. Hide it, then cap it.

## Union filesystems: what its files are

The third primitive answers a question that stumped me at first: how does Docker store images so efficiently, when a 900MB Python image and a 900MB Node image share 800MB of base layers? The answer is a **union filesystem** — a way to overlay multiple directories (called *layers*) into one virtual tree without copying anything [6][7].

Each line in a Dockerfile creates a new layer. Read a file and the union FS walks the stack top-down and returns the first match; write a file and it lands in the topmost writable layer. The read-only layers below are shared across every image and every container that descends from them, which is why pulling a second image built on node:20 barely downloads anything new — the shared layers are already on disk [1][6]. Common implementations include OverlayFS (the modern default), and historically AUFS, Btrfs, and ZFS [6][7][8][9].

This is also the root of the "ephemeral filesystem" behavior I'll cover in the persistence post: a container's writable layer is thrown away when the container is removed, because the layer was never meant to be the source of truth.

## The Linux prerequisite skills

Before I could read any of this comfortably, I had to admit how much of Docker is "just Linux." The roadmap lists four prerequisite skills, and they all boil down to: containers run Linux processes, so I need to be literate in the environment those processes live in.

- **Shell commands.** ls, cd, ps, grep, find, cat — these are how I poke around inside a container with docker exec, and how I read what a RUN instruction in a Dockerfile is actually doing [10][11].
- **Shell scripting.** Dockerfile RUN lines and entrypoint scripts are shell. Variables, conditionals, and the difference between CMD and an entrypoint script all make more sense once I've written a little Bash [12][13].
- **Package managers.** Base images are Linux distros, so installing a dependency means apt-get (Debian/Ubuntu), dnf/yum (Fedora/RHEL), or apk (Alpine). Cleaning the package cache afterward is how I keep image size down [14][15].
- **Users, groups, permissions.** chmod, chown, and the USER instruction matter because containers run as root by default — a real security smell. Creating a non-root user and switching to it is the baseline hardening I now reach for automatically [16][17].

I list these together because they share one trait: none of them are Docker-specific. They're the cost of entry for working confidently with Linux processes, and a container is just a Linux process.

## How I use this

The payoff shows up in debugging. When a container can't reach another service, I now know to suspect the **NET namespace** — is it on the same Docker network? When it gets OOM-killed, I check the **cgroup** memory limit, not the host's free RAM. When a "saved" file vanishes after a redeploy, I remember the **union FS** writable layer was discarded, and reach for a volume. Naming the primitive underneath the symptom is the whole reason I wrote this down — once I see namespaces, cgroups, and layers as separate knobs, most container mysteries collapse into "which of the three is misconfigured?"

## References

[1] Docker, Inc., "Open Source Components — Underlying Technologies," docker.com, 2024. [Online]. Available: [https://www.docker.com/resources/what-container/#underlying-technologies](https://www.docker.com/resources/what-container/#underlying-technologies)

[2] F. Turkal, "How does Docker actually work? The hard way: A technical deep diving," Medium, 2021. [Online]. Available: [https://medium.com/@furkan.turkal/how-does-docker-actually-work-the-hard-way-a-technical-deep-diving-c5b8ea2f0422](https://medium.com/@furkan.turkal/how-does-docker-actually-work-the-hard-way-a-technical-deep-diving-c5b8ea2f0422)

[3] T. Oberleiter, "Containers — Namespaces, Cgroups and Overlay Filesystem," YouTube, 2022. [Video]. Available: [https://www.youtube.com/watch?v=wJdDWc6zO4U](https://www.youtube.com/watch?v=wJdDWc6zO4U)

[4] Linux man-pages project, "namespaces(7) — overview of Linux namespaces," man7.org, 2024. [Online]. Available: [https://man7.org/linux/man-pages/man7/namespaces.7.html](https://man7.org/linux/man-pages/man7/namespaces.7.html)

[5] Docker, Inc., "Control Groups," docker.com, 2024. [Online]. Available: [https://www.docker.com/resources/what-container/#control-groups](https://www.docker.com/resources/what-container/#control-groups)

[6] Linux Kernel Documentation, "OverlayFS," kernel.org, 2024. [Online]. Available: [https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html](https://www.kernel.org/doc/html/latest/filesystems/overlayfs.html)

[7] AUFS Project, "Advanced Multi-Layered Unification Filesystem," aufs.sourceforge.net. [Online]. Available: [http://aufs.sourceforge.net/](http://aufs.sourceforge.net/)

[8] Btrfs Project, "Btrfs Documentation," btrfs.readthedocs.io, 2024. [Online]. Available: [https://btrfs.readthedocs.io/en/stable/](https://btrfs.readthedocs.io/en/stable/)

[9] ZFS on Linux, "ZFS," zfsonlinux.org, 2024. [Online]. Available: [https://zfsonlinux.org/](https://zfsonlinux.org/)

[10] freeCodeCamp, "The Linux Commands Handbook," freeCodeCamp, 2024. [Online]. Available: [https://www.freecodecamp.org/news/the-linux-commands-handbook/](https://www.freecodecamp.org/news/the-linux-commands-handbook/)

[11] Codecademy, "Shell Commands Every Developer Should Know," codecademy.com, 2024. [Online]. Available: [https://www.codecademy.com/article/command-line-commands](https://www.codecademy.com/article/command-line-commands)

[12] shellscript.sh, "Shell Scripting Tutorial," 2024. [Online]. Available: [https://www.shellscript.sh/](https://www.shellscript.sh/)

[13] linuxconfig.org, "Bash Scripting Tutorial for Beginners," 2024. [Online]. Available: [https://linuxconfig.org/bash-scripting-tutorial-for-beginners](https://linuxconfig.org/bash-scripting-tutorial-for-beginners)

[14] Ubuntu, "APT Package Management Guide," ubuntu.com, 2024. [Online]. Available: [https://ubuntu.com/server/docs/package-management](https://ubuntu.com/server/docs/package-management)

[15] Alpine Linux, "Alpine Package Keeper (apk)," Alpine Wiki, 2024. [Online]. Available: [https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper](https://wiki.alpinelinux.org/wiki/Alpine_Package_Keeper)

[16] Red Hat, "Linux File Permissions Explained," redhat.com, 2022. [Online]. Available: [https://www.redhat.com/en/blog/linux-file-permissions-explained](https://www.redhat.com/en/blog/linux-file-permissions-explained)

[17] Docker, Inc., "Dockerfile reference — USER," Docker Docs, 2024. [Online]. Available: [https://docs.docker.com/reference/dockerfile/#user](https://docs.docker.com/reference/dockerfile/#user)

```quiz
Q: Which kernel primitive answers the question "how much CPU and memory can this container use?"
- namespaces
- cgroups
- union filesystem
correct: 1
explain: cgroups (control groups) limit and account for resource use — CPU, memory, I/O. Namespaces only hide resources; they don't cap them.

Q: A container sees itself as PID 1 and cannot see host processes. Which namespace provides that?
- NET
- PID
- USER
correct: 1
explain: The PID namespace gives the container its own process-number space, so the first process inside is PID 1 and host processes are invisible to it.

Q: Two images share a large base layer. Why does pulling the second one barely download anything?
- Docker re-encodes the layers into a single tarball
- The union filesystem stores layers separately and reuses identical layers across images
correct: 1
explain: A union filesystem overlays read-only layers into one virtual tree. Identical layers are shared across images and containers, so they're downloaded once and reused.

Q: Why is running a container as root by default considered a security smell?
- Because root can bypass cgroup limits
- Because if the process escapes the container boundary, it has root privileges mapped on the host
correct: 1
explain: Namespaces isolate, but a process running as root inside still maps to a privileged identity. Hardening means creating a non-root user (USER instruction) so an escape carries fewer privileges.

Q: Which statement best captures how Docker relates to namespaces, cgroups, and union filesystems?
- Docker invented all three as part of its container runtime
- Docker packages three pre-existing Linux kernel features into one usable tool
correct: 1
explain: The three primitives existed in Linux for years before Docker. Docker's contribution was convenience — bundling them into `docker run` so you never touch the kernel plumbing directly.
```
