<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[nesar.dev]]></title><description><![CDATA[Deep dives into backend engineering, algorithms, and the tools behind modern infrastructure]]></description><link>https://nesar.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!CkJF!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F99bedafa-2bd5-4561-a19f-02fd0652ebbd_512x512.png</url><title>nesar.dev</title><link>https://nesar.dev</link></image><generator>Substack</generator><lastBuildDate>Thu, 07 May 2026 10:06:58 GMT</lastBuildDate><atom:link href="https://nesar.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Nesar]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[nesarjony@substack.com]]></webMaster><itunes:owner><itunes:email><![CDATA[nesarjony@substack.com]]></itunes:email><itunes:name><![CDATA[Nesar]]></itunes:name></itunes:owner><itunes:author><![CDATA[Nesar]]></itunes:author><googleplay:owner><![CDATA[nesarjony@substack.com]]></googleplay:owner><googleplay:email><![CDATA[nesarjony@substack.com]]></googleplay:email><googleplay:author><![CDATA[Nesar]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[From Dockerfile to Registry — Your First Real Docker Build]]></title><description><![CDATA[The hands-on guide to writing Dockerfiles, building images, and shipping containers.]]></description><link>https://nesar.dev/p/from-dockerfile-to-registry-your</link><guid isPermaLink="false">https://nesar.dev/p/from-dockerfile-to-registry-your</guid><dc:creator><![CDATA[Nesar]]></dc:creator><pubDate>Fri, 20 Mar 2026 11:21:26 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/6f07d97c-2d63-4710-9786-cc3aa388ca40_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In Part 1, we explained the&nbsp;<strong>reasons</strong>&nbsp;behind Docker: how environment mismatches silently break deployments, and how Docker packages your code and its environment into a single, portable unit. We explored the three kernel features that power containers &#8212; <strong>namespaces</strong>, <strong>cgroups</strong>, and <strong>OverlayFS</strong> &#8212; and nailed down the mental model that an <strong>image</strong> is a read-only blueprint while a <strong>container</strong> is a running instance of it.</p><p>Now it&#8217;s time to put that understanding to work. In this post, you&#8217;ll learn every <strong>Dockerfile instruction</strong>, containerize a real <strong>FastAPI</strong> application from scratch, master Docker&#8217;s <strong>layer caching</strong> strategy, debug running containers, and push your image to a <strong>registry</strong>.</p><h2><strong>The Dockerfile &#8212; Your Build Instructions</strong></h2><p><br>A <code>Dockerfile</code> specifies the sequence of instructions for assembling a Docker container image, with nearly every instruction creating an immutable layer for efficient reuse and caching.</p><p>Here&#8217;s a quick-reference cheat sheet of every instruction &#8212; we&#8217;ll break down the important ones in detail below.</p><ul><li><p><code>FROM</code> &#8212; Sets the base image for the build</p></li><li><p><code>RUN</code> &#8212; Executes a command and commits the result as a new layer</p></li><li><p><code>COPY</code> &#8212; Copies files from your project into the image</p></li><li><p><code>ADD</code> &#8212; Like <code>COPY</code>, but also extracts tar archives and supports remote URLs</p></li><li><p><code>CMD</code> &#8212; Default arguments passed to the container&#8217;s entrypoint at launch</p></li><li><p><code>ENTRYPOINT</code> &#8212; The executable that runs when the container starts</p></li><li><p><code>ENV</code> &#8212; Sets environment variables for build and runtime</p></li><li><p><code>ARG</code> &#8212; Sets build-time-only variables (not available at runtime)</p></li><li><p><code>WORKDIR</code> &#8212; Sets the working directory for subsequent instructions</p></li><li><p><code>EXPOSE</code> &#8212; Declares which port the container listens on</p></li><li><p><code>USER</code> &#8212; Sets the non-root user the container runs as</p></li></ul><blockquote><p><code>LABEL</code> and <code>VOLUME</code> exist too &#8212; we&#8217;ll cover them when they&#8217;re relevant in later parts.</p></blockquote><p>Here&#8217;s a closer look at each one.</p><h3><strong>FROM &#8212; Where Everything Starts</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;59846c10-fd3f-45e7-b1d7-5d95562cc11a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">FROM python:3.12-slim-bookworm</code></pre></div><p>Every Dockerfile begins with <code>FROM</code>. It sets the <strong>base image</strong> &#8212; the starting point your application builds on. For Python projects, you generally have three choices:</p><ul><li><p><code>python:3.12</code> &#8212; Full Debian with build tools (~900MB). Use it if your dependencies require compiling C extensions.</p></li><li><p><code>python:3.12-slim</code> &#8212; Slimmed-down Debian (~150MB). Good enough for most applications.</p></li><li><p><code>python:3.12-alpine</code> &#8212; Alpine Linux (~50MB). Smallest, but many Python packages ship prebuilt binaries that aren&#8217;t compatible with Alpine&#8217;s <code>musl libc</code>. Use with caution.</p></li></ul><p><code>python:3.12-slim-bookworm</code> It is the sweet spot for most production Python services.</p><h3>RUN &#8212; Executing Commands</h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;5e582f50-b70e-4b3c-abd1-93dc5f362193&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">RUN pip install --no-cache-dir -r requirements.txt</code></pre></div><p><code>RUN</code> executes a command during the build and saves the result as a new image layer. This is where you install dependencies, compile code, and set up your environment.</p><p>Two things worth knowing:</p><ol><li><p><strong>Every </strong><code>RUN</code><strong> creates a layer.</strong> Each layer adds to your image size &#8212; even if a later step deletes the files. We&#8217;ll cover how to keep images lean in a future post on multi-stage builds.</p></li><li><p><strong>Docker caches layers.</strong> If a <code>RUN</code> instruction hasn&#8217;t changed since the last build, Docker skips it and reuses the cached result. Fast rebuilds &#8212; but it also means Docker won&#8217;t pick up new package versions unless you change the instruction.</p></li></ol><h3><strong>COPY &#8212; Bringing Your Files In</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;270ecde9-e21e-4c44-991a-be66e833ce93&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">COPY . .</code></pre></div><p><code>COPY</code> scoops up files from your project and drops them into the image.</p><p>There&#8217;s also <code>ADD</code>, which copies files, auto-extracts tar archives, and can fetch from URLs. In real-world Dockerfiles, prefer <code>COPY</code> &#8212; It&#8217;s explicit and predictable. Reserve <code>ADD</code> only for cases where you specifically need its extra capabilities.</p><h3>CMD and ENTRYPOINT &#8212; Starting Your App</h3><p><code>CMD</code> defines the default command that runs when a container starts. It&#8217;s easy to override &#8212; <code>docker run myimage python worker.py</code> ignores the entire CMD and runs <code>python worker.py</code> instead.</p><p>There are two ways to write <code>CMD</code>, and the difference matters more than most people realize:</p><p><strong>Exec form (use this):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;f216f159-b126-4982-8b99-252a287adedd&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]</code></pre></div><p>The square bracket syntax is exec form. It runs your process directly as the main process inside the container (PID 1 &#8212; the first process the container sees). When you run <code>docker stop</code>, Docker sends a shutdown signal directly to your app. Graceful shutdown works exactly as expected.</p><p><strong>Shell form (avoid this):</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;bc2820b5-354f-465d-889d-c765efe6f03d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">CMD uvicorn app:app --host 0.0.0.0 --port $PORT</code></pre></div><p>Without the square brackets, Docker quietly wraps your command in <code>/bin/sh -c</code>, like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;235c0e5d-7e8a-4a50-90d8-2d54493c62e7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">/bin/sh -c "uvicorn app:app --host 0.0.0.0 --port $PORT"</code></pre></div><p>Now the shell is the main process inside the container &#8212; not your app. Your app is running behind the shell.</p><p>This matters when you stop the container. <code>docker stop</code> sends a shutdown signal to the main process. The shell receives it, but it doesn&#8217;t pass it along to your app. Your app has no idea it&#8217;s supposed to stop.</p><p>After a 10-second timeout, Docker force-kills everything. No graceful shutdown &#8212; any work your app was doing at that moment just disappears.</p><p>The only reason to use shell form is when you need environment variable expansion at runtime (like <code>$PORT</code>). In every other case, exec form is the safer choice.</p><p><strong>What about ENTRYPOINT?</strong></p><p>Without an <code>ENTRYPOINT</code>, <code>CMD</code> is the full command Docker runs when the container starts. That&#8217;s the setup most Python applications need.</p><p>If you set an <code>ENTRYPOINT</code>, it becomes the fixed executable &#8212; and <code>CMD</code> just provides default arguments.</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;557a1c78-dbff-4000-9225-24e61d5bfefb&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">ENTRYPOINT ["python"]
CMD ["app.py"]</code></pre></div><p>Now, when the container starts, Docker runs <code>python app.py</code>. But if someone runs:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;66043b3e-fe12-4f9a-9d73-462cd72fedc4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">docker run myimage worker.py</code></pre></div><p>Docker replaces only the <code>CMD</code> part &#8212; so it runs <code>python worker.py</code> instead. The entrypoint (<code>python</code>) stays the same. Only the argument changed.</p><p>We&#8217;ll cover more advanced <code>ENTRYPOINT</code> patterns and signal handling in a future post.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!36VM!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!36VM!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 424w, https://substackcdn.com/image/fetch/$s_!36VM!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 848w, https://substackcdn.com/image/fetch/$s_!36VM!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 1272w, https://substackcdn.com/image/fetch/$s_!36VM!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!36VM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png" width="1456" height="797" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/cc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:797,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!36VM!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 424w, https://substackcdn.com/image/fetch/$s_!36VM!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 848w, https://substackcdn.com/image/fetch/$s_!36VM!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 1272w, https://substackcdn.com/image/fetch/$s_!36VM!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fcc9a5692-6975-4825-9c85-e81b4b44591b_1680x920.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h3><strong>ENV &#8212; Setting Environment Variables</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;d49737cf-b5f0-4469-848e-8c2fe171c65b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">ENV APP_ENV=production</code></pre></div><p>Sets variables available both during the build and when the container runs. Your application can read these with <code>os.environ.get("APP_ENV")</code>.</p><p>There&#8217;s also <code>ARG</code>, which sets variables available <strong>only during the build</strong> &#8212; useful for version numbers or build-time configuration that shouldn&#8217;t persist into the final image.</p><h3><strong>WORKDIR &#8212; Setting the Directory</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;591c4c0a-519c-4e3b-b7ba-27a29a72bf16&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">WORKDIR /app</code></pre></div><p><code>WORKDIR</code> sets the working directory for everything that follows. If the folder doesn&#8217;t exist, Docker creates it for you. Always use <code>WORKDIR</code> instead of <code>RUN cd /somewhere</code> &#8212; <code>cd</code> only lasts for a single <code>RUN</code> step.</p><h3><strong>EXPOSE &#8212; Declares Ports</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;4607fbc6-2f6b-4d6e-9af0-494a386b22b7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">EXPOSE 8000</code></pre></div><p><code>EXPOSE</code> signals that the container expects traffic on port 8000. Real port binding happens when you run the container with <code>-p</code>. Treat <code>EXPOSE</code> as built-in documentation for your Dockerfile.</p><h3><strong>USER &#8212; Not Running as Root</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;bb71accd-3d58-4461-8b7b-e0b1985bf276&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">RUN groupadd --system appgroup &amp;&amp; useradd --system --gid appgroup appuser
USER appuser</code></pre></div><p>By default, containers run as root. That&#8217;s acceptable for development, but dangerous in production. If a vulnerability slips through, an attacker running as root can do far more damage. Switching to a non-root user is a small change with a meaningful security payoff.</p><div><hr></div><h2><strong>Let&#8217;s Build Something Real</strong></h2><p>Enough theory &#8212; let&#8217;s roll up our sleeves and containerize a real application.</p><p>We&#8217;ll use a simple <strong>FastAPI</strong> server with <strong>uvicorn</strong>. Here&#8217;s the application:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;301335d6-c7ed-4ff1-9169-dac97dcfc21d&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"># app.py
from fastapi import FastAPI

