<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: amir</title>
    <description>The latest articles on DEV Community by amir (@amirsefati).</description>
    <link>https://dev.to/amirsefati</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F161199%2F1386903f-a273-4172-a7ba-0585d3e4d5dd.jpeg</url>
      <title>DEV Community: amir</title>
      <link>https://dev.to/amirsefati</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amirsefati"/>
    <language>en</language>
    <item>
      <title>Building tiny-docker-go in Go: What I Learned from Building a Tiny Docker-like Runtime</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Sat, 16 May 2026 15:14:08 +0000</pubDate>
      <link>https://dev.to/amirsefati/building-tiny-docker-go-in-go-what-i-learned-from-building-a-tiny-docker-like-runtime-57g9</link>
      <guid>https://dev.to/amirsefati/building-tiny-docker-go-in-go-what-i-learned-from-building-a-tiny-docker-like-runtime-57g9</guid>
      <description>&lt;h1&gt;
  
  
  Building &lt;code&gt;tiny-docker-go&lt;/code&gt; in Go: What I Learned from Building a Tiny Docker-like Runtime
&lt;/h1&gt;

&lt;p&gt;I use Docker almost every day.&lt;/p&gt;

&lt;p&gt;I use it for local development, backend services, databases, staging environments, CI/CD pipelines, and sometimes even for debugging production-like issues. Like many developers, I became comfortable with commands like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run
docker ps
docker logs
docker stop
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for a long time, Docker still felt like a black box to me.&lt;/p&gt;

&lt;p&gt;I knew how to use it.&lt;/p&gt;

&lt;p&gt;I knew how to write Dockerfiles.&lt;/p&gt;

&lt;p&gt;I knew how to debug containers when something failed.&lt;/p&gt;

&lt;p&gt;But I did not deeply understand what actually happens under the hood when we run a container.&lt;/p&gt;

&lt;p&gt;So I decided to build a small Docker-like container runtime in Go.&lt;/p&gt;

&lt;p&gt;The project is called &lt;code&gt;tiny-docker-go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;GitHub repository:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/amirsefati/tiny-docker-go" rel="noopener noreferrer"&gt;https://github.com/amirsefati/tiny-docker-go&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The goal was not to rebuild Docker.&lt;/p&gt;

&lt;p&gt;Docker is a mature platform with a huge ecosystem: image builds, registries, storage drivers, networking drivers, logging drivers, security features, orchestration integrations, plugins, and many other production-grade details.&lt;/p&gt;

&lt;p&gt;My goal was much smaller:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build a tiny runtime step by step, so I can understand the Linux ideas behind containers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Docker’s own documentation describes containers as isolated processes that run on a host and have their own filesystem, networking, and process tree. That sentence looks simple, but it hides a lot of Linux internals.&lt;/p&gt;

&lt;p&gt;To understand that sentence, I needed to touch the real building blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux namespaces&lt;/li&gt;
&lt;li&gt;cgroups&lt;/li&gt;
&lt;li&gt;root filesystems&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chroot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/proc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;process lifecycle&lt;/li&gt;
&lt;li&gt;signals&lt;/li&gt;
&lt;li&gt;logs&lt;/li&gt;
&lt;li&gt;network namespaces&lt;/li&gt;
&lt;li&gt;bridge networking&lt;/li&gt;
&lt;li&gt;veth pairs&lt;/li&gt;
&lt;li&gt;NAT&lt;/li&gt;
&lt;li&gt;container metadata&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a summary of the full 10-day journey.&lt;/p&gt;

&lt;p&gt;It is not a tutorial for building a production runtime.&lt;/p&gt;

&lt;p&gt;It is a developer story about learning containers by building a tiny version of one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I started with Go
&lt;/h2&gt;

&lt;p&gt;I chose Go because it fits this kind of project very well.&lt;/p&gt;

&lt;p&gt;Go makes it simple to build CLI tools, execute processes, work with files, handle signals, and call lower-level Linux syscalls when needed.&lt;/p&gt;

&lt;p&gt;Also, many important container projects are written in Go. Docker itself, containerd, runc, Kubernetes, and many cloud-native tools use Go heavily.&lt;/p&gt;

&lt;p&gt;So using Go felt natural.&lt;/p&gt;

&lt;p&gt;For this project, I wanted the code to stay simple and readable. I did not want to hide everything behind too many abstractions too early.&lt;/p&gt;

&lt;p&gt;At the same time, I wanted the structure to be extensible enough so I could add one feature every day without rewriting the whole project.&lt;/p&gt;

&lt;p&gt;That balance became one of the main lessons of the project.&lt;/p&gt;

&lt;p&gt;When you build systems software, the hard part is not only writing code that works today.&lt;/p&gt;

&lt;p&gt;The hard part is writing code that can survive the next feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  The 10-day plan
&lt;/h2&gt;

&lt;p&gt;I split the project into 10 small parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Project structure and CLI foundation&lt;/li&gt;
&lt;li&gt;Linux namespaces&lt;/li&gt;
&lt;li&gt;Root filesystem isolation&lt;/li&gt;
&lt;li&gt;Container IDs and metadata&lt;/li&gt;
&lt;li&gt;Logs&lt;/li&gt;
&lt;li&gt;Stop and lifecycle management&lt;/li&gt;
&lt;li&gt;cgroups and memory limits&lt;/li&gt;
&lt;li&gt;Network namespace&lt;/li&gt;
&lt;li&gt;Bridge and veth networking&lt;/li&gt;
&lt;li&gt;Polish, README, roadmap, and lessons learned&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This helped me avoid one common mistake:&lt;/p&gt;

&lt;p&gt;Trying to build “Docker” in one step.&lt;/p&gt;

&lt;p&gt;That is too much.&lt;/p&gt;

&lt;p&gt;Instead, I treated each day as one small question.&lt;/p&gt;

&lt;p&gt;Day 1:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I execute a command through my own CLI?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 2:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I run that command inside new Linux namespaces?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 3:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I give that process a different root filesystem?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 4:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I remember what I started?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 5:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I capture logs?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 6:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I stop a running container?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 7:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I limit memory?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 8:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I isolate networking?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 9:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I connect the container back to the outside world?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Day 10:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can I explain the architecture clearly?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That made the project much easier to continue.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 1: Project Setup and CLI Foundation
&lt;/h2&gt;

&lt;p&gt;On Day 1, I did not start with namespaces.&lt;/p&gt;

&lt;p&gt;That may sound strange because namespaces are one of the most exciting parts of containers.&lt;/p&gt;

&lt;p&gt;But I wanted to start with the boring foundation first.&lt;/p&gt;

&lt;p&gt;The initial project structure looked like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go/
├── cmd/
│   └── tiny-docker-go/
│       └── main.go
├── internal/
│   ├── app/
│   ├── cli/
│   └── runtime/
├── go.mod
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea was simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cmd/&lt;/code&gt; contains the executable entrypoint.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;internal/cli&lt;/code&gt; handles user-facing commands.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;internal/runtime&lt;/code&gt; handles process execution.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;internal/app&lt;/code&gt; wires things together.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I added basic commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run
tiny-docker-go ps
tiny-docker-go stop
tiny-docker-go logs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this stage, only &lt;code&gt;run&lt;/code&gt; actually did something.&lt;/p&gt;

&lt;p&gt;It executed a normal Linux command on the host.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go run ./cmd/tiny-docker-go run &lt;span class="nb"&gt;echo &lt;/span&gt;hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was not a container yet.&lt;/p&gt;

&lt;p&gt;There was no isolation.&lt;/p&gt;

&lt;p&gt;No cgroups.&lt;/p&gt;

&lt;p&gt;No rootfs.&lt;/p&gt;

&lt;p&gt;No networking.&lt;/p&gt;

&lt;p&gt;But this step mattered because it gave me a stable CLI shape.&lt;/p&gt;

&lt;p&gt;I wanted the outside interface to look like a tiny version of Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run /bin/sh
tiny-docker-go ps
tiny-docker-go logs &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
tiny-docker-go stop &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even before the internals were ready, the product shape was clear.&lt;/p&gt;

&lt;p&gt;That helped a lot later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small lesson from Day 1
&lt;/h3&gt;

&lt;p&gt;A container runtime is still a command runner at the beginning.&lt;/p&gt;

&lt;p&gt;Before thinking about advanced kernel features, I needed a clean way to receive a command, validate it, execute it, and return output to the terminal.&lt;/p&gt;

&lt;p&gt;A lot of systems projects start like this.&lt;/p&gt;

&lt;p&gt;First, build a simple interface.&lt;/p&gt;

&lt;p&gt;Then make the implementation smarter behind that interface.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 2: Adding Linux Namespaces
&lt;/h2&gt;

&lt;p&gt;Day 2 was where the project started to feel like a real container runtime.&lt;/p&gt;

&lt;p&gt;Linux namespaces are one of the core ideas behind containers.&lt;/p&gt;

&lt;p&gt;A namespace gives a process a different view of some system resource.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PID namespace gives a different process tree.&lt;/li&gt;
&lt;li&gt;UTS namespace gives a different hostname.&lt;/li&gt;
&lt;li&gt;Mount namespace gives a different mount table.&lt;/li&gt;
&lt;li&gt;Network namespace gives a different network stack.&lt;/li&gt;
&lt;li&gt;User namespace gives a different view of user and group IDs.&lt;/li&gt;
&lt;li&gt;IPC namespace isolates IPC resources.&lt;/li&gt;
&lt;li&gt;Cgroup namespace isolates cgroup views.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important thing is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A container is not a virtual machine. It is still a Linux process, but it sees a more isolated view of the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence changed how I think about Docker.&lt;/p&gt;

&lt;p&gt;When I run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run alpine sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker does not boot a new kernel like a VM.&lt;/p&gt;

&lt;p&gt;It starts a process on the host kernel, but configures isolation around it.&lt;/p&gt;

&lt;p&gt;In Go, I started experimenting with &lt;code&gt;syscall.SysProcAttr&lt;/code&gt; and clone flags.&lt;/p&gt;

&lt;p&gt;A simplified version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SysProcAttr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SysProcAttr&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Cloneflags&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_NEWUTS&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_NEWPID&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_NEWNS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates the child process in new namespaces.&lt;/p&gt;

&lt;p&gt;The first namespaces I added were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UTS namespace
PID namespace
Mount namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  UTS namespace
&lt;/h3&gt;

&lt;p&gt;UTS namespace lets the container have its own hostname.&lt;/p&gt;

&lt;p&gt;Inside the child process, I could call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sethostname&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"tiny-container"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;hostname&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;would show:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That was a small moment, but it felt important.&lt;/p&gt;

&lt;p&gt;The process was still running on my machine, but it had its own hostname.&lt;/p&gt;

&lt;p&gt;That was the first visible sign of isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  PID namespace
&lt;/h3&gt;

&lt;p&gt;PID namespace was more interesting.&lt;/p&gt;

&lt;p&gt;With a new PID namespace, the process inside the container can see itself as PID 1.&lt;/p&gt;

&lt;p&gt;That is a big deal.&lt;/p&gt;

&lt;p&gt;On Linux, PID 1 is special.&lt;/p&gt;

&lt;p&gt;It is the init process of that namespace. It has responsibilities around signal handling and reaping zombie processes.&lt;/p&gt;

&lt;p&gt;This is why container entrypoints matter.&lt;/p&gt;

&lt;p&gt;If the main process inside a container does not handle signals correctly, stopping the container can behave badly.&lt;/p&gt;

&lt;p&gt;This also helped me understand why tools like &lt;code&gt;tini&lt;/code&gt; exist in container environments.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mount namespace
&lt;/h3&gt;

&lt;p&gt;Mount namespace gave the container its own mount table.&lt;/p&gt;

&lt;p&gt;That means the process can have different mounts from the host.&lt;/p&gt;

&lt;p&gt;At this point, I was not yet fully changing the filesystem, but I prepared the project for mounting &lt;code&gt;/proc&lt;/code&gt; later.&lt;/p&gt;

&lt;p&gt;One small Linux detail I learned here:&lt;/p&gt;

&lt;p&gt;When working with mount namespaces, mount propagation can surprise you.&lt;/p&gt;

&lt;p&gt;If mounts are shared with the host, changes inside one namespace may propagate in ways you do not expect. Real runtimes are careful about making mounts private before doing container setup.&lt;/p&gt;

&lt;p&gt;This is one of those details that you do not think about when using Docker normally.&lt;/p&gt;

&lt;p&gt;But when building a runtime, it becomes visible very quickly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parent and child process model
&lt;/h3&gt;

&lt;p&gt;One design pattern I used was the parent/child model with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/proc/self/exe
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent process receives the CLI command.&lt;/p&gt;

&lt;p&gt;Then it starts a child process by re-executing the same binary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/proc/self/exe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"child"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parent is responsible for setup and management.&lt;/p&gt;

&lt;p&gt;The child enters the isolated environment and runs the target command.&lt;/p&gt;

&lt;p&gt;This pattern made the code easier to reason about.&lt;/p&gt;

&lt;p&gt;There is a clear split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;parent process
├── parse CLI
├── prepare config
├── start child with namespaces
└── track metadata

child process
├── set hostname
├── prepare filesystem
├── mount proc
└── exec user command
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was the first time &lt;code&gt;tiny-docker-go&lt;/code&gt; started to feel like a real runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 3: RootFS and &lt;code&gt;chroot&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;On Day 3, I added filesystem isolation.&lt;/p&gt;

&lt;p&gt;Namespaces isolate views of system resources, but a container also needs a filesystem.&lt;/p&gt;

&lt;p&gt;When I run an Alpine container, I expect to see Alpine files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/bin/sh
/etc/os-release
/lib
/usr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I should not see the host root filesystem.&lt;/p&gt;

&lt;p&gt;For the first version, I used &lt;code&gt;chroot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chroot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rootfs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, &lt;code&gt;/&lt;/code&gt; inside the process points to the rootfs directory.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;tiny-docker-go run &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/os-release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shows Alpine information if the rootfs is Alpine.&lt;/p&gt;

&lt;p&gt;This was another important moment.&lt;/p&gt;

&lt;p&gt;Now the process had:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;its own hostname&lt;/li&gt;
&lt;li&gt;its own PID namespace&lt;/li&gt;
&lt;li&gt;its own mount namespace&lt;/li&gt;
&lt;li&gt;its own root filesystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It still was not Docker, but it started to look like the core of a container.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;chroot&lt;/code&gt; is not full container security
&lt;/h3&gt;

&lt;p&gt;One important note:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;chroot&lt;/code&gt; is useful for learning, but it is not complete container isolation by itself.&lt;/p&gt;

&lt;p&gt;Historically, &lt;code&gt;chroot&lt;/code&gt; was not designed as a full security boundary.&lt;/p&gt;

&lt;p&gt;A real runtime usually uses more careful filesystem setup, often with &lt;code&gt;pivot_root&lt;/code&gt;, mount namespaces, read-only mounts, bind mounts, capabilities, seccomp, AppArmor or SELinux, and other hardening layers.&lt;/p&gt;

&lt;p&gt;For this project, &lt;code&gt;chroot&lt;/code&gt; was enough because my goal was educational.&lt;/p&gt;

&lt;p&gt;I wanted to understand the basic idea:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Give the process a different &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That one idea explains a lot.&lt;/p&gt;

&lt;p&gt;A container process does not magically have a filesystem.&lt;/p&gt;

&lt;p&gt;The runtime prepares one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mounting &lt;code&gt;/proc&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;After entering the rootfs, I mounted &lt;code&gt;/proc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Mount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"proc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"/proc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"proc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;/proc&lt;/code&gt;, commands like &lt;code&gt;ps&lt;/code&gt; may not work correctly inside the container.&lt;/p&gt;

&lt;p&gt;This helped me understand another detail:&lt;/p&gt;

&lt;p&gt;Many Linux tools do not get information from some secret API.&lt;/p&gt;

&lt;p&gt;They read from virtual filesystems like &lt;code&gt;/proc&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For example, &lt;code&gt;ps&lt;/code&gt; depends on &lt;code&gt;/proc&lt;/code&gt; to inspect processes.&lt;/p&gt;

&lt;p&gt;So if the container has a PID namespace but &lt;code&gt;/proc&lt;/code&gt; is not mounted correctly, the view inside the container can be confusing.&lt;/p&gt;

&lt;p&gt;This is one of those small details that makes containers feel less magical.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 4: Container ID and Metadata
&lt;/h2&gt;

&lt;p&gt;After Day 3, I could start isolated processes.&lt;/p&gt;

&lt;p&gt;But I had a new problem:&lt;/p&gt;

&lt;p&gt;How do I remember them?&lt;/p&gt;

&lt;p&gt;Docker can do:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
docker inspect &amp;lt;container&amp;gt;
docker logs &amp;lt;container&amp;gt;
docker stop &amp;lt;container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means Docker stores metadata about containers.&lt;/p&gt;

&lt;p&gt;So on Day 4, I added a simple metadata store.&lt;/p&gt;

&lt;p&gt;I used a local directory like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/var/lib/tiny-docker/containers/&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each container gets a &lt;code&gt;config.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Example fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"/bin/sh"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"hostname"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tiny-container"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rootfs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"./rootfs/alpine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"running"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-12T10:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"pid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12345&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was simple, but it changed the architecture.&lt;/p&gt;

&lt;p&gt;Before this, &lt;code&gt;run&lt;/code&gt; was just executing a process.&lt;/p&gt;

