<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>How Docker Works: Container Internals Under the Hood | unkletayo.dev</title>
    <link>https://unkletayo.dev</link>
    <description>How containers work under the hood: namespaces, cgroups v2, overlay2 storage and the runc runtime.</description>
    <language>en</language>
    <managingEditor>contact@unkletayo.dev (Adetayo Akinsanya)</managingEditor>
    <lastBuildDate>Tue, 06 Oct 2026 07:00:00 GMT</lastBuildDate>
    <atom:link href="https://unkletayo.dev/engineering/docker/rss.xml" rel="self" type="application/rss+xml"/>
    <item>
      <title>OCI Specifications Explained: How runc and Container Images Actually Work</title>
      <link>https://unkletayo.dev/engineering/docker/03-open-container-initiative-oci-specifications/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/docker/03-open-container-initiative-oci-specifications/</guid>
      <description>How the Open Container Initiative standards fit together: Image Spec manifests, configuration JSON, layer diff tarballs, and the Runtime Spec filesystem bundle.</description>
      <pubDate>Tue, 06 Oct 2026 07:00:00 GMT</pubDate>
      <category>docker</category>
      <category>oci</category>
      <category>containerstandards</category>
      <category>runc</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/docker/03-open-container-initiative-oci-specifications.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: you should know roughly what a container and an image are (Part 1), have seen the containerd, shim and runc chain from Part 2, and be able to read simple JSON (text made of <code>"key": value</code> pairs, with <code>{ }</code> for objects and <code>[ ]</code> for lists).</p>
<p>Shipping containers work all over the world because everyone agreed on one set of sizes and one kind of lock. A crane in any port can lift a box that was loaded in any other country, and nobody has to know who packed it. Electrical plugs and sockets work the same way: any appliance with the right plug fits any socket that follows the same standard. The Open Container Initiative (OCI) is that kind of agreement for software containers. If a tool follows the agreed format, it can build, move, or run what another tool made.</p>
<p>Here is how that agreement came about.</p>
<p>In 2015, the container ecosystem faced a major risk of fragmentation.</p>
<p>Docker was the dominant container engine, but competing vendors and cloud providers began creating rival container formats and execution runtimes (such as CoreOS's <code>rkt</code> and appc specification). <code>rkt</code> was a container runtime from the company CoreOS, and appc (App Container) was CoreOS's own written format for container images, a rival to Docker's.</p>
<p>If every cloud vendor required a different image format or runtime engine, developers would be trapped rebuilding separate container packages for every cloud infrastructure target.</p>
<p>To prevent vendor lock-in and standardize container primitives, tech leaders formed the <strong>Open Container Initiative (OCI)</strong> under the Linux Foundation. Vendor lock-in means being stuck with one company's product because switching away from it costs too much. The Linux Foundation is a non-profit organization that hosts open-source projects, and the OCI is an industry group it hosts that writes shared, public rules (specifications) for containers.</p>
<p>The OCI maintains three open standards. This article covers the first two in depth and names the third at the end:</p>
<ol>
<li><strong>The OCI Image Specification (Image-Spec)</strong>: Defines how container images are formatted, stored, content-addressed, and archived on disk or in registries. Content-addressed means a file is named by a fingerprint of its contents instead of a name someone chose. A registry is a server that stores container images and hands them out on request (Docker Hub is one).</li>
<li><strong>The OCI Runtime Specification (Runtime-Spec)</strong>: Defines how an unpacked container bundle is executed on a host system by a low-level runtime (such as <code>runc</code>).</li>
<li><strong>The OCI Distribution Specification (Distribution-Spec)</strong>: Defines the HTTP API (the agreed set of web requests) that tools use to push images to a registry and pull them back down. It appears again in the table near the end.</li>
</ol>
<p>Understanding the first two specifications explains how any OCI-compliant runtime (like <code>runc</code>, <code>crun</code>, or <code>gVisor</code>) can execute an image built by Docker, Podman, or BuildKit. Compliant means it follows the specification. <code>crun</code> is a runtime written in C, and <code>gVisor</code> is a Google project that provides an OCI runtime (<code>runsc</code>) which adds an extra layer of isolation between the application and the host operating system. Podman is a container tool that, like Docker, can build and run containers, but it does not need a background daemon (a program that keeps running in the background and waits for commands). BuildKit is the build engine Docker uses to turn a Dockerfile into an image. Because the builder and the runner both read the same file layout, they do not need to know anything about each other.</p>
<hr />
<h2>1. The OCI Image Specification</h2>
<p>Think of an image as a recipe box. It has a table of contents (the index), a card listing the parts of one recipe (the manifest), a card with the cooking settings (the config), and the ingredient bags (the layers).</p>
<p>An OCI container image is not a virtual machine disk image (a VM disk image is a full copy of a whole computer's disk, and an image is not that). It is an archived tarball containing a set of cryptographic, content-addressable JSON files and tar compressed layer diffs.</p>
<p>A tarball is a single file that packs many files and folders together, like a zip file (the <code>tar</code> tool does the packing, and <code>gzip</code> is a separate step that compresses the result). A layer is one step's worth of file changes in an image, and a layer diff is just the files that were added, changed, or removed in that step. Stacking the layers in order gives the full set of files.</p>
<p>Two terms appear throughout this section. SHA-256 is a fingerprint function: it turns any file into a fixed 64-character code, and a different file gives a different code. That code is called a digest. A blob is simply a file stored under the name of its digest.</p>
<p>When you inspect an OCI image layout on disk, you find three top-level entries (<code>index.json</code>, <code>oci-layout</code>, and the <code>blobs/</code> folder), and inside <code>blobs/sha256/</code> four blobs that hold the real content:</p>
<pre><code>[ OCI Image Layout Structure ]
├── index.json               &lt;-- Image Index (List of manifests for multi-arch support)
├── oci-layout               &lt;-- Layout version identifier ({"imageLayoutVersion": "1.0.0"})
└── blobs/
    └── sha256/
        ├── e3b0c44298fc1c... &lt;-- Image Manifest JSON
        ├── 7a8b9c1d2e3f4a... &lt;-- Image Configuration JSON
        ├── 1a2b3c4d5e6f7a... &lt;-- Layer 1 tar.gz payload
        └── 9f8e7d6c5b4a3f... &lt;-- Layer 2 tar.gz payload
</code></pre>
<p>The long hex names are the digests (the fingerprints) of each file, and the hashes in these examples are shortened or made up to illustrate the layout. Multi-arch means one image name that has separate builds for different processor types, such as Intel/AMD (<code>amd64</code>) and ARM (<code>arm64</code>). The index is what lists those builds. The <code>oci-layout</code> file only records which version of the layout format the folder uses.</p>
<hr />
<h3>A. The Image Index (<code>index.json</code>) and the Image Manifest (Manifest Blob)</h3>
<p>The index (<code>index.json</code>) lists manifests, one for each platform build of an image. Each manifest lists one image's config and layers.</p>
<p>The <strong>Image Manifest</strong> acts as the entry point for an image version tag (e.g., <code>nginx:latest</code>). A tag is a human-readable label for one version of an image, so <code>nginx:latest</code> means the image named <code>nginx</code> with the tag <code>latest</code>. The manifest links the image configuration JSON file to its underlying filesystem layers:</p>
<pre><code class="language-json">{
  "schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "digest": "sha256:7a8b9c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b",
    "size": 1472
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b",
      "size": 2814021
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "digest": "sha256:9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e",
      "size": 1420102
    }
  ]
}
</code></pre>
<p>Notice that every referenced file is identified by its SHA-256 Digest. If a layer's contents change by a single byte, its SHA-256 digest changes, enforcing immutable content-addressable storage. Immutable means it cannot be changed. If anyone alters a file, its fingerprint no longer matches the one written in the manifest, so the change is detected, and the same file can safely be shared between many images.</p>
<p>Here is what each key in the manifest means:</p>
<ul>
<li><code>schemaVersion</code> is the version of the manifest format. It is always <code>2</code> for current manifests.</li>
<li><code>mediaType</code> at the top says what kind of document this file is (an OCI image manifest). Everywhere else in the file, <code>mediaType</code> says what kind of file the entry points to: a config JSON, or a gzip-compressed tar layer.</li>
<li><code>config</code> points to the Image Configuration JSON blob.</li>
<li><code>layers</code> is the list of layer blobs, in the order they are stacked.</li>
<li><code>digest</code> is the SHA-256 fingerprint of the file being pointed to, which is also its name under <code>blobs/sha256/</code>.</li>
<li><code>size</code> is the length of that file in bytes.</li>
</ul>
<hr />
<h3>B. The Image Configuration JSON</h3>
<p>While layers store the filesystem files, the <strong>Image Config JSON</strong> defines execution metadata (data about how to run the image, not part of its files) for the target runtime:</p>
<pre><code class="language-json">{
  "architecture": "amd64",
  "os": "linux",
  "config": {
    "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin", "NGINX_VERSION=1.25.1"],
    "Cmd": ["nginx", "-g", "daemon off;"],
    "WorkingDir": "/",
    "ExposedPorts": { "80/tcp": {} }
  },
  "rootfs": {
    "type": "layers",
    "diff_ids": [
      "sha256:c2ad4e0... (Uncompressed Layer 1 SHA)",
      "sha256:e8f1b2a... (Uncompressed Layer 2 SHA)"
    ]
  }
}
</code></pre>
<p>The hashes here are placeholders. Here is what the keys mean:</p>
<ul>
<li><code>architecture</code> is the processor type the image was built for. <code>amd64</code> is the 64-bit Intel/AMD type.</li>
<li><code>os</code> is the operating system the image expects.</li>
<li><code>Env</code> sets environment variables, which are named settings a program can read. <code>PATH</code> is the list of folders the system searches when you type a command name.</li>
<li><code>Cmd</code> is the command to run when the container starts. In <code>"daemon off;"</code>, a daemon is a program that detaches and keeps running in the background. This nginx setting tells nginx to stay in the foreground instead, because a container exits when its main process exits. (This is nginx's own use of the word, not Docker's daemon.)</li>
<li><code>WorkingDir</code> is the folder the command starts in.</li>
<li><code>ExposedPorts</code> records that the program listens on port 80 over TCP. It is a note for people and tools, and does not open the port by itself.</li>
<li><code>rootfs</code> lists the layers. Each entry in <code>diff_ids</code> is the SHA-256 of one layer after it has been uncompressed.</li>
</ul>
<p>That last point means each layer has two different fingerprints. The <code>digest</code> in the manifest is the fingerprint of the compressed file (the <code>tar+gzip</code> blob that is stored and downloaded). The <code>diff_ids</code> entry in the config is the fingerprint of the same layer's tar once uncompressed, which is what matters after you unpack it.</p>
<hr />
<h2>2. The OCI Runtime Specification &amp; Filesystem Bundle</h2>
<p>The image is the packed, stored form, like a zipped game. The bundle is the unpacked, ready-to-launch form: the game's installed folder plus its launch settings.</p>
<p>While the Image Spec governs static image archives, the <strong>OCI Runtime Specification</strong> governs how a container is executed at runtime.</p>
<p>To execute a container, an OCI-compliant runtime like <code>runc</code> requires an OCI Filesystem Bundle. A filesystem is the organized tree of files and folders on a disk. Part 2 followed the chain <code>containerd</code>, then the shim, then <code>runc</code>, and said the shim hands <code>runc</code> an OCI bundle. This part opens that bundle.</p>
<p>An OCI Bundle is a simple directory on disk containing exactly two things:</p>
<pre><code>[ OCI Bundle Directory ]
├── config.json         &lt;-- Execution configuration required by runc
└── rootfs/             &lt;-- Unpacked root filesystem directory
</code></pre>
<p><code>rootfs/</code> is the folder the container will see as its entire disk, holding <code>/bin</code>, <code>/etc</code>, and so on. The layers from the image, unpacked and stacked, end up here.</p>
<hr />
<h3>The <code>config.json</code> Runtime Contract</h3>
<p>When <code>containerd</code> unpacks an OCI image for <code>runc</code>, it reads the Image Config JSON and translates it into an OCI Runtime <code>config.json</code>.</p>
<p>This is the step where the stored form (the image) becomes the runnable form (the bundle). Recapping Part 2: <code>containerd</code> fetches the image's files and manages the container, the shim stays beside the container and calls <code>runc</code>, and <code>runc</code> reads the bundle to set up and start the process. The translation maps image settings to runtime settings. Image <code>Cmd</code> becomes <code>process.args</code>, image <code>Env</code> becomes <code>process.env</code>, and image <code>WorkingDir</code> becomes <code>process.cwd</code>. <code>ExposedPorts</code> has no counterpart here, because networking is set up by the container engine and not by this file.</p>
<p>This file does not name system calls. It lists the settings <code>runc</code> uses when it asks the Linux kernel to isolate and start the process. The kernel is the core of the operating system, and a system call is a request a program makes to it. The spec describes the file as the container's process, its root filesystem, mounts, namespaces, cgroup resource limits, capabilities, and more:</p>
<pre><code class="language-json">{
  "ociVersion": "1.0.2",
  "process": {
    "terminal": false,
    "user": { "uid": 0, "gid": 0 },
    "args": ["nginx", "-g", "daemon off;"],
    "env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"]
  },
  "root": {
    "path": "rootfs",
    "readonly": true
  },
  "linux": {
    "namespaces": [
      { "type": "pid" },
      { "type": "network" },
      { "type": "ipc" },
      { "type": "uts" },
      { "type": "mount" }
    ],
    "resources": {
      "memory": { "limit": 536870912 }
    }
  }
}
</code></pre>
<p>Here is what each part of the file means. This example is shortened, and the spec also requires a <code>process.cwd</code> entry in a full file.</p>
<ul>
<li><code>ociVersion</code> is the version of the Runtime Spec the file follows.</li>
<li><code>process</code> is what to run and as whom. <code>terminal: false</code> means no terminal is attached. <code>user</code> gives the user and group numbers (<code>uid</code> and <code>gid</code>), where <code>0</code> is the all-powerful root user. <code>args</code> is the command line to start, and <code>env</code> is its environment variables.</li>
<li><code>root</code> is the folder to use as the container's disk. <code>readonly: true</code> means the container cannot write to it.</li>
<li><code>linux.namespaces</code> are the private views to create. A namespace gives the process its own view of one thing: <code>pid</code> is its own process list, <code>network</code> its own network, <code>ipc</code> its own inter-process messaging, <code>uts</code> its own hostname, and <code>mount</code> its own set of mounted filesystems.</li>
<li><code>linux.resources.memory.limit</code> is the cgroup memory cap in bytes. A cgroup is a kernel feature that caps resources such as memory for a group of processes. $512 \times 1024 \times 1024 = 536{,}870{,}912$ bytes, which is 512MB.</li>
</ul>
<p>Part 2 showed the shim calling <code>runc create</code>, which sets up the container without running your program yet. <code>runc start</code> then runs it. <code>runc run</code> does both in one command: it creates the container and starts it. That is why the line below says <code>run</code>.</p>
<p>When you invoke <code>runc run my-container</code>, <code>runc</code> parses this <code>config.json</code> file, configures the requested Linux namespaces and cgroup memory limits (<code>536870912</code> bytes = 512MB), enters <code>rootfs</code>, and executes <code>execve("nginx")</code>. As in Part 2, <code>execve</code> swaps the current process for the <code>nginx</code> program.</p>
<hr />
<h2>OCI Architectural Decoupling Matrix</h2>
<p>Decoupled means each part can be swapped for another without changing the others. A protocol is an agreed way for two programs to talk. In the table, the middle column is the spec, and the third column names example tools that implement it. <code>runc</code> is the reference runtime for the Runtime Spec, and <code>distribution/distribution</code> (the CNCF Distribution project) is the reference registry for the Distribution Spec. BuildKit and Docker Engine are widely used tools that produce OCI images, not reference implementations.</p>
<table>
<thead>
<tr>
<th>Component</th>
<th>Standard Specification</th>
<th>Example Tools / Implementations</th>
<th>Responsibilities</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Container Image Format</strong></td>
<td>OCI Image Specification</td>
<td>BuildKit / Docker Engine</td>
<td>Defines image layers, digests, manifests, and config JSON</td>
</tr>
<tr>
<td><strong>Container Execution</strong></td>
<td>OCI Runtime Specification</td>
<td><code>runc</code> / <code>crun</code> / <code>gVisor</code></td>
<td>Converts OCI bundle (<code>config.json</code> + <code>rootfs/</code>) into isolated Linux process</td>
</tr>
<tr>
<td><strong>Registry Protocol</strong></td>
<td>OCI Distribution Specification</td>
<td>CNCF Distribution / Docker Registry</td>
<td>Defines HTTP API for pushing and pulling content-addressed blobs</td>
</tr>
</tbody>
</table>
<hr />
<h2>Summary</h2>
<p>The Open Container Initiative standardizes containerization across vendor ecosystems. In short, the Image Spec is how a package is stored and labeled, and the Runtime Spec is how an unpacked package is started:</p>
<ul>
<li><strong>OCI Image Spec</strong> defines content-addressable manifests, configuration JSON, and tarball layer diffs.</li>
<li>OCI Runtime Spec defines the filesystem bundle (<code>config.json</code> + <code>rootfs/</code>) passed to low-level runtimes.</li>
<li><code>runc</code> reads the OCI bundle configuration to configure namespaces and cgroups before executing the target binary.</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>What Happens When You Type docker run? From CLI to containerd to runc</title>
      <link>https://unkletayo.dev/engineering/docker/02-docker-toolchain-cli-daemon-containerd-runc/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/docker/02-docker-toolchain-cli-daemon-containerd-runc/</guid>
      <description>Follow one docker run command through the docker CLI, the dockerd daemon, containerd over gRPC, containerd-shim and runc, the pieces that actually start a container.</description>
      <pubDate>Tue, 29 Sep 2026 07:00:00 GMT</pubDate>
      <category>docker</category>
      <category>architecture</category>
      <category>containerd</category>
      <category>runc</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/docker/02-docker-toolchain-cli-daemon-containerd-runc.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: you should know what a container and an image are (part 1 of this series) and be able to type a command into a terminal. You do not need to know anything about operating systems. Every unfamiliar term is explained where it first appears.</p>
<p>You log into a production host (the computer, often a remote Linux server, that runs your containers) and execute a simple command:</p>
<pre><code class="language-bash">docker run -d --name web-server -p 80:80 nginx
</code></pre>
<p>The flags are small instructions to Docker. <code>-d</code> runs the container in the background so your terminal is free again. <code>--name web-server</code> gives the container a name you can use later. <code>-p 80:80</code> forwards port 80 on the host to port 80 inside the container (a port is a numbered doorway for network traffic, and web servers listen on port 80 by default). <code>nginx</code> is the image to run, and Nginx is a widely used web server program.</p>
<p>The command returns a 64-character container ID, and Nginx starts serving web traffic.</p>
<p>Later, an engineer performs a rolling software update on the Docker system daemon:</p>
<pre><code class="language-bash">systemctl restart docker
</code></pre>
<p><code>systemctl</code> is the Linux command that starts, stops and restarts background services, and this one restarts the Docker background program. A daemon is a program that runs in the background with no window, waiting for requests. <code>dockerd</code> is the daemon that does Docker's work.</p>
<p>While the <code>dockerd</code> daemon restarts, <strong>not a single HTTP request to the running Nginx container drops</strong>. The web server keeps running uninterrupted.</p>
<p>How does Docker restart its main control daemon without killing active running containers?</p>
<p>To answer this, we must examine the multi-layered process architecture that executes underneath the <code>docker</code> command line interface (CLI, a program you control by typing commands): CLI to <code>dockerd</code> to <code>containerd</code> to <code>containerd-shim</code> to <code>runc</code>. The answer arrives in the process tree section near the end of this article.</p>
<p>An everyday picture helps. Think of a restaurant. The host who takes your order is the CLI. The manager is <code>dockerd</code>, who sets up the table and the bill. The kitchen lead is <code>containerd</code>, who runs each dish from start to finish. A cook assigned to your table is the shim, and the builder who prepares the cooking station and then leaves is <code>runc</code>. If the manager goes home, the cook who is already cooking your dish keeps going. Each name in the chain is a separate program that hands work to the next one, and the rest of the article walks down that chain.</p>
<hr />
<h2>The Architecture of the Process Chain</h2>
<p>Modern Docker is not a single monolithic process. It is a collection of modular tools connected via UNIX sockets and gRPC interfaces.</p>
<p>A UNIX socket is a special file that two programs on the same machine use to send messages to each other. gRPC is a standard way for one program to call another program's functions by sending structured messages. A process is one running program, and every process is started by a parent process.</p>
<p>Read the diagram below from top to bottom. Each box receives a request from the box above it and passes work to the box below it. The paths like <code>/usr/bin/docker</code> show where each program file lives on a Linux disk, and you do not need to memorize them.</p>
<pre><code>[ User Shell ]
      |
      |  docker run nginx
      v