app = FastAPI()

@app.get(&#8221;/health&#8221;)
def health_check():
    return {&#8221;status&#8221;: &#8220;healthy&#8221;, &#8220;version&#8221;: &#8220;1.0.0&#8221;}

@app.get(&#8221;/&#8221;)
def root():
    return {&#8221;message&#8221;: &#8220;FastAPI running inside Docker&#8221;}</code></pre></div><p>And the dependencies:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;8f77b659-9f61-41d5-8266-d429c5cef1a7&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext"># requirements.txt
fastapi==0.115.0
uvicorn[standard]==0.30.0</code></pre></div><h3><strong>First &#8212; Make Sure It Works Without Docker</strong></h3><p>Before containerizing anything, verify the app runs normally:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;90ca38dc-778c-4ace-af60-4863d1b8bd6b&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
uvicorn app:app --host 0.0.0.0 --port 8000</code></pre></div><p>Test it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;9f85f91d-ae50-4390-ad2f-c0dcebf7b001&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">curl localhost:8000/health
# {&#8221;status&#8221;:&#8221;healthy&#8221;,&#8221;version&#8221;:&#8221;1.0.0&#8221;}</code></pre></div><blockquote><p><strong>Rule of thumb:</strong> If your app fails outside Docker, it will fail inside Docker too. Docker guarantees consistency, not bug fixes.</p></blockquote><h3><strong>The .dockerignore File</strong></h3><p>When you run <code>docker build</code>, Docker ships your whole project directory to the build engine. That means <code>venv</code> folders (200MB+), <code>.git</code> directories, <code>__pycache__</code> files &#8212; all of it &#8212; unless you tell it otherwise.</p><p>Create a <code>.dockerignore</code> file:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;35478454-e69e-4439-94e5-1e9d154fa529&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">__pycache__
*.pyc
.git
.gitignore
.env
Dockerfile
README.md
.vscode
.idea</code></pre></div><p>This speeds up builds and keeps sensitive files &#8212; like <code>.env</code> with database passwords &#8212; out of your image, where they belong.</p><h3><strong>Writing the Dockerfile</strong></h3><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;86c5a305-6125-47ba-835e-bf2967ae93b8&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile">FROM python:3.12-slim-bookworm

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD [&#8221;uvicorn&#8221;, &#8220;app:app&#8221;, &#8220;--host&#8221;, &#8220;0.0.0.0&#8221;, &#8220;--port&#8221;, &#8220;8000&#8221;]</code></pre></div><p>It&#8217;s clean and straightforward, but one decision here makes all the difference.</p><p><strong>Why do we copy </strong><code>requirements.txt</code><strong> separately before copying everything else?</strong></p><p>This is about <strong>Docker layer caching</strong>. Each instruction creates a layer. If the inputs to that layer haven&#8217;t changed, Docker reuses the cached version and skips re-executing it.</p><p>By copying only <code>requirements.txt</code> the first and running <code>pip install</code>, we create a cached layer for our dependencies. The next time we build, if we&#8217;ve only changed our Python code (not our dependencies), Docker skips the <code>pip install</code> step entirely.</p><p>Watch the difference:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;dockerfile&quot;,&quot;nodeId&quot;:&quot;4e32640d-e8a6-4aba-9195-fd992e94edcf&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-dockerfile"># &#10060; Bad ordering &#8212; a code change forces pip to reinstall every time
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

# &#9989; Good ordering &#8212; pip install is cached unless requirements.txt changes
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .</code></pre></div><p>The difference between a <strong>40-second rebuild</strong> and a <strong>3-second one</strong>. Multiply that by a team rebuilding all day, and the savings stack up quickly.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xiit!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xiit!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 424w, https://substackcdn.com/image/fetch/$s_!xiit!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 848w, https://substackcdn.com/image/fetch/$s_!xiit!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 1272w, https://substackcdn.com/image/fetch/$s_!xiit!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xiit!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png" width="1456" height="901" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:901,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!xiit!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 424w, https://substackcdn.com/image/fetch/$s_!xiit!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 848w, https://substackcdn.com/image/fetch/$s_!xiit!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 1272w, https://substackcdn.com/image/fetch/$s_!xiit!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0f0f8c73-5c29-4f11-b388-f520d3616a37_1680x1040.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>The <code>--no-cache-dir</code> flag tells pip not to stash downloaded packages inside the image. While that cache is handy locally, it just eats up space in a Docker build.</p><blockquote><p><strong>Note:</strong> This Dockerfile is intentionally simple. It runs as root and uses a single build stage. For production, you&#8217;d add a non-root user, use multi-stage builds to trim the image, and include a <code>HEALTHCHECK</code>. We&#8217;ll cover those upgrades soon.</p></blockquote><div><hr></div><h2><strong>Building the Image</strong></h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;fbe79074-8120-4be8-9b20-79405b52a395&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker build --tag fastapi-demo .</code></pre></div><p>Docker reads the Dockerfile, executes each instruction, and produces a tagged image:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;e002c0b9-0443-4e45-9e8e-94ea3a4f21aa&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker images
# REPOSITORY     TAG       IMAGE ID       CREATED          SIZE
# fastapi-demo   latest    a3b8e2c91f4d   5 seconds ago    195MB</code></pre></div><p>195MB &#8212; most of it from the Python runtime and system libraries in the slim base image. Use the full <code>python:3.12</code> base, and you&#8217;re looking at over 1GB.</p><div><hr></div><h2><strong>Running the Container</strong></h2><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;ecb8a9c7-9077-483e-9a5c-52592e753aff&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker run --detach --publish 8000:8000 --name my-api fastapi-demo</code></pre></div><p>Breaking down the flags:</p><ul><li><p><code>--detach</code> (<code>-d</code>) &#8212; runs the container in the background</p></li><li><p><code>--publish 8000:8000</code> (<code>-p</code>) &#8212; maps port 8000 on your machine to port 8000 inside the container</p></li><li><p><code>--name my-api</code> &#8212; gives the container a memorable name instead of a random one</p></li></ul><p>Test it exactly as before:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a1e6bdd0-367f-4e00-930f-61316fe58334&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">curl localhost:8000/health
# {&#8221;status&#8221;:&#8221;healthy&#8221;,&#8221;version&#8221;:&#8221;1.0.0&#8221;}</code></pre></div><p>You get the same response as before &#8212; but now your app is running inside a container, with its own Python runtime, its own packages, and its own filesystem. Move this image to any Docker-equipped machine, and it will run exactly the same.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3NCO!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3NCO!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 424w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 848w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 1272w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3NCO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png" width="1456" height="763" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:763,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:&quot;&quot;,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!3NCO!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 424w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 848w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 1272w, https://substackcdn.com/image/fetch/$s_!3NCO!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F26b9e66b-2834-42ff-b007-7275ff03dc0f_1680x880.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div><hr></div><h2><strong>Debugging a Running Container</strong></h2><p>Containers aren&#8217;t black boxes. Docker hands you plenty of tools to peek inside and see what&#8217;s going on.</p><p><strong>View logs:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;c2d71e9a-c6df-4768-b7f2-486aa31b57ee&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker logs my-api
# INFO:     Started server process [1]
# INFO:     Waiting for application startup.
# INFO:     Application startup complete.
# INFO:     Uvicorn running on http://0.0.0.0:8000</code></pre></div><p><strong>Follow logs in real time</strong> (like <code>tail -f</code>):</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;af68a43b-a228-4f60-9475-992b3bd5cee6&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker logs -f my-api</code></pre></div><p><strong>Open a shell inside the running container:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;a5ecc864-3db2-4081-aad0-b7ef1f927551&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker exec -it my-api /bin/bash</code></pre></div><p>This drops you right into the container&#8217;s filesystem. Poke around, check installed packages with <code>pip list</code>, inspect environment variables, test network connections &#8212; whatever you need to squash a bug. The <code>-it</code> flags give you an interactive terminal.</p><p><strong>Get the full container details as JSON:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;32d8ce0d-d1e2-4a8e-9d9c-874918226320&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker inspect my-api</code></pre></div><p>This spills out everything: network settings, volume mounts, environment variables and restart policies. Invaluable when something isn&#8217;t behaving as expected.</p><p><strong>Stop and remove a container:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;37d89cd4-0df1-4386-8292-57418d4627c3&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker stop my-api
docker rm my-api</code></pre></div><p>Or in one step: <code>docker rm -f my-api</code>.</p><div><hr></div><h2><strong>Sharing Your Image</strong></h2><p>To share your image across a team or deploy it from CI, you&#8217;ll need to push it to a <strong>container registry</strong> &#8212; a remote home for Docker images.</p><p>Common registries include <strong>Docker Hub</strong>, <strong>GitHub Container Registry (GHCR)</strong>, <strong>Amazon ECR</strong>, and <strong>Google Artifact Registry</strong>.</p><p>Let&#8217;s use GHCR as an example.</p><p><strong>Step 1 &#8212; Log in:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;97e9e477-ce96-4640-a5cf-4377add6cfd4&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker login ghcr.io -u YOUR_GITHUB_USERNAME --password YOUR_PAT
# Login Succeeded</code></pre></div><p><strong>Step 2 &#8212; Tag your image with the registry path:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;c12bf165-0a7a-4f2e-9900-6161d74d6eda&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker tag fastapi-demo ghcr.io/YOUR_USERNAME/fastapi-demo:1.0.0</code></pre></div><p>The tag format is <code>registry/namespace/image-name:version</code>. Always tag with a specific version &#8212; <code>1.0.0</code>, or a Git commit hash.</p><blockquote><p><strong>Warning:</strong> Avoid <code>:latest</code> in production &#8212; it&#8217;s a moving target that gets overwritten with every push. When things break at 2 AM, <code>:latest</code> leaves you guessing which version is actually running.</p></blockquote><p><strong>Step 3 &#8212; Push:</strong></p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;5cd28d62-1c35-41a0-b623-b7b7b0910633&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker push ghcr.io/YOUR_USERNAME/fastapi-demo:1.0.0</code></pre></div><p>Or build and push in one shot:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;plaintext&quot;,&quot;nodeId&quot;:&quot;3727f78f-7b9c-40a6-ba81-f0ed37878673&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-plaintext">docker build -t ghcr.io/YOUR_USERNAME/fastapi-demo:1.0.0 --push .</code></pre></div><p>Your image now lives in the registry, ready to be pulled onto any machine or deployment environment. This is the foundation of CI/CD: build once, push once, deploy anywhere.</p><blockquote><p><strong>Best practice:</strong> Use <strong>pinned version tags</strong> instead of relying on <code>:latest</code>.</p></blockquote><div><hr></div><h2><strong>What&#8217;s Next</strong></h2><p>This Dockerfile gets the job done, but it&#8217;s not ready for production. It runs as root, skips health checks, and doesn&#8217;t use multi-stage builds to reduce the image size.</p><p>In the next post, we'll dig deeper into &#8212; the kernel features behind container isolation. You&#8217;ll see how Docker builds private worlds, enforces resource limits, and why knowing this helps you debug real production problems.</p><p>Once you grasp what&#8217;s happening beneath <code>docker run</code>, Docker stops being a black box and becomes a tool you truly command.</p><div><hr></div><p><em>Next up &#8594; Linux Namespaces &amp; cgroups: How Docker Isolates Containers at the Kernel Level.</em></p>]]></content:encoded></item><item><title><![CDATA[Two Engineers, Three Hours, Zero Bugs — Why Docker Exists]]></title><description><![CDATA[The "it works on my machine" problem, and the tool that killed it for good.]]></description><link>https://nesar.dev/p/two-engineers-three-hours-zero-bugs</link><guid isPermaLink="false">https://nesar.dev/p/two-engineers-three-hours-zero-bugs</guid><pubDate>Thu, 19 Mar 2026 17:59:59 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/28ae08f4-99f3-4fed-9e92-82d102447b85_1200x630.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Imagine the scene: you&#8217;ve just wrapped up your <strong>FastAPI</strong> server. It runs smoothly on your laptop. But the moment you deploy to staging, it explodes with a <code>ModuleNotFoundError: No module named 'pydantic'.</code></p><p>You give a sharp look at the error. Pydantic is clearly listed in <code>requirements.txt</code>. You&#8217;ve installed it countless times. But here&#8217;s the catch: your laptop runs Python 3.12, while staging is stuck on Python 3.9 with a system-managed <code>pip</code>. That mismatch in version numbers made <code>pip</code> skip the package without a word.</p><p><strong>Two engineers. Three hours. Not a single buggy line.</strong> The culprit wasn&#8217;t your code &#8212; it was the environment all along.</p><p>This is the headache Docker is designed to erase. Instead of adding more complexity, Docker simplifies the variable entirely. Your code and its environment travel together as a single, inseparable unit &#8212; wherever you go.</p><div><hr></div><h2>What Docker Actually Is</h2><p>Docker is a platform that enables you to package your application and all its necessary dependencies &#8212; the language runtime, libraries, system tools, and configuration files &#8212; into a standardized, portable unit known as a <strong>container</strong>.</p><p>Build it once, and it runs identically &#8212; on your laptop, your teammate&#8217;s computer, a CI server, or a sprawling AWS production cluster.</p><h3>How It Works Under the Hood</h3><p>Docker isn&#8217;t magic. It&#8217;s a clean interface over three Linux kernel features:</p><ul><li><p><strong>Linux namespaces</strong> &#8212; give each container its own isolated view of the system: its own process list, its own network, its own filesystem. One container can&#8217;t see what another container is doing.</p></li><li><p><strong>cgroups</strong> &#8212; enforce resource constraints on each container, such as CPU and memory limits, so no container can exceed its allocation or disrupt others.</p></li><li><p><strong>OverlayFS</strong> &#8212; a layered filesystem that allows Docker to store file changes in efficient, stackable layers, enabling images to remain lightweight by sharing unchanged data across containers.</p></li></ul><p>Docker didn&#8217;t invent these features &#8212; it just made them easily accessible for everyone. Before Docker, managing these kernel powers meant writing complex C code. Docker gave us a much simpler way to handle it.</p><blockquote><p><strong>Key point:</strong> Docker is not a virtual machine. VMs spin up whole operating systems. Containers simply borrow the host&#8217;s kernel, each with its own private view. That&#8217;s why containers launch in milliseconds, while VMs take minutes.</p></blockquote><div><hr></div><h2>Images vs Containers &#8212; The Core Difference</h2><p>This trips up almost everyone at first, so let&#8217;s make it unforgettable.</p><p><strong>A Docker image</strong> is a read-only snapshot. It includes application code, installed dependencies, the runtime environment, and metadata &#8212; each packaged into distinct layers. It cannot be executed directly, but it serves as a blueprint for creating containers.</p><p><strong>A Docker container</strong> is a running instance created from an image. It operates as a live process in an isolated environment, allowing multiple independent containers from a single image, each with its own scope to avoid interference.</p><p>Think of it this way:</p><ul><li><p><strong>Image</strong> = a class definition in code</p></li><li><p><strong>Container</strong> = an instance of that class</p></li></ul><p>You define the image once. You instantiate containers from it as many times as you need.</p><p>Containers mount a writable layer on top of the image&#8217;s read-only layers. Any file changes within a running container are stored in this writable layer and are deleted when the container is removed. To persist data across container restarts, use <strong>volume mounts</strong>.</p><div><hr></div><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!09qt!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!09qt!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 424w, https://substackcdn.com/image/fetch/$s_!09qt!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 848w, https://substackcdn.com/image/fetch/$s_!09qt!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!09qt!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!09qt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png" width="1456" height="1040" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:1040,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:null,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:null,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!09qt!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 424w, https://substackcdn.com/image/fetch/$s_!09qt!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 848w, https://substackcdn.com/image/fetch/$s_!09qt!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 1272w, https://substackcdn.com/image/fetch/$s_!09qt!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F200a9536-7e77-4ec7-8873-05ea9cb77efd_1680x1200.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><h2>What&#8217;s Next</h2><p>Now you know <strong>what</strong> Docker is and <strong>why</strong> it exists. You understand the three kernel features powering it, the difference between VMs and containers, and the image-vs-container mental model that everything else builds on.</p><p>But understanding isn&#8217;t the same as doing.</p><p>In <strong>Part 2</strong>, we&#8217;ll roll up our sleeves and put all of this into practice. You&#8217;ll learn every Dockerfile instruction, containerize a real FastAPI application from scratch, master Docker&#8217;s layer caching strategy, debug running containers, and push your image to a registry &#8212; ready for deployment.</p><p>It&#8217;s time to build.</p><p><strong><a href="https://nesar.dev/p/from-dockerfile-to-registry-your">Next up &#8594; From Dockerfile to Registry &#8212; Your First Real Docker Build</a></strong></p>]]></content:encoded></item></channel></rss>