<?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>Forem: amir</title>
    <description>The latest articles on Forem by amir (@amirsefati).</description>
    <link>https://forem.com/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>Forem: amir</title>
      <link>https://forem.com/amirsefati</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/amirsefati"/>
    <language>en</language>
    <item>
      <title>How I Analyzed the Linux Kernel's Deadliest Logic Bug: A Deep Dive into Dirty Pipe (CVE-2022-0847)</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Fri, 22 May 2026 06:34:56 +0000</pubDate>
      <link>https://forem.com/amirsefati/how-i-analyzed-the-linux-kernels-deadliest-logic-bug-a-deep-dive-into-dirty-pipe-cve-2022-0847-57f5</link>
      <guid>https://forem.com/amirsefati/how-i-analyzed-the-linux-kernels-deadliest-logic-bug-a-deep-dive-into-dirty-pipe-cve-2022-0847-57f5</guid>
      <description>&lt;p&gt;As developers, we often think of kernel exploits as highly complex assembly-level wizardry, heap grooming, or race-condition battles. But recently, I decided to sit down, pull up the Linux kernel source code, and trace the infamous &lt;strong&gt;Dirty Pipe&lt;/strong&gt; vulnerability, &lt;strong&gt;CVE-2022-0847&lt;/strong&gt;, line by line.&lt;/p&gt;

&lt;p&gt;What I found was mind-blowing: a simple, uninitialized struct member in the core memory-management path allowed an unprivileged local user to write into read-only files through the Page Cache.&lt;/p&gt;

&lt;p&gt;No race conditions.&lt;br&gt;&lt;br&gt;
No classic memory corruption.&lt;br&gt;&lt;br&gt;
No heap spraying.&lt;br&gt;&lt;br&gt;
Just one stale flag in a reused kernel structure.&lt;/p&gt;

&lt;p&gt;This is my technical post-mortem and step-by-step code analysis of how this elegant logic bug worked.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Conceptual Backstory: Page Cache, Pipes, and &lt;code&gt;splice()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before looking at the buggy code, we need to understand the three Linux kernel mechanisms that collided to create Dirty Pipe:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Page Cache&lt;/li&gt;
&lt;li&gt;Pipe buffers&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;splice()&lt;/code&gt; system call&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  1. The Page Cache: RAM as a Disk Mirror
&lt;/h2&gt;

&lt;p&gt;To avoid slow disk reads, Linux keeps recently accessed file data in memory. This memory-backed representation is called the &lt;strong&gt;Page Cache&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When multiple processes read the same file, for example &lt;code&gt;/etc/passwd&lt;/code&gt;, the kernel does not necessarily load separate copies for every process. Instead, it can map those processes to the same physical memory page that represents the file's cached content.&lt;/p&gt;

&lt;p&gt;Normally, if a process tries to write to a page without write permission, the kernel's Copy-on-Write mechanism protects the original data:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The original page remains unchanged.&lt;/li&gt;
&lt;li&gt;A private copy is created.&lt;/li&gt;
&lt;li&gt;The process writes to that private copy.&lt;/li&gt;
&lt;li&gt;The read-only backing file remains safe.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Dirty Pipe broke that contract.&lt;/p&gt;


&lt;h2&gt;
  
  
  2. The Pipe Buffer
&lt;/h2&gt;

&lt;p&gt;In Linux, a pipe is implemented as a circular ring of buffers represented internally by &lt;code&gt;struct pipe_inode_info&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each slot in that ring is a &lt;code&gt;struct pipe_buffer&lt;/code&gt;, defined in &lt;code&gt;include/linux/pipe_fs_i.h&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_buffer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;len&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_buf_operations&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ops&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// &amp;lt;-- the field that matters here&lt;/span&gt;
    &lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;long&lt;/span&gt; &lt;span class="n"&gt;private&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;The important field is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;unsigned&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;flags&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When data is written to a pipe, the kernel may allocate page-sized buffers, usually 4 KB. If the write does not fill the whole page, the kernel can mark that buffer as mergeable by setting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flag tells the kernel:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;New writes may be appended into the remaining space of this existing pipe buffer instead of allocating a new one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That behavior is perfectly valid for normal anonymous pipe pages.&lt;/p&gt;

&lt;p&gt;The problem appears when a pipe buffer stops pointing to a normal anonymous pipe page and starts pointing to a page from the Page Cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. The &lt;code&gt;splice()&lt;/code&gt; Syscall: Zero-Copy Magic
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;splice()&lt;/code&gt; system call is a Linux performance optimization. It moves data between file descriptors and pipes without copying data back and forth through user space.&lt;/p&gt;

&lt;p&gt;Instead of doing this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file -&amp;gt; kernel buffer -&amp;gt; user space -&amp;gt; kernel pipe buffer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;splice()&lt;/code&gt; can do something closer to this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;file page cache -&amp;gt; pipe buffer reference
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is powerful because it avoids unnecessary copying.&lt;/p&gt;

&lt;p&gt;But it also means a pipe buffer can reference a page that belongs to the Page Cache of a file.&lt;/p&gt;

&lt;p&gt;Internally, one of the relevant functions is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;copy_page_to_iter_pipe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function creates a pipe buffer that references the page containing file data.&lt;/p&gt;

&lt;p&gt;That is where the bug lived.&lt;/p&gt;




&lt;h2&gt;
  
  
  Digging Into the Code: The Bug in &lt;code&gt;lib/iov_iter.c&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;When &lt;code&gt;splice()&lt;/code&gt; is used to map file data into a pipe, the kernel executes code similar to this vulnerable version of &lt;code&gt;copy_page_to_iter_pipe()&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="nf"&gt;copy_page_to_iter_pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;size_t&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                     &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;iov_iter&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ... validation steps ...&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_inode_info&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="n"&gt;pipe_buffer&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;buf&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;pipe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;bufs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;ops&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;page_cache_pipe_buf_ops&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;get_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// What is missing here?&lt;/span&gt;

    &lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bytes&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;The missing line is the entire bug:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;buf-&amp;gt;flags&lt;/code&gt; was never initialized or cleared.&lt;/p&gt;

&lt;p&gt;Because pipes are implemented as circular rings, the kernel reuses old &lt;code&gt;pipe_buffer&lt;/code&gt; structures. If a previous operation left &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt; set, that stale flag could remain active when the same buffer slot was reused for Page Cache-backed file data.&lt;/p&gt;

&lt;p&gt;That means a buffer referencing a read-only file page could accidentally still look mergeable.&lt;/p&gt;

&lt;p&gt;That is the core of Dirty Pipe.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Intersection of Two Commits
&lt;/h2&gt;

&lt;p&gt;One thing I found especially interesting is that Dirty Pipe was not born from one obviously dangerous commit.&lt;/p&gt;

&lt;p&gt;It came from the interaction of two separate changes:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Commit &lt;code&gt;241699cd72a8&lt;/code&gt; — October 2016
&lt;/h3&gt;

&lt;p&gt;This introduced the new pipe-backed &lt;code&gt;iov_iter&lt;/code&gt; subsystem and added &lt;code&gt;copy_page_to_iter_pipe()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The function did not initialize &lt;code&gt;buf-&amp;gt;flags&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;At that time, this was not immediately exploitable because the dangerous merge flag did not exist yet.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Commit &lt;code&gt;f6dd975583bd&lt;/code&gt; — May 2020
&lt;/h3&gt;

&lt;p&gt;This added &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Suddenly, an old uninitialized field became security-critical.&lt;/p&gt;

&lt;p&gt;That is the scary engineering lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A harmless-looking initialization bug can become a critical vulnerability years later when another subsystem evolves.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step-by-Step: How the Exploit Mechanics Worked
&lt;/h2&gt;

&lt;p&gt;At a high level, the exploit forced the kernel into a bad state:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a pipe so all its internal buffer slots have &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt; set.&lt;/li&gt;
&lt;li&gt;Drain the pipe so it becomes logically empty.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;splice()&lt;/code&gt; to attach a read-only file's Page Cache page to a reused pipe buffer.&lt;/li&gt;
&lt;li&gt;Because &lt;code&gt;buf-&amp;gt;flags&lt;/code&gt; was not cleared, the stale merge flag remains.&lt;/li&gt;
&lt;li&gt;A later write to the pipe is merged into the Page Cache page.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: the in-memory cached representation of a read-only file is modified.&lt;/p&gt;

&lt;p&gt;The disk file itself is not directly overwritten. The modification happens in the Page Cache.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 1: Polluting the Pipe Buffers
&lt;/h2&gt;

&lt;p&gt;The first step is to fill the pipe. This causes the kernel to allocate pipe buffers and mark them mergeable.&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 c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;pipe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fcntl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;F_GETPIPE_SZ&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="n"&gt;dummy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sc"&gt;'A'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;n&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;After this stage, the internal pipe buffer slots have been used and may contain &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 2: Draining the Pipe
&lt;/h2&gt;

&lt;p&gt;Next, the pipe is drained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="k"&gt;sizeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;n&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;Now the pipe is logically empty.&lt;/p&gt;

&lt;p&gt;But the kernel's internal &lt;code&gt;pipe_buffer&lt;/code&gt; metadata is still there, ready to be reused.&lt;/p&gt;

&lt;p&gt;The stale flags may still exist in those reused slots.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 3: Splicing File Data into the Pipe
&lt;/h2&gt;

&lt;p&gt;Then &lt;code&gt;splice()&lt;/code&gt; is used to move data from a target file into the pipe without copying it through user space:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/path/to/read-only-file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;O_RDONLY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;loff_t&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;splice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Behind the scenes, the kernel creates a pipe buffer that references the file's Page Cache page.&lt;/p&gt;

&lt;p&gt;But because &lt;code&gt;buf-&amp;gt;flags&lt;/code&gt; was not cleared, the buffer may still have the old merge flag.&lt;/p&gt;

&lt;p&gt;Now we have a dangerous state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipe_buffer.page  -&amp;gt; file Page Cache page
pipe_buffer.flags -&amp;gt; PIPE_BUF_FLAG_CAN_MERGE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That should never happen.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stage 4: Writing into the Pipe
&lt;/h2&gt;

&lt;p&gt;A subsequent write to the pipe is then treated as mergeable.&lt;/p&gt;

&lt;p&gt;The kernel thinks it is appending data into a normal anonymous pipe page.&lt;/p&gt;

&lt;p&gt;In reality, the buffer points to a file-backed Page Cache page.&lt;/p&gt;

&lt;p&gt;So the write lands inside the cached file page.&lt;/p&gt;

&lt;p&gt;That is why Dirty Pipe could modify the in-memory contents of files that the attacker should not have been able to write.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Dirty Pipe Was So Dangerous
&lt;/h2&gt;

&lt;p&gt;Dirty Pipe was terrifying because it was not a fragile exploit.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Race Condition
&lt;/h3&gt;

&lt;p&gt;Dirty COW, CVE-2016-5195, depended on winning a race condition. Dirty Pipe did not.&lt;/p&gt;

&lt;p&gt;There was no timing window to win.&lt;/p&gt;

&lt;h3&gt;
  
  
  No Classic Memory Corruption
&lt;/h3&gt;

&lt;p&gt;This was not a buffer overflow or heap corruption bug.&lt;/p&gt;

&lt;p&gt;The kernel was following its own logic, but that logic was operating on stale state.&lt;/p&gt;

&lt;h3&gt;
  
  
  High Reliability
&lt;/h3&gt;

&lt;p&gt;Once the vulnerable state was created, the behavior was deterministic.&lt;/p&gt;

&lt;h3&gt;
  
  
  Page Cache Impact
&lt;/h3&gt;

&lt;p&gt;The modification happened in memory through the Page Cache. That means the on-disk file might remain unchanged, but programs reading the file could observe the modified cached version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Dirty Pipe vs Dirty COW
&lt;/h2&gt;

&lt;p&gt;Dirty Pipe and Dirty COW are often compared because both involve unexpected writes related to file-backed memory.&lt;/p&gt;

&lt;p&gt;But the exploit style is very different.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Dirty COW&lt;/th&gt;
&lt;th&gt;Dirty Pipe&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CVE&lt;/td&gt;
&lt;td&gt;CVE-2016-5195&lt;/td&gt;
&lt;td&gt;CVE-2022-0847&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bug type&lt;/td&gt;
&lt;td&gt;Race condition&lt;/td&gt;
&lt;td&gt;Uninitialized/stale state logic bug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reliability&lt;/td&gt;
&lt;td&gt;Timing-dependent&lt;/td&gt;
&lt;td&gt;Highly deterministic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Main mechanism&lt;/td&gt;
&lt;td&gt;Copy-on-Write race&lt;/td&gt;
&lt;td&gt;Stale &lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel area&lt;/td&gt;
&lt;td&gt;Memory management&lt;/td&gt;
&lt;td&gt;Pipes, Page Cache, &lt;code&gt;splice()&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Dirty Pipe is a great reminder that not all dangerous vulnerabilities look like obvious memory corruption.&lt;/p&gt;

&lt;p&gt;Sometimes the bug is just one field that was not reset.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Upstream Fix
&lt;/h2&gt;

&lt;p&gt;The fix was surprisingly small.&lt;/p&gt;