&lt;p&gt;After this, &lt;code&gt;run&lt;/code&gt; was creating a managed container record.&lt;/p&gt;

&lt;p&gt;That is a big conceptual difference.&lt;/p&gt;

&lt;p&gt;A runtime needs memory.&lt;/p&gt;

&lt;p&gt;Not RAM memory, but operational memory.&lt;/p&gt;

&lt;p&gt;It needs to remember:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What did I start?&lt;/li&gt;
&lt;li&gt;What PID belongs to this container?&lt;/li&gt;
&lt;li&gt;Where are its logs?&lt;/li&gt;
&lt;li&gt;Is it running or stopped?&lt;/li&gt;
&lt;li&gt;What command did it start with?&lt;/li&gt;
&lt;li&gt;What rootfs did it use?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then &lt;code&gt;ps&lt;/code&gt; became meaningful.&lt;/p&gt;

&lt;p&gt;Instead of being a placeholder, it could read metadata files and show containers.&lt;/p&gt;

&lt;p&gt;A very simple output could look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;CONTAINER ID   PID     STATUS    COMMAND
abc123         12345   running   /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Small lesson from Day 4
&lt;/h3&gt;

&lt;p&gt;A container runtime is partly a process manager and partly a state manager.&lt;/p&gt;

&lt;p&gt;Starting the process is only half of the job.&lt;/p&gt;

&lt;p&gt;Remembering and managing it is the other half.&lt;/p&gt;

&lt;p&gt;This helped me understand why Docker has a daemon.&lt;/p&gt;

&lt;p&gt;If containers can continue running after the CLI exits, something needs to track them.&lt;/p&gt;

&lt;p&gt;My tiny runtime did this in a simple way with JSON files.&lt;/p&gt;

&lt;p&gt;Docker does it in a much more complete way.&lt;/p&gt;

&lt;p&gt;But the idea is similar.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 5: Logs
&lt;/h2&gt;

&lt;p&gt;On Day 5, I added logging.&lt;/p&gt;

&lt;p&gt;This sounded easy at first.&lt;/p&gt;

&lt;p&gt;Just redirect stdout and stderr to a file, right?&lt;/p&gt;

&lt;p&gt;Something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;logFile&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"container.log"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stdout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logFile&lt;/span&gt;
&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Stderr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logFile&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For detached containers, that works.&lt;/p&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go logs &amp;lt;container-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/var/lib/tiny-docker/containers/&amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/container.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and print it.&lt;/p&gt;

&lt;p&gt;But logs became more interesting when I thought about interactive mode.&lt;/p&gt;

&lt;p&gt;If I run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I want stdin, stdout, and stderr attached to my terminal.&lt;/p&gt;

&lt;p&gt;But if I run a detached process, I want logs written to a file.&lt;/p&gt;

&lt;p&gt;So the runtime needs to understand different modes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;interactive mode
├── stdin  -&amp;gt; terminal
├── stdout -&amp;gt; terminal
└── stderr -&amp;gt; terminal

detached mode
├── stdin  -&amp;gt; maybe closed
├── stdout -&amp;gt; log file
└── stderr -&amp;gt; log file
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker has this same concept in a more advanced way.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;docker logs&lt;/code&gt; reads logs from the container’s configured logging driver, and &lt;code&gt;docker logs --follow&lt;/code&gt; streams new output.&lt;/p&gt;

&lt;p&gt;For my tiny version, I kept it simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go logs &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
tiny-docker-go logs &lt;span class="nt"&gt;-f&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; mode can be implemented like a basic &lt;code&gt;tail -f&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small Linux detail: stdout and stderr matter
&lt;/h3&gt;

&lt;p&gt;A container does not need to know about “logging” as a high-level concept.&lt;/p&gt;

&lt;p&gt;Most container logging starts from something simple:&lt;/p&gt;

&lt;p&gt;The process writes to stdout and stderr.&lt;/p&gt;

&lt;p&gt;The runtime captures those streams.&lt;/p&gt;

&lt;p&gt;That is why good containerized apps usually log to stdout/stderr instead of writing only to local files.&lt;/p&gt;

&lt;p&gt;This is a small detail, but it matters a lot in production.&lt;/p&gt;

&lt;p&gt;If your app logs only to a file inside the container, then your logging pipeline may not see it unless you mount volumes or configure extra collection.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 6: Stop and Lifecycle Management
&lt;/h2&gt;

&lt;p&gt;On Day 6, I implemented &lt;code&gt;stop&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first version was simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go stop &amp;lt;container-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The runtime reads metadata, gets the PID, and sends a signal.&lt;/p&gt;

&lt;p&gt;The normal graceful flow is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;send SIGTERM
wait
if still running, send SIGKILL
update metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is similar to Docker’s stop behavior.&lt;/p&gt;

&lt;p&gt;Docker sends a termination signal first, and after a timeout it sends SIGKILL if the process does not exit.&lt;/p&gt;

&lt;p&gt;This taught me a practical lesson:&lt;/p&gt;

&lt;p&gt;Stopping a container is not the same as killing a process immediately.&lt;/p&gt;

&lt;p&gt;A good runtime gives the process a chance to clean up.&lt;/p&gt;

&lt;p&gt;For example, a backend service may need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;close database connections&lt;/li&gt;
&lt;li&gt;flush logs&lt;/li&gt;
&lt;li&gt;finish current requests&lt;/li&gt;
&lt;li&gt;release locks&lt;/li&gt;
&lt;li&gt;write final state&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If we send SIGKILL immediately, the process cannot handle it.&lt;/p&gt;

&lt;p&gt;SIGKILL cannot be caught.&lt;/p&gt;

&lt;p&gt;SIGTERM can be caught.&lt;/p&gt;

&lt;p&gt;So graceful shutdown starts with SIGTERM.&lt;/p&gt;

&lt;h3&gt;
  
  
  PID 1 problem
&lt;/h3&gt;

&lt;p&gt;This day also connected back to PID namespaces.&lt;/p&gt;