+-------------------------------------------------------------------+
| 1. Docker CLI (/usr/bin/docker)                                   |
+-------------------------------------------------------------------+
      |
      |  HTTP REST API payload over /var/run/docker.sock
      v
+-------------------------------------------------------------------+
| 2. Docker Daemon (/usr/bin/dockerd)                               |
|    - Handles image builds, network bridges, volume management     |
+-------------------------------------------------------------------+
      |
      |  gRPC Requests over /run/containerd/containerd.sock
      v
+-------------------------------------------------------------------+
| 3. containerd Daemon (/usr/bin/containerd)                        |
|    - Manages image snapshots, container lifecycles, content store |
+-------------------------------------------------------------------+
      |
      |  Forks a light daemonless shim process per container
      v
+-------------------------------------------------------------------+
| 4. containerd-shim (/usr/bin/containerd-shim-runc-v2)             |
|    - Holds open stdin/stdout/stderr file descriptors              |
|    - Manages container exit status codes                          |
+-------------------------------------------------------------------+
      |
      |  Executes OCI Bundle
      v
+-------------------------------------------------------------------+
| 5. runc CLI (/usr/bin/runc)                                       |
|    - Sets up Linux namespaces &amp; cgroups via libcontainer          |
|    - Executes entrypoint binary (PID 1)                           |
|    - Replaces itself with that binary (execve), so it is gone     |
+-------------------------------------------------------------------+
      |
      v
+-------------------------------------------------------------------+
| 6. Containerized Process (nginx: master process)                  |
+-------------------------------------------------------------------+
</code></pre>
<p>In plain words, the diagram says this:</p>
<ol>
<li>You type <code>docker run nginx</code>. The CLI turns it into a web-style request and sends it through the socket file <code>/var/run/docker.sock</code>.</li>
<li><code>dockerd</code> receives the request. It prepares the parts around the container (images, networking, storage) and then asks <code>containerd</code> to run it, using gRPC over <code>/run/containerd/containerd.sock</code>.</li>
<li><code>containerd</code> manages the container's life: fetching its files and deciding when it starts and stops. For each container it starts one small helper process, the shim.</li>
<li>The shim stays beside the container. It keeps the container's input and output connected (stdin is where a program reads input, stdout is where it prints normal output, and stderr is where it prints errors) and records how the container ends. It calls <code>runc</code> to do the actual setup.</li>
<li><code>runc</code> asks the Linux kernel (the core of the operating system) to build the isolated environment, then turns itself into the container's program. Namespaces and cgroups, explained in section 5, are how that isolation is built.</li>
<li>The last box is the container's own program, here the Nginx master process, which is the main Nginx process that starts the others.</li>
</ol>
<p>The shim's real file name is <code>containerd-shim-runc-v2</code>. The <code>containerd-shim</code> in the box title and in the process tree later is the same program, shortened. Tools that list processes often show a shortened name, because Linux keeps only the first 15 characters of a process name, and <code>containerd-shim</code> is exactly 15 characters.</p>
<hr />
<h2>1. Docker CLI (<code>/usr/bin/docker</code>)</h2>
<p>The <code>docker</code> command is a client-side Go binary. A binary is a ready-to-run program file, Go is the programming language it was written in, and client-side means it only sends requests and does not do the work itself. It performs no container management or namespace creation itself.</p>
<p>When you type <code>docker run</code>, the CLI translates flags (<code>-d</code>, <code>-p 80:80</code>) into an HTTP JSON payload and transmits it across a local UNIX domain socket at <code>/var/run/docker.sock</code> to the Docker Daemon. HTTP is the request and reply format web browsers use, JSON is a text format for structured data such as <code>{"name": "web"}</code>, and a payload is the data being sent. The socket is a file path standing in for a website address, so the request never leaves the machine:</p>
<pre><code class="language-http">POST /v1.43/containers/create?name=web-server HTTP/1.1
Host: localhost
Content-Type: application/json