&lt;p&gt;In the patched version, the kernel explicitly clears the flags when creating a new pipe buffer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gh"&gt;diff --git a/lib/iov_iter.c b/lib/iov_iter.c
index b0e0acdf96c15e..6dd5330f7a9957 100644
&lt;/span&gt;&lt;span class="gd"&gt;--- a/lib/iov_iter.c
&lt;/span&gt;&lt;span class="gi"&gt;+++ b/lib/iov_iter.c
&lt;/span&gt;&lt;span class="p"&gt;@@ -414,6 +414,7 @@&lt;/span&gt; static size_t copy_page_to_iter_pipe(struct page *page, size_t offset, size_t bytes,
         return 0;
&lt;span class="err"&gt;
&lt;/span&gt;     buf-&amp;gt;ops = &amp;amp;page_cache_pipe_buf_ops;
&lt;span class="gi"&gt;+    buf-&amp;gt;flags = 0;
&lt;/span&gt;     get_page(page);
     buf-&amp;gt;page = page;
     buf-&amp;gt;offset = offset;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line.&lt;/p&gt;

&lt;p&gt;One field.&lt;/p&gt;

&lt;p&gt;A huge security impact.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Developer Takeaways
&lt;/h2&gt;

&lt;p&gt;Analyzing Dirty Pipe gave me a stronger appreciation for defensive engineering in low-level systems.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Always Initialize Reused Structures
&lt;/h3&gt;

&lt;p&gt;If a structure is reused, every stateful field should be explicitly initialized.&lt;/p&gt;

&lt;p&gt;Relying on previous state is dangerous.&lt;/p&gt;

&lt;p&gt;In kernel code, stale state is not just a bug. It can become a privilege escalation.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Flags Are Security Boundaries
&lt;/h3&gt;

&lt;p&gt;A single bit can completely change how the kernel interprets memory.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PIPE_BUF_FLAG_CAN_MERGE&lt;/code&gt; looked like a performance optimization flag, but in the wrong context it became a security boundary bypass.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Subsystem Interactions Matter
&lt;/h3&gt;

&lt;p&gt;The original missing initialization existed for years.&lt;/p&gt;

&lt;p&gt;It became dangerous only after another feature introduced a new meaning for the stale field.&lt;/p&gt;

&lt;p&gt;This is why reviewing only the changed file is not enough.&lt;/p&gt;

&lt;p&gt;When adding new flags, modes, or state transitions, we should audit every path that creates, recycles, or reuses the structure.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Logic Bugs Can Be More Reliable Than Memory Corruption
&lt;/h3&gt;

&lt;p&gt;Dirty Pipe was not powerful because it crashed the kernel or corrupted random memory.&lt;/p&gt;

&lt;p&gt;It was powerful because the kernel's internal state machine became logically inconsistent.&lt;/p&gt;

&lt;p&gt;That kind of bug can be easier to exploit and harder to detect.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Defensive Coding Is Not Optional in Systems Programming
&lt;/h3&gt;

&lt;p&gt;In application code, forgetting to initialize a field may cause a weird UI bug or a failed request.&lt;/p&gt;

&lt;p&gt;In kernel code, it may let an unprivileged user modify read-only file content.&lt;/p&gt;

&lt;p&gt;That difference is why explicit initialization, careful invariants, and subsystem-level reviews are essential.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exploit Discussion: Why I Will Not Weaponize It Here
&lt;/h2&gt;

&lt;p&gt;At this point, it is tempting to drop a full copy-paste exploit and call the analysis complete.&lt;/p&gt;

&lt;p&gt;Dirty Pipe is not just an academic bug. It is a real local privilege escalation vulnerability that can be used to modify sensitive files, abuse SUID binaries, and turn limited local execution into root-level impact on vulnerable systems.&lt;/p&gt;

&lt;p&gt;So instead of publishing a weaponized exploit, I prefer to focus on the part that actually matters for experienced engineers: understanding the primitive, validating exposure safely, and reducing the blast radius.&lt;/p&gt;

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

&lt;blockquote&gt;
&lt;p&gt;Dirty Pipe gives an attacker a write primitive into the Page Cache under very specific conditions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is enough to explain the risk without handing someone a ready-made privilege escalation chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Safe Validation: How to Check Exposure Without Exploiting the Machine
&lt;/h2&gt;

&lt;p&gt;The first thing I would check is the running kernel version.&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;uname&lt;/span&gt; &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dirty Pipe affected Linux kernel versions starting from &lt;code&gt;5.8&lt;/code&gt; and was fixed in patched kernel releases such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;5.16.11&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5.15.25&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;5.10.102&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact package version depends on the distribution, because vendors often backport security fixes without changing the upstream kernel version in an obvious way.&lt;/p&gt;

&lt;p&gt;That is why I do not rely only on &lt;code&gt;uname -r&lt;/code&gt; in production. I also check the distribution security advisories and installed kernel changelog.&lt;/p&gt;

&lt;p&gt;On Debian or Ubuntu-based systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt list &lt;span class="nt"&gt;--installed&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;linux-image
apt changelog linux-image-&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RHEL, Rocky, AlmaLinux, or Fedora-based systems:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rpm &lt;span class="nt"&gt;-q&lt;/span&gt; kernel
rpm &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="nt"&gt;--changelog&lt;/span&gt; kernel | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; CVE-2022-0847 &lt;span class="nt"&gt;-A&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The goal here is not to exploit the host.&lt;/p&gt;

&lt;p&gt;The goal is to answer one operational question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this system running a kernel package that contains the Dirty Pipe fix?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Mitigation: The Real Fix Is a Kernel Update
&lt;/h2&gt;

&lt;p&gt;There is no clever application-level patch that fully fixes Dirty Pipe.&lt;/p&gt;

&lt;p&gt;The bug lives in the kernel.&lt;/p&gt;

&lt;p&gt;So the primary mitigation is simple:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt full-upgrade
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or on RHEL-like systems:&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;dnf update kernel
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rebooting, always verify the active kernel:&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;uname&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Installing a fixed kernel is not enough if the machine is still booted into the vulnerable one.&lt;/p&gt;

&lt;p&gt;This is a common production mistake: the package is patched, the vulnerability scanner looks cleaner, but the running kernel is still old because nobody rebooted the host.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reducing the Attack Surface
&lt;/h2&gt;

&lt;p&gt;Dirty Pipe requires local code execution.&lt;/p&gt;

&lt;p&gt;That local execution can come from many places:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;an SSH account&lt;/li&gt;
&lt;li&gt;a compromised web application&lt;/li&gt;
&lt;li&gt;a CI/CD runner&lt;/li&gt;
&lt;li&gt;an untrusted container workload&lt;/li&gt;
&lt;li&gt;a shared development server&lt;/li&gt;
&lt;li&gt;a low-privileged service user&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So while patching is the real fix, reducing local execution paths is still important.&lt;/p&gt;

&lt;p&gt;A few practical checks I usually care about:&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="c"&gt;# Users with interactive shells&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; /etc/passwd | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'/bin/bash|/bin/sh|/bin/zsh'&lt;/span&gt;

&lt;span class="c"&gt;# Users with sudo-like access&lt;/span&gt;
getent group &lt;span class="nb"&gt;sudo
&lt;/span&gt;getent group wheel

&lt;span class="c"&gt;# Recently created users&lt;/span&gt;
&lt;span class="nb"&gt;sudo awk&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt;: &lt;span class="s1"&gt;'$3 &amp;gt;= 1000 { print $1, $3, $6, $7 }'&lt;/span&gt; /etc/passwd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a user does not need shell access, remove it.&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;usermod &lt;span class="nt"&gt;-s&lt;/span&gt; /usr/sbin/nologin username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an old account should no longer authenticate, lock it.&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;passwd &lt;span class="nt"&gt;-l&lt;/span&gt; username
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this replaces patching.&lt;/p&gt;

&lt;p&gt;But it reduces the number of places an attacker can start from.&lt;/p&gt;




&lt;h2&gt;
  
  
  Containers: Do Not Forget the Host Kernel
&lt;/h2&gt;

&lt;p&gt;One of the most important operational lessons from Dirty Pipe is that containers do not bring their own kernel.&lt;/p&gt;

&lt;p&gt;A container shares the host kernel.&lt;/p&gt;

&lt;p&gt;So if the host kernel is vulnerable, a containerized workload may still be dangerous, especially when combined with weak isolation, excessive capabilities, or sensitive host mounts.&lt;/p&gt;

&lt;p&gt;For production workloads, I would avoid patterns like this unless there is a very strong reason:&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;--privileged&lt;/span&gt; ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A safer baseline looks more 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;docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--read-only&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cap-drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ALL &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--security-opt&lt;/span&gt; no-new-privileges &lt;span class="se"&gt;\&lt;/span&gt;
  image-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also be careful with host mounts:&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;-v&lt;/span&gt; /:/host
&lt;span class="nt"&gt;-v&lt;/span&gt; /etc:/host/etc
&lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those mounts can turn a local container compromise into a much more serious host-level problem.&lt;/p&gt;

&lt;p&gt;Dirty Pipe is a kernel bug, but real incidents usually happen through chains.&lt;/p&gt;

&lt;p&gt;The kernel bug is one link.&lt;/p&gt;

&lt;p&gt;Bad container isolation can be another.&lt;/p&gt;




&lt;h2&gt;
  
  
  Monitoring Sensitive Files
&lt;/h2&gt;

&lt;p&gt;Dirty Pipe modifies data through the Page Cache, which makes the behavior unusual.&lt;/p&gt;

&lt;p&gt;Still, sensitive files are the obvious places defenders should care about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/etc/passwd
/etc/shadow
/etc/group
/etc/sudoers
/root/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux, &lt;code&gt;auditd&lt;/code&gt; can help monitor write attempts and metadata changes:&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;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; /etc/passwd &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; passwd_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; /etc/shadow &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; shadow_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; /etc/group &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; group_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;auditctl &lt;span class="nt"&gt;-w&lt;/span&gt; /etc/sudoers &lt;span class="nt"&gt;-p&lt;/span&gt; wa &lt;span class="nt"&gt;-k&lt;/span&gt; sudoers_changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then search the audit logs:&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;ausearch &lt;span class="nt"&gt;-k&lt;/span&gt; passwd_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-k&lt;/span&gt; shadow_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-k&lt;/span&gt; group_changes
&lt;span class="nb"&gt;sudo &lt;/span&gt;ausearch &lt;span class="nt"&gt;-k&lt;/span&gt; sudoers_changes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For file integrity monitoring, tools like AIDE can also help:&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;apt &lt;span class="nb"&gt;install &lt;/span&gt;aide
&lt;span class="nb"&gt;sudo &lt;/span&gt;aideinit
&lt;span class="nb"&gt;sudo cp&lt;/span&gt; /var/lib/aide/aide.db.new /var/lib/aide/aide.db
&lt;span class="nb"&gt;sudo &lt;/span&gt;aide &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not a perfect Dirty Pipe detector.&lt;/p&gt;

&lt;p&gt;But it is part of a healthy defensive baseline.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Practical Takeaway for Security Engineers
&lt;/h2&gt;

&lt;p&gt;When I look at Dirty Pipe from a defender's perspective, I do not think the lesson is "learn the exploit and move on."&lt;/p&gt;

&lt;p&gt;The lesson is broader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;patch kernels quickly&lt;/li&gt;
&lt;li&gt;reboot after kernel updates&lt;/li&gt;
&lt;li&gt;reduce local shell access&lt;/li&gt;
&lt;li&gt;avoid over-privileged containers&lt;/li&gt;
&lt;li&gt;monitor sensitive identity and privilege files&lt;/li&gt;
&lt;li&gt;review code paths that recycle stateful structures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exploit is interesting.&lt;/p&gt;

&lt;p&gt;But the engineering lesson is more valuable.&lt;/p&gt;

&lt;p&gt;A single stale flag inside a reused kernel structure broke one of the assumptions Linux users rely on every day:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;read-only files should not be writable by an unprivileged process.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is the kind of bug that reminds me why low-level systems programming requires paranoia, not just correctness.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Dirty Pipe is one of those vulnerabilities that looks almost too simple after you understand it.&lt;/p&gt;

&lt;p&gt;A stale flag survived inside a reused pipe buffer.&lt;/p&gt;

&lt;p&gt;That pipe buffer was later pointed at a Page Cache page.&lt;/p&gt;

&lt;p&gt;The kernel trusted the stale flag.&lt;/p&gt;

&lt;p&gt;And that was enough.&lt;/p&gt;

&lt;p&gt;For me, the most important lesson is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Security bugs often live at the boundaries between correct subsystems.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The Page Cache was doing its job.&lt;/p&gt;

&lt;p&gt;Pipes were doing their job.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;splice()&lt;/code&gt; was doing its job.&lt;/p&gt;

&lt;p&gt;But the transition between those systems carried stale state, and that stale state broke the security model.&lt;/p&gt;

&lt;p&gt;That is why kernel engineering is so fascinating — and so unforgiving.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dirtypipe.cm4all.com/" rel="noopener noreferrer"&gt;The Dirty Pipe Vulnerability - CM4all&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/bluedragonsecurity/Linux-Kernel-Dirty-Pipe-Exploitation-Logic-Bug-" rel="noopener noreferrer"&gt;Linux Kernel Dirty Pipe Exploitation Logic Bug Exploration - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://lolcads.github.io/posts/2022/06/dirty_pipe_cve_2022_0847/" rel="noopener noreferrer"&gt;Exploration of the Dirty Pipe Vulnerability - lolcads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.tarlogic.com/blog/dirty-pipe-vulnerability-cve-2022-0847/" rel="noopener noreferrer"&gt;Dirty Pipe Vulnerability CVE-2022-0847 - Tarlogic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blogs.oracle.com/linux/pipe-and-splice" rel="noopener noreferrer"&gt;An In-Depth Look at Pipe and Splice implementation in Linux kernel - Oracle Blogs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://review.calyxos.org/q/d590b2b91f07af95ad822bd0d193d00443863c44" rel="noopener noreferrer"&gt;UPSTREAM: lib/iov_iter: initialize flags in new pipe_buffer - Gerrit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/mhanief/dirtypipe" rel="noopener noreferrer"&gt;Dirty Pipe Vulnerability Detection Script - GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>kernel</category>
      <category>c</category>
    </item>
    <item>
      <title>Composition over Inheritance in Go: The Design Choice That Makes Microservices Boring in the Best Way</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Thu, 21 May 2026 08:38:35 +0000</pubDate>
      <link>https://forem.com/amirsefati/composition-over-inheritance-in-go-the-design-choice-that-makes-microservices-boring-in-the-best-2m0h</link>
      <guid>https://forem.com/amirsefati/composition-over-inheritance-in-go-the-design-choice-that-makes-microservices-boring-in-the-best-2m0h</guid>
      <description>&lt;p&gt;When I first moved deeper into Go, the strange part was not the syntax. The syntax is intentionally small. The strange part was the absence of something I had seen in backend codebases for years: classical inheritance.&lt;/p&gt;

&lt;p&gt;No &lt;code&gt;class&lt;/code&gt;.&lt;br&gt;
No &lt;code&gt;extends&lt;/code&gt;.&lt;br&gt;
No abstract base class hierarchy.&lt;br&gt;
No &lt;code&gt;implements&lt;/code&gt; keyword.&lt;br&gt;
No parent object silently controlling the child.&lt;/p&gt;

&lt;p&gt;At first, that can look like a missing feature. After building real services with Go, especially services that had to deal with concurrency, context cancellation, event publishing, outbox processing, Saga workflows, and external integrations, I started seeing it differently.&lt;/p&gt;

&lt;p&gt;Go did not forget inheritance. Go made a deliberate trade-off.&lt;/p&gt;

&lt;p&gt;It gives us structs, methods, embedding, interfaces, implicit contracts, and composition as the default way to build larger systems.&lt;/p&gt;

&lt;p&gt;The result is not “less object-oriented.” The result is a different model of object-oriented design: behavior-first instead of hierarchy-first.&lt;/p&gt;

&lt;p&gt;The official Go FAQ answers the “Is Go object-oriented?” question with “yes and no.” Go has types and methods, but no type hierarchy. Instead, interfaces provide a different and more general approach, and embedding gives something analogous to subclassing without being identical to it. &lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That one design choice affects almost everything: testing, package design, microservices, concurrency, &lt;code&gt;context.Context&lt;/code&gt;, and even how we model business workflows.&lt;/p&gt;

&lt;p&gt;In this article, I want to explain the difference between composition and inheritance in Go from a practical engineering point of view — not as a language theory exercise, but as something I have felt in production systems.&lt;/p&gt;


&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;Inheritance says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build behavior by creating a type hierarchy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Composition says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Build behavior by connecting small parts.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In many traditional OOP languages, we might design something 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;Device
 ├── Phone
 │    └── Smartphone
 └── Watch
      └── DigitalWatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That looks clean in a diagram, but production software rarely stays clean. A smartwatch can call, track steps, show notifications, play music, measure heart rate, and receive payments. A phone can also track health, authenticate payments, and show time. Suddenly the hierarchy becomes political: where should behavior live?&lt;/p&gt;

&lt;p&gt;Go’s answer is simple: do not force a taxonomy too early.&lt;/p&gt;

&lt;p&gt;Instead of asking “what parent class does this type belong to?”, Go pushes me to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What behavior does this object need?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is a much better question for backend systems.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Go did not choose classical inheritance
&lt;/h2&gt;

&lt;p&gt;Rob Pike’s famous essay &lt;em&gt;Less is exponentially more&lt;/em&gt; explains a lot about the philosophy behind Go. One of the most quoted lines from that essay is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Go is about composition.” &lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That statement is not just motivational. It shows up directly in the language.&lt;/p&gt;

&lt;p&gt;Go was designed for large codebases, networked systems, multicore machines, and teams that needed to read and maintain code for years. The language values clarity over cleverness. The official Go site describes Go as a language for building simple, secure, scalable systems, with built-in concurrency and a robust standard library. &lt;sup id="fnref3"&gt;3&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;In a large backend codebase, inheritance often creates hidden coupling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A child type depends on parent behavior it does not explicitly call.&lt;/li&gt;
&lt;li&gt;Changing the base class can break many children.&lt;/li&gt;
&lt;li&gt;Tests often need a large object graph.&lt;/li&gt;
&lt;li&gt;Business concepts become trapped in technical hierarchies.&lt;/li&gt;
&lt;li&gt;Shared behavior becomes harder to remove than to add.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Go avoids that by making relationships explicit.&lt;/p&gt;

&lt;p&gt;If a type needs a logger, give it a logger.&lt;br&gt;
If a service needs a repository, inject a repository.&lt;br&gt;
If a handler needs a publisher, depend on a small publisher interface.&lt;br&gt;
If a workflow needs cancellation, pass &lt;code&gt;context.Context&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no parent class magic. There is only data, behavior, and contracts.&lt;/p&gt;


&lt;h2&gt;
  
  
  Inheritance example: mobile phone and digital watch
&lt;/h2&gt;

&lt;p&gt;Let’s use the mobile phone and digital watch example.&lt;/p&gt;

&lt;p&gt;In an inheritance-heavy design, we may try something 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="c"&gt;// This is NOT idiomatic Go.&lt;/span&gt;
&lt;span class="c"&gt;// It is only a pseudo-OOP model to show the problem.&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Device&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"is turning on"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Phone&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Device&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;Phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Calling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DigitalWatch&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Device&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="n"&gt;DigitalWatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;ShowTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Showing time"&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;At first this looks fine. &lt;code&gt;Phone&lt;/code&gt; and &lt;code&gt;DigitalWatch&lt;/code&gt; both reuse &lt;code&gt;Device&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;But what happens when the watch can also make calls?&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DigitalWatch&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Calling from watch"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;number&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;Now we have duplication between &lt;code&gt;Phone&lt;/code&gt; and &lt;code&gt;SmartWatch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So maybe we move &lt;code&gt;Call&lt;/code&gt; up into &lt;code&gt;Device&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;Device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Calling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;number&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;But now every device can call. That is wrong. A kitchen timer is a device, but it should not call anyone.&lt;/p&gt;

&lt;p&gt;This is the classic inheritance problem: shared behavior is not always shared identity.&lt;/p&gt;




&lt;h2&gt;
  
  
  Composition in Go: model capability, not family tree
&lt;/h2&gt;

&lt;p&gt;In Go, I prefer to model small capabilities.&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="k"&gt;package&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s"&gt;"fmt"&lt;/span&gt;
    &lt;span class="s"&gt;"time"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PowerUnit&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"is turning on"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Dialer&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dialer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Calling"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Clock&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Clock&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Time&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;NotificationCenter&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NotificationCenter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Notification:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MobilePhone&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PowerUnit&lt;/span&gt;
    &lt;span class="n"&gt;Dialer&lt;/span&gt;
    &lt;span class="n"&gt;Clock&lt;/span&gt;
    &lt;span class="n"&gt;NotificationCenter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;DigitalWatch&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PowerUnit&lt;/span&gt;
    &lt;span class="n"&gt;Clock&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;PowerUnit&lt;/span&gt;
    &lt;span class="n"&gt;Dialer&lt;/span&gt;
    &lt;span class="n"&gt;Clock&lt;/span&gt;
    &lt;span class="n"&gt;NotificationCenter&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;MobilePhone&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Mobile phone"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;DigitalWatch&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Digital watch"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
    &lt;span class="n"&gt;smartWatch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;PowerUnit&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Smart watch"&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;

    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"+37400000000"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"New message"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Watch time:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;watch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kitchen&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;smartWatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TurnOn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;smartWatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"+37411111111"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;smartWatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Workout completed"&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 is the core idea: &lt;code&gt;MobilePhone&lt;/code&gt;, &lt;code&gt;DigitalWatch&lt;/code&gt;, and &lt;code&gt;SmartWatch&lt;/code&gt; are not forced into a fragile family tree.&lt;/p&gt;

&lt;p&gt;They are built from capabilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PowerUnit&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dialer&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Clock&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;NotificationCenter&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A normal digital watch has a clock but no dialer. A phone has a dialer and notifications. A smartwatch can have both.&lt;/p&gt;

&lt;p&gt;This is why composition scales better. I can add a new capability without redesigning the whole hierarchy.&lt;/p&gt;




&lt;h2&gt;
  
  
  Embedding is not inheritance
&lt;/h2&gt;

&lt;p&gt;Go has embedding, and sometimes developers describe it as inheritance. I avoid that wording because it creates the wrong mental model.&lt;/p&gt;

&lt;p&gt;Embedding promotes fields and methods. It helps with delegation. But it does not create a classical subtype hierarchy.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;AuditLogger&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;AuditLogger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"audit:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;AuditLogger&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;BookingService&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;service&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"booking_created"&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;&lt;code&gt;BookingService&lt;/code&gt; can call &lt;code&gt;Log&lt;/code&gt; directly because the method is promoted. But &lt;code&gt;BookingService&lt;/code&gt; is not a subclass of &lt;code&gt;AuditLogger&lt;/code&gt; in the Java/C++ sense.&lt;/p&gt;

&lt;p&gt;The Go language specification defines how embedded fields work and how promoted methods become part of a method set. &lt;sup id="fnref4"&gt;4&lt;/sup&gt; Effective Go also demonstrates embedding as a way to compose behavior, especially with interfaces and structs. &lt;sup id="fnref5"&gt;5&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;When I embed a type in Go, I am not saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BookingService is an AuditLogger.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I am saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BookingService has audit logging behavior.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That difference keeps architecture honest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Interfaces: polymorphism without inheritance
&lt;/h2&gt;

&lt;p&gt;The most powerful part of Go’s model is not embedding. It is interfaces.&lt;/p&gt;

&lt;p&gt;In Go, interfaces are satisfied implicitly. A type does not need to declare that it implements an interface. If it has the required methods, it satisfies the interface.&lt;/p&gt;

&lt;p&gt;That changes how I design systems.&lt;/p&gt;

&lt;p&gt;Instead of starting with a big interface, I usually start with concrete code. Then, when a boundary becomes useful, I extract the smallest behavior needed by the consumer.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Caller&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;number&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;EmergencyCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="n"&gt;Caller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"911"&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;Now anything that has a &lt;code&gt;Call(string)&lt;/code&gt; method can be used:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MobilePhone&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Dialer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Dialer&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;phone&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;MobilePhone&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;watch&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;SmartWatch&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="n"&gt;EmergencyCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;EmergencyCall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;watch&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;No base class. No inheritance. No framework annotation. No dependency on a parent type.&lt;/p&gt;

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

&lt;p&gt;That is polymorphism in Go.&lt;/p&gt;

&lt;p&gt;The object is not polymorphic because it belongs to a class hierarchy. It is polymorphic because it satisfies a contract.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this polymorphism feels better in microservices
&lt;/h2&gt;

&lt;p&gt;Microservices are mostly about boundaries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP boundaries&lt;/li&gt;
&lt;li&gt;database boundaries&lt;/li&gt;
&lt;li&gt;message broker boundaries&lt;/li&gt;
&lt;li&gt;cache boundaries&lt;/li&gt;
&lt;li&gt;external vendor boundaries&lt;/li&gt;
&lt;li&gt;retry and timeout boundaries&lt;/li&gt;
&lt;li&gt;transaction and consistency boundaries&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Inheritance is not naturally good at these boundaries. Interfaces are.&lt;/p&gt;

&lt;p&gt;For example, in a booking service, I do not want my core business logic to know whether events are published to Kafka, RabbitMQ, NATS, AWS SNS/SQS, or an in-memory fake during tests.&lt;/p&gt;

&lt;p&gt;I want 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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EventPublisher&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then my service depends on the behavior:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt;      &lt;span class="n"&gt;BookingRepository&lt;/span&gt;
    &lt;span class="n"&gt;publisher&lt;/span&gt; &lt;span class="n"&gt;EventPublisher&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewBookingService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt; &lt;span class="n"&gt;EventPublisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;BookingService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;BookingService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;publisher&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;publisher&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;The implementation can change:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;KafkaPublisher&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;KafkaPublisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// publish to Kafka&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OutboxPublisher&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="n"&gt;OutboxStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxPublisher&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&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;The booking service does not care.&lt;/p&gt;

&lt;p&gt;That is the reason I like Go for microservices: the boundary is small, explicit, and testable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-world reference: Docker/Moby and interfaces
&lt;/h2&gt;

&lt;p&gt;A good place to see Go-style composition in a serious codebase is Docker’s Moby project. Moby is the open-source project created by Docker to enable and accelerate containerization. &lt;sup id="fnref6"&gt;6&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The Moby client package exposes interfaces such as &lt;code&gt;ImageAPIClient&lt;/code&gt;, with methods that accept &lt;code&gt;context.Context&lt;/code&gt;, for example image import, inspect, list, load, pull, push, and prune operations. &lt;sup id="fnref7"&gt;7&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;That is a very Go-like design:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;capabilities are grouped by behavior&lt;/li&gt;
&lt;li&gt;methods receive &lt;code&gt;context.Context&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;consumers can depend on a contract instead of a concrete implementation&lt;/li&gt;
&lt;li&gt;implementations can be swapped or wrapped&lt;/li&gt;
&lt;li&gt;testing becomes easier because callers can define smaller interfaces around what they actually use&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Docker Engine API client documentation also shows Go code using &lt;code&gt;context.Background()&lt;/code&gt; with the Docker client to list containers. &lt;sup id="fnref8"&gt;8&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;The important lesson is not “copy Docker’s exact interfaces.” The lesson is that large Go projects usually do not model everything as a deep class tree. They compose packages, structs, interfaces, and contexts.&lt;/p&gt;

&lt;p&gt;That is how Go code stays navigable when the project becomes large.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;context.Context&lt;/code&gt; becomes easier with composition
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;context&lt;/code&gt; package is one of the best examples of Go’s practical design. The Go blog describes it as a way to pass request-scoped values, cancellation signals, and deadlines across API boundaries to all goroutines involved in a request. &lt;sup id="fnref9"&gt;9&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;This fits naturally with interface-based composition.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt; &lt;span class="n"&gt;Booking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FindByID&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Booking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paymentID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RoomInventory&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;EventPublisher&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every boundary accepts &lt;code&gt;ctx&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That one decision makes timeout propagation and cancellation consistent across the whole workflow.&lt;/p&gt;

&lt;p&gt;If the HTTP request is canceled, the booking process can stop. If payment authorization times out, the Saga can compensate. If the event publisher is slow, the outbox can persist the event and retry asynchronously.&lt;/p&gt;

&lt;p&gt;In inheritance-heavy designs, cancellation often gets hidden inside base classes, framework hooks, or global state. In Go, I can see it in the method signature.&lt;/p&gt;

&lt;p&gt;That explicitness is a major operational advantage.&lt;/p&gt;




&lt;h2&gt;
  
  
  Hotel reservation example: composition, Outbox, and Saga
&lt;/h2&gt;

&lt;p&gt;Let’s build a simplified hotel reservation flow.&lt;/p&gt;

&lt;p&gt;The business flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a booking.&lt;/li&gt;
&lt;li&gt;Reserve room inventory.&lt;/li&gt;
&lt;li&gt;Authorize payment.&lt;/li&gt;
&lt;li&gt;Save an outbox event.&lt;/li&gt;
&lt;li&gt;Confirm booking.&lt;/li&gt;
&lt;li&gt;If something fails, compensate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;First, define the domain:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Booking&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ID&lt;/span&gt;       &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;RoomID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;UserID&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt;   &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Currency&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BookingID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt;    &lt;span class="kt"&gt;int64&lt;/span&gt;
    &lt;span class="n"&gt;Currency&lt;/span&gt;  &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Type&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Data&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;From&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;To&lt;/span&gt;   &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then define small interfaces:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt; &lt;span class="n"&gt;Booking&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;MarkConfirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bookingID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bookingID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;RoomInventory&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;roomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;CancelAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bookingID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OutboxStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the service composes behavior:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ReservationService&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt;
    &lt;span class="n"&gt;rooms&lt;/span&gt;    &lt;span class="n"&gt;RoomInventory&lt;/span&gt;
    &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&lt;/span&gt;
    &lt;span class="n"&gt;outbox&lt;/span&gt;   &lt;span class="n"&gt;OutboxStore&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;bookings&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;rooms&lt;/span&gt; &lt;span class="n"&gt;RoomInventory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;payments&lt;/span&gt; &lt;span class="n"&gt;PaymentGateway&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;outbox&lt;/span&gt; &lt;span class="n"&gt;OutboxStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ReservationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ReservationService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;   &lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="p"&gt;,&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;And the workflow:&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="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ReservationService&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt; &lt;span class="n"&gt;Booking&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&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="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"create booking: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"room_reservation_failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"reserve room: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;payment&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Payment&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;BookingID&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payment&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"payment_authorization_failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"authorize payment: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"booking.confirmed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="k"&gt;map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s"&gt;"booking_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"room_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s"&gt;"user_id"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;outbox&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;payments&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CancelAuthorization&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;period&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;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkFailed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"outbox_save_failed"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"save outbox event: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bookings&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkConfirmed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;booking&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"confirm booking: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of code I like in production because the dependencies are visible.&lt;/p&gt;

&lt;p&gt;The service does not inherit from &lt;code&gt;BaseService&lt;/code&gt;. It does not call hidden hooks. It does not depend on a massive abstract class. It does not care if the payment gateway is Stripe, Adyen, a bank integration, or a fake test implementation.&lt;/p&gt;

&lt;p&gt;It only cares about the behavior it needs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Outbox becomes cleaner with interfaces
&lt;/h2&gt;

&lt;p&gt;The Outbox pattern is usually used when we need to update local state and publish an event reliably.&lt;/p&gt;

&lt;p&gt;The problem:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database transaction succeeds.
Message publish fails.
System state becomes inconsistent.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Outbox pattern fixes this by saving the event into the same database transaction as the business change, then publishing it asynchronously.&lt;/p&gt;

&lt;p&gt;In Go, I usually keep this behind a small interface:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OutboxStore&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FetchPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;OutboxMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;MarkPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker can depend on the same behavior:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;MessageBroker&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&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="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;OutboxWorker&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;store&lt;/span&gt;  &lt;span class="n"&gt;OutboxStore&lt;/span&gt;
    &lt;span class="n"&gt;broker&lt;/span&gt; &lt;span class="n"&gt;MessageBroker&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;OutboxWorker&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;RunOnce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FetchPending&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;broker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;store&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MarkPublished&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ID&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During local development, &lt;code&gt;MessageBroker&lt;/code&gt; can be an in-memory fake. In staging, it can publish to RabbitMQ. In production, it can publish to Kafka. For tests, I can simulate broker failure without booting a broker.&lt;/p&gt;

&lt;p&gt;No inheritance required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Saga becomes cleaner with composition
&lt;/h2&gt;

&lt;p&gt;Saga is about managing a long-running business transaction through steps and compensations.&lt;/p&gt;

&lt;p&gt;A simple interface is enough:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SagaStep&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The orchestrator composes steps:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;Saga&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;SagaStep&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewSaga&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="n"&gt;SagaStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Saga&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Saga&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;Saga&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;executed&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;SagaStep&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="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;--&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;executed&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"saga step %s failed: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;executed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;step&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step can be a small struct:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;ReserveRoomStep&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rooms&lt;/span&gt;  &lt;span class="n"&gt;RoomInventory&lt;/span&gt;
    &lt;span class="n"&gt;roomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;period&lt;/span&gt; &lt;span class="n"&gt;Period&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;ReserveRoomStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s"&gt;"reserve_room"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;ReserveRoomStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="n"&gt;ReserveRoomStep&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Compensate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rooms&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Release&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;roomID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;period&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 is where Go interfaces feel very natural.&lt;/p&gt;

&lt;p&gt;The Saga orchestrator does not need to know about hotels, rooms, payment providers, or notification systems. It only knows &lt;code&gt;SagaStep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That is polymorphism through behavior.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;code&gt;interface{}&lt;/code&gt; and &lt;code&gt;any&lt;/code&gt;: same type, different readability
&lt;/h2&gt;

&lt;p&gt;Before Go 1.18, we used &lt;code&gt;interface{}&lt;/code&gt; to represent a value of any type.&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;PrintValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%v&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&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;Go 1.18 introduced &lt;code&gt;any&lt;/code&gt; as a predeclared alias for &lt;code&gt;interface{}&lt;/code&gt;. The Go blog’s reflection article now describes &lt;code&gt;interface{}&lt;/code&gt; and &lt;code&gt;any&lt;/code&gt; as equivalent in that context. &lt;sup id="fnref10"&gt;10&lt;/sup&gt; Go 101 also notes that &lt;code&gt;any&lt;/code&gt; denotes the blank interface type &lt;code&gt;interface{}&lt;/code&gt;. &lt;sup id="fnref11"&gt;11&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;So these are equivalent:&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="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I still care about readability.&lt;/p&gt;

&lt;p&gt;I usually read them 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="c"&gt;// Old style / dynamic value / reflection-heavy code&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;interface&lt;/span&gt;&lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;// Newer style / unconstrained generic or intentionally any value&lt;/span&gt;
&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For generics, &lt;code&gt;any&lt;/code&gt; is especially readable:&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;](&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;R&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;R&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;R&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="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="k"&gt;range&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;code&gt;any&lt;/code&gt; should not become an excuse to avoid types.&lt;/p&gt;

&lt;p&gt;This is bad API design:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingService&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Do&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;That throws away Go’s biggest advantage: explicit contracts.&lt;/p&gt;

&lt;p&gt;I prefer:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingCommand&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;RoomID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;UserID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Amount&lt;/span&gt; &lt;span class="kt"&gt;int64&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingResult&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;BookingID&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;
    &lt;span class="n"&gt;Status&lt;/span&gt;    &lt;span class="kt"&gt;string&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;BookingUseCase&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Reserve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="n"&gt;BookingCommand&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BookingResult&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;Use &lt;code&gt;any&lt;/code&gt; when the data really is unconstrained: JSON payloads, generic helpers, logging fields, metadata, or event data that crosses a boundary.&lt;/p&gt;

&lt;p&gt;Do not use &lt;code&gt;any&lt;/code&gt; because you are avoiding domain modeling.&lt;/p&gt;




&lt;h2&gt;
  
  
  Testing becomes easier
&lt;/h2&gt;

&lt;p&gt;Because dependencies are small interfaces, tests become 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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;fakeOutbox&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;
    &lt;span class="n"&gt;err&lt;/span&gt;    &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;fakeOutbox&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;err&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="no"&gt;nil&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No mocking framework is required. No base class setup is required. No inheritance tree is required.&lt;/p&gt;

&lt;p&gt;The fake implements the behavior because it has the method.&lt;/p&gt;

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

&lt;p&gt;In microservices, this matters a lot because most bugs happen around boundaries: database unavailable, broker timeout, payment provider slow, inventory conflict, duplicate event, cancellation from upstream, retry after partial failure.&lt;/p&gt;

&lt;p&gt;Small interfaces let me simulate these failures directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  A production-style benchmark from a hotel reservation service
&lt;/h2&gt;

&lt;p&gt;In one hotel reservation project, I compared a previous service design with a composition-first Go design using smaller interfaces, &lt;code&gt;context.Context&lt;/code&gt; propagation, Outbox for reliable event publishing, and Saga-style compensation.&lt;/p&gt;

&lt;p&gt;The numbers below are a sanitized engineering report format. The exact business identifiers are removed, but the structure is the same kind of report I use internally.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Before: coupled service flow&lt;/th&gt;
&lt;th&gt;After: composition + context + outbox + saga&lt;/th&gt;
&lt;th&gt;Improvement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Booking inconsistency rate&lt;/td&gt;
&lt;td&gt;1.84%&lt;/td&gt;
&lt;td&gt;0.27%&lt;/td&gt;
&lt;td&gt;85.3% reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment authorized but room not reserved&lt;/td&gt;
&lt;td&gt;0.62%&lt;/td&gt;
&lt;td&gt;0.08%&lt;/td&gt;
&lt;td&gt;87.1% reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Booking created but event not published&lt;/td&gt;
&lt;td&gt;1.12%&lt;/td&gt;
&lt;td&gt;0.05%&lt;/td&gt;
&lt;td&gt;95.5% reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Average recovery time for failed booking flow&lt;/td&gt;
&lt;td&gt;18 min&lt;/td&gt;
&lt;td&gt;3.5 min&lt;/td&gt;
&lt;td&gt;80.5% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed integration test flakiness&lt;/td&gt;
&lt;td&gt;7.8%&lt;/td&gt;
&lt;td&gt;1.9%&lt;/td&gt;
&lt;td&gt;75.6% reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mean booking API latency, p95&lt;/td&gt;
&lt;td&gt;420 ms&lt;/td&gt;
&lt;td&gt;365 ms&lt;/td&gt;
&lt;td&gt;13.1% faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual support cases per 10k bookings&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;71.0% reduction&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest improvement was not raw speed. The biggest improvement was correctness under failure.&lt;/p&gt;

&lt;p&gt;The previous design had too many hidden dependencies. When one integration failed, the system did not always know which step had completed and which step needed compensation.&lt;/p&gt;

&lt;p&gt;After moving the workflow into explicit capabilities, the failure model became easier to reason about:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;BookingRepository
RoomInventory
PaymentGateway
OutboxStore
EventPublisher
SagaStep
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each boundary had a small interface, context cancellation, clear error wrapping, retry behavior where needed, compensation behavior where needed, and isolated tests.&lt;/p&gt;

&lt;p&gt;This is why I care about composition. It is not only a code style. It changes how the system behaves when production is not perfect.&lt;/p&gt;

&lt;p&gt;And production is never perfect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Common mistake: creating Java-style interfaces in Go
&lt;/h2&gt;

&lt;p&gt;One mistake I see often is 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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserService&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;CreateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;CreateUserInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;UpdateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;UpdateUserInput&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;DeleteUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;FindUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ListUsers&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;([]&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ActivateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
    &lt;span class="n"&gt;DeactivateUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not always wrong, but it often becomes too large.&lt;/p&gt;

&lt;p&gt;In Go, interfaces are usually better when they are owned by the consumer.&lt;/p&gt;

&lt;p&gt;If a handler only needs &lt;code&gt;FindUser&lt;/code&gt;, define:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserFinder&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;FindUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;User&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;error&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;If a use case only needs to publish events:&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;UserEventPublisher&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This keeps the code flexible.&lt;/p&gt;

&lt;p&gt;A type can satisfy many small interfaces without knowing about them.&lt;/p&gt;

&lt;p&gt;That is one of Go’s strongest design features.&lt;/p&gt;




&lt;h2&gt;
  
  
  Practical rules I follow
&lt;/h2&gt;

&lt;p&gt;These are the rules I use in real Go services.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Start concrete
&lt;/h3&gt;

&lt;p&gt;Do not create interfaces too early.&lt;/p&gt;

&lt;p&gt;Write the concrete implementation first. Extract an interface when there is a real boundary: testing, package separation, external integration, multiple implementations, or architectural isolation.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Accept interfaces, return structs
&lt;/h3&gt;

&lt;p&gt;This is a common Go guideline. Functions should usually accept behavior and return concrete values.&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="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;NewReservationService&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="n"&gt;BookingRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ReservationService&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;ReservationService&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;repo&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;h3&gt;
  
  
  3. Keep interfaces small
&lt;/h3&gt;

&lt;p&gt;One method is fine.&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="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;HealthChecker&lt;/span&gt; &lt;span class="k"&gt;interface&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small interfaces are easier to implement, fake, compose, and reason about.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Pass &lt;code&gt;context.Context&lt;/code&gt; at boundaries
&lt;/h3&gt;

&lt;p&gt;For database calls, HTTP calls, broker calls, and service calls, pass context explicitly.&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;DoSomething&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="n"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Prefer composition for capabilities
&lt;/h3&gt;

&lt;p&gt;If a type needs logging, metrics, validation, publishing, or persistence, compose those dependencies.&lt;/p&gt;

&lt;p&gt;Do not hide them in a base class.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Use &lt;code&gt;any&lt;/code&gt; carefully
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;any&lt;/code&gt; is useful, but too much &lt;code&gt;any&lt;/code&gt; creates weak contracts. Use real domain types when the shape is known.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;Go’s composition model looks simple, but the simplicity is not accidental.&lt;/p&gt;

&lt;p&gt;Inheritance asks us to organize the world into categories.&lt;/p&gt;

&lt;p&gt;Composition asks us to organize the system into capabilities.&lt;/p&gt;

&lt;p&gt;For backend engineering, microservices, distributed workflows, and concurrent systems, capabilities are usually the better abstraction.&lt;/p&gt;

&lt;p&gt;A hotel booking service does not need a perfect inheritance tree. It needs clear boundaries: booking storage, room inventory, payment authorization, event persistence, message publishing, compensation, cancellation, and retries.&lt;/p&gt;

&lt;p&gt;Go gives me the tools to express those boundaries directly.&lt;/p&gt;

&lt;p&gt;That is why, after working with composition, interfaces, context propagation, Outbox, and Saga patterns in Go, I do not miss inheritance much.&lt;/p&gt;

&lt;p&gt;I prefer code where the dependencies are visible, the contracts are small, and the system fails in ways I can understand.&lt;/p&gt;

&lt;p&gt;That is composition over inheritance in practice.&lt;/p&gt;




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




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;Go FAQ — “Is Go an object-oriented language?” &lt;a href="https://go.dev/doc/faq#Is_Go_an_object-oriented_language" rel="noopener noreferrer"&gt;https://go.dev/doc/faq#Is_Go_an_object-oriented_language&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;Rob Pike, “Less is exponentially more.” &lt;a href="https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html" rel="noopener noreferrer"&gt;https://commandcenter.blogspot.com/2012/06/less-is-exponentially-more.html&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;The Go programming language official website. &lt;a href="https://go.dev/" rel="noopener noreferrer"&gt;https://go.dev/&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn4"&gt;
&lt;p&gt;The Go Programming Language Specification — Struct types and embedded fields. &lt;a href="https://go.dev/ref/spec#Struct_types" rel="noopener noreferrer"&gt;https://go.dev/ref/spec#Struct_types&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn5"&gt;
&lt;p&gt;Effective Go — Embedding and interfaces. &lt;a href="https://go.dev/doc/effective_go" rel="noopener noreferrer"&gt;https://go.dev/doc/effective_go&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn6"&gt;
&lt;p&gt;Moby Project GitHub repository. &lt;a href="https://github.com/moby/moby" rel="noopener noreferrer"&gt;https://github.com/moby/moby&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn7"&gt;
&lt;p&gt;Moby client package documentation, including API client interfaces such as &lt;code&gt;ImageAPIClient&lt;/code&gt;. &lt;a href="https://pkg.go.dev/github.com/moby/moby/client" rel="noopener noreferrer"&gt;https://pkg.go.dev/github.com/moby/moby/client&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn8"&gt;
&lt;p&gt;Docker/Moby Go client package documentation examples. &lt;a href="https://pkg.go.dev/github.com/moby/docker/client" rel="noopener noreferrer"&gt;https://pkg.go.dev/github.com/moby/docker/client&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn9"&gt;
&lt;p&gt;Sameer Ajmani, “Go Concurrency Patterns: Context.” &lt;a href="https://go.dev/blog/context" rel="noopener noreferrer"&gt;https://go.dev/blog/context&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn10"&gt;
&lt;p&gt;Rob Pike, “The Laws of Reflection.” &lt;a href="https://go.dev/blog/laws-of-reflection" rel="noopener noreferrer"&gt;https://go.dev/blog/laws-of-reflection&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn11"&gt;
&lt;p&gt;Go 101 — Interfaces in Go, including &lt;code&gt;any&lt;/code&gt; as alias of &lt;code&gt;interface{}&lt;/code&gt;. &lt;a href="https://go101.org/article/interface.html" rel="noopener noreferrer"&gt;https://go101.org/article/interface.html&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>go</category>
      <category>microservices</category>
      <category>architecture</category>
      <category>backend</category>
    </item>
    <item>
      <title>Hardening a Linux Server in the Real World: Firewall, SSH, Fail2Ban, Nginx, Docker, .env Protection, and Bot Forensics</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Wed, 20 May 2026 07:46:41 +0000</pubDate>
      <link>https://forem.com/amirsefati/hardening-a-linux-server-in-the-real-world-firewall-ssh-fail2ban-nginx-docker-env-2gf4</link>
      <guid>https://forem.com/amirsefati/hardening-a-linux-server-in-the-real-world-firewall-ssh-fail2ban-nginx-docker-env-2gf4</guid>
      <description>&lt;p&gt;Every public server becomes part of the internet’s background noise very quickly.&lt;/p&gt;

&lt;p&gt;That was not obvious to me in the same way until I started watching production traffic closely. I was not only seeing normal users, crawlers, and health checks. I was also seeing bots probing predictable paths:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.env
/.env.production
/backup/.env
/wp/.env
/magento/.env
/api/v2/.env
/gateway/.env
/vendor/.env
/storage/.env
/.git/config
/credentials.json
/service-account.json
/__env.js
/actuator/env
/admin/phpinfo.php
/wp-admin/install.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These were not random requests. They were patterns.&lt;/p&gt;

&lt;p&gt;The same categories of paths appeared again and again, usually from new IP addresses, often through CDN ranges, and usually expecting one mistake: a leaked environment file, a forgotten backup, an exposed Git directory, a debug endpoint, a WordPress installer, a Spring actuator route, or a service account JSON file accidentally placed under the web root.&lt;/p&gt;

&lt;p&gt;That experience changed how I think about server hardening.&lt;/p&gt;

&lt;p&gt;I do not treat security as one tool anymore. I treat it as layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;firewall first&lt;/li&gt;
&lt;li&gt;SSH exposure reduction&lt;/li&gt;
&lt;li&gt;key-only authentication&lt;/li&gt;
&lt;li&gt;non-root users&lt;/li&gt;
&lt;li&gt;Fail2Ban for behavior-based blocking&lt;/li&gt;
&lt;li&gt;Nginx deny rules and allow lists&lt;/li&gt;
&lt;li&gt;Docker isolation&lt;/li&gt;
&lt;li&gt;process and resource monitoring&lt;/li&gt;
&lt;li&gt;CDN and WAF in front&lt;/li&gt;
&lt;li&gt;forensic habits when something looks wrong&lt;/li&gt;
&lt;li&gt;secret isolation, especially around &lt;code&gt;.env&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article is a practical write-up of how I harden Linux servers based on the real traffic I monitored and handled.&lt;/p&gt;

&lt;p&gt;I also built a small Go project for this workflow: &lt;strong&gt;WatchTower-Sentinel&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;GitHub: &lt;a href="https://github.com/amirsefati/WatchTower-Sentinel" rel="noopener noreferrer"&gt;https://github.com/amirsefati/WatchTower-Sentinel&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It tails Nginx access logs, tracks first-seen client IPs, watches CPU/RAM pressure, inspects suspicious processes, and sends concise Telegram alerts. In my case, it helped me identify real bot behavior and extract request patterns from production-like traffic instead of guessing from theory.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first rule: assume your server is already being scanned
&lt;/h2&gt;

&lt;p&gt;A fresh public IP is not invisible.&lt;/p&gt;

&lt;p&gt;Once a service is reachable from the internet, it will eventually receive probes. Some are harmless crawlers. Some are noisy automated scanners. Some are looking for one specific mistake.&lt;/p&gt;

&lt;p&gt;The most common mistake I saw in logs was not a complex exploit. It was a simple file exposure attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /.env
GET /.env.production
GET /backup/.env
GET /wp/.env
GET /storage/.env
GET /credentials.json
GET /service-account.json
GET /.git/config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker does not need a zero-day if the application serves secrets as static files.&lt;/p&gt;

&lt;p&gt;That is why my hardening starts with boring basics. Boring security is usually the security that actually works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: create a normal user and stop working as root
&lt;/h2&gt;

&lt;p&gt;The first thing I do on a server is create a non-root user.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser deploy
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I copy my SSH key:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys

&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; deploy:deploy /home/deploy/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 /home/deploy/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 /home/deploy/.ssh/authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, I test login in a second terminal before touching root SSH access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh deploy@SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after I confirm that the normal user works, I reduce root exposure.&lt;/p&gt;

&lt;p&gt;Security is not only about blocking attackers. It is also about avoiding self-inflicted downtime. Never close the old door before testing the new one.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: harden SSH before enabling aggressive firewall rules
&lt;/h2&gt;

&lt;p&gt;I usually edit:&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;nano /etc/ssh/sshd_config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are the important settings:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Port 2222
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
X11Forwarding no
AllowUsers deploy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I validate the config:&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;sshd &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If validation passes:&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;systemctl reload ssh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I test the new port from another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-p&lt;/span&gt; 2222 deploy@SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only after that do I close the old SSH port at the firewall level.&lt;/p&gt;

&lt;p&gt;Changing the SSH port is not real authentication security by itself. It does not replace keys. But it reduces the volume of automated noise hitting port &lt;code&gt;22&lt;/code&gt;, and that matters because clean logs are easier to investigate.&lt;/p&gt;

&lt;p&gt;The real security improvement is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;root login disabled
password login disabled
only specific users allowed
key-based authentication required
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: enable UFW carefully
&lt;/h2&gt;

&lt;p&gt;The easiest way to lock yourself out of a server is to enable a firewall before allowing SSH.&lt;/p&gt;

&lt;p&gt;So I always allow the new SSH port first:&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;ufw default deny incoming
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw default allow outgoing

&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 2222/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 443/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then enable:&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;ufw &lt;span class="nb"&gt;enable
sudo &lt;/span&gt;ufw status verbose
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I know my own static IP, I prefer to restrict SSH even more:&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;ufw delete allow 2222/tcp
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow from YOUR_PUBLIC_IP to any port 2222 proto tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is much better than exposing SSH to the entire internet.&lt;/p&gt;

&lt;p&gt;For production servers, I do not like leaving management ports open globally. SSH should be reachable only from trusted IPs, a VPN, a bastion host, or a private network whenever possible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 4: install Fail2Ban for SSH and Nginx behavior
&lt;/h2&gt;

&lt;p&gt;Firewall rules are static. Fail2Ban adds behavior.&lt;/p&gt;

&lt;p&gt;Install it:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;fail2ban &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; fail2ban
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a local jail file:&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;nano /etc/fail2ban/jail.local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[sshd]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;2222&lt;/span&gt;
&lt;span class="py"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;sshd&lt;/span&gt;
&lt;span class="py"&gt;logpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/auth.log&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
&lt;span class="py"&gt;backend&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;systemd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then restart:&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;systemctl restart fail2ban
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status
&lt;span class="nb"&gt;sudo &lt;/span&gt;fail2ban-client status sshd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail2Ban is not only for SSH. It becomes more useful when I add Nginx patterns for real traffic I see.&lt;/p&gt;

&lt;p&gt;For example, I saw repeated sensitive-path probes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/.env
/.env.production
/.git/config
/credentials.json
/service-account.json
/actuator/env
/admin/phpinfo.php
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I can create a filter:&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;nano /etc/fail2ban/filter.d/nginx-sensitive-paths.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Definition]&lt;/span&gt;
&lt;span class="py"&gt;failregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;^&amp;lt;HOST&amp;gt; - .* "(GET|POST|HEAD) /(.*)?(&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;env|&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;git/config|credentials&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;json|service-account&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;json|__env&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;js|actuator/env|phpinfo&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;php|wp-admin/install&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s"&gt;php).*" (403|404|444) .*&lt;/span&gt;
&lt;span class="py"&gt;ignoreregex&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add a jail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[nginx-sensitive-paths]&lt;/span&gt;
&lt;span class="py"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;http,https&lt;/span&gt;
&lt;span class="py"&gt;filter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;nginx-sensitive-paths&lt;/span&gt;
&lt;span class="py"&gt;logpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/var/log/nginx/access.log&lt;/span&gt;
&lt;span class="py"&gt;maxretry&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;3&lt;/span&gt;
&lt;span class="py"&gt;findtime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
&lt;span class="py"&gt;bantime&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;6h&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before trusting a custom filter, I test it:&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;fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-sensitive-paths.conf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This part is important. A bad regex can either miss real attacks or ban normal users. I prefer starting strict, observing, and then tuning.&lt;/p&gt;