&lt;p&gt;Inside a PID namespace, the main process becomes PID 1.&lt;/p&gt;

&lt;p&gt;PID 1 has special behavior on Linux.&lt;/p&gt;

&lt;p&gt;If it does not handle signals properly, stopping the container may not behave as expected.&lt;/p&gt;

&lt;p&gt;That helped me understand why some containers use an init process.&lt;/p&gt;

&lt;p&gt;It also made me more careful about what command I use as the container entrypoint.&lt;/p&gt;

&lt;p&gt;A simple shell may behave differently from a proper application process.&lt;/p&gt;

&lt;p&gt;This is one reason container lifecycle management is more subtle than it looks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 7: cgroups and Memory Limits
&lt;/h2&gt;

&lt;p&gt;Day 7 was about cgroups.&lt;/p&gt;

&lt;p&gt;Namespaces answer this question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What can the process see?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;cgroups answer a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much can the process use?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference is important.&lt;/p&gt;

&lt;p&gt;Namespaces isolate visibility.&lt;/p&gt;

&lt;p&gt;cgroups control resources.&lt;/p&gt;

&lt;p&gt;With cgroups, the runtime can limit or account for resources such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory&lt;/li&gt;
&lt;li&gt;CPU&lt;/li&gt;
&lt;li&gt;pids&lt;/li&gt;
&lt;li&gt;IO&lt;/li&gt;
&lt;li&gt;sometimes devices and other controllers depending on system configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For this project, I focused on memory limit using cgroup v2.&lt;/p&gt;

&lt;p&gt;On many modern Linux systems, cgroup v2 is mounted around:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/sys/fs/cgroup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A simplified container cgroup path might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/sys/fs/cgroup/tiny-docker/&amp;lt;container-id&amp;gt;/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To limit memory, the runtime can write to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;memory.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo &lt;/span&gt;134217728 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; memory.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means 128 MB.&lt;/p&gt;

&lt;p&gt;Then the runtime adds the process PID to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cgroup.procs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &amp;lt;pid&amp;gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; cgroup.procs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, the kernel applies the limit to that process group.&lt;/p&gt;

&lt;p&gt;This was one of my favorite parts of the project.&lt;/p&gt;

&lt;p&gt;Because suddenly “memory limit” stopped being an abstract Docker option.&lt;/p&gt;

&lt;p&gt;When I write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--memory&lt;/span&gt; 128m ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;behind the scenes, the runtime eventually has to express that limit to the kernel.&lt;/p&gt;

&lt;p&gt;The exact implementation is more complex in Docker, but the basic idea became clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing memory limits
&lt;/h3&gt;

&lt;p&gt;A simple way to test memory limits is to run a command that allocates memory.&lt;/p&gt;

&lt;p&gt;For example, inside a container rootfs with Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"a = 'x' * 200 * 1024 * 1024; print('allocated')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the memory limit is 128 MB, the process should fail or be killed by the kernel.&lt;/p&gt;

&lt;p&gt;This is where container behavior becomes very real.&lt;/p&gt;

&lt;p&gt;The runtime does not “watch memory” manually in a loop.&lt;/p&gt;

&lt;p&gt;The kernel enforces the limit.&lt;/p&gt;

&lt;p&gt;That is the power of cgroups.&lt;/p&gt;

&lt;h3&gt;
  
  
  cgroup v1 vs cgroup v2
&lt;/h3&gt;

&lt;p&gt;I focused on cgroup v2 because it is the modern unified hierarchy.&lt;/p&gt;

&lt;p&gt;In cgroup v1, different controllers could be mounted in different hierarchies.&lt;/p&gt;

&lt;p&gt;In cgroup v2, the model is unified and cleaner.&lt;/p&gt;

&lt;p&gt;But cgroup v2 also has rules that you need to respect.&lt;/p&gt;

&lt;p&gt;For example, controller availability depends on the system, and some controllers must be enabled in parent cgroups before child cgroups can use them.&lt;/p&gt;

&lt;p&gt;This is where I learned another systems programming lesson:&lt;/p&gt;

&lt;p&gt;The code can be correct but the host can still reject the setup because the kernel or systemd cgroup configuration is different.&lt;/p&gt;

&lt;p&gt;So a real runtime needs strong detection, good errors, and compatibility handling.&lt;/p&gt;

&lt;p&gt;My tiny runtime does not handle every host setup.&lt;/p&gt;

&lt;p&gt;But it made the concept clear.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 8: Network Namespace
&lt;/h2&gt;

&lt;p&gt;On Day 8, I added network namespace support.&lt;/p&gt;

&lt;p&gt;This was the day where containers became both clearer and more confusing.&lt;/p&gt;

&lt;p&gt;A network namespace gives a process its own network stack.&lt;/p&gt;

&lt;p&gt;That includes its own:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;interfaces&lt;/li&gt;
&lt;li&gt;routing table&lt;/li&gt;
&lt;li&gt;IP addresses&lt;/li&gt;
&lt;li&gt;firewall rules view&lt;/li&gt;
&lt;li&gt;loopback device&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;syscall&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CLONE_NEWNET&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the container got its own network namespace.&lt;/p&gt;

&lt;p&gt;But then something interesting happened:&lt;/p&gt;

&lt;p&gt;The container had no network.&lt;/p&gt;

&lt;p&gt;That is expected.&lt;/p&gt;

&lt;p&gt;A new network namespace starts isolated.&lt;/p&gt;

&lt;p&gt;Even loopback may need to be brought up manually.&lt;/p&gt;

&lt;p&gt;So the first step was simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nb"&gt;link set &lt;/span&gt;lo up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;inside the namespace.&lt;/p&gt;

&lt;p&gt;This taught me a simple but important point:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Network isolation does not automatically mean working networking.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It means the container has a separate network world.&lt;/p&gt;

&lt;p&gt;The runtime still needs to connect that world to something.&lt;/p&gt;