{
  "Image": "nginx",
  "ExposedPorts": { "80/tcp": {} },
  "HostConfig": {
    "PortBindings": { "80/tcp": [{ "HostPort": "80" }] }
  }
}
</code></pre>
<p><code>POST</code> means "create something", and <code>/containers/create</code> names what to create. The <code>v1.43</code> is the version number of Docker's API, and the <code>name=web-server</code> part comes from your <code>--name</code> flag. An API (application programming interface) is a set of web-style addresses a program exposes so that other programs can ask it to do things, and a REST API is one built in that style. In the body, <code>80/tcp</code> means port 80 using TCP, the usual protocol for web traffic.</p>
<hr />
<h2>2. Docker Daemon (<code>dockerd</code>)</h2>
<p>The <code>dockerd</code> process acts as the high-level orchestrator for host-level container features. In the restaurant picture, it is the manager who sets up everything around the dish:</p>
<ul>
<li><strong>Build Engine</strong>: Translates Dockerfile instructions into image layers. A Dockerfile is a text recipe of steps for building an image, an image is a read-only package of files plus start settings, and a layer is one step's file changes stacked on the previous ones.</li>
<li><strong>Virtual Networking</strong>: Allocates IP addresses, updates iptables rules, and sets up virtual bridge interfaces (<code>docker0</code>). An IP address is the numeric address that identifies a machine on a network, and each container gets its own. iptables is Linux's built-in list of rules for filtering and routing network traffic. A virtual bridge is a software network switch that connects containers to each other and to the host.</li>
<li><strong>Volume Management</strong>: Mounts host directories or cloud block storage into container paths. Mounting makes a folder or disk appear at a chosen path inside the container, and a host directory is a folder on the real machine.</li>
<li><strong>REST API Server</strong>: Authenticates requests and exposes endpoints.</li>
</ul>
<p>Once <code>dockerd</code> completes high-level network and storage preparation, <strong>it hands off container execution to <code>containerd</code></strong>.</p>
<hr />
<h2>3. <code>containerd</code></h2>
<p>If <code>dockerd</code> is the manager who takes the customer's requirements, <code>containerd</code> is the kitchen lead who runs each dish from start to finish.</p>
<p><code>containerd</code> is an industry-standard, CNCF-graduated container runtime daemon. CNCF is the Cloud Native Computing Foundation, a non-profit that hosts open-source infrastructure projects, and graduated is its highest maturity level. A container runtime is the software that actually starts and manages containers.</p>
<p>While <code>dockerd</code> handles high-level user features, <code>containerd</code> manages low-level container lifecycle execution:</p>
<ul>
<li>Pulling and pushing container images from registries. A registry is a server that stores images for download, and Docker Hub is one.</li>
<li>Unpacking image layers into root filesystems using OverlayFS snapshotters. A root filesystem is the folder tree that becomes the container's <code>/</code>. OverlayFS is a Linux feature that stacks read-only layers into one combined view, and a snapshotter is the <code>containerd</code> component that prepares that view.</li>
<li>Managing container execution state (start, stop, pause).</li>
</ul>
<p><code>containerd</code> listens on a gRPC socket at <code>/run/containerd/containerd.sock</code>. It can operate completely independent of Docker (for example, Kubernetes uses <code>containerd</code> directly via CRI without <code>dockerd</code>). Kubernetes is software that runs many containers across many machines, and CRI (Container Runtime Interface) is the interface it uses to ask a runtime to start containers.</p>
<hr />
<h2>4. <code>containerd-shim</code></h2>
<p>A shim is a small adapter that sits between two parts. Here it is a tiny babysitter program that stays alive next to one container even if its bosses restart.</p>
<p>When <code>containerd</code> is instructed to start a container, it does <strong>not</strong> launch the container process directly under its own process tree. Every running program is a process started by a parent process, and together they form a tree. Fork means to create a new child process.</p>
<p>Instead, <code>containerd</code> forks an intermediate process called <code>containerd-shim</code>. The file on disk is named <code>containerd-shim-runc-v2</code>, which is the same program.</p>
<h3>Why the Shim Exists:</h3>
<ol>
<li><strong>Daemonless Container Execution</strong>: The shim acts as the parent process for the container's PID 1. If <code>dockerd</code> or <code>containerd</code> crash or undergo system upgrades, the shim keeps running, keeping the container process alive! A PID is the number Linux gives each process, and PID 1 here means the first process inside the container, the one the container lives and dies with. The parent matters because a process is tied to the one that started it. If the container were a direct child of <code>dockerd</code>, stopping <code>dockerd</code> would leave it orphaned, with no parent left to track it. Because the container's parent is the shim, and the shim is not <code>dockerd</code>, restarting <code>dockerd</code> does not touch the container. This is the answer to the question at the top, and the process tree below shows it.</li>
<li><strong>I/O Streaming</strong>: Holds open the standard I/O pipes (<code>stdin</code>, <code>stdout</code>, <code>stderr</code>) so logs are captured even if the daemon restarts. Stdin is where a program reads input, stdout is where it prints normal output, stderr is where it prints errors, and a pipe carries that data between programs. Each of these is a file descriptor, a small number a process uses to refer to an open file or pipe.</li>
<li><strong>Exit Code Reporting</strong>: Waits for the container PID 1 process to terminate and reports its exit code back to <code>containerd</code>. An exit code is the number a program returns when it ends, and 0 usually means success.</li>
</ol>
<hr />
<h2>5. <code>runc</code></h2>
<p><code>runc</code> is like the builder who sets up a sealed room (the container), moves the program in, and leaves. It does not stay to supervise.</p>
<p><code>runc</code> is the reference implementation of the Open Container Initiative (OCI) Runtime Specification. A specification is a written rulebook, and a reference implementation is a program that shows how to follow it. OCI is a group that writes shared standards for containers, and part 3 covers it.</p>
<p>When <code>containerd-shim</code> starts, it invokes <code>runc create</code> passing an <strong>OCI Bundle</strong> (a directory containing a <code>config.json</code> file and a <code>rootfs/</code> directory). The <code>config.json</code> file holds the container's settings, and <code>rootfs/</code> holds the files the container will see as its whole disk.</p>
<h3>How <code>runc</code> Creates the Container:</h3>
<p><code>runc</code> translates the settings file into requests to the Linux kernel. Those requests are system calls: the way a program asks the kernel (the core of the operating system) to do something. The <code>(2)</code> after each name is the manual page section number for system calls, and you can ignore it.</p>
<ol>
<li>Reads <code>config.json</code> to extract namespace settings, cgroup limits, capabilities, and environment variables. A namespace gives a process its own private view of one system resource, such as the process list, the network or the files. A cgroup limits how much CPU or memory a group of processes may use. Capabilities are the individual special privileges a process may hold, and environment variables are named settings handed to a program when it starts.</li>
<li>Invokes Linux system calls (<code>clone(2)</code> or <code>unshare(2)</code>) to create new namespaces (PID, NET, MNT, UTS, IPC). These are the process IDs, the network, the mounts (what is attached where in the file tree), the hostname, and the messaging between processes. <code>clone</code> creates a new process, and <code>unshare</code> moves the calling process into new namespaces.</li>
<li>Configures cgroup resource limits in <code>/sys/fs/cgroup/</code>, a special folder where Linux exposes cgroup settings as files.</li>
<li>Executes <code>pivot_root(2)</code> to lock the process into <code>rootfs/</code>. This makes <code>rootfs/</code> appear as the whole filesystem, so the process cannot see the host's files.</li>
<li>Executes <code>execve(2)</code> to replace its own process memory with the target container binary (e.g., <code>nginx</code>). <code>execve</code> swaps the program running in a process for another program, inside the same process.</li>
</ol>
<p><strong>Crucially, <code>execve(2)</code> does not start a second process. The <code>runc</code> process replaces itself with Nginx, so no <code>runc</code> is left running.</strong> Because the same process simply becomes Nginx, <code>runc</code> uses no memory or CPU of its own while the container runs.</p>
<hr />
<h2>Inspecting the Live Process Tree</h2>
<p><code>pstree</code> prints running programs as a family tree. In the command below, <code>pgrep containerd</code> finds containerd's process number and <code>$(...)</code> pastes that number into the command. You can observe this process chain directly on any Linux host using <code>pstree</code>:</p>
<pre><code class="language-bash">$ pstree -plu $(pgrep containerd)
containerd(1042)─┬─containerd-shim(8920)───nginx(8945)───nginx(8990)
                 └─{containerd}(1043)