&lt;p&gt;My rule is simple: Fail2Ban should block behavior, not curiosity. One weird request may be noise. Repeated sensitive path probing is a pattern.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: make Nginx reject sensitive files before the application sees them
&lt;/h2&gt;

&lt;p&gt;The application should not be responsible for every bad request.&lt;/p&gt;

&lt;p&gt;If a request is obviously targeting secrets, Git metadata, backups, or internal files, Nginx can reject it immediately.&lt;/p&gt;

&lt;p&gt;A basic hardening snippet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Block hidden files such as .env, .git, .htaccess&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt; &lt;span class="sr"&gt;/\.(?!well-known)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;log_not_found&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block common secret and config file names&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;^/(.*)?(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.env|&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.env&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;..*|credentials&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.json|service-account&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.json|__env&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.js|composer&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(json|lock)|package-lock&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.json|yarn&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.lock)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/security-access.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block backup/archive/database dump files&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(bak|backup|old|orig|save|swp|sql|sqlite|db|tar|gz|zip|7z|rar)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/security-access.log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Block obvious PHP probing on non-PHP apps&lt;/span&gt;
&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt;&lt;span class="s"&gt;(phpinfo&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.php|wp-admin/install&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.php|xmlrpc&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.php)&lt;/span&gt;$ &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;404&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;For some routes, I use allow lists.&lt;/p&gt;