&lt;p&gt;At this stage, I added a &lt;code&gt;--net none&lt;/code&gt; or &lt;code&gt;--net isolated&lt;/code&gt; style mode.&lt;/p&gt;

&lt;p&gt;That made the behavior explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run &lt;span class="nt"&gt;--net&lt;/span&gt; isolated &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;would show only the isolated namespace interfaces.&lt;/p&gt;

&lt;p&gt;No internet.&lt;/p&gt;

&lt;p&gt;No host access.&lt;/p&gt;

&lt;p&gt;Just isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Small lesson from Day 8
&lt;/h3&gt;

&lt;p&gt;Before this project, I mostly thought about Docker networking from the user side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80
docker network &lt;span class="nb"&gt;ls
&lt;/span&gt;docker network inspect
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But from the runtime side, networking starts much lower:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create network namespace
create interface
move interface into namespace
assign IP
set route
configure NAT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker hides all of that.&lt;/p&gt;

&lt;p&gt;Building even a tiny version forced me to see the real steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 9: Bridge and veth Networking
&lt;/h2&gt;

&lt;p&gt;Day 9 was one of the most difficult and useful parts.&lt;/p&gt;

&lt;p&gt;The goal was to give the container internet access.&lt;/p&gt;

&lt;p&gt;For that, I needed a simple bridge and veth pair.&lt;/p&gt;

&lt;p&gt;The model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host network namespace
│
├── eth0 / main host interface
│
├── td0 bridge
│   └── veth-host
│
└── container network namespace
    └── veth-container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A veth pair works like a virtual cable.&lt;/p&gt;

&lt;p&gt;Whatever enters one side comes out the other side.&lt;/p&gt;

&lt;p&gt;The host keeps one side.&lt;/p&gt;

&lt;p&gt;The container gets the other side.&lt;/p&gt;

&lt;p&gt;The bridge connects the host-side veth to a small virtual network.&lt;/p&gt;

&lt;p&gt;A simple IP plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bridge td0:       10.10.0.1/24
container eth0:   10.10.0.2/24
default gateway:  10.10.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The steps are roughly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip &lt;span class="nb"&gt;link &lt;/span&gt;add td0 &lt;span class="nb"&gt;type &lt;/span&gt;bridge
ip addr add 10.10.0.1/24 dev td0
ip &lt;span class="nb"&gt;link set &lt;/span&gt;td0 up

ip &lt;span class="nb"&gt;link &lt;/span&gt;add veth-host &lt;span class="nb"&gt;type &lt;/span&gt;veth peer name veth-container
ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-host master td0
ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-host up

ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-container netns &amp;lt;container-pid&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then inside the container namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr add 10.10.0.2/24 dev veth-container
ip &lt;span class="nb"&gt;link set &lt;/span&gt;veth-container name eth0
ip &lt;span class="nb"&gt;link set &lt;/span&gt;eth0 up
ip route add default via 10.10.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, on the host, NAT is needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-A&lt;/span&gt; POSTROUTING &lt;span class="nt"&gt;-s&lt;/span&gt; 10.10.0.0/24 &lt;span class="nt"&gt;-j&lt;/span&gt; MASQUERADE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also IP forwarding must be enabled:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sysctl &lt;span class="nt"&gt;-w&lt;/span&gt; net.ipv4.ip_forward&lt;span class="o"&gt;=&lt;/span&gt;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the point where I started to appreciate Docker networking much more.&lt;/p&gt;

&lt;p&gt;Because every simple Docker command hides many small Linux networking operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging container networking
&lt;/h3&gt;

&lt;p&gt;The useful commands were:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ip addr
ip &lt;span class="nb"&gt;link
&lt;/span&gt;ip route
ip netns
iptables &lt;span class="nt"&gt;-t&lt;/span&gt; nat &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt;
sysctl net.ipv4.ip_forward
ping
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some issues I hit or expected:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;loopback was down&lt;/li&gt;
&lt;li&gt;veth interface was created but not moved correctly&lt;/li&gt;
&lt;li&gt;IP address was missing&lt;/li&gt;
&lt;li&gt;default route was missing&lt;/li&gt;
&lt;li&gt;NAT rule was missing&lt;/li&gt;
&lt;li&gt;host forwarding was disabled&lt;/li&gt;
&lt;li&gt;DNS was not configured&lt;/li&gt;
&lt;li&gt;interface name inside namespace was not what I expected&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This part reminded me that networking bugs are usually not one big bug.&lt;/p&gt;

&lt;p&gt;They are often one missing small step.&lt;/p&gt;

&lt;p&gt;One missing route.&lt;/p&gt;

&lt;p&gt;One down interface.&lt;/p&gt;

&lt;p&gt;One missing NAT rule.&lt;/p&gt;

&lt;p&gt;One wrong namespace.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day 10: Polish, README, and Architecture
&lt;/h2&gt;

&lt;p&gt;On Day 10, I focused on making the project understandable.&lt;/p&gt;

&lt;p&gt;A learning project is more valuable when other people can read it.&lt;/p&gt;

&lt;p&gt;So I improved the README and documented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;project goal&lt;/li&gt;
&lt;li&gt;architecture&lt;/li&gt;
&lt;li&gt;installation&lt;/li&gt;
&lt;li&gt;usage examples&lt;/li&gt;
&lt;li&gt;known limitations&lt;/li&gt;
&lt;li&gt;roadmap&lt;/li&gt;
&lt;li&gt;what each feature demonstrates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The final mental model looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tiny-docker-go
│
├── CLI
│   ├── run
│   ├── ps
│   ├── logs
│   └── stop
│
├── Runtime
│   ├── parent process
│   ├── child process
│   ├── namespace setup
│   ├── rootfs setup
│   └── command execution
│
├── State
│   ├── container id
│   ├── metadata json
│   ├── pid
│   ├── status
│   └── created_at
│
├── Logs
│   └── stdout/stderr capture
│
├── Cgroups
│   ├── memory.max
│   └── cgroup.procs
│
└── Network
    ├── network namespace
    ├── bridge
    ├── veth pair
    └── NAT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the user-facing commands look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine /bin/sh