</code></pre>
<p>The shim appears here as <code>containerd-shim</code>, the shortened form of <code>containerd-shim-runc-v2</code>. The line <code>{containerd}(1043)</code> is a thread of <code>containerd</code>, meaning a separate strand of work inside the same process. In the output, the number in parentheses is each process's ID.</p>
<p>Notice the process tree hierarchy:</p>
<ul>
<li><code>containerd</code> (PID 1042) is the parent of <code>containerd-shim</code> (PID 8920).</li>
<li><code>containerd-shim</code> is the parent of the <code>nginx</code> master process (PID 8945).</li>
<li><code>runc</code> is nowhere in the process tree because it replaced itself with Nginx after setup!</li>
<li><code>dockerd</code> is absent from the container process parentage, which is why restarting <code>dockerd</code> does not disrupt Nginx. It handed the job to <code>containerd</code> and only talks to it by message, so it is not a parent of anything here.</li>
</ul>
<hr />
<h2>Summary &amp; Next Steps</h2>
<p>Docker's modular toolchain separates high-level developer APIs from low-level kernel isolation execution:</p>
<ul>
<li><strong><code>docker</code> CLI</strong> converts terminal commands into REST API requests sent over <code>/var/run/docker.sock</code>.</li>
<li><code>dockerd</code> orchestrates networks, volumes, and API requests.</li>
<li><code>containerd</code> manages image snapshots and container lifecycles via gRPC.</li>
<li><code>containerd-shim</code> keeps container processes alive independently of daemon restarts.</li>
<li><code>runc</code> executes system calls to set up namespaces/cgroups and then replaces itself with the container process.</li>
</ul>
<p>In the next article, we examine The Open Container Initiative (OCI): Image Specification and Runtime Specification.</p>
]]></content:encoded>
    </item>
    <item>
      <title>What Docker Actually Adds to Linux Isolation Primitives</title>
      <link>https://unkletayo.dev/engineering/docker/01-what-docker-adds-to-kernel-primitives/</link>
      <guid isPermaLink="true">https://unkletayo.dev/engineering/docker/01-what-docker-adds-to-kernel-primitives/</guid>
      <description>What Docker added on top of the namespaces, cgroups and OverlayFS already in the Linux kernel, and why raw kernel interfaces needed a container runtime around them.</description>
      <pubDate>Tue, 22 Sep 2026 07:00:00 GMT</pubDate>
      <category>docker</category>
      <category>linux</category>
      <category>containers</category>
      <category>kernel</category>
      <enclosure url="https://unkletayo.dev/cover/engineering/docker/01-what-docker-adds-to-kernel-primitives.png" length="0" type="image/png"/>
      <content:encoded><![CDATA[<p>Before you start: this article assumes you have run a few shell commands, know that a process is one running instance of a program, and have typed <code>docker run</code> at least once. Every Linux term is explained where it first appears.</p>
<p>Think of a container as a hotel room. You share the building (the machine) with other guests, but your room has its own door, its own bathroom, and a meter that limits how much water you can use. Nothing in the building was invented for you. The hotel just arranges existing walls and plumbing so that your room feels private.</p>
<p>Docker didn't invent container isolation.</p>
<p>A container is an ordinary program run so that it sees only its own files, its own network, and a limited share of the machine's resources, as if it had a computer to itself. Linux can do this without Docker.</p>
<p>The kernel is the core of the operating system: the part that controls the hardware, the memory, and the running programs on behalf of every other program. A process is one running instance of a program, and Linux gives each process a number called a process ID (PID).</p>
<p>By the late 2000s, the Linux kernel already had the building blocks for running an isolated process:</p>
<ul>
<li><strong>Namespaces</strong> (<code>clone(2)</code> flags like <code>CLONE_NEWPID</code>, <code>CLONE_NEWNET</code>, and <code>CLONE_NEWNS</code>) to give processes their own isolated view of system trees. A namespace gives a process its own private view of one kind of system resource, such as the list of process IDs, the network interfaces, or the mounted filesystems. <code>clone(2)</code> is a system call, which is a request a program makes to the kernel for something it cannot do itself, such as creating a process. The <code>CLONE_NEW...</code> flags ask the kernel to create new namespaces for that process. The <code>(2)</code> is the section of the Linux manual where system calls are documented.</li>
<li><strong>Control Groups (cgroups)</strong> to cap CPU cycles, RAM, and block I/O. A cgroup is a kernel feature that sets limits on how much processor time, memory, and disk reading and writing (block I/O) a group of processes may use.</li>
<li><strong><code>pivot_root</code> and <code>chroot</code></strong> to swap root filesystems. Both are system calls that change what a process treats as the top-level folder <code>/</code>, so it cannot see files outside that folder. A root filesystem is the full directory tree a program expects to find, starting at <code>/</code>.</li>
</ul>
<p>Yet almost nobody was deploying microservices in containers back then. Microservices means building one application as many small programs that talk to each other over a network.</p>
<p>The reason was practical ergonomics: stitching those primitives together by hand was a nightmare. Setting up an isolated environment meant calling those system calls yourself, mounting virtual <code>/proc</code> filesystems manually, setting up virtual ethernet pairs with raw <code>ip link</code> commands, and tweaking pseudo-files inside <code>/sys/fs/cgroup/</code>.</p>
<p>Some of those terms need unpacking. Mounting attaches a filesystem to a folder so its contents appear there. <code>/proc</code> and <code>/sys</code> are special folders where Linux exposes kernel information as files, and writing to some of them changes kernel settings. A virtual ethernet pair (veth) is two software network ports joined like the two ends of a cable: whatever enters one end comes out the other. <code>ip link</code> is a command-line tool for managing network interfaces.</p>
<p>Miss a single cleanup step in a bash script (a text file of shell commands that run in order), and your process leaked network interfaces into the host namespace or left root mount points exposed. The host is the real machine, or the operating system, that runs the containers. Leaking means leaving stray network devices or mounts behind on it.</p>
<p>Docker succeeded because it solved the distribution and developer experience problem. It took the disconnected kernel interfaces and wrapped them in a consistent runtime model. A runtime is the software that actually starts and manages containers, and distribution means getting software onto other machines.</p>
<hr />
<h2>The Raw Primitive Gap</h2>
<p>To see why the abstraction matters, contrast what the kernel expects with what Docker presents to the user. It is like cooking from raw ingredients versus ordering a ready meal: the left side is the manual steps, and the right side is the one Docker command that does them all.</p>
<pre><code>RAW LINUX KERNEL CALLS                         DOCKER RUNTIME ABSTRACTION
+---------------------------------------+     +---------------------------------------+
|  sys_clone(CLONE_NEWPID | NEWNET...)  | --&gt; |  docker run -d --name web -p 80:80 \  |
|  /sys/fs/cgroup/memory/my_app/        |     |             -m 512m nginx             |
|  mount -t overlay ...                 |     |  (Single declarative execution)       |
+---------------------------------------+     +---------------------------------------+
</code></pre>
<p>In the Docker command, <code>-d</code> runs the container in the background, <code>--name web</code> names it, <code>-p 80:80</code> connects port 80 on the host to port 80 in the container, <code>-m 512m</code> caps its memory at 512 megabytes, and <code>nginx</code> is the image to run. nginx is a popular web server program, and an image is the packaged software the container starts from (more on images below).</p>
<p>Without Docker or a container manager, running an isolated web server requires orchestrating five separate subsystems:</p>
<ol>
<li><strong>Process Isolation</strong>: Calling <code>clone(2)</code> or <code>unshare(2)</code> with half a dozen flags to detach PID, mount, IPC, and network trees. <code>unshare(2)</code> is a system call that detaches the calling process from namespaces it shares with others. PID means process ID numbers, and IPC (inter-process communication) means shared memory and message queues.</li>
<li><strong>Filesystem Pivoting</strong>: Unpacking a rootfs tarball, setting up <code>tmpfs</code> mounts, and executing <code>pivot_root(2)</code> so the process can't escape into the host root. A tarball is a single archive file (<code>.tar</code>) that bundles many files, and <code>tmpfs</code> is a filesystem kept in memory instead of on disk.</li>
<li><strong>Resource Guardrails</strong>: Writing memory limits into <code>/sys/fs/cgroup/memory.max</code> and writing the target process ID into <code>cgroup.procs</code>. In plain terms, you write a number into one file and the process's ID into another, and the kernel reads those files as instructions.</li>
<li><strong>Network Plumbing</strong>: Giving the container a working network connection and making it reachable from outside. In plain terms, this takes five steps:
<ul>
<li>Create a <code>veth</code> pair, the two linked virtual ports described earlier.</li>
<li>Move one end into the container's network namespace, so the container owns it.</li>
<li>Attach the other end to a bridge on the host. A bridge is a software network switch that connects several virtual ports.</li>
<li>Give the container an IP address from a subnet. An IP address identifies a machine on a network, and a subnet is a block of related addresses.</li>
<li>Add an <code>iptables</code> rule for port forwarding. <code>iptables</code> is the Linux firewall and packet-rewriting tool. DNAT (destination network address translation) rewrites a packet's destination address, which is how traffic arriving at a port on the host gets sent on to the container. A port is a numbered door on an IP address that a program listens on.</li>
</ul>
</li>
<li><strong>Artifact Packaging</strong>: Figuring out how to ship dependencies and runtime binaries in a repeatable bundle. Dependencies are the other pieces of software a program needs to run, and binaries are compiled, ready-to-run program files.</li>
</ol>
<p>Docker packaged those responsibilities into four core components. These are the four numbered sections below. They are a different grouping from the five jobs above, not a one-to-one match, so do not expect job 1 to line up with section 1.</p>
<hr />
<h3>1. The Portable Root Filesystem (OCI Image Format)</h3>
<p>A shipping container holds goods in a standard box that any ship or truck can carry. A Docker image does the same for software: the program plus everything it needs, in one standard package. OCI (Open Container Initiative) is the published standard for that image format.</p>
<p>Before container images became standard, distributing software meant building RPMs, Debian packages, or shipping fat tarballs. RPM and Debian packages are the install formats used by the Red Hat and Debian families of Linux. If the host machine had a mismatched <code>glibc</code> version or was missing <code>libssl.so.1.1</code>, the deployment broke on startup. <code>glibc</code> is the standard C library most Linux programs depend on, and <code>libssl.so.1.1</code> is a shared library file (a file of code that many programs load) used for encryption. If the machine has a different version than the program expects, the program fails.</p>
<p>Docker paired the root filesystem with a JSON manifest, a small text file that describes what the image contains and how to run it. The image contains:</p>
<ul>
<li>The exact root directory tree (<code>rootfs</code>) required by the app.</li>
<li>Every shared library, binary, and configuration file.</li>
<li>Metadata describing entrypoints, environment defaults, and exposed ports. The entrypoint is the command that starts when the container runs, and environment variables are named settings passed to the program.</li>
</ul>
<p>Because the process runs against its own bundled rootfs rather than the host's <code>/usr/lib</code> (the folder where Linux keeps shared libraries for installed programs), it runs with identical behavior on a local laptop or an AWS instance running a completely different Linux distribution. A distribution is a packaged version of Linux, such as Debian or Fedora, and an AWS instance is a virtual server rented from Amazon Web Services.</p>
<hr />
<h3>2. Copy-on-Write Layering via OverlayFS</h3>
<p>Shipping a full root filesystem on every minor code change would slow deployment and waste disk space.</p>
<p>Imagine a printed page covered by clear plastic sheets. You write on the top sheet and never touch the page underneath. Layers work like those sheets. Copy-on-write means a file is copied only at the moment someone first changes it. OverlayFS is the Linux filesystem that stacks layers so they appear as one folder.</p>
<p>Docker solved this with layered storage drivers (primarily OverlayFS). When building an image, each instruction produces an immutable layer identified by a SHA-256 digest. Immutable means never changed after creation. A SHA-256 digest (a hash) is a fixed-length fingerprint computed from content, so identical content always gets the same fingerprint. If five different services build on <code>debian:bookworm-slim</code> (the name and tag of a small Debian base image), they all share the exact same base layer on disk and in the page cache, which is the memory the kernel uses to keep recently read file data:</p>
<pre><code>+---------------------------------------------------------------+
| Container Writable Layer (Read/Write Delta)                    |
+---------------------------------------------------------------+
| Layer 3: Application Binary (12 MB)                           |
+---------------------------------------------------------------+
| Layer 2: Go Runtime / Libraries (110 MB)                      |
+---------------------------------------------------------------+
| Layer 1: Debian Base OS (55 MB) [Shared across all containers]|
+---------------------------------------------------------------+
</code></pre>
<p>The sizes in this diagram are illustrative example values, not measurements. The example assumes the app was written in Go, a programming language, so layer 2 holds the Go runtime and libraries. The top box is the container's own writable layer, where its changes are stored.</p>
<p>The container only writes changes to its thin top layer. The heavy base remains read-only and shared. If a program edits a file that lives in a lower layer, that file is first copied up into the top layer and edited there, and the lower layers stay unchanged.</p>
<hr />
<h3>3. Automated Bridge Networking and Embedded DNS</h3>
<p>Each container is like a separate house that needs a street, an address, and a way for mail to reach it. Docker lays the street (a bridge), assigns the addresses, and forwards the mail (ports).</p>
<p>Configuring Linux network namespaces by hand is tedious and error-prone. Here, <code>&lt;container_pid&gt;</code> stands for the process ID of the container's main process, and <code>br0</code> is a bridge you created earlier:</p>
<pre><code class="language-bash"># Manual Linux Networking setup (WITHOUT Docker):
# 1. Create a linked pair of virtual ports, veth0 and veth1.
ip link add veth0 type veth peer name veth1
# 2. Move one end (veth1) into the container's network namespace.
ip link set veth1 netns &lt;container_pid&gt;
# 3. Give that end an IP address. The /24 means the first 24 bits
#    identify the network (CIDR notation for the subnet size).
ip addr add 192.168.1.2/24 dev veth1
# 4. Plug the other end (veth0) into the bridge br0.
ip link set veth0 master br0
# 5. Forward TCP traffic arriving on port 80 to the container.
iptables -t nat -A PREROUTING -p tcp --dport 80 -j DNAT --to-destination 192.168.1.2:80
</code></pre>
<p>With Docker, <code>-p 8080:80</code> triggers all of this automatically. It means connections to port 8080 on the host go to port 80 inside the container. The daemon (a program that runs continuously in the background; Docker's is called <code>dockerd</code>) manages virtual bridge devices (<code>docker0</code>), allocates private IPs from an internal subnet, sets up kernel forwarding rules, and runs an internal DNS resolver at <code>127.0.0.11</code> so containers talk to each other by container name instead of volatile IP addresses. DNS translates names into IP addresses. The <code>127.x.x.x</code> addresses refer to the local machine, so <code>127.0.0.11</code> is reachable only from inside the container. Volatile means the address can change each time a container restarts.</p>
<hr />
<h3>4. Content-Addressable Distribution (Registry Protocol)</h3>
<p>A registry is like a library of images: you push to upload one and pull to download one. Content-addressable means each item is looked up by a fingerprint of its content, not by a name someone chose.</p>
<p>Docker standardized how images get pushed, pulled, and verified through the Docker Registry HTTP API V2. HTTP is the standard protocol web browsers use, and this API is how Docker talks to a registry over it.</p>
<p>Because every layer is identified by its SHA-256 hash, pushing an update transfers only the modified layer delta, meaning only the part that changed. If your base layers didn't change, the registry skips them entirely. Before uploading each layer, Docker checks whether the registry already has one with the same fingerprint, and skips it if so.</p>
<hr />
<h2>What Comes Next</h2>
<p>Understanding these boundaries changes how you debug container issues. When an application gets killed with exit code 137, that's not a mystery inside Docker. An exit code is the number a program returns when it stops, and 137 means the process was killed by signal 9 (SIGKILL), because shells report 128 plus the signal number. One common cause is the kernel's cgroup memory controller enforcing a memory limit, but any SIGKILL produces 137, so treat it as a hint and check the container's memory limit and logs. When a port is unreachable, the culprit is usually an <code>iptables</code> rule or a missing route on the bridge. A route is a rule telling the kernel where to send packets for a given address.</p>
<p>In Part 2, we trace the execution path from the Docker CLI command down through <code>dockerd</code>, <code>containerd</code>, and <code>runc</code> (the three programs in Docker's execution chain, each explained there) to see what happens the moment you press enter.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