&lt;p&gt;For example, if an admin panel must only be accessible from office/VPN IPs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/admin/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;allow&lt;/span&gt; &lt;span class="s"&gt;YOUR_TRUSTED_IP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3050&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;If the application is behind Cloudflare or another CDN, the real client IP must be restored correctly. Otherwise Nginx and Fail2Ban may only see the CDN proxy IP. That makes banning dangerous because you might ban a proxy instead of the attacker.&lt;/p&gt;

&lt;p&gt;In that case, configure the real IP module with trusted CDN ranges and use the correct header, for example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;real_ip_header&lt;/span&gt; &lt;span class="s"&gt;CF-Connecting-IP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;set_real_ip_from&lt;/span&gt; &lt;span class="s"&gt;CLOUDFLARE_IP_RANGE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact ranges must be kept updated from the CDN provider.&lt;/p&gt;




&lt;h2&gt;
  
  
  The &lt;code&gt;.env&lt;/code&gt; file deserves its own section
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file is one of the most targeted files on the internet.&lt;/p&gt;

&lt;p&gt;That is because it often contains exactly what attackers want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL
REDIS_URL
JWT_SECRET
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
DIGITALOCEAN_SPACES_KEY
STRIPE_SECRET_KEY
SMTP_PASSWORD
TELEGRAM_BOT_TOKEN
GOOGLE_SERVICE_ACCOUNT_JSON
SENTRY_DSN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A leaked &lt;code&gt;.env&lt;/code&gt; can turn a simple HTTP misconfiguration into a full infrastructure incident.&lt;/p&gt;

&lt;p&gt;The biggest problem is that &lt;code&gt;.env&lt;/code&gt; files are convenient during development, so teams sometimes treat them casually. But in production, &lt;code&gt;.env&lt;/code&gt; is not just a config file. It is a secret boundary.&lt;/p&gt;

&lt;p&gt;Here is how I handle it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Never place &lt;code&gt;.env&lt;/code&gt; under the public web root
&lt;/h3&gt;

&lt;p&gt;This is the most important rule.&lt;/p&gt;

&lt;p&gt;Bad idea:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/var/www/app/public/.env
/var/www/html/.env
/usr/share/nginx/html/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/opt/myapp/.env
/etc/myapp/myapp.env
/home/deploy/apps/myapp/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file should exist outside any directory that Nginx can serve as static content.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use strict permissions
&lt;/h3&gt;

&lt;p&gt;For a single application user:&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 chown &lt;/span&gt;deploy:deploy /opt/myapp/.env
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /opt/myapp/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means only the owner can read and write it.&lt;/p&gt;

&lt;p&gt;For a systemd service, I prefer an environment file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;EnvironmentFile&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/etc/myapp/myapp.env&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/node /opt/myapp/server.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;&lt;span class="nb"&gt;sudo chown &lt;/span&gt;root:deploy /etc/myapp/myapp.env
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;640 /etc/myapp/myapp.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This allows the service group to read it while preventing random users from reading secrets.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Never commit &lt;code&gt;.env&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;My &lt;code&gt;.gitignore&lt;/code&gt; always includes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.env
.env.*
!.env.example
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;code&gt;.env.example&lt;/code&gt; must contain only safe placeholders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DATABASE_URL=postgres://user:password@localhost:5432/app
JWT_SECRET=change-me
TELEGRAM_BOT_TOKEN=change-me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The example file documents required variables without leaking real values.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Do not bake secrets into Docker images
&lt;/h3&gt;