tiny-docker-go ps
tiny-docker-go logs &amp;lt;container-id&amp;gt;
tiny-docker-go stop &amp;lt;container-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is still tiny.&lt;/p&gt;

&lt;p&gt;But it is not just a toy CLI anymore.&lt;/p&gt;

&lt;p&gt;It demonstrates many of the core ideas behind containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I learned about containers
&lt;/h2&gt;

&lt;p&gt;After building this project, my mental model of Docker changed.&lt;/p&gt;

&lt;p&gt;Before, I thought of Docker mostly as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;images + containers + Dockerfile + ports + volumes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I think about it more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;container = isolated Linux process + prepared filesystem + resource limits + networking + lifecycle metadata
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a much more useful model.&lt;/p&gt;

&lt;p&gt;A container is not magic.&lt;/p&gt;

&lt;p&gt;It is a process.&lt;/p&gt;

&lt;p&gt;But it is a carefully prepared process.&lt;/p&gt;

&lt;p&gt;The runtime says:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this process should see this hostname&lt;/li&gt;
&lt;li&gt;this process should see this PID tree&lt;/li&gt;
&lt;li&gt;this process should use this root filesystem&lt;/li&gt;
&lt;li&gt;this process should have this memory limit&lt;/li&gt;
&lt;li&gt;this process should write logs here&lt;/li&gt;
&lt;li&gt;this process should be connected to this network&lt;/li&gt;
&lt;li&gt;this process should be stopped with these signals&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the core idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  Namespaces vs cgroups
&lt;/h2&gt;

&lt;p&gt;One of the clearest lessons was the difference between namespaces and cgroups.&lt;/p&gt;

&lt;p&gt;I would explain it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Namespaces control what a process can see.
Cgroups control what a process can use.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Examples:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID namespace:
The process sees its own process tree.

UTS namespace:
The process sees its own hostname.

Mount namespace:
The process sees its own mount table.

Network namespace:
The process sees its own network interfaces and routes.

Cgroups:
The process can only use a limited amount of memory, CPU, pids, or IO.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This distinction is simple, but it explains so much.&lt;/p&gt;

&lt;p&gt;If a container cannot see host processes, that is namespace isolation.&lt;/p&gt;

&lt;p&gt;If a container gets killed after using too much memory, that is cgroup enforcement.&lt;/p&gt;

&lt;p&gt;If a container has its own IP address, that is network namespace plus virtual networking.&lt;/p&gt;

&lt;p&gt;If a container sees Alpine files instead of host files, that is rootfs setup plus mount isolation.&lt;/p&gt;

&lt;p&gt;Docker combines all of these into one clean developer experience.&lt;/p&gt;




&lt;h2&gt;
  
  
  Small Linux details that mattered
&lt;/h2&gt;

&lt;p&gt;This project taught me many small Linux details that are easy to miss when only using Docker.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. PID 1 is special
&lt;/h3&gt;

&lt;p&gt;The first process inside a PID namespace becomes PID 1.&lt;/p&gt;

&lt;p&gt;PID 1 handles signals differently and is responsible for reaping orphaned child processes.&lt;/p&gt;

&lt;p&gt;This matters for container shutdown.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;/proc&lt;/code&gt; must match the PID namespace
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;/proc&lt;/code&gt; is not mounted inside the container correctly, tools like &lt;code&gt;ps&lt;/code&gt; may show confusing information.&lt;/p&gt;

&lt;p&gt;Mounting &lt;code&gt;proc&lt;/code&gt; inside the container is not just cosmetic.&lt;/p&gt;

&lt;p&gt;It affects how process information is visible.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. &lt;code&gt;chroot&lt;/code&gt; changes &lt;code&gt;/&lt;/code&gt;, but it is not a complete security model
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;chroot&lt;/code&gt; is useful for learning filesystem isolation.&lt;/p&gt;

&lt;p&gt;But real containers need stronger filesystem and security handling.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Logs are mostly stdout and stderr
&lt;/h3&gt;

&lt;p&gt;Container logging starts with capturing process output.&lt;/p&gt;

&lt;p&gt;If your app logs to stdout/stderr, the runtime can collect it naturally.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Graceful stop matters
&lt;/h3&gt;

&lt;p&gt;A runtime should usually send SIGTERM first.&lt;/p&gt;

&lt;p&gt;SIGKILL should be the fallback.&lt;/p&gt;

&lt;p&gt;This gives the process a chance to shut down cleanly.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. cgroups are kernel-enforced
&lt;/h3&gt;

&lt;p&gt;The runtime does not manually police memory in a loop.&lt;/p&gt;

&lt;p&gt;It writes limits into cgroup files, then the kernel enforces them.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. A new network namespace has no useful network by default
&lt;/h3&gt;

&lt;p&gt;Isolation comes first.&lt;/p&gt;

&lt;p&gt;Connectivity must be built.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. veth pairs are like virtual cables
&lt;/h3&gt;

&lt;p&gt;One side stays on the host.&lt;/p&gt;

&lt;p&gt;One side goes into the container.&lt;/p&gt;

&lt;p&gt;That simple idea powers a lot of container networking.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. NAT is what makes outbound internet work in the simple bridge model
&lt;/h3&gt;

&lt;p&gt;Without NAT and IP forwarding, the container may have an IP but still not reach the internet.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Metadata turns a process into something manageable
&lt;/h3&gt;

&lt;p&gt;Without metadata, you only started a process.&lt;/p&gt;

&lt;p&gt;With metadata, you can list it, stop it, inspect it, and read its logs.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this project is not
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;tiny-docker-go&lt;/code&gt; is not a Docker replacement.&lt;/p&gt;

&lt;p&gt;It does not support real image pulling.&lt;/p&gt;

&lt;p&gt;It does not implement OCI fully.&lt;/p&gt;

&lt;p&gt;It does not have production security.&lt;/p&gt;

&lt;p&gt;It does not have a daemon.&lt;/p&gt;

&lt;p&gt;It does not have advanced volume management.&lt;/p&gt;

&lt;p&gt;It does not have complete port publishing.&lt;/p&gt;

&lt;p&gt;It does not handle all cgroup configurations.&lt;/p&gt;

&lt;p&gt;It does not support all namespace combinations safely.&lt;/p&gt;

&lt;p&gt;It does not include seccomp, AppArmor, SELinux, or capabilities hardening yet.&lt;/p&gt;

&lt;p&gt;And that is okay.&lt;/p&gt;

&lt;p&gt;The goal is not production.&lt;/p&gt;

&lt;p&gt;The goal is learning.&lt;/p&gt;

&lt;p&gt;Actually, keeping it small made the learning better.&lt;/p&gt;

&lt;p&gt;When a project becomes too complete, it can hide the concept again.&lt;/p&gt;

&lt;p&gt;I wanted the opposite.&lt;/p&gt;

&lt;p&gt;I wanted the concept to stay visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I want to add next
&lt;/h2&gt;

&lt;p&gt;After these 10 days, there are many possible next steps.&lt;/p&gt;

&lt;p&gt;Some features I want to explore:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Better image support
&lt;/h3&gt;

&lt;p&gt;Right now, rootfs is local.&lt;/p&gt;

&lt;p&gt;A next step could be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go pull alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if it is not a full registry implementation, I can start with downloading and unpacking rootfs archives.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. OverlayFS
&lt;/h3&gt;

&lt;p&gt;Docker images are layer-based.&lt;/p&gt;

&lt;p&gt;A good next step is to use OverlayFS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lowerdir = image layer
upperdir = container writable layer
workdir  = overlay work directory
merged   = final container rootfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would make the filesystem model closer to real containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Port mapping
&lt;/h3&gt;

&lt;p&gt;Outbound internet is one thing.&lt;/p&gt;

&lt;p&gt;Publishing container ports is another.&lt;/p&gt;

&lt;p&gt;A next step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tiny-docker-go run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:80 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This would require NAT/DNAT rules or a proxy approach.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Better process supervision
&lt;/h3&gt;

&lt;p&gt;The runtime could track exit status, update metadata automatically, and clean up resources more reliably.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Capabilities
&lt;/h3&gt;

&lt;p&gt;Linux capabilities are very important for container security.&lt;/p&gt;

&lt;p&gt;Instead of giving a process full root power, Linux can split privileges into smaller capabilities.&lt;/p&gt;

&lt;p&gt;Dropping capabilities would make the runtime more realistic.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Seccomp
&lt;/h3&gt;

&lt;p&gt;Seccomp can restrict which syscalls a process can use.&lt;/p&gt;

&lt;p&gt;This is another important container hardening feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. User namespace
&lt;/h3&gt;

&lt;p&gt;User namespaces are powerful because they can make a process think it is root inside the container while mapping it to a less privileged user on the host.&lt;/p&gt;

&lt;p&gt;This is a very interesting security feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. OCI runtime spec
&lt;/h3&gt;

&lt;p&gt;Eventually, I want to read more about the OCI runtime spec and compare my tiny runtime with how real runtimes are structured.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;This project made Docker feel less magical and more impressive.&lt;/p&gt;

&lt;p&gt;Less magical because I can now see the Linux pieces behind it.&lt;/p&gt;

&lt;p&gt;More impressive because I understand how many details Docker handles for us.&lt;/p&gt;

&lt;p&gt;Running a container sounds simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But under that command, a runtime needs to prepare isolation, filesystem, networking, logs, metadata, signals, and resource limits.&lt;/p&gt;

&lt;p&gt;Building &lt;code&gt;tiny-docker-go&lt;/code&gt; helped me understand those pieces one by one.&lt;/p&gt;

&lt;p&gt;The most important lesson for me was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A container is just a Linux process, but the runtime carefully shapes the world around that process.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That world includes what the process can see, what it can use, where its files come from, how its logs are captured, how it receives signals, and how it connects to the network.&lt;/p&gt;

&lt;p&gt;This is why building a tiny container runtime is such a useful learning project.&lt;/p&gt;

&lt;p&gt;You do not need to rebuild Docker completely.&lt;/p&gt;

&lt;p&gt;You only need to rebuild enough of it to understand the ideas.&lt;/p&gt;

&lt;p&gt;That is what I tried to do with &lt;code&gt;tiny-docker-go&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can follow the project here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/amirsefati/tiny-docker-go" rel="noopener noreferrer"&gt;https://github.com/amirsefati/tiny-docker-go&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Docker docs — Running containers: &lt;a href="https://docs.docker.com/engine/containers/run/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/containers/run/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker docs — &lt;code&gt;docker run&lt;/code&gt;: &lt;a href="https://docs.docker.com/reference/cli/docker/container/run/" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/cli/docker/container/run/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker docs — Container logs: &lt;a href="https://docs.docker.com/reference/cli/docker/container/logs/" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/cli/docker/container/logs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker docs — Container stop: &lt;a href="https://docs.docker.com/reference/cli/docker/container/stop/" rel="noopener noreferrer"&gt;https://docs.docker.com/reference/cli/docker/container/stop/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux man-pages — namespaces: &lt;a href="https://man7.org/linux/man-pages/man7/namespaces.7.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man7/namespaces.7.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux man-pages — PID namespaces: &lt;a href="https://man7.org/linux/man-pages/man7/pid_namespaces.7.html" rel="noopener noreferrer"&gt;https://man7.org/linux/man-pages/man7/pid_namespaces.7.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Linux kernel docs — cgroup v2: &lt;a href="https://docs.kernel.org/admin-guide/cgroup-v2.html" rel="noopener noreferrer"&gt;https://docs.kernel.org/admin-guide/cgroup-v2.html&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>go</category>
      <category>docker</category>
      <category>containers</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