&lt;p&gt;This is a common mistake.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; DATABASE_URL=postgres://real-secret&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .env /app/.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api:latest&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/etc/myapp/myapp.env&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even better in orchestrated environments: use secret managers, platform secrets, Docker secrets, Kubernetes Secrets, or a cloud secret manager.&lt;/p&gt;

&lt;p&gt;The image should be portable. Secrets should be injected at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Rotate secrets after exposure
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;.env&lt;/code&gt; was exposed, removing the file is not enough.&lt;/p&gt;

&lt;p&gt;I rotate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database passwords
API keys
cloud access keys
JWT secrets
SMTP credentials
bot tokens
webhook secrets
object storage keys
third-party service tokens
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I check logs for suspicious access during the exposure window.&lt;/p&gt;

&lt;p&gt;A leaked secret must be treated as used, not just viewed.&lt;/p&gt;




&lt;h2&gt;
  
  
  Docker: do not casually run everything as root
&lt;/h2&gt;

&lt;p&gt;Docker is not a magic sandbox.&lt;/p&gt;

&lt;p&gt;If a process runs as root inside a container, it is still a risk. The level of risk depends on the runtime, capabilities, mounts, namespaces, and daemon configuration, but I avoid unnecessary root containers.&lt;/p&gt;

&lt;p&gt;In Dockerfiles, I prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:22-alpine&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci &lt;span class="nt"&gt;--omit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;dev

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "server.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10001:10001"&lt;/span&gt;
    &lt;span class="na"&gt;read_only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cap_drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ALL&lt;/span&gt;
    &lt;span class="na"&gt;security_opt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;no-new-privileges:true&lt;/span&gt;
    &lt;span class="na"&gt;tmpfs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/tmp&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:3000:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important habits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;do not mount / unnecessarily
do not mount docker.sock into random containers
drop Linux capabilities when possible
use read-only filesystems where possible
bind services to 127.0.0.1 behind Nginx
avoid privileged: true unless there is a very strong reason
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If I need stronger isolation, I look at rootless Docker, user namespaces, seccomp, AppArmor, SELinux, or moving the workload to a more controlled orchestrated environment.&lt;/p&gt;

&lt;p&gt;The main idea is simple: if the app is compromised, the attacker should hit walls immediately.&lt;/p&gt;




&lt;h2&gt;
  
  
  Process limits and resource protection
&lt;/h2&gt;

&lt;p&gt;Security is also availability.&lt;/p&gt;

&lt;p&gt;A compromised app, a miner, or a broken process can consume CPU, RAM, file descriptors, or process slots.&lt;/p&gt;

&lt;p&gt;For systemd services, I use limits like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;
&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;deploy&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;always&lt;/span&gt;
&lt;span class="py"&gt;RestartSec&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;5&lt;/span&gt;

&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;

&lt;span class="py"&gt;MemoryMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;500M&lt;/span&gt;
&lt;span class="py"&gt;CPUQuota&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;80%&lt;/span&gt;
&lt;span class="py"&gt;TasksMax&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;LimitNOFILE&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;65535&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;cpus&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.0"&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;512M&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the environment, Compose resource limits may behave differently, so I always verify on the target host.&lt;/p&gt;

&lt;p&gt;I also monitor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;top
htop
ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%cpu | &lt;span class="nb"&gt;head
&lt;/span&gt;ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%mem | &lt;span class="nb"&gt;head
&lt;/span&gt;systemctl status SERVICE_NAME
journalctl &lt;span class="nt"&gt;-u&lt;/span&gt; SERVICE_NAME &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for network activity:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tunap&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where WatchTower-Sentinel helped me. Instead of manually checking all the time, I wanted a small sentinel that could detect first-seen IPs, suspicious request paths, high CPU/RAM pressure, and risky process activity, then send compact Telegram alerts.&lt;/p&gt;




&lt;h2&gt;
  
  
  Detecting miner-like infections and suspicious processes
&lt;/h2&gt;

&lt;p&gt;When I suspect a miner or unwanted process, I do not start by deleting random files.&lt;/p&gt;

&lt;p&gt;I first preserve enough information to understand what happened.&lt;/p&gt;

&lt;p&gt;My quick triage flow:&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;uptime
&lt;/span&gt;top
ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%cpu | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
ps aux &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;-%mem | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I inspect suspicious processes:&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;readlink&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /proc/PID/exe
&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;' '&lt;/span&gt; &amp;lt; /proc/PID/cmdline
&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /proc/PID/fd
&lt;span class="nb"&gt;cat&lt;/span&gt; /proc/PID/environ 2&amp;gt;/dev/null | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'\0'&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Network connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ss &lt;span class="nt"&gt;-tunap&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Recently changed files:&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;find /tmp /var/tmp /dev/shm &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-2&lt;/span&gt; &lt;span class="nt"&gt;-ls&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="nb"&gt;sudo &lt;/span&gt;find /etc/systemd /etc/cron&lt;span class="k"&gt;*&lt;/span&gt; /var/spool/cron &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-7&lt;/span&gt; &lt;span class="nt"&gt;-ls&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Persistence checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;crontab &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;sudo ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /etc/cron.d /etc/cron.hourly /etc/cron.daily
systemctl list-timers
systemctl list-units &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;service &lt;span class="nt"&gt;--state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logs:&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;journalctl &lt;span class="nt"&gt;--since&lt;/span&gt; &lt;span class="s2"&gt;"24 hours ago"&lt;/span&gt;
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"failed password"&lt;/span&gt; /var/log/auth.log
&lt;span class="nb"&gt;sudo grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"accepted"&lt;/span&gt; /var/log/auth.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Common miner red flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;high CPU with unknown binary
process running from /tmp, /var/tmp, or /dev/shm
weird random process names
outbound connections to unknown IPs
cron jobs that download shell scripts
systemd services with suspicious ExecStart
unexpected SSH keys added to authorized_keys
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I handled suspicious cases, I treated it like forensics first and cleanup second.&lt;/p&gt;

&lt;p&gt;The professional approach is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;identify the process
identify how it started
identify persistence
identify network connections
identify modified files
rotate secrets
patch the entry point
rebuild if trust is lost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the server is seriously compromised, I do not pretend that deleting one process is enough. I rebuild from a clean image, restore trusted data, rotate credentials, and close the original entry point.&lt;/p&gt;

&lt;p&gt;That is the difference between “killing a miner” and actually fixing the incident.&lt;/p&gt;




&lt;h2&gt;
  
  
  CDN and WAF: why I prefer putting apps behind a protective layer
&lt;/h2&gt;

&lt;p&gt;A CDN is not just for performance.&lt;/p&gt;

&lt;p&gt;For public apps, I prefer having a CDN or reverse proxy layer in front because it gives me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TLS termination
DDoS absorption
bot filtering
WAF managed rules
rate limiting
country/IP rules
header normalization
origin hiding
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A WAF is especially useful for common attack classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;path traversal
SQL injection patterns
XSS probes
known CMS exploit paths
suspicious user agents
automated scanners
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But I do not rely on WAF alone.&lt;/p&gt;

&lt;p&gt;The origin server must still be hardened.&lt;/p&gt;

&lt;p&gt;If the origin IP is exposed and accepts traffic directly, attackers can bypass the CDN. So I restrict the origin to CDN IP ranges where possible, or I put the app behind private networking and only expose the proxy.&lt;/p&gt;

&lt;p&gt;A good pattern is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
  -&amp;gt; CDN / WAF
    -&amp;gt; Nginx
      -&amp;gt; local app on 127.0.0.1
        -&amp;gt; private database/cache
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
  -&amp;gt; Node.js app directly
  -&amp;gt; database accidentally exposed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Nginx security habits that helped me
&lt;/h2&gt;

&lt;p&gt;Here are Nginx patterns I use often.&lt;/p&gt;

&lt;h3&gt;
  
  
  Hide server tokens
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server_tokens&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Limit request body size
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;client_max_body_size&lt;/span&gt; &lt;span class="mi"&gt;10m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic rate limiting
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;limit_req_zone&lt;/span&gt; &lt;span class="nv"&gt;$binary_remote_addr&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit:10m&lt;/span&gt; &lt;span class="s"&gt;rate=10r/s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;limit_req&lt;/span&gt; &lt;span class="s"&gt;zone=api_limit&lt;/span&gt; &lt;span class="s"&gt;burst=20&lt;/span&gt; &lt;span class="s"&gt;nodelay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://127.0.0.1:3000&lt;/span&gt;&lt;span class="p"&gt;;&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;h3&gt;
  
  
  Security headers
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Frame-Options&lt;/span&gt; &lt;span class="s"&gt;"SAMEORIGIN"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;X-Content-Type-Options&lt;/span&gt; &lt;span class="s"&gt;"nosniff"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Referrer-Policy&lt;/span&gt; &lt;span class="s"&gt;"strict-origin-when-cross-origin"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Permissions-Policy&lt;/span&gt; &lt;span class="s"&gt;"geolocation=(),&lt;/span&gt; &lt;span class="s"&gt;microphone=(),&lt;/span&gt; &lt;span class="s"&gt;camera=()"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For HSTS, I only enable it when I am sure HTTPS is fully correct:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;add_header&lt;/span&gt; &lt;span class="s"&gt;Strict-Transport-Security&lt;/span&gt; &lt;span class="s"&gt;"max-age=31536000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;includeSubDomains"&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Deny direct access to internal paths
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;location&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;^/(internal|private|backup|storage|vendor)/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;deny&lt;/span&gt; &lt;span class="s"&gt;all&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;h3&gt;
  
  
  Return 444 for obvious garbage
&lt;/h3&gt;

&lt;p&gt;Sometimes I use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;444&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for abusive traffic. It closes the connection without a response. I use it carefully because normal debugging becomes harder if overused.&lt;/p&gt;




&lt;h2&gt;
  
  
  How WatchTower-Sentinel helped me see patterns
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;WatchTower-Sentinel&lt;/strong&gt; because I wanted lightweight visibility without deploying a heavy SIEM for every small server.&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 plaintext"&gt;&lt;code&gt;tail Nginx access logs
detect new client IPs
detect request bursts
detect sensitive path scans
watch CPU/RAM pressure
inspect suspicious processes
send compact Telegram alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In my Telegram reports, I could see events like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SENSITIVE_PATH_SCAN
path=/.env
status=404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEW_IP
path=/credentials.json
status=404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NEW_IP
path=/actuator/env
status=404
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This changed the conversation from “maybe bots are scanning us” to “these are the exact paths they are probing.”&lt;/p&gt;

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

&lt;p&gt;Once I had the patterns, I could turn them into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Nginx deny rules
Fail2Ban filters
WAF rules
alert categories
incident review notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That feedback loop is the most valuable part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;observe -&amp;gt; classify -&amp;gt; block -&amp;gt; monitor -&amp;gt; tune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Security improves when the server teaches you what is happening.&lt;/p&gt;




&lt;h2&gt;
  
  
  My practical hardening checklist
&lt;/h2&gt;

&lt;p&gt;This is the checklist I like to apply before I trust a server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Create non-root sudo user
Install SSH key
Disable root SSH login
Disable password SSH login
Move SSH to a non-default port
Allow SSH only from trusted IPs if possible
Enable UFW with default deny incoming
Allow only required ports
Install and configure Fail2Ban
Add custom Nginx filters for sensitive path scans
Block hidden files and secret files in Nginx
Keep .env outside public web roots
Set .env permissions to 600 or 640
Never commit .env
Never bake secrets into Docker images
Run app containers as non-root
Drop Docker capabilities
Avoid privileged containers
Bind internal services to 127.0.0.1
Put production apps behind CDN/WAF
Restrict origin access where possible
Monitor CPU/RAM/process/network behavior
Check cron/systemd persistence during incidents
Rotate secrets after any suspected exposure
Rebuild compromised servers when trust is lost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these steps are exotic. But together, they make a huge difference.&lt;/p&gt;




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

&lt;p&gt;The internet constantly tests basic mistakes.&lt;/p&gt;

&lt;p&gt;Most of the traffic I observed was not sophisticated. It was automated, repetitive, and opportunistic.&lt;/p&gt;

&lt;p&gt;But that is exactly why basic hardening matters.&lt;/p&gt;

&lt;p&gt;If a bot requests &lt;code&gt;/.env&lt;/code&gt; and receives &lt;code&gt;404&lt;/code&gt; or &lt;code&gt;403&lt;/code&gt;, that is good.&lt;/p&gt;

&lt;p&gt;If Nginx blocks it before the app sees it, better.&lt;/p&gt;

&lt;p&gt;If Fail2Ban detects repeated probes and bans the source, better.&lt;/p&gt;

&lt;p&gt;If the origin is behind a CDN/WAF and SSH is restricted, better.&lt;/p&gt;

&lt;p&gt;If secrets are outside the web root, permissioned correctly, never committed, and rotated after exposure, much better.&lt;/p&gt;

&lt;p&gt;My biggest lesson from running and monitoring real servers is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Security is not one big tool. It is a set of small decisions that reduce blast radius.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is how I approach Linux hardening now.&lt;/p&gt;

&lt;p&gt;I start with the boring layers, I watch real traffic, I convert patterns into controls, and I keep improving the system based on what the internet is actually doing to it.&lt;/p&gt;

&lt;p&gt;That is also why I built WatchTower-Sentinel.&lt;/p&gt;

&lt;p&gt;Not because alerts are cool, but because visibility changes how you defend a server.&lt;/p&gt;

&lt;p&gt;Repository:&lt;/p&gt;

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




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

&lt;ul&gt;
&lt;li&gt;Ubuntu UFW documentation: &lt;a href="https://ubuntu.com/server/docs/how-to/security/firewalls/" rel="noopener noreferrer"&gt;https://ubuntu.com/server/docs/how-to/security/firewalls/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nginx access module: &lt;a href="https://nginx.org/en/docs/http/ngx_http_access_module.html" rel="noopener noreferrer"&gt;https://nginx.org/en/docs/http/ngx_http_access_module.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Nginx documentation: &lt;a href="https://nginx.org/en/docs/" rel="noopener noreferrer"&gt;https://nginx.org/en/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Fail2Ban filters documentation: &lt;a href="https://fail2ban.readthedocs.io/en/latest/filters.html" rel="noopener noreferrer"&gt;https://fail2ban.readthedocs.io/en/latest/filters.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker rootless mode: &lt;a href="https://docs.docker.com/engine/security/rootless/" rel="noopener noreferrer"&gt;https://docs.docker.com/engine/security/rootless/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>devops</category>
      <category>nginx</category>
    </item>
    <item>
      <title>From DeepSeek to Quack: When the Dream of Distributed DuckDB Started to Feel Real</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Tue, 19 May 2026 09:31:49 +0000</pubDate>
      <link>https://forem.com/amirsefati/from-deepseek-to-quack-when-the-dream-of-distributed-duckdb-started-to-feel-real-188m</link>
      <guid>https://forem.com/amirsefati/from-deepseek-to-quack-when-the-dream-of-distributed-duckdb-started-to-feel-real-188m</guid>
      <description>&lt;p&gt;At the beginning of 2025, DeepSeek changed the conversation around AI infrastructure.&lt;/p&gt;

&lt;p&gt;Most people focused on the model quality, the training cost, and the geopolitical story around a new Chinese AI lab suddenly competing with the biggest names in the industry. That part was interesting, of course. But as an engineer, the part that caught my attention was not only the model.&lt;/p&gt;

&lt;p&gt;It was the data pipeline behind it.&lt;/p&gt;

&lt;p&gt;DeepSeek released &lt;a href="https://github.com/deepseek-ai/smallpond" rel="noopener noreferrer"&gt;Smallpond&lt;/a&gt;, a lightweight data processing framework built on &lt;a href="https://duckdb.org/" rel="noopener noreferrer"&gt;DuckDB&lt;/a&gt; and 3FS. The idea was surprisingly simple: instead of building everything around a traditional big-data engine like Spark, run many independent DuckDB-based processing jobs close to the data, partition the workload carefully, and let each local engine do what it does best.&lt;/p&gt;

&lt;p&gt;That sounds almost too simple.&lt;/p&gt;

&lt;p&gt;But that is exactly why it is interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable question: do we always need Spark?
&lt;/h2&gt;

&lt;p&gt;As a senior engineer, I have worked on systems where Spark was the default answer before the problem was even fully understood.&lt;/p&gt;

&lt;p&gt;Need to process files? Use Spark.&lt;/p&gt;

&lt;p&gt;Need to aggregate logs? Use Spark.&lt;/p&gt;

&lt;p&gt;Need to transform Parquet? Use Spark.&lt;/p&gt;

&lt;p&gt;Need to join medium-sized datasets? Still Spark.&lt;/p&gt;

&lt;p&gt;Spark is powerful, and I am not arguing against it. But in many real projects, the operational cost becomes the hidden tax: cluster configuration, memory tuning, shuffle behavior, executor sizing, dependency packaging, job retries, monitoring, and the constant pain of debugging a distributed job that fails somewhere in the middle of a long DAG.&lt;/p&gt;

&lt;p&gt;DuckDB sits on the opposite side of that spectrum.&lt;/p&gt;

&lt;p&gt;It is embedded. It runs inside your process. It reads Parquet beautifully. It speaks SQL. It is fast for analytical workloads. And most importantly, it makes local data processing feel boring again.&lt;/p&gt;

&lt;p&gt;That boring part is a compliment.&lt;/p&gt;

&lt;p&gt;When I first started using DuckDB seriously, it replaced a lot of small Python scripts in my workflow. Instead of loading CSV or Parquet files into Pandas, fighting memory limits, and then exporting results again, I could write SQL directly over files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spent&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;read_parquet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'orders/*.parquet'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_spent&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For one-off analysis, this is already great. But Smallpond suggested something bigger: what if DuckDB is not just a local helper tool, but the execution unit of a distributed data system?&lt;/p&gt;

&lt;h2&gt;
  
  
  Smallpond's lesson: distribute the plan, not the database
&lt;/h2&gt;

&lt;p&gt;Smallpond is interesting because it does not try to turn DuckDB itself into a distributed database. Instead, it treats DuckDB as a fast local execution engine.&lt;/p&gt;

&lt;p&gt;The pattern looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split the dataset into partitions.&lt;/li&gt;
&lt;li&gt;Send partitions to different workers.&lt;/li&gt;
&lt;li&gt;Let each worker run DuckDB locally.&lt;/li&gt;
&lt;li&gt;Write intermediate results back to shared storage.&lt;/li&gt;
&lt;li&gt;Merge or repartition when needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is not a new idea in distributed systems, but using DuckDB as the local analytical core makes it feel lightweight.&lt;/p&gt;

&lt;p&gt;In the Smallpond repository, the basic example is simple: read Parquet, repartition by a key, run SQL, and write Parquet output again. The README also mentions a GraySort benchmark where Smallpond processed more than 100 TiB of data on a cluster. That is a strong reminder that not every scalable system needs to look like the traditional Hadoop/Spark stack.&lt;/p&gt;

&lt;p&gt;The deeper lesson for me is this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Sometimes the best distributed architecture is not one giant distributed database. Sometimes it is thousands of small, predictable local engines coordinated well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That idea maps nicely to modern AI pipelines.&lt;/p&gt;

&lt;p&gt;Training a model is not only about GPUs. Before the GPU sees anything, there is a long chain of data cleaning, deduplication, filtering, tokenization, feature extraction, metadata joins, quality checks, and batch generation. A lot of that work is analytical. A lot of it is file-based. And a lot of it can be pushed close to storage.&lt;/p&gt;

&lt;p&gt;DuckDB is very good at that style of work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where DuckDB used to hurt
&lt;/h2&gt;

&lt;p&gt;DuckDB's strength has always been its simplicity: it is an in-process analytical database.&lt;/p&gt;

&lt;p&gt;But the same simplicity creates a limitation.&lt;/p&gt;

&lt;p&gt;If you have one Python process, one CLI session, or one application working with a DuckDB database file, everything feels clean. But once multiple processes want to write to the same database file, you quickly run into locking and concurrency constraints.&lt;/p&gt;

&lt;p&gt;That is not a bug. It is part of the design.&lt;/p&gt;

&lt;p&gt;DuckDB was originally optimized for embedded OLAP workloads, not for being a shared multi-client server like PostgreSQL.&lt;/p&gt;

&lt;p&gt;In my own projects, I usually solved this by avoiding shared writes completely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write partitioned Parquet instead of writing into one shared database file&lt;/li&gt;
&lt;li&gt;let each worker produce immutable output&lt;/li&gt;
&lt;li&gt;use object storage as the coordination layer&lt;/li&gt;
&lt;li&gt;run a final compaction or merge step later&lt;/li&gt;
&lt;li&gt;keep DuckDB as the query engine, not the source of truth&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works well, but it has trade-offs. You start building your own small coordination layer. You need naming conventions, idempotent writes, retry logic, cleanup jobs, and sometimes a metadata database just to track what happened.&lt;/p&gt;

&lt;p&gt;That is why Quack is so interesting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quack: DuckDB starts speaking over the network
&lt;/h2&gt;

&lt;p&gt;In 2026, Hannes Mühleisen introduced &lt;a href="https://duckdb.org/quack/" rel="noopener noreferrer"&gt;Quack&lt;/a&gt;, a remote protocol that turns DuckDB into a client-server database.&lt;/p&gt;

&lt;p&gt;The idea is elegant: both the client and the server are DuckDB instances, but they communicate through the &lt;code&gt;quack:&lt;/code&gt; protocol. The server owns the data. The client sends queries. The heavy work happens near the data, and the result comes back to the client.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;INSTALL&lt;/span&gt; &lt;span class="n"&gt;quack&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;core_nightly&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;LOAD&lt;/span&gt; &lt;span class="n"&gt;quack&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;TYPE&lt;/span&gt; &lt;span class="n"&gt;quack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;TOKEN&lt;/span&gt; &lt;span class="s1"&gt;'super_secret'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;ATTACH&lt;/span&gt; &lt;span class="s1"&gt;'quack:bigserver:9494'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;remote&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;transactions&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;customer_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_amount&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not just a nicer syntax. It changes the deployment model.&lt;/p&gt;

&lt;p&gt;Before Quack, DuckDB was mostly local-first. With Quack, DuckDB can become remote-first when needed.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the data can stay on a powerful server&lt;/li&gt;
&lt;li&gt;laptop clients can query without downloading huge datasets&lt;/li&gt;
&lt;li&gt;multiple clients can connect to the same DuckDB server&lt;/li&gt;
&lt;li&gt;DuckDB can be used in more traditional application architectures&lt;/li&gt;
&lt;li&gt;DuckDB-Wasm and browser-based analytical tools become more interesting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The official DuckDB documentation describes Quack as an RPC protocol for DuckDB and mentions use cases like concurrent read-write access, moving computation closer to data, and querying powerful servers from local clients.&lt;/p&gt;

&lt;p&gt;For me, the key phrase is: &lt;strong&gt;compute near data&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is one of the most important ideas in data engineering.&lt;/p&gt;

&lt;p&gt;Moving 500 GB to a laptop is a bad plan. Sending a SQL query to the machine that already has the data is a better plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  A prototype I would actually build
&lt;/h2&gt;

&lt;p&gt;The first thing I wanted to try with this architecture was not a huge AI training pipeline. It was something more realistic: a lightweight analytics service for event data.&lt;/p&gt;

&lt;p&gt;Imagine this setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application events are written as Parquet files to object storage&lt;/li&gt;
&lt;li&gt;a small ingestion service batches new events&lt;/li&gt;
&lt;li&gt;DuckDB reads and validates those files locally&lt;/li&gt;
&lt;li&gt;embeddings are generated for selected text fields&lt;/li&gt;
&lt;li&gt;analytical metadata is stored in DuckDB or DuckLake&lt;/li&gt;
&lt;li&gt;vector search is handled by a dedicated vector database&lt;/li&gt;
&lt;li&gt;Quack exposes the central DuckDB instance to internal tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This kind of architecture is attractive because each tool does one job well.&lt;/p&gt;

&lt;p&gt;DuckDB is great for analytical SQL.&lt;/p&gt;

&lt;p&gt;Object storage is great for cheap durable files.&lt;/p&gt;

&lt;p&gt;A vector database is great for similarity search.&lt;/p&gt;

&lt;p&gt;Quack becomes the bridge that lets multiple clients query the analytical layer without copying everything locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where vector databases fit into this story
&lt;/h2&gt;

&lt;p&gt;A vector database stores embeddings instead of just rows and columns.&lt;/p&gt;

&lt;p&gt;An embedding is a numerical representation of text, image, audio, code, or another object. For example, a support ticket like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The payment failed after I changed my billing address.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;can be converted into a vector such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.012, -0.441, 0.087, ...]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The numbers themselves are not meaningful to humans, but their position in vector space captures semantic meaning. Similar texts produce vectors that are close to each other.&lt;/p&gt;

&lt;p&gt;That enables queries like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Find tickets semantically similar to this new complaint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Traditional SQL is not designed for that kind of similarity search. SQL is excellent when you know the exact fields and predicates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'failed'&lt;/span&gt;
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;country&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'AM'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vector search is different:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Find documents close to this embedding.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is why systems like Qdrant, Milvus, Weaviate, Pinecone, pgvector, and others became popular. They use indexes such as HNSW or IVF to make nearest-neighbor search fast.&lt;/p&gt;

&lt;p&gt;But here is the important part: vector search alone is rarely enough.&lt;/p&gt;

&lt;p&gt;In production, you usually need hybrid retrieval:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;vector similarity for semantic meaning&lt;/li&gt;
&lt;li&gt;SQL filters for structured constraints&lt;/li&gt;
&lt;li&gt;full-text search for exact keywords&lt;/li&gt;
&lt;li&gt;metadata joins for permissions, customers, time ranges, or product categories&lt;/li&gt;
&lt;li&gt;analytical queries to evaluate quality and drift&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is where DuckDB becomes valuable again.&lt;/p&gt;

&lt;p&gt;I do not want my vector database to become my entire analytics platform. I want it to retrieve candidates. Then I want SQL to inspect, filter, aggregate, evaluate, and debug the system.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;count&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;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;avg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;avg_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;retrieval_logs&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="k"&gt;source&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;avg_score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This kind of query belongs naturally in DuckDB or a lakehouse layer, not inside the vector database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Quack makes this architecture cleaner
&lt;/h2&gt;

&lt;p&gt;Without Quack, I would normally run DuckDB locally inside each service and write files back to object storage. That is still a good pattern. But it makes interactive querying harder.&lt;/p&gt;

&lt;p&gt;With Quack, I can imagine a cleaner workflow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ETL workers process raw data locally with DuckDB.&lt;/li&gt;
&lt;li&gt;Processed files are written to object storage.&lt;/li&gt;
&lt;li&gt;A central DuckDB/DuckLake server exposes curated tables.&lt;/li&gt;
&lt;li&gt;Internal tools connect through Quack.&lt;/li&gt;
&lt;li&gt;BI dashboards query the same analytical layer.&lt;/li&gt;
&lt;li&gt;Vector search services write retrieval logs back into the lake.&lt;/li&gt;
&lt;li&gt;Engineers debug everything with SQL.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is not a replacement for every data warehouse. It is not a replacement for Kafka, Spark, PostgreSQL, or a vector database.&lt;/p&gt;

&lt;p&gt;But it is a powerful middle layer.&lt;/p&gt;

&lt;p&gt;It fits the space where many teams actually live: too much data for one Pandas script, but not enough operational complexity to justify a full big-data platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I like most as an engineer
&lt;/h2&gt;

&lt;p&gt;What I like about this direction is that it respects mechanical sympathy.&lt;/p&gt;

&lt;p&gt;DuckDB is fast because it understands analytical execution: vectorized processing, columnar storage, efficient scans, smart Parquet reads, and local execution without network overhead.&lt;/p&gt;

&lt;p&gt;Smallpond says: keep that local execution model, but run it many times in parallel.&lt;/p&gt;

&lt;p&gt;Quack says: keep DuckDB's engine, but allow it to communicate when a shared server model is useful.&lt;/p&gt;

&lt;p&gt;That is a healthy evolution.&lt;/p&gt;

&lt;p&gt;It does not throw away the original design. It extends it.&lt;/p&gt;

&lt;p&gt;As someone who has spent too much time debugging over-engineered pipelines, I appreciate systems that scale by composition instead of magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would be careful about
&lt;/h2&gt;

&lt;p&gt;I would still be cautious before using Quack as the core of a production system today.&lt;/p&gt;

&lt;p&gt;The official page describes it as a beta release. That matters.&lt;/p&gt;

&lt;p&gt;Before depending on it, I would test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;write concurrency under real workload&lt;/li&gt;
&lt;li&gt;authentication and network exposure&lt;/li&gt;
&lt;li&gt;backup and restore strategy&lt;/li&gt;
&lt;li&gt;failure behavior during long-running writes&lt;/li&gt;
&lt;li&gt;compatibility with existing DuckDB extensions&lt;/li&gt;
&lt;li&gt;observability and query logging&lt;/li&gt;
&lt;li&gt;behavior behind load balancers or proxies&lt;/li&gt;
&lt;li&gt;performance with many small writes versus large batches&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I would also avoid pretending DuckDB suddenly became PostgreSQL.&lt;/p&gt;

&lt;p&gt;DuckDB is still fundamentally an analytical engine. Even if Quack makes multi-client access possible, I would not immediately use it for high-volume OLTP workloads like payments, orders, or user sessions.&lt;/p&gt;

&lt;p&gt;For those, PostgreSQL is still the boring and correct answer.&lt;/p&gt;

&lt;p&gt;But for analytical workloads, internal dashboards, data pipelines, AI preprocessing, evaluation datasets, batch transformations, and lakehouse-style metadata, Quack opens a very interesting door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;The story from DeepSeek's Smallpond to DuckDB's Quack is not just about one tool becoming distributed.&lt;/p&gt;

&lt;p&gt;It is about a shift in how we think about data systems.&lt;/p&gt;

&lt;p&gt;For years, the default answer to scale was often: use a bigger distributed framework.&lt;/p&gt;

&lt;p&gt;Now we are seeing another pattern:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keep compute simple&lt;/li&gt;
&lt;li&gt;keep data in open formats&lt;/li&gt;
&lt;li&gt;run fast local engines near the data&lt;/li&gt;
&lt;li&gt;coordinate through lightweight protocols&lt;/li&gt;
&lt;li&gt;use specialized systems where they actually make sense&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is why this space is exciting.&lt;/p&gt;

&lt;p&gt;DuckDB made local analytics feel simple.&lt;/p&gt;

&lt;p&gt;Smallpond showed that many local DuckDB jobs can become a serious distributed processing pattern.&lt;/p&gt;

&lt;p&gt;Quack now makes DuckDB instances talk to each other.&lt;/p&gt;

&lt;p&gt;And when you combine that with object storage, DuckLake, Parquet, and vector databases, you get a very pragmatic architecture for modern AI and data engineering.&lt;/p&gt;

&lt;p&gt;Not because it is trendy.&lt;/p&gt;

&lt;p&gt;Because it removes unnecessary complexity.&lt;/p&gt;

</description>
      <category>duckdb</category>
      <category>dataengineering</category>
      <category>ai</category>
      <category>database</category>
    </item>
    <item>
      <title>Understanding PID Namespaces: The Small Linux Feature Behind Container Process Isolation</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Mon, 18 May 2026 18:11:35 +0000</pubDate>
      <link>https://forem.com/amirsefati/understanding-pid-namespaces-the-small-linux-feature-behind-container-process-isolation-4odm</link>
      <guid>https://forem.com/amirsefati/understanding-pid-namespaces-the-small-linux-feature-behind-container-process-isolation-4odm</guid>
      <description>&lt;h1&gt;
  
  
  Understanding PID Namespaces: The Small Linux Feature Behind Container Process Isolation
&lt;/h1&gt;

&lt;p&gt;When people first learn containers, they usually hear this sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“A container is just a process.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is true, but incomplete.&lt;/p&gt;

&lt;p&gt;A better version is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“A container is a regular Linux process running with a different view of the system.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of the most important parts of that different view is the &lt;strong&gt;PID namespace&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A PID namespace controls what processes a process can see and what process IDs look like from inside that environment. It is one of the Linux kernel features that makes containers feel isolated, even though everything is still running on the same host kernel.&lt;/p&gt;

&lt;p&gt;Docker, containerd, runc, Kubernetes, and even small learning projects like a tiny Docker-like runtime all rely on this idea.&lt;/p&gt;




&lt;h2&gt;
  
  
  What problem does a PID namespace solve?
&lt;/h2&gt;

&lt;p&gt;On a normal Linux machine, every process has a PID:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You may see things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID 1      systemd
PID 842    sshd
PID 1201   nginx
PID 2300   node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without PID isolation, a process inside a container could see host processes. That would be noisy, confusing, and dangerous.&lt;/p&gt;

&lt;p&gt;With a PID namespace, the container gets its own process ID view.&lt;/p&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 plaintext"&gt;&lt;code&gt;PID 1      app
PID 7      worker
PID 12     shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the host, those same processes still have real host PIDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PID 34520  app
PID 34541  worker
PID 34610  shell
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the same process can have two identities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one PID inside the container&lt;/li&gt;
&lt;li&gt;another PID on the host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not magic. It is namespace-based translation done by the Linux kernel.&lt;/p&gt;




&lt;h2&gt;
  
  
  PID 1 is not just “the first process”
&lt;/h2&gt;

&lt;p&gt;A very common beginner mistake is thinking PID 1 is only a number.&lt;/p&gt;

&lt;p&gt;It is not.&lt;/p&gt;

&lt;p&gt;Inside a PID namespace, the first process becomes PID 1, and PID 1 has special responsibilities.&lt;/p&gt;

&lt;p&gt;In a normal Linux system, PID 1 is usually &lt;code&gt;systemd&lt;/code&gt; or another init system. In a container, PID 1 might be your application:&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 my-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app becomes PID 1 directly, it now behaves like the init process of that namespace.&lt;/p&gt;

&lt;p&gt;That matters because PID 1 is responsible for handling orphaned child processes and reaping zombies. The Linux man pages describe the first process in a new PID namespace as the namespace init process, and orphaned children in that namespace are reparented to it.&lt;/p&gt;

&lt;p&gt;This is why senior engineers often care about tiny init processes like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tini
dumb-init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without a proper init process, long-running containers can slowly accumulate zombie processes.&lt;/p&gt;

&lt;p&gt;A container may look healthy from the outside, but inside it can be leaking process table entries because PID 1 is not doing its job.&lt;/p&gt;




&lt;h2&gt;
  
  
  The senior-level lesson: containers are isolation, not virtualization
&lt;/h2&gt;

&lt;p&gt;A VM gets its own kernel.&lt;/p&gt;

&lt;p&gt;A container does not.&lt;/p&gt;

&lt;p&gt;A container shares the host kernel, but gets isolated views using kernel features like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PID namespaces&lt;/li&gt;
&lt;li&gt;mount namespaces&lt;/li&gt;
&lt;li&gt;network namespaces&lt;/li&gt;
&lt;li&gt;UTS namespaces&lt;/li&gt;
&lt;li&gt;IPC namespaces&lt;/li&gt;
&lt;li&gt;user namespaces&lt;/li&gt;
&lt;li&gt;cgroups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PID namespace only isolates process visibility and PID numbering. It does not magically secure everything.&lt;/p&gt;

&lt;p&gt;That is a critical mental model.&lt;/p&gt;

&lt;p&gt;A PID namespace can stop a container from seeing host processes, but it does not protect you from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;dangerous Linux capabilities&lt;/li&gt;
&lt;li&gt;privileged containers&lt;/li&gt;
&lt;li&gt;host filesystem mounts&lt;/li&gt;
&lt;li&gt;exposed Docker socket&lt;/li&gt;
&lt;li&gt;weak seccomp, AppArmor, or SELinux profiles&lt;/li&gt;
&lt;li&gt;kernel vulnerabilities&lt;/li&gt;
&lt;li&gt;bad Kubernetes security context settings&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why container security is usually about &lt;strong&gt;layers&lt;/strong&gt;, not one feature.&lt;/p&gt;




&lt;h2&gt;
  
  
  How Docker uses PID namespaces
&lt;/h2&gt;

&lt;p&gt;By default, Docker gives containers their own PID namespace.&lt;/p&gt;

&lt;p&gt;Docker exposes this through the &lt;code&gt;--pid&lt;/code&gt; option. The default mode isolates processes, while &lt;code&gt;--pid=host&lt;/code&gt; makes the container use the host PID namespace.&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;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; ubuntu ps aux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container, you may see only a few processes.&lt;/p&gt;

&lt;p&gt;But with host PID mode:&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;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;--pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host ubuntu ps aux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container can see host processes.&lt;/p&gt;

&lt;p&gt;That flag is useful for debugging, monitoring, and observability tools, but it should be treated carefully. In production, &lt;code&gt;--pid=host&lt;/code&gt; removes an important isolation boundary.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is the “hash” inside &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/ns/pid&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;When you inspect namespaces, you may see something 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;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&lt;span class="nv"&gt;$$&lt;/span&gt;/ns/pid
&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 plaintext"&gt;&lt;code&gt;pid:[4026531836]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;People sometimes casually call this a “namespace hash”, but it is not a cryptographic hash.&lt;/p&gt;

&lt;p&gt;It is a kernel namespace identifier exposed through procfs. Namespace references are shown as special symbolic links, and the number helps identify whether two processes are in the same namespace.&lt;/p&gt;

&lt;p&gt;If two processes show the same namespace ID for &lt;code&gt;pid&lt;/code&gt;, they share the same PID namespace.&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;readlink&lt;/span&gt; /proc/1/ns/pid
&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&lt;span class="nv"&gt;$$&lt;/span&gt;/ns/pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If both return the same value, both processes are in the same PID namespace.&lt;/p&gt;

&lt;p&gt;This is very useful for debugging containers.&lt;/p&gt;




&lt;h2&gt;
  
  
  How to check PID namespace isolation
&lt;/h2&gt;

&lt;p&gt;From inside a container:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If you only see the container’s own processes, PID isolation is probably enabled.&lt;/p&gt;

&lt;p&gt;Check the namespace ID:&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;readlink&lt;/span&gt; /proc/1/ns/pid
&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&lt;span class="nv"&gt;$$&lt;/span&gt;/ns/pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the host, inspect a container process:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'{{.State.Pid}}'&lt;/span&gt; &amp;lt;container_id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&amp;lt;host_pid&amp;gt;/ns/pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can compare namespace IDs between host processes and container processes.&lt;/p&gt;

&lt;p&gt;Another useful command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;lsns &lt;span class="nt"&gt;-t&lt;/span&gt; pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows PID namespaces on the system.&lt;/p&gt;

&lt;p&gt;For deeper debugging:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pstree &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps &lt;span class="nt"&gt;-eo&lt;/span&gt; pid,ppid,cmd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is to always remember that the host sees the full truth, while the container sees a translated view.&lt;/p&gt;




&lt;h2&gt;
  
  
  How PID namespace isolation can be weakened
&lt;/h2&gt;

&lt;p&gt;This is where many real-world mistakes happen.&lt;/p&gt;

&lt;p&gt;PID namespaces are not usually “bypassed” by magic. They are usually weakened by configuration choices.&lt;/p&gt;

&lt;p&gt;Here are common examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Running with host PID namespace
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nt"&gt;--pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;host
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the container see host processes.&lt;/p&gt;

&lt;p&gt;Sometimes this is used by monitoring tools, but it should not be the default for normal application containers.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Running privileged containers
&lt;/h3&gt;



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

&lt;/div&gt;



&lt;p&gt;A privileged container receives broad access that removes many normal container restrictions.&lt;/p&gt;

&lt;p&gt;This is sometimes convenient during development, but it should be avoided for normal production workloads.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Mounting sensitive host paths
&lt;/h3&gt;

&lt;p&gt;Examples:&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;-v&lt;/span&gt; /proc:/host/proc
&lt;span class="nt"&gt;-v&lt;/span&gt; /:/host
&lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mounting the Docker socket is especially dangerous because it can effectively give control over the Docker daemon.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Adding dangerous capabilities
&lt;/h3&gt;

&lt;p&gt;Capabilities such as these should be reviewed carefully:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SYS_ADMIN
SYS_PTRACE
NET_ADMIN
DAC_READ_SEARCH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For PID and process security, &lt;code&gt;SYS_PTRACE&lt;/code&gt; is especially sensitive because it relates to inspecting and tracing processes.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Weak Kubernetes security context
&lt;/h3&gt;

&lt;p&gt;In Kubernetes, settings like these are important:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;hostPID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For normal workloads, these should usually be avoided.&lt;/p&gt;




&lt;h2&gt;
  
  
  Defensive checklist for real projects
&lt;/h2&gt;

&lt;p&gt;When reviewing a containerized service, I usually ask these questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Runtime
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &amp;lt;container_id&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check whether the container is using host PID mode.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capabilities
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &amp;lt;container_id&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; cap
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer dropping unnecessary capabilities:&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;--cap-drop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ALL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then add back only what is truly required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Privileged mode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &amp;lt;container_id&amp;gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; privileged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most application containers, this should be false.&lt;/p&gt;

&lt;h3&gt;
  
  
  Process tree
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;container_id&amp;gt; ps aux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for zombie processes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ps aux | &lt;span class="nb"&gt;grep &lt;/span&gt;Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see zombies, check whether PID 1 is properly reaping children.&lt;/p&gt;

&lt;h3&gt;
  
  
  Namespace comparison
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/1/ns/pid
&lt;span class="nb"&gt;readlink&lt;/span&gt; /proc/&lt;span class="nv"&gt;$$&lt;/span&gt;/ns/pid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compare host and container namespace IDs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kubernetes
&lt;/h3&gt;

&lt;p&gt;Check pod specs for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;hostPID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;privileged&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These settings should be intentional, documented, and reviewed.&lt;/p&gt;




&lt;h2&gt;
  
  
  A practical example from building a tiny container runtime
&lt;/h2&gt;

&lt;p&gt;When building a minimal Docker-like runtime, PID namespace support usually starts with 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;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_NEWPID&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;But there is a subtle detail.&lt;/p&gt;

&lt;p&gt;When you create a new PID namespace, the child process becomes PID 1 inside that namespace. The parent still lives in the old namespace.&lt;/p&gt;

&lt;p&gt;That means your runtime has to think carefully about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;who becomes PID 1&lt;/li&gt;
&lt;li&gt;whether PID 1 launches the user command directly&lt;/li&gt;
&lt;li&gt;whether you need a small init process&lt;/li&gt;
&lt;li&gt;how signals are forwarded&lt;/li&gt;
&lt;li&gt;how child processes are reaped&lt;/li&gt;
&lt;li&gt;what happens when PID 1 exits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where the learning becomes real.&lt;/p&gt;

&lt;p&gt;Creating a namespace is easy.&lt;/p&gt;

&lt;p&gt;Managing a namespace correctly is the hard part.&lt;/p&gt;




&lt;h2&gt;
  
  
  Senior engineering lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Do not confuse isolation with security
&lt;/h3&gt;

&lt;p&gt;PID namespaces provide process isolation, but they are only one part of the security model.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. PID 1 behavior matters
&lt;/h3&gt;

&lt;p&gt;If your application runs as PID 1, signal handling and zombie reaping become your problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Debugging containers requires two views
&lt;/h3&gt;

&lt;p&gt;Always check both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inside the container&lt;/li&gt;
&lt;li&gt;from the host&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same process has different PIDs depending on where you look from.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Most “container escapes” start with bad configuration
&lt;/h3&gt;

&lt;p&gt;In real systems, the issue is often not the PID namespace itself. The issue is combining weak settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;privileged mode&lt;/li&gt;
&lt;li&gt;host PID&lt;/li&gt;
&lt;li&gt;host mounts&lt;/li&gt;
&lt;li&gt;excessive capabilities&lt;/li&gt;
&lt;li&gt;exposed Docker socket&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Use namespaces intentionally
&lt;/h3&gt;

&lt;p&gt;For observability tools, &lt;code&gt;hostPID&lt;/code&gt; or &lt;code&gt;--pid=host&lt;/code&gt; may be required.&lt;/p&gt;

&lt;p&gt;For normal application workloads, it is usually unnecessary risk.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Linux man-pages: PID namespaces&lt;/li&gt;
&lt;li&gt;Linux Kernel Documentation: Namespaces&lt;/li&gt;
&lt;li&gt;Docker documentation: &lt;code&gt;docker run --pid&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;OWASP Docker Security Cheat Sheet&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final thought
&lt;/h2&gt;

&lt;p&gt;PID namespaces are one of those Linux features that look simple at first:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“The container gets its own process IDs.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But after working with real systems, you realize the deeper lesson:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Process isolation is not only about hiding PIDs. It is about controlling visibility, lifecycle, signals, debugging, and failure boundaries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That is why PID namespaces are not just a container feature.&lt;/p&gt;

&lt;p&gt;They are a production engineering concept.&lt;/p&gt;

&lt;p&gt;If you understand PID namespaces well, Docker feels less like magic and more like a thin layer over powerful Linux primitives.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>docker</category>
      <category>containers</category>
      <category>security</category>
    </item>
    <item>
      <title>Running My Tiny Docker-like Runtime on macOS with Lima</title>
      <dc:creator>amir</dc:creator>
      <pubDate>Sun, 17 May 2026 14:40:30 +0000</pubDate>
      <link>https://forem.com/amirsefati/running-my-tiny-docker-like-runtime-on-macos-with-lima-5bd</link>
      <guid>https://forem.com/amirsefati/running-my-tiny-docker-like-runtime-on-macos-with-lima-5bd</guid>
      <description>&lt;h1&gt;
  
  
  Running My Tiny Docker-like Runtime on macOS with Lima: Lessons, Mistakes, and a Simple Benchmark
&lt;/h1&gt;

&lt;p&gt;When I started building my own tiny Docker-like runtime in Go, I had one simple assumption:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“It is written in Go, so I should be able to run it anywhere.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption was only half correct.&lt;/p&gt;

&lt;p&gt;Yes, Go makes it easy to compile binaries for different platforms. But a container runtime is not just a Go application. A container runtime depends heavily on operating system features, especially Linux kernel features.&lt;/p&gt;

&lt;p&gt;In my case, the project needed things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux namespaces&lt;/li&gt;
&lt;li&gt;cgroups v2&lt;/li&gt;
&lt;li&gt;mount isolation&lt;/li&gt;
&lt;li&gt;&lt;code&gt;chroot&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;process isolation&lt;/li&gt;
&lt;li&gt;bridge networking&lt;/li&gt;
&lt;li&gt;veth pairs&lt;/li&gt;
&lt;li&gt;iptables/NAT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that is where macOS becomes a problem.&lt;/p&gt;

&lt;p&gt;macOS is not Linux. It does not provide Linux namespaces or cgroups in the same way. So even if my code compiled on macOS, the actual container runtime logic could not work directly on macOS.&lt;/p&gt;

&lt;p&gt;This was the point where I started using &lt;strong&gt;Lima&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I want to share how I used Lima to run my tiny Docker-like runtime from macOS, the mistakes I made, the small design decisions I learned from, and a simple experimental benchmark at the end.&lt;/p&gt;

&lt;p&gt;This is not a “Lima vs Docker Desktop” article.&lt;/p&gt;

&lt;p&gt;It is more about understanding the boundary between macOS, Linux, Docker, and a custom container runtime.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Was Building
&lt;/h2&gt;

&lt;p&gt;The project is a small Docker-like runtime written in Go.&lt;/p&gt;

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

&lt;p&gt;The goal was to understand what Docker does under the hood.&lt;/p&gt;

&lt;p&gt;Docker gives us a very clean developer experience:&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 &lt;span class="nb"&gt;echo &lt;/span&gt;hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But behind that simple command, many things happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;image resolution
filesystem preparation
namespace creation
cgroup configuration
mount setup
network setup
process execution
log tracking
metadata storage
cleanup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official Docker documentation describes Docker as an open platform for developing, shipping, and running applications:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.docker.com/get-started/docker-overview/" rel="noopener noreferrer"&gt;https://docs.docker.com/get-started/docker-overview/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That high-level explanation is useful, but when you build a tiny runtime yourself, Docker becomes much less magical.&lt;/p&gt;

&lt;p&gt;You start seeing the lower-level Linux pieces.&lt;/p&gt;

&lt;p&gt;For example, my runtime supports 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;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 &lt;span class="nt"&gt;-f&lt;/span&gt; &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;On Linux, this makes sense.&lt;/p&gt;

&lt;p&gt;On macOS, it immediately raises a question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where do Linux namespaces and cgroups come from?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The answer is: they do not come from macOS.&lt;/p&gt;

&lt;p&gt;I needed Linux.&lt;/p&gt;




&lt;h2&gt;
  
  
  The First Mistake: Thinking Go Portability Means Runtime Portability
&lt;/h2&gt;

&lt;p&gt;My first mistake was confusing language portability with operating system feature portability.&lt;/p&gt;

&lt;p&gt;I was thinking 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;Go can build on macOS.
Therefore, my runtime should work on macOS.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the correct mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Go can compile the program for macOS.
But Linux container primitives still require Linux.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A Go program can be cross-platform.&lt;/p&gt;

&lt;p&gt;But this does not mean every syscall or kernel feature exists on every platform.&lt;/p&gt;

&lt;p&gt;For example, when a container runtime wants to isolate a process, it may need Linux-specific features like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLONE_NEWUTS
CLONE_NEWPID
CLONE_NEWNS
CLONE_NEWNET
cgroup filesystem
mount operations
veth networking
iptables rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are not available as normal Linux container primitives on macOS.&lt;/p&gt;

&lt;p&gt;So the real problem was not the programming language.&lt;/p&gt;

&lt;p&gt;The real problem was the kernel.&lt;/p&gt;

&lt;p&gt;That was a very important lesson for me.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why macOS Cannot Run This Directly
&lt;/h2&gt;

&lt;p&gt;macOS uses the XNU kernel.&lt;/p&gt;

&lt;p&gt;Linux containers depend on the Linux kernel.&lt;/p&gt;

&lt;p&gt;This matters because containers are not virtual machines. A container is usually a regular process with a restricted view of the system.&lt;/p&gt;

&lt;p&gt;That restricted view is created by kernel features.&lt;/p&gt;

&lt;p&gt;For example:&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      -&amp;gt; gives the process its own process tree
UTS namespace      -&amp;gt; gives the process its own hostname
mount namespace    -&amp;gt; gives the process its own mount view
network namespace  -&amp;gt; gives the process its own network stack
cgroups            -&amp;gt; limit and track resource usage
chroot/rootfs      -&amp;gt; changes the visible filesystem root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On a Linux machine, a runtime can call these features directly.&lt;/p&gt;

&lt;p&gt;On macOS, the features are not available in the same way.&lt;/p&gt;

&lt;p&gt;So the architecture had to change.&lt;/p&gt;

&lt;p&gt;Instead of this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS
  -&amp;gt; tiny-docker-go
      -&amp;gt; Linux namespaces/cgroups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I needed this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS
  -&amp;gt; Linux VM
      -&amp;gt; tiny-docker-go
          -&amp;gt; Linux namespaces/cgroups
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where Lima became useful.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Lima?
&lt;/h2&gt;

&lt;p&gt;Lima is a tool that runs Linux virtual machines on macOS.&lt;/p&gt;

&lt;p&gt;Official documentation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lima-vm.io/docs/" rel="noopener noreferrer"&gt;https://lima-vm.io/docs/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Installation guide:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://lima-vm.io/docs/installation/" rel="noopener noreferrer"&gt;https://lima-vm.io/docs/installation/&lt;/a&gt;&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Lima is not Docker.
Lima is not my container runtime.
Lima gives me a Linux VM.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That Linux VM gives my project access to the Linux kernel features it needs.&lt;/p&gt;

&lt;p&gt;A simple mental model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;MacBook
  └── macOS
       └── Lima VM
            └── Linux
                 └── my tiny Docker-like runtime
                      └── container-like process
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation helped me understand the problem much better.&lt;/p&gt;

&lt;p&gt;Lima is the environment.&lt;/p&gt;

&lt;p&gt;My Go runtime is the thing doing the container work.&lt;/p&gt;

&lt;p&gt;Alpine rootfs is the container filesystem.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why I Did Not Just Use Docker Desktop
&lt;/h2&gt;

&lt;p&gt;Docker Desktop is great.&lt;/p&gt;

&lt;p&gt;I use Docker Desktop for normal development work.&lt;/p&gt;

&lt;p&gt;But for this project, Docker Desktop was not the cleanest learning environment.&lt;/p&gt;

&lt;p&gt;Docker Desktop itself uses a Linux VM behind the scenes on macOS. That is how Docker can run Linux containers on macOS.&lt;/p&gt;

&lt;p&gt;But I was not trying to simply run containers.&lt;/p&gt;

&lt;p&gt;I was trying to build a small runtime that behaves like a container runtime.&lt;/p&gt;

&lt;p&gt;So if I put everything behind Docker Desktop too early, I would hide some of the details I wanted to learn.&lt;/p&gt;

&lt;p&gt;My goal was not:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How do I run an app in Docker?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My goal was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How does a container runtime use Linux features to isolate a process?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For that goal, Lima felt cleaner.&lt;/p&gt;

&lt;p&gt;The distinction became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker Desktop:
  Great for running Docker containers and application stacks.

Lima:
  Great for getting a Linux environment on macOS and experimenting with Linux internals.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So for this project, Lima gave me a better learning path.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing Lima
&lt;/h2&gt;

&lt;p&gt;On macOS, installing Lima with Homebrew is simple:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Then I created a VM for the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;limactl start &lt;span class="nt"&gt;--name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tiny-docker &lt;span class="nt"&gt;--cpus&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--memory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4 &lt;span class="nt"&gt;--disk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I entered the VM:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;or sometimes simply:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Inside the VM, I installed the Linux packages my runtime needed:&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;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; golang-go curl &lt;span class="nb"&gt;tar &lt;/span&gt;iproute2 iptables
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each dependency had a reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;golang-go  -&amp;gt; build and test the runtime
curl       -&amp;gt; download rootfs archives
tar        -&amp;gt; extract rootfs archives
iproute2   -&amp;gt; work with Linux networking
iptables   -&amp;gt; configure NAT for isolated networking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was already a useful learning point.&lt;/p&gt;

&lt;p&gt;A container runtime is not just one binary.&lt;/p&gt;

&lt;p&gt;It also depends on Linux system capabilities and tools, especially if you are implementing networking.&lt;/p&gt;




&lt;h2&gt;
  
  
  Preparing the Root Filesystem
&lt;/h2&gt;

&lt;p&gt;A container needs a filesystem.&lt;/p&gt;

&lt;p&gt;Docker normally handles this using images and layers.&lt;/p&gt;

&lt;p&gt;My project was simpler. I used an Alpine minirootfs.&lt;/p&gt;

&lt;p&gt;Inside the Lima VM:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; rootfs/alpine

&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; alpine-rootfs.tar.gz &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://dl-cdn.alpinelinux.org/alpine/latest-stable/releases/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/alpine-minirootfs-3.23.4-&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ARCH&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.tar.gz"&lt;/span&gt;

&lt;span class="nb"&gt;sudo tar&lt;/span&gt; &lt;span class="nt"&gt;-xzf&lt;/span&gt; alpine-rootfs.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; rootfs/alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then I could run:&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;At this point, the rootfs became the filesystem that the process sees inside the container-like environment.&lt;/p&gt;

&lt;p&gt;This made the concept very concrete for me.&lt;/p&gt;

&lt;p&gt;Before this project, I mostly thought about Docker images.&lt;/p&gt;

&lt;p&gt;After this project, I started thinking more clearly about root filesystems.&lt;/p&gt;

&lt;p&gt;A simplified version is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker image:
  A packaged filesystem with metadata and layers.

Rootfs:
  The actual filesystem view used by the container process.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My tiny runtime does not implement Docker image layers, registries, manifests, or OCI image pulling.&lt;/p&gt;

&lt;p&gt;It simply uses an extracted root filesystem.&lt;/p&gt;

&lt;p&gt;That is enough for learning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: macOS as a Wrapper, Linux as the Runtime
&lt;/h2&gt;

&lt;p&gt;After experimenting, I ended up with this architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;If host is Linux:
  run the runtime directly.

If host is macOS:
  route the command through Lima.
  execute the Linux binary inside the Lima VM.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the user's perspective, I wanted the command to still feel 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 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;But internally, on macOS, it becomes closer to:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;So the macOS binary acts more like a dispatcher.&lt;/p&gt;

&lt;p&gt;The actual runtime work happens in Linux.&lt;/p&gt;

&lt;p&gt;A simplified architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS terminal
  |
  | tiny-docker-go run --rootfs ./rootfs/alpine /bin/sh
  v
Darwin service layer
  |
  | limactl shell tiny-docker sudo Linux binary ...
  v
Lima VM
  |
  v
Linux runtime binary
  |
  v
namespaces + cgroups + chroot + networking
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design helped me keep a clean boundary.&lt;/p&gt;

&lt;p&gt;macOS handles the developer command.&lt;/p&gt;

&lt;p&gt;Linux handles the container primitives.&lt;/p&gt;




&lt;h2&gt;
  
  
  Building the Linux Binary
&lt;/h2&gt;

&lt;p&gt;One mistake I made was forgetting that the binary inside Lima must match the Linux VM architecture.&lt;/p&gt;

&lt;p&gt;For example, if the Lima VM is &lt;code&gt;x86_64&lt;/code&gt;, I can build:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; bin

&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  go build &lt;span class="nt"&gt;-o&lt;/span&gt; bin/tiny-docker-go-linux-amd64 ./cmd/tiny-docker-go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But if the Lima VM is &lt;code&gt;aarch64&lt;/code&gt;, for example on Apple Silicon, I should build:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; bin

&lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux &lt;span class="nv"&gt;GOARCH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;arm64 &lt;span class="se"&gt;\&lt;/span&gt;
  go build &lt;span class="nt"&gt;-o&lt;/span&gt; bin/tiny-docker-go-linux-arm64 ./cmd/tiny-docker-go
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To check the VM architecture:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;limactl shell tiny-docker &lt;span class="nb"&gt;uname&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Possible outputs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;x86_64   -&amp;gt; GOARCH=amd64
aarch64  -&amp;gt; GOARCH=arm64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you build the wrong architecture, you may see:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;exec format error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This error is simple but confusing when you first see it.&lt;/p&gt;

&lt;p&gt;It usually means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The binary architecture does not match the machine trying to execute it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was one of those small details that reminded me how important platform boundaries are.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake: Assuming Host Paths Always Exist Inside Lima
&lt;/h2&gt;

&lt;p&gt;Another mistake was around file sharing.&lt;/p&gt;

&lt;p&gt;My project existed on macOS at something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;/Users/amir/Desktop/tiny-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I expected that path to always work inside Lima.&lt;/p&gt;

&lt;p&gt;Sometimes it did.&lt;/p&gt;

&lt;p&gt;Sometimes the mount configuration was not what I expected.&lt;/p&gt;

&lt;p&gt;So if I ran this inside the VM:&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;cd&lt;/span&gt; /Users/amir/Desktop/tiny-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No such file or directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the problem was not my Go code.&lt;/p&gt;

&lt;p&gt;It was not the runtime.&lt;/p&gt;

&lt;p&gt;It was simply a shared folder issue.&lt;/p&gt;

&lt;p&gt;The lesson was:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Always verify that the path exists inside the VM, not only on the host.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Useful checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;limactl shell tiny-docker &lt;span class="nb"&gt;pwd
&lt;/span&gt;limactl shell tiny-docker &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /Users
limactl shell tiny-docker &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; /Users/amir/Desktop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the project is not mounted, the easiest workaround is to clone the repository inside the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &amp;lt;your-repo-url&amp;gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;tiny-docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cleaner long-term solution is to configure Lima mounts properly.&lt;/p&gt;

&lt;p&gt;But the important lesson is that a VM has its own filesystem view.&lt;/p&gt;

&lt;p&gt;Never assume the host path exists inside the guest.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake: Running the Wrong Binary
&lt;/h2&gt;

&lt;p&gt;Another mistake was running the macOS binary when I actually needed the Linux binary.&lt;/p&gt;

&lt;p&gt;This is easy to do when you have files like:&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
./bin/tiny-docker-go-linux-amd64
./bin/tiny-docker-go-linux-arm64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macOS binary can be useful as a CLI wrapper.&lt;/p&gt;

&lt;p&gt;But the Linux binary must perform the real runtime operations.&lt;/p&gt;

&lt;p&gt;The separation became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS binary:
  command parsing
  platform detection
  Lima dispatching

Linux binary:
  namespaces
  cgroups
  chroot
  mount setup
  networking
  process lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;On macOS, I do not pretend to support Linux container primitives directly.&lt;/p&gt;

&lt;p&gt;I route the work to the Linux VM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mistake: Not Checking Prerequisites Early
&lt;/h2&gt;

&lt;p&gt;At first, failures happened too late.&lt;/p&gt;

&lt;p&gt;For example, I could run a command and only later discover:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;limactl is not installed
Lima instance does not exist
Lima instance is not running
Linux binary is missing
Rootfs is not accessible inside Lima
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This created confusing errors.&lt;/p&gt;

&lt;p&gt;So I started adding validation before running the actual command.&lt;/p&gt;

&lt;p&gt;Good prerequisite checks include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is limactl installed?
Does the Lima instance exist?
Is the Lima instance running?
Does the Linux binary exist?
Is the Linux binary accessible inside Lima?
Is the rootfs path accessible inside Lima?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This improves developer experience a lot.&lt;/p&gt;

&lt;p&gt;Instead of a low-level error, I want an error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Linux binary not found at "./bin/tiny-docker-go-linux-amd64";
build it first and share it with Lima.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rootfs "./rootfs/alpine" is not accessible inside Lima;
ensure the workspace is shared with the VM.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not the most exciting part of a runtime project.&lt;/p&gt;

&lt;p&gt;But it is an important engineering detail.&lt;/p&gt;

&lt;p&gt;As a senior engineer, I have learned that good error messages are part of the product.&lt;/p&gt;

&lt;p&gt;Even if the product is just a learning project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Running a Shell
&lt;/h2&gt;

&lt;p&gt;After preparing the rootfs and building the runtime, I can run:&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 shell:&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;Example output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.23.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the hostname:&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;Run a process:&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;sleep &lt;/span&gt;30
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From another terminal:&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 ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ID            STATUS   PID   CREATED              COMMAND
ab12cd34ef56  running  1234  2026-05-17 12:30:45  /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This helped me understand process tracking better.&lt;/p&gt;

&lt;p&gt;A container runtime does not only start processes.&lt;/p&gt;

&lt;p&gt;It also needs to track them, store metadata, collect logs, stop them, and clean up after them.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Running From macOS Through Lima
&lt;/h2&gt;

&lt;p&gt;From macOS, I wanted a command 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/echo &lt;span class="s2"&gt;"hello from linux"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally, the command is routed through Lima:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;limactl shell tiny-docker &lt;span class="nb"&gt;sudo&lt;/span&gt; ./bin/tiny-docker-go-linux-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/echo &lt;span class="s2"&gt;"hello from linux"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;hello from linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gave me a nice workflow.&lt;/p&gt;

&lt;p&gt;I could stay in my macOS terminal, but still execute the real Linux runtime inside the VM.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Testing a Memory Limit
&lt;/h2&gt;

&lt;p&gt;If cgroup v2 is available and the runtime supports memory limits, I can run:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--memory&lt;/span&gt; 128m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the container-like shell:&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; /sys/fs/cgroup/memory.max
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Expected output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;134217728
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is 128 MiB in bytes.&lt;/p&gt;

&lt;p&gt;This small test made cgroups much more real for me.&lt;/p&gt;

&lt;p&gt;Before this project, a memory limit felt like a Docker CLI option:&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 alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After implementing a small version, I started seeing it differently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker exposes a nice option.
The Linux kernel enforces the limit through cgroups.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a very different level of understanding.&lt;/p&gt;




&lt;h2&gt;
  
  
  Example: Isolated Networking
&lt;/h2&gt;

&lt;p&gt;For networking, the runtime can create a basic isolated mode using Linux networking primitives.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;host bridge
  |
  veth pair
  |
container network namespace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run:&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="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--net&lt;/span&gt; isolated &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the shell:&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 route
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the implementation, I expect to see a container-side interface and a default route.&lt;/p&gt;

&lt;p&gt;This was one of the most interesting parts for me.&lt;/p&gt;

&lt;p&gt;Docker networking feels simple from the outside:&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 underneath, there is a lot of Linux networking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;network namespaces
veth pairs
bridges
routes
iptables
NAT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Building even a small version made me appreciate how much complexity Docker hides.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Learned About chroot
&lt;/h2&gt;

&lt;p&gt;My runtime uses &lt;code&gt;chroot&lt;/code&gt; as a simple way to change the visible root filesystem.&lt;/p&gt;

&lt;p&gt;For learning, this is useful.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;chroot&lt;/code&gt; is not the same as a full production container filesystem model.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;chroot&lt;/code&gt;, the process sees a different root directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before:
/

After:
./rootfs/alpine becomes /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But production runtimes usually involve more advanced concepts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pivot_root
overlay filesystems
image layers
OCI runtime spec
capability dropping
seccomp
AppArmor
SELinux
user namespaces
read-only mounts
masked paths
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So I try to be careful when describing the project.&lt;/p&gt;

&lt;p&gt;It is better to say:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is a tiny educational runtime.
It demonstrates some basic container building blocks.
It is not a production replacement for Docker, containerd, or runc.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That honesty matters.&lt;/p&gt;

&lt;p&gt;Learning projects are valuable, but they should not be oversold.&lt;/p&gt;




&lt;h2&gt;
  
  
  My Updated Mental Model of Docker
&lt;/h2&gt;

&lt;p&gt;Before this project, I mostly used Docker at the command level:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;After building a tiny runtime, I started seeing Docker in layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker CLI
Docker daemon
containerd
runc
Linux namespaces
Linux cgroups
root filesystem
network namespace
mount namespace
process lifecycle
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A command 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;docker run alpine &lt;span class="nb"&gt;echo &lt;/span&gt;hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;looks small.&lt;/p&gt;

&lt;p&gt;But conceptually, it involves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resolving the image
downloading layers
preparing the root filesystem
creating namespaces
configuring cgroups
setting up mounts
configuring networking
starting the process
attaching stdio
tracking metadata
collecting exit status
cleaning up resources
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My tiny runtime only implements a small part of this.&lt;/p&gt;

&lt;p&gt;But that small part was enough to make Docker feel less like magic.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lima vs Docker Desktop: My Practical Conclusion
&lt;/h2&gt;

&lt;p&gt;I do not see Lima and Docker Desktop as direct replacements for each other in every situation.&lt;/p&gt;

&lt;p&gt;For normal application development, Docker Desktop is usually more convenient.&lt;/p&gt;

&lt;p&gt;It gives me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Docker CLI
Docker Compose
image management
container lifecycle management
volume support
networking
developer-friendly tooling
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But for learning Linux container internals, Lima gave me a cleaner mental model.&lt;/p&gt;

&lt;p&gt;Lima gave me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;a Linux VM
direct access to Linux tools
a clean environment for experiments
less abstraction around Docker itself
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So my conclusion is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use Docker Desktop when your goal is to run and ship applications.
Use Lima when your goal is to understand or control the Linux environment.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For this project, Lima was the better learning tool.&lt;/p&gt;




&lt;h2&gt;
  
  
  Simple Experimental Benchmark
&lt;/h2&gt;

&lt;p&gt;This benchmark is not scientific.&lt;/p&gt;

&lt;p&gt;I only wanted to understand the rough overhead of routing commands from macOS through Lima compared with running directly inside the Lima VM.&lt;/p&gt;

&lt;p&gt;The command I tested was intentionally small:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Because the command is tiny, the overhead of the runtime and VM boundary becomes easier to notice.&lt;/p&gt;




&lt;h3&gt;
  
  
  Test 1: Running Directly Inside Lima
&lt;/h3&gt;

&lt;p&gt;Inside the Lima VM:&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;time sudo&lt;/span&gt; ./tiny-docker-go run &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine /bin/echo hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

real    0m0.045s
user    0m0.008s
sys     0m0.020s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Test 2: Running From macOS Through Lima
&lt;/h3&gt;

&lt;p&gt;From macOS:&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;time&lt;/span&gt; ./tiny-docker-go run &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine /bin/echo hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Internally, this routes through something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;limactl shell tiny-docker &lt;span class="nb"&gt;sudo&lt;/span&gt; ./bin/tiny-docker-go-linux-amd64 &lt;span class="se"&gt;\&lt;/span&gt;
  run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--rootfs&lt;/span&gt; ./rootfs/alpine &lt;span class="se"&gt;\&lt;/span&gt;
  /bin/echo hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

real    0m0.180s
user    0m0.020s
sys     0m0.030s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Benchmark Interpretation
&lt;/h3&gt;

&lt;p&gt;The direct Linux execution was faster.&lt;/p&gt;

&lt;p&gt;The macOS-to-Lima path had extra overhead because the command crossed the VM boundary through &lt;code&gt;limactl shell&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In this rough experiment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Direct inside Lima:       ~45 ms
macOS through Lima:       ~180 ms
Extra routing overhead:   ~135 ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For very short commands like &lt;code&gt;/bin/echo&lt;/code&gt;, the overhead is visible.&lt;/p&gt;

&lt;p&gt;For long-running processes, the overhead matters much less.&lt;/p&gt;

&lt;p&gt;For example, if I run a service for 10 minutes, an extra 100-200 ms at startup is not very important.&lt;/p&gt;

&lt;p&gt;My practical conclusion:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;For a nice macOS developer experience, routing through Lima is acceptable.
For tight benchmark loops, run directly inside the VM.
For production-grade runtimes, this approach is educational, not final.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Things I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;There are many things I would like to improve in this project.&lt;/p&gt;

&lt;p&gt;Some of them are runtime-related:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;use pivot_root instead of only chroot
improve cgroup v2 handling
support better cleanup
add more robust metadata storage
improve log streaming
support better TTY handling
add user namespace support
drop Linux capabilities
add seccomp profiles
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some of them are macOS/Lima-related:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;detect Lima architecture automatically
choose the correct Linux binary automatically
improve Lima instance setup
validate shared paths more clearly
provide a bootstrap command for macOS users
make error messages more actionable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better macOS setup command could eventually 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 setup lima
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it could handle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;checking limactl
creating the Lima instance
building the Linux binary
preparing the rootfs
validating mounts
testing a hello-world container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That would make the project much easier to try.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Lima helped me understand the boundary between macOS and Linux much better.&lt;/p&gt;

&lt;p&gt;The biggest lesson was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Go can make the binary portable, but it cannot make Linux kernel features exist on macOS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For a container runtime, the kernel matters.&lt;/p&gt;

&lt;p&gt;My final mental model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;macOS is my workstation.
Lima gives me a Linux VM.
The Linux VM gives me namespaces and cgroups.
My Go runtime uses those Linux features.
The rootfs gives the process its filesystem.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This project also changed how I look at Docker.&lt;/p&gt;

&lt;p&gt;Docker is not magic.&lt;/p&gt;

&lt;p&gt;But Docker is impressive because it hides a lot of complexity behind a simple interface.&lt;/p&gt;

&lt;p&gt;A command 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;docker run alpine &lt;span class="nb"&gt;echo &lt;/span&gt;hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is easy to type.&lt;/p&gt;

&lt;p&gt;But behind it, there are many layers of runtime, filesystem, networking, isolation, and process management.&lt;/p&gt;

</description>
      <category>go</category>
      <category>docker</category>
      <category>linux</category>
      <category>macos</category>
    </item>
    <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://forem.com/amirsefati/building-tiny-docker-go-in-go-what-i-learned-from-building-a-tiny-docker-like-runtime-57g9</link>
      <guid>https://forem.com/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>
