<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alan Varghese</title>
    <description>The latest articles on DEV Community by Alan Varghese (@alanvarghese-dev).</description>
    <link>https://dev.to/alanvarghese-dev</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3473225%2Fde3d35b1-66b1-4626-b31e-63a10ff5ced0.jpg</url>
      <title>DEV Community: Alan Varghese</title>
      <link>https://dev.to/alanvarghese-dev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alanvarghese-dev"/>
    <language>en</language>
    <item>
      <title>How I Slashed a Docker Image from 442 MB to 56 MB (87% Cut) and Hardened It for Production</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Thu, 10 Sep 2026 22:17:59 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/how-i-slashed-a-docker-image-from-442-mb-to-56-mb-87-cut-and-hardened-it-for-production-1om4</link>
      <guid>https://dev.to/alanvarghese-dev/how-i-slashed-a-docker-image-from-442-mb-to-56-mb-87-cut-and-hardened-it-for-production-1om4</guid>
      <description>&lt;p&gt;We've all been there: you whip up a simple microservice in Python, write a quick Dockerfile, run &lt;code&gt;docker build&lt;/code&gt;, and suddenly your container image weighs almost &lt;strong&gt;half a gigabyte&lt;/strong&gt; (or &lt;strong&gt;1.75 GB&lt;/strong&gt; uncompressed on disk!).&lt;/p&gt;

&lt;p&gt;For a 15-line Flask application with two routes? That felt unacceptable.&lt;/p&gt;

&lt;p&gt;Bloated Docker images slow down CI/CD pipelines, increase registry storage bills, consume unnecessary bandwidth during deployment, and—worst of all—expand the security attack surface with packages that have no business being in a production container.&lt;/p&gt;

&lt;p&gt;In this project, I took a bloated baseline image and systematically redesigned it. The outcome?&lt;/p&gt;

&lt;p&gt;🔥 &lt;strong&gt;Image Content Size:&lt;/strong&gt; Reduced from &lt;strong&gt;442 MB&lt;/strong&gt; to &lt;strong&gt;56.6 MB&lt;/strong&gt; (&lt;strong&gt;87.19% reduction&lt;/strong&gt;)&lt;br&gt;&lt;br&gt;
💾 &lt;strong&gt;Disk Usage:&lt;/strong&gt; Dropped from &lt;strong&gt;1.75 GB&lt;/strong&gt; to &lt;strong&gt;256 MB&lt;/strong&gt; (&lt;strong&gt;85.37% reduction&lt;/strong&gt;)&lt;br&gt;&lt;br&gt;
🛡️ &lt;strong&gt;Security:&lt;/strong&gt; Transitioned from running as root to a hardened, non-root system user&lt;br&gt;&lt;br&gt;
⚡ &lt;strong&gt;Build Time:&lt;/strong&gt; Dramatically improved iterative build speeds using Docker layer caching&lt;br&gt;&lt;br&gt;
🚀 &lt;strong&gt;Runtime:&lt;/strong&gt; Replaced Flask's single-threaded dev server with a production WSGI server (Gunicorn)  &lt;/p&gt;

&lt;p&gt;Here is the full breakdown of how I did it, the pitfalls I ran into, and the key lessons you can apply to your own containers today.&lt;/p&gt;


&lt;h2&gt;
  
  
  🛑 The "Before": A Naive, Bloated Baseline
&lt;/h2&gt;

&lt;p&gt;Here is what our initial &lt;code&gt;Dockerfile.baseline&lt;/code&gt; looked like:&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; python:3.12&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; . .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl git vim

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At first glance, it looks familiar. It sets a workdir, copies files, installs tools, installs dependencies, and runs the app.&lt;/p&gt;

&lt;p&gt;Let's build it and inspect the damage:&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 &lt;span class="nt"&gt;-f&lt;/span&gt; Dockerfile.baseline &lt;span class="nt"&gt;-t&lt;/span&gt; myapp:baseline &lt;span class="nb"&gt;.&lt;/span&gt;
docker images myapp:baseline
&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;REPOSITORY   TAG        IMAGE ID       CREATED          SIZE
myapp        baseline   a1b2c3d4e5f6   10 seconds ago   442MB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking uncompressed disk usage with Docker desktop / inspect: &lt;strong&gt;1.75 GB&lt;/strong&gt;!&lt;/p&gt;

&lt;h3&gt;
  
  
  What Went Wrong Here?
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The Base Image:&lt;/strong&gt; &lt;code&gt;python:3.12&lt;/code&gt; is built on a full Debian distribution packed with compilers, header files, and utilities our web app will never call.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unnecessary Packages:&lt;/strong&gt; We installed &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and &lt;code&gt;vim&lt;/code&gt;. Why does a production container need a text editor and a version control tool?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate &lt;code&gt;RUN&lt;/code&gt; instructions:&lt;/strong&gt; &lt;code&gt;RUN apt-get update&lt;/code&gt; and &lt;code&gt;RUN apt-get install&lt;/code&gt; created two separate filesystem layers, storing temporary cache files forever in the image layer history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Poor Layer Caching:&lt;/strong&gt; &lt;code&gt;COPY . .&lt;/code&gt; came &lt;em&gt;before&lt;/em&gt; &lt;code&gt;RUN pip install&lt;/code&gt;. Any tiny change to &lt;code&gt;app.py&lt;/code&gt; busted Docker's cache and forced &lt;code&gt;pip install&lt;/code&gt; to execute again from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;/strong&gt; Test files, git history, and local virtual environments were beamed right into the Docker daemon context.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Insecure Execution:&lt;/strong&gt; The app runs as &lt;code&gt;root&lt;/code&gt; (UID 0).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Server:&lt;/strong&gt; Flask’s built-in server is not built for production workloads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let's fix this step-by-step.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The 6-Step Optimization Playbook
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Switch to a Minimal Base Image (&lt;code&gt;-slim&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The single highest-leverage change you can make is picking the right base image.&lt;/p&gt;

&lt;p&gt;Instead of the full &lt;code&gt;python:3.12&lt;/code&gt;, we switched to &lt;code&gt;python:3.12-slim&lt;/code&gt;:&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; python:3.12-slim&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why not Alpine (&lt;code&gt;python:3.12-alpine&lt;/code&gt;)?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Alpine uses &lt;code&gt;musl&lt;/code&gt; libc instead of &lt;code&gt;glibc&lt;/code&gt;. While Alpine is tiny, Python packages with C extensions (like numpy, cryptography, etc.) frequently lack pre-compiled wheels for musl, triggering slow compilation during build or subtle runtime bugs. Debian slim is the sweet spot for Python: rock-solid compatibility with standard &lt;code&gt;glibc&lt;/code&gt; wheels and a tiny footprint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 2: Ruthlessly Prune Unnecessary OS Packages
&lt;/h3&gt;

&lt;p&gt;Running &lt;code&gt;docker history myapp:baseline&lt;/code&gt; exposed where the bloat lived:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;apt-get update&lt;/code&gt; layer: &lt;strong&gt;~21.3 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl git vim&lt;/code&gt; layer: &lt;strong&gt;~53.1 MB&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Neither &lt;code&gt;git&lt;/code&gt; nor &lt;code&gt;vim&lt;/code&gt; belong in a running container. If you need to debug a running container, use ephemeral debugging sidecars or mount volumes—don't permanently ship development utilities to production.&lt;/p&gt;

&lt;p&gt;We dropped the &lt;code&gt;apt-get&lt;/code&gt; commands entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Add a Scrupulous &lt;code&gt;.dockerignore&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Whenever you run &lt;code&gt;docker build&lt;/code&gt;, Docker first transfers the entire directory (the "build context") to the Docker daemon.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;.dockerignore&lt;/code&gt;, you're sending &lt;code&gt;.git&lt;/code&gt; logs, &lt;code&gt;.venv&lt;/code&gt;, &lt;code&gt;.pytest_cache&lt;/code&gt;, and temporary files.&lt;/p&gt;

&lt;p&gt;We added &lt;code&gt;.dockerignore&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.git
.gitignore
__pycache__
.pytest_cache
.venv
tests
*.pyc
README.md
Dockerfile*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This trimmed build context overhead and ensured sensitive or extraneous files could never leak into the container.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Master Layer Caching (Order Matters!)
&lt;/h3&gt;

&lt;p&gt;Docker caches image layers. A layer is invalidated as soon as the files it depends on change.&lt;/p&gt;

&lt;p&gt;In our baseline:&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="c"&gt;# ❌ BAD: Edits to app.py invalidate pip install cache&lt;/span&gt;
&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;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In our optimized build:&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="c"&gt;# ✅ GOOD: Dependencies change rarely, application code changes frequently&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, during local development, editing &lt;code&gt;app.py&lt;/code&gt; results in a rebuild that finishes in &lt;strong&gt;under a second&lt;/strong&gt; because the heavy &lt;code&gt;pip install&lt;/code&gt; layer is pulled straight from cache!&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Adopt a Production WSGI Server (Gunicorn)
&lt;/h3&gt;

&lt;p&gt;Flask's built-in server warns you right in the logs:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"WARNING: This is a development server. Do not use it in a production deployment."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We added &lt;code&gt;gunicorn&lt;/code&gt; to &lt;code&gt;requirements.txt&lt;/code&gt; and updated our entrypoint:&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;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gunicorn gives us process management, worker concurrency, and resilient request handling without bloating image size.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Hardening Security — Run as Non-Root
&lt;/h3&gt;

&lt;p&gt;By default, Docker containers run as &lt;code&gt;root&lt;/code&gt;. If an attacker discovers a Remote Code Execution (RCE) vulnerability inside your app, they are root inside the container, making container breakout attacks significantly easier.&lt;/p&gt;

&lt;p&gt;We created an unprivileged system user and switched to it:&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;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash appuser

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;

&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can verify this directly on the running container:&lt;br&gt;
&lt;/p&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;docker-opt-optimized &lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;span class="c"&gt;# Output: appuser&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏆 The "After": Hardened &amp;amp; Optimized Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here is our final, production-ready &lt;code&gt;Dockerfile.optimized&lt;/code&gt;:&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; python:3.12-slim&lt;/span&gt;

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

&lt;span class="c"&gt;# 1. Leverage layer caching for dependencies&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# 2. Security: Create dedicated unprivileged user&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;useradd &lt;span class="nt"&gt;--create-home&lt;/span&gt; &lt;span class="nt"&gt;--shell&lt;/span&gt; /bin/bash appuser

&lt;span class="c"&gt;# 3. Copy application code with proper ownership&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --chown=appuser:appuser app.py .&lt;/span&gt;

&lt;span class="c"&gt;# 4. Drop root privileges&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser &lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="c"&gt;# 5. Production WSGI server&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["gunicorn", "--bind", "0.0.0.0:5000", "app:app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Total lines: 18. Clean, readable, and lightning-fast.&lt;/p&gt;




&lt;h2&gt;
  
  
  📊 The Scorecard: Baseline vs. Optimized
&lt;/h2&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;Baseline&lt;/th&gt;
&lt;th&gt;Optimized&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base Image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python:3.12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python:3.12-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Streamlined&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Content Size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;442 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56.6 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-385.4 MB (-87.2%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uncompressed Disk&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1.75 GB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;256 MB&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;-1.49 GB (-85.4%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Extra OS Tools&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt; (~74 MB)&lt;/td&gt;
&lt;td&gt;Zero&lt;/td&gt;
&lt;td&gt;Clean runtime&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Layer Caching&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Broken on every commit&lt;/td&gt;
&lt;td&gt;Optimized&lt;/td&gt;
&lt;td&gt;Instant rebuilds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;User&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;root&lt;/code&gt; (UID 0)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;appuser&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Least privilege&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Web Server&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Dev server&lt;/td&gt;
&lt;td&gt;Gunicorn WSGI&lt;/td&gt;
&lt;td&gt;Production ready&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🥊 Real-World Gotchas &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Optimization isn't just about shaving megabytes in a spreadsheet—here are real obstacles encountered during the project:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The "Host Port Already in Use" Trap
&lt;/h3&gt;

&lt;p&gt;When launching 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;docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 myapp:optimized
&lt;span class="c"&gt;# Error: bind: address already in use&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On macOS, port &lt;code&gt;5000&lt;/code&gt; is frequently taken by the OS AirPlay Receiver service.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Understand Docker's port mapping format (&lt;code&gt;HOST_PORT:CONTAINER_PORT&lt;/code&gt;).&lt;br&gt;&lt;br&gt;
We mapped &lt;code&gt;-p 5001:5000&lt;/code&gt;, letting the internal app stay on &lt;code&gt;5000&lt;/code&gt; while exposing it cleanly on host port &lt;code&gt;5001&lt;/code&gt;.&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;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; docker-opt-optimized &lt;span class="nt"&gt;-p&lt;/span&gt; 5001:5000 docker-image-optimization:optimized
curl http://localhost:5001/health
&lt;span class="c"&gt;# {"status":"healthy"}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. "Disk Usage" vs "Content Size"
&lt;/h3&gt;

&lt;p&gt;When running &lt;code&gt;docker images&lt;/code&gt;, Docker may report one size, while &lt;code&gt;docker system df -v&lt;/code&gt; or registry push reports another.  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Content Size:&lt;/strong&gt; The compressed size of layers transferred across networks/registries (56.6 MB).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk Usage:&lt;/strong&gt; The uncompressed layer footprint unpacked on the host filesystem (256 MB vs 1.75 GB).
Always use consistent metrics when publishing benchmarks!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Never Optimize Without Validation
&lt;/h3&gt;

&lt;p&gt;An image with 0 MB size that crashes is useless. After trimming the image, always test both endpoints and run automated test suites:&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;# Automated tests via pytest&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/test_app.py ..                             [100%]
====================== 2 passed in 0.08s =======================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Quick Docker Optimization Checklist for Your Projects
&lt;/h2&gt;

&lt;p&gt;Save this checklist for your next Dockerfile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Use &lt;code&gt;-slim&lt;/code&gt; or minimal official base images.&lt;/li&gt;
&lt;li&gt;[ ] Maintain a &lt;code&gt;.dockerignore&lt;/code&gt; containing &lt;code&gt;.git&lt;/code&gt;, caches, virtual environments, and tests.&lt;/li&gt;
&lt;li&gt;[ ] Copy &lt;code&gt;requirements.txt&lt;/code&gt; / &lt;code&gt;package.json&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; copying application code.&lt;/li&gt;
&lt;li&gt;[ ] Remove &lt;code&gt;curl&lt;/code&gt;, &lt;code&gt;vim&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt;, and build tools from final production images.&lt;/li&gt;
&lt;li&gt;[ ] Use &lt;code&gt;--no-cache-dir&lt;/code&gt; (Python) or &lt;code&gt;--no-cache&lt;/code&gt; / clean commands when installing dependencies.&lt;/li&gt;
&lt;li&gt;[ ] Create and switch to a non-root &lt;code&gt;USER&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;[ ] Replace development servers with production application servers (Gunicorn, Uvicorn, Nginx).&lt;/li&gt;
&lt;li&gt;[ ] Validate image behavior with health checks and unit tests.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💬 Over to You!
&lt;/h2&gt;

&lt;p&gt;Have you ever inspected your production Docker images with &lt;code&gt;docker history&lt;/code&gt; and found unexpected surprises? What's your favorite trick for shrinking containers? Drop your thoughts in the comments below! 👇&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
      <category>containers</category>
    </item>
    <item>
      <title>Building a Multi-Container App with Docker Compose: 10 Real-World Gotchas and Lessons Learned</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 08 Sep 2026 21:07:27 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/building-a-multi-container-app-with-docker-compose-10-real-world-gotchas-and-lessons-learned-10j</link>
      <guid>https://dev.to/alanvarghese-dev/building-a-multi-container-app-with-docker-compose-10-real-world-gotchas-and-lessons-learned-10j</guid>
      <description>&lt;p&gt;When you read basic Docker Compose tutorials, orchestrating a multi-tier application looks effortless: write a quick &lt;code&gt;docker-compose.yml&lt;/code&gt;, run &lt;code&gt;docker-compose up&lt;/code&gt;, and everything just magically talks to each other.&lt;/p&gt;

&lt;p&gt;Then you actually build a full-stack project with &lt;strong&gt;Nginx (Frontend)&lt;/strong&gt;, &lt;strong&gt;Flask (Backend API)&lt;/strong&gt;, and &lt;strong&gt;PostgreSQL (Database)&lt;/strong&gt; inside a modern containerized environment like &lt;strong&gt;DevPod / Dev Containers&lt;/strong&gt;—and reality sets in.&lt;/p&gt;

&lt;p&gt;Suddenly, you're wrestling with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker socket permission errors inside your dev container.&lt;/li&gt;
&lt;li&gt;Host port vs. container port confusion across multiple environments.&lt;/li&gt;
&lt;li&gt;Browser CORS errors on requests that returned &lt;code&gt;HTTP 200&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Database connections reporting &lt;code&gt;"disconnected"&lt;/code&gt; despite PostgreSQL running fine.&lt;/li&gt;
&lt;li&gt;Containers starting before the database is actually ready to accept queries.&lt;/li&gt;
&lt;li&gt;Data disappearing because of a single careless CLI flag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this post, I will walk you through the architecture of a resilient multi-container web application and break down &lt;strong&gt;10 critical bugs, troubleshooting workflows, and practical lessons&lt;/strong&gt; learned while building it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ The Application Architecture
&lt;/h2&gt;

&lt;p&gt;The stack consists of three isolated services running on an internal Docker bridge network:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Host Machine (Browser / curl)
                                     |
               +---------------------+---------------------+
               |                                           |
               | http://localhost:8081                     | http://localhost:5002
               v                                           v
       +---------------+                           +---------------+
       |   Frontend    |  (Client-side Fetch)      |    Backend    |
       |  Nginx Alpine |--------------------------&amp;gt;|  Flask (Py3)  |
       |  (Port: 80)   |                           |  (Port: 5000) |
       +---------------+                           +-------+-------+
                                                           |
                                                           | database:5432
                                                           v
                                                   +---------------+
                                                   |   Database    |
                                                   | PostgreSQL 16 |
                                                   |  (Port: 5432) |
                                                   +-------+-------+
                                                           |
                                                           v
                                                  [ postgres_data ]
                                                   (Named Volume)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Port Mappings at a Glance
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Container Port&lt;/th&gt;
&lt;th&gt;Host Port&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Frontend&lt;/strong&gt; (&lt;code&gt;Nginx&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;80&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;8081&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Serves static HTML/JS UI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Backend&lt;/strong&gt; (&lt;code&gt;Flask&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5000&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5002&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;REST API (&lt;code&gt;/health&lt;/code&gt;, &lt;code&gt;/db-health&lt;/code&gt;, &lt;code&gt;/users&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Database&lt;/strong&gt; (&lt;code&gt;Postgres&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5432&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;em&gt;Internal&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;Data persistence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  💥 10 Real-World Gotchas &amp;amp; Lessons Learned
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The DevPod / Docker Socket Permission Trap
&lt;/h3&gt;

&lt;p&gt;When developing inside a containerized dev environment (like DevPod or VS Code Dev Containers) that mounts the host Docker socket (&lt;code&gt;/var/run/docker.sock&lt;/code&gt;), running &lt;code&gt;docker ps&lt;/code&gt; can throw this classic roadblock:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Why It Happens
&lt;/h4&gt;

&lt;p&gt;The non-root container user (&lt;code&gt;vscode&lt;/code&gt;) doesn't belong to the group that owns the mounted socket file on the host.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Wrong Fix vs. The Right Fix
&lt;/h4&gt;

&lt;p&gt;❌ &lt;strong&gt;Don't do:&lt;/strong&gt; &lt;code&gt;sudo chmod 777 /var/run/docker.sock&lt;/code&gt;. Changing permissions on the host socket punches a major security hole into your host daemon.&lt;br&gt;&lt;br&gt;
✅ &lt;strong&gt;Do:&lt;/strong&gt; Add the dev container user to the group ID matching &lt;code&gt;/var/run/docker.sock&lt;/code&gt;. In our &lt;code&gt;.devcontainer/Dockerfile&lt;/code&gt;:&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;RUN &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; root vscode
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Host Port vs. Container Port Confusion
&lt;/h3&gt;

&lt;p&gt;In our &lt;code&gt;docker-compose.yml&lt;/code&gt;, the backend configuration was:&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;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;5002:5000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This syntax always means: &lt;strong&gt;&lt;code&gt;&amp;lt;Host Port&amp;gt;:&amp;lt;Container Port&amp;gt;&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Flask listens on &lt;code&gt;0.0.0.0:5000&lt;/code&gt; &lt;strong&gt;inside&lt;/strong&gt; the container.&lt;/li&gt;
&lt;li&gt;Docker forwards connections from port &lt;code&gt;5002&lt;/code&gt; on the &lt;strong&gt;host machine&lt;/strong&gt; to port &lt;code&gt;5000&lt;/code&gt; in the container.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  The Trap
&lt;/h4&gt;

&lt;p&gt;Running &lt;code&gt;curl http://localhost:5002/health&lt;/code&gt; worked directly from the host Mac terminal, but failed inside the DevPod dev container. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; &lt;code&gt;localhost&lt;/code&gt; is scoped to your current network namespace:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On the host Mac: &lt;code&gt;localhost:5002&lt;/code&gt; reaches the published Docker port.&lt;/li&gt;
&lt;li&gt;Inside DevPod: &lt;code&gt;localhost&lt;/code&gt; is the DevPod container itself, which isn't listening on 5002!&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Always know which network namespace your command is executing in.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Frontend-to-Backend Port Mismatch
&lt;/h3&gt;

&lt;p&gt;Our frontend static &lt;code&gt;index.html&lt;/code&gt; originally had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5000/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When opened in the browser at &lt;code&gt;http://localhost:8081&lt;/code&gt;, clicking the button failed with a connection error.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why?&lt;/strong&gt; The browser is running on the host machine. The backend isn't exposed on &lt;code&gt;localhost:5000&lt;/code&gt; on the host; it was published on port &lt;code&gt;5002&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Fix
&lt;/h4&gt;

&lt;p&gt;Update the browser fetch call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://localhost:5002/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. The CORS Paradox: When HTTP 200 Still Fails
&lt;/h3&gt;

&lt;p&gt;Once the port was corrected, the network tab showed HTTP &lt;code&gt;200 OK&lt;/code&gt;, but the browser console threw this error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at http://localhost:5002/health. 
(Reason: CORS header 'Access-Control-Allow-Origin' missing). Status code: 200.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Insight
&lt;/h4&gt;

&lt;p&gt;An HTTP status of &lt;code&gt;200 OK&lt;/code&gt; means the backend &lt;strong&gt;successfully received and processed the request&lt;/strong&gt;. However, because the frontend is served from &lt;code&gt;http://localhost:8081&lt;/code&gt; and the backend is at &lt;code&gt;http://localhost:5002&lt;/code&gt;, they are &lt;strong&gt;different origins&lt;/strong&gt; (different ports = different origins). The browser's Same-Origin Policy blocks client-side JavaScript from reading the response unless the backend explicitly provides CORS headers.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Fix
&lt;/h4&gt;

&lt;p&gt;Install &lt;code&gt;flask-cors&lt;/code&gt; in &lt;code&gt;backend/requirements.txt&lt;/code&gt; and wrap the Flask app:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask_cors&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;CORS&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&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="nc"&gt;CORS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. The Phantom Disconnection: Environment Variable Misalignment
&lt;/h3&gt;

&lt;p&gt;Testing the database health check endpoint (&lt;code&gt;curl http://localhost:5002/db-health&lt;/code&gt;) returned:&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="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"disconnected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fe_sendauth: no password supplied"&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;Yet running a manual test using &lt;code&gt;docker exec&lt;/code&gt; worked seamlessly:&lt;br&gt;
&lt;/p&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; multi-container-backend python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"
import psycopg2
conn = psycopg2.connect(host='database', dbname='appdb', user='appuser', password='apppassword')
print('Connected!')
"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  The Culprit
&lt;/h4&gt;

&lt;p&gt;We inspected the environment using &lt;code&gt;docker-compose config&lt;/code&gt; and found:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;docker-compose.yml&lt;/code&gt; was injecting &lt;code&gt;DB_NAME&lt;/code&gt;, &lt;code&gt;DB_USER&lt;/code&gt;, &lt;code&gt;DB_PASSWORD&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Flask's &lt;code&gt;app.py&lt;/code&gt; was looking for &lt;code&gt;POSTGRES_DB&lt;/code&gt;, &lt;code&gt;POSTGRES_USER&lt;/code&gt;, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only &lt;code&gt;DB_HOST&lt;/code&gt; matched; the user and password fell back to non-matching defaults!&lt;/p&gt;

&lt;h4&gt;
  
  
  The Fix
&lt;/h4&gt;

&lt;p&gt;Always ensure your variable names match seamlessly across &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt;, and application source code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_db_connection&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;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_HOST&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;dbname&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_NAME&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;appdb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_USER&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;appuser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apppassword&lt;/span&gt;&lt;span class="sh"&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;
  
  
  6. Container-to-Container DNS vs. &lt;code&gt;localhost&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inside a container, &lt;code&gt;localhost&lt;/code&gt; refers strictly to that container.&lt;br&gt;
If your Flask backend tries to connect to &lt;code&gt;localhost:5432&lt;/code&gt;, it attempts to find PostgreSQL inside the Flask container and fails.&lt;/p&gt;
&lt;h4&gt;
  
  
  The Docker Solution
&lt;/h4&gt;

&lt;p&gt;Docker Compose automatically creates an internal bridge network and sets up DNS records using the service names:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Backend connects to &lt;code&gt;host="database"&lt;/code&gt;, port &lt;code&gt;5432&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Docker resolves &lt;code&gt;database&lt;/code&gt; directly to the PostgreSQL container IP.&lt;/li&gt;
&lt;/ul&gt;


&lt;h3&gt;
  
  
  7. Verifying True Data Persistence (and the Dangerous &lt;code&gt;-v&lt;/code&gt; Flag)
&lt;/h3&gt;

&lt;p&gt;A common mistake in Docker development is assuming your database data is safe without testing container destruction.&lt;/p&gt;

&lt;p&gt;To verify persistence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Insert a user:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:5002/users &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"name":"Alan Turing"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Stop and remove the database container:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker-compose stop database
   docker-compose &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Spin up a brand new container:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt; database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ol&gt;
&lt;li&gt;Query &lt;code&gt;/users&lt;/code&gt;: &lt;strong&gt;Alan Turing is still there!&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because we declared a named volume in &lt;code&gt;docker-compose.yml&lt;/code&gt;:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ &lt;strong&gt;Warning:&lt;/strong&gt; Never use &lt;code&gt;docker-compose down -v&lt;/code&gt; when testing persistence. The &lt;code&gt;-v&lt;/code&gt; flag deletes all named volumes, wiping out your database!&lt;/p&gt;




&lt;h3&gt;
  
  
  8. Health Checks: Running vs. Ready
&lt;/h3&gt;

&lt;p&gt;If service A depends on service B, using only &lt;code&gt;depends_on: [database]&lt;/code&gt; is not enough. Docker starts the backend as soon as the PostgreSQL container &lt;em&gt;process&lt;/em&gt; spawns—which is seconds before the database is actually initialized and accepting connections.&lt;/p&gt;

&lt;h4&gt;
  
  
  The Solution: Healthcheck + &lt;code&gt;condition: service_healthy&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;In &lt;code&gt;docker-compose.yml&lt;/code&gt;:&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;database&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;postgres:16&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;backend&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;./backend&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&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;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;python"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;import&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request;&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;urllib.request.urlopen('http://localhost:5000/health')"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, Docker delays starting Flask until &lt;code&gt;pg_isready&lt;/code&gt; returns exit code 0.&lt;/p&gt;




&lt;h3&gt;
  
  
  9. Pytest in Dev Environments vs. Docker Integration Tests
&lt;/h3&gt;

&lt;p&gt;When running &lt;code&gt;pytest&lt;/code&gt; in your local or dev-container environment, you might be tempted to test database endpoints directly. However:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your local Python interpreter isn't inside the Compose bridge network.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;database:5432&lt;/code&gt; won't resolve locally unless PostgreSQL's port is published to the host.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Strategy
&lt;/h4&gt;

&lt;p&gt;Keep your test tiers clear:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Unit/API Tests (Pytest)&lt;/strong&gt;: Test business logic and mock external network calls. Verify &lt;code&gt;/health&lt;/code&gt; via Flask's &lt;code&gt;test_client()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration Tests (cURL/HTTP client)&lt;/strong&gt;: Execute queries against the live, running Compose stack.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  10. The Systematic 6-Step Debugging Workflow
&lt;/h3&gt;

&lt;p&gt;When multi-container stacks misbehave, resist the urge to randomly change settings. Follow this deterministic sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check Status &amp;amp; Health&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check Logs&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker-compose logs backend
   docker-compose logs database
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check Resolved Configuration&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Catches 90% of missing &lt;code&gt;.env&lt;/code&gt; variable substitutions!)&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inspect Live Container Environment&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&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; multi-container-backend &lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;DB_
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Direct Endpoint Testing&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:5002/health
   curl &lt;span class="nt"&gt;-i&lt;/span&gt; http://localhost:5002/db-health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Browser Developer Tools&lt;/strong&gt;: Inspect the Network &amp;amp; Console tabs for CORS headers and origin mismatches.&lt;/li&gt;
&lt;/ol&gt;




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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Containers are disposable; volumes are permanent.&lt;/strong&gt; Treat containers as ephemeral compute instances.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service names are hostnames.&lt;/strong&gt; Inside the Docker network, use &lt;code&gt;database&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;, and &lt;code&gt;frontend&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;HTTP 200&lt;/code&gt; doesn't mean your frontend works.&lt;/strong&gt; Always account for CORS when frontend and backend run on different ports.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;depends_on&lt;/code&gt; needs &lt;code&gt;condition: service_healthy&lt;/code&gt;.&lt;/strong&gt; Never assume a running container is a ready service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;docker-compose config&lt;/code&gt; is your best friend.&lt;/strong&gt; Run it whenever environment variables behave unpredictably.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💬 Discussion
&lt;/h2&gt;

&lt;p&gt;Have you run into CORS surprises or database startup race conditions in Docker Compose? What is your favorite healthcheck pattern? Let me know in the comments below!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>python</category>
      <category>webdev</category>
    </item>
    <item>
      <title>From Code to Container: What Building &amp; Debugging My First Dockerized App Taught Me About Real-World DevOps</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 08 Sep 2026 21:03:58 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/from-code-to-container-what-building-debugging-my-first-dockerized-app-taught-me-about-2bmp</link>
      <guid>https://dev.to/alanvarghese-dev/from-code-to-container-what-building-debugging-my-first-dockerized-app-taught-me-about-2bmp</guid>
      <description>&lt;p&gt;We’ve all heard the timeless developer cliché:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"It works on my machine!"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
&lt;em&gt;"Well, then we'll ship your machine!"&lt;/em&gt;&lt;br&gt;&lt;br&gt;
...and that is practically how Docker was born.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Recently, I set out on a DevOps homelab project to containerize a Python Flask web application. On the surface, the tutorial playbook sounded deceptively simple: write an &lt;code&gt;app.py&lt;/code&gt;, slap together a &lt;code&gt;Dockerfile&lt;/code&gt;, run &lt;code&gt;docker build&lt;/code&gt;, run &lt;code&gt;docker run&lt;/code&gt;, and celebrate.&lt;/p&gt;

&lt;p&gt;Except in real-world DevOps, &lt;strong&gt;getting the app to run is only 20% of the battle&lt;/strong&gt;. &lt;/p&gt;

&lt;p&gt;The real learning happened when things broke: when Unix socket permissions locked me out inside &lt;strong&gt;DevPod&lt;/strong&gt;, when port conflicts greeted me on macOS, when containers exited unexpectedly, and when network interfaces refused to talk to each other.&lt;/p&gt;

&lt;p&gt;Here is a comprehensive breakdown of my journey, the architecture, the "aha!" moments, and the systematic troubleshooting mindset that completely changed how I look at containerization.&lt;/p&gt;




&lt;h2&gt;
  
  
  🛠️ The Architecture &amp;amp; The Environment
&lt;/h2&gt;

&lt;p&gt;Before writing any code, I wanted a modern, reproducible setup rather than polluting my personal macOS host machine.&lt;/p&gt;

&lt;p&gt;Here is the tech stack I used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Host Machine:&lt;/strong&gt; macOS (Apple Silicon / aarch64) running &lt;strong&gt;Docker Desktop&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Environment:&lt;/strong&gt; &lt;strong&gt;DevPod&lt;/strong&gt; (using Docker as its provider) with an Ubuntu 24.04 container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Toolchain Manager:&lt;/strong&gt; &lt;strong&gt;Mise&lt;/strong&gt; (managing Python, pipx, and Docker CLI)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Framework:&lt;/strong&gt; &lt;strong&gt;Flask&lt;/strong&gt; (Python 3.12)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Engine:&lt;/strong&gt; &lt;strong&gt;Docker&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  How the Pieces Fit Together
&lt;/h3&gt;

&lt;p&gt;Here is the mental model of how the host, DevPod, the Docker daemon, and the containerized app communicate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│ macOS Host (Docker Desktop Engine &amp;amp; Daemon)                 │
│                                                             │
│   ┌──────────────────────────────────────────────────────┐  │
│   │ DevPod Development Container (Ubuntu 24.04)          │  │
│   │   - Managed with Mise (Python 3.12, Docker CLI)      │  │
│   │   - Connected via /var/run/docker.sock bind mount     │  │
│   │   - Builds and controls containers                   │  │
│   └──────────────────────┬───────────────────────────────┘  │
│                          │ Controls via Docker API          │
│                          ▼                                  │
│   ┌──────────────────────────────────────────────────────┐  │
│   │ Application Container: containarize-web-app          │  │
│   │   - Python 3.12-slim base                            │  │
│   │   - Flask app listening on 0.0.0.0:5000              │  │
│   └──────────────────────▲───────────────────────────────┘  │
│                          │                                  │
│                 Port Mapping (-p 5001:5000)                 │
│                          │                                  │
│   Browser / Curl ────────┴───────────────────────────────   │
│   http://localhost:5001                                     │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🐍 1. The Application &amp;amp; The Dockerfile
&lt;/h2&gt;

&lt;p&gt;The web application itself is a minimal Flask service in &lt;code&gt;app.py&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;flask&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Flask&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Flask&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="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from my containerized application!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.0.0.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;host="0.0.0.0"&lt;/code&gt;. More on why that is critical in a moment!&lt;/p&gt;

&lt;p&gt;Next came the &lt;code&gt;Dockerfile&lt;/code&gt;:&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; python:3.12-slim&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; requirements.txt .&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

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

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  💡 Lesson 1: Build Order &amp;amp; Layer Caching Matter
&lt;/h3&gt;

&lt;p&gt;Notice that I didn’t just do &lt;code&gt;COPY . .&lt;/code&gt; at the top. &lt;/p&gt;

&lt;p&gt;Docker builds images in sequential, cacheable layers. By copying &lt;strong&gt;only&lt;/strong&gt; &lt;code&gt;requirements.txt&lt;/code&gt; and running &lt;code&gt;pip install&lt;/code&gt; &lt;strong&gt;before&lt;/strong&gt; copying &lt;code&gt;app.py&lt;/code&gt;, Docker caches the installed packages. If I tweak a single string or route inside &lt;code&gt;app.py&lt;/code&gt;, Docker re-uses the cached dependency layer and rebuilds in milliseconds instead of re-downloading packages every single time.&lt;/p&gt;

&lt;p&gt;I also added a &lt;code&gt;.dockerignore&lt;/code&gt; to ensure &lt;code&gt;.git&lt;/code&gt;, &lt;code&gt;__pycache__&lt;/code&gt;, and local &lt;code&gt;.venv&lt;/code&gt; folders never bloat the Docker build context.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ 2. The DevPod Challenge: The "Docker-in-Docker" Socket Mystery
&lt;/h2&gt;

&lt;p&gt;Because I was developing inside a DevPod devcontainer, running &lt;code&gt;docker info&lt;/code&gt; initially spat out a fatal error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What was happening?
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;Docker CLI&lt;/strong&gt; is just a client—a messenger. It doesn't actually build or run containers; the &lt;strong&gt;Docker Daemon&lt;/strong&gt; does. My devcontainer had the Docker CLI installed, but the daemon lived outside on my Mac (Docker Desktop).&lt;/p&gt;

&lt;p&gt;To fix this, I needed to bind-mount the Docker Unix socket in &lt;code&gt;.devcontainer/devcontainer.json&lt;/code&gt;:&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;"build"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dockerfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dockerfile"&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;span class="nl"&gt;"mounts"&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="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"&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;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;h3&gt;
  
  
  The Permissions Curveball: &lt;code&gt;srw-rw---- root root&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Once the socket was mounted, I ran &lt;code&gt;docker info&lt;/code&gt; again, only to be stopped by another error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;permission denied &lt;span class="k"&gt;while &lt;/span&gt;trying to connect to the Docker API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking the socket with &lt;code&gt;ls -la /var/run/docker.sock&lt;/code&gt; revealed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;srw-rw---- 1 root root 0 /var/run/docker.sock
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unix sockets obey standard Linux file permission bits. The socket was owned by &lt;code&gt;root:root&lt;/code&gt;, but DevPod runs as the non-root &lt;code&gt;vscode&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;Instead of doing an insecure &lt;code&gt;chmod 777&lt;/code&gt;, the clean fix was getting my active shell to recognize root/docker group privileges:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Immediately after running &lt;code&gt;newgrp root&lt;/code&gt;, &lt;code&gt;docker info&lt;/code&gt; connected cleanly, outputting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Server Version: 29.7.2
Operating System: Docker Desktop
Architecture: aarch64
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The bridge between DevPod and Docker Desktop was live! 🚀&lt;/p&gt;




&lt;h2&gt;
  
  
  🌐 3. Networking Lessons: The Tale of Two IP Addresses &amp;amp; Port Mapping
&lt;/h2&gt;

&lt;p&gt;Once my image was built with:&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 &lt;span class="nt"&gt;-t&lt;/span&gt; containarize-web-app &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;it was time to run the container. That’s when container networking fundamentals kicked in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why &lt;code&gt;0.0.0.0&lt;/code&gt; vs. &lt;code&gt;127.0.0.1&lt;/code&gt;?
&lt;/h3&gt;

&lt;p&gt;When developing locally, you usually bind to &lt;code&gt;127.0.0.1&lt;/code&gt; (localhost). But inside a container:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt; is the &lt;strong&gt;loopback interface of the container itself&lt;/strong&gt;. Any request originating from outside the container (even from the host through Docker's bridge) gets discarded at the door.&lt;/li&gt;
&lt;li&gt;Binding to &lt;code&gt;0.0.0.0&lt;/code&gt; tells Flask: &lt;em&gt;"Listen on all network interfaces inside this container."&lt;/em&gt; This allows traffic forwarded by Docker to actually reach the application.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Port Conflict: &lt;code&gt;5001:5000&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When I tried running:&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;--name&lt;/span&gt; containarize-web-app &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 containarize-web-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I got hit with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bind: address already in use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Port 5000 was already occupied on my macOS host (often taken by macOS AirPlay Receiver or another local daemon).&lt;/p&gt;

&lt;p&gt;Here is the beauty of Docker port mapping: &lt;strong&gt;You do not need to change your application code or container configuration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; syntax represents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-p &amp;lt;HOST_PORT&amp;gt;:&amp;lt;CONTAINER_PORT&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By changing the mapping to &lt;code&gt;-p 5001:5000&lt;/code&gt;, requests hitting my Mac on port &lt;code&gt;5001&lt;/code&gt; get transparently routed to port &lt;code&gt;5000&lt;/code&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;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; containarize-web-app &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 5001:5000 &lt;span class="se"&gt;\&lt;/span&gt;
  containarize-web-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🧪 4. Testing at Every Layer
&lt;/h2&gt;

&lt;p&gt;One huge habit I developed during this project was &lt;strong&gt;testing layer-by-layer&lt;/strong&gt; rather than guessing when something failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Test directly inside the container
&lt;/h3&gt;

&lt;p&gt;First, verify whether Flask is alive inside its own container boundary using &lt;code&gt;docker exec&lt;/code&gt;:&lt;br&gt;
&lt;/p&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;containarize-web-app &lt;span class="se"&gt;\&lt;/span&gt;
  python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:5000').read().decode())"&lt;/span&gt;
&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;Hello from my containerized application!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;(Flask is alive and answering on port 5000!)&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Test from the host via published port
&lt;/h3&gt;

&lt;p&gt;Now, test through Docker’s network port forwarder from the host terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:5001
&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;Hello from my containerized application!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And opening &lt;code&gt;http://localhost:5001&lt;/code&gt; in the browser yielded the same greeting. The end-to-end network chain was completely validated!&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 5. The Golden Rule of DevOps Debugging: Isolate the Layers
&lt;/h2&gt;

&lt;p&gt;When an error strikes in containerized environments, our natural instinct is often to frantically change five things at once: modify &lt;code&gt;app.py&lt;/code&gt;, change the Dockerfile, restart Docker Desktop, and tweak host settings.&lt;/p&gt;

&lt;p&gt;This project taught me a much more structured debugging ladder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1. Application Layer   ──&amp;gt; Does Python/Flask run locally? Check syntax &amp;amp; deps.
          ↓
 2. Container Lifecycle ──&amp;gt; Did the container start or crash? (docker ps -a, docker logs)
          ↓
 3. Process Binding     ──&amp;gt; Is Flask listening on 0.0.0.0 (not 127.0.0.1)?
          ↓
 4. Port Forwarding     ──&amp;gt; Is the host port conflicting? Is HOST:CONTAINER mapped right?
          ↓
 5. Daemon &amp;amp; Socket     ──&amp;gt; Is the Docker socket mounted and readable? (newgrp root)
          ↓
 6. Host Ingress        ──&amp;gt; Can curl or the browser reach localhost:HOST_PORT?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By moving down the ladder one rung at a time, finding the root cause becomes deterministic rather than a guessing game.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧰 My Essential Docker Toolkit
&lt;/h2&gt;

&lt;p&gt;Here are the commands I found myself relying on constantly:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker build -t &amp;lt;name&amp;gt; .&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds the image using the current directory context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker run -d --name &amp;lt;name&amp;gt; -p &amp;lt;host&amp;gt;:&amp;lt;container&amp;gt; &amp;lt;image&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Runs the container detached in the background&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker ps -a&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lists all containers, including stopped/exited ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker logs &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prints stdout/stderr logs from the container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker inspect &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspects IP addresses, network settings, and state&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker exec -it &amp;lt;container&amp;gt; sh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Drops you into an interactive shell inside the running container&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker rm -f &amp;lt;container&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops and removes a container in one shot&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🔮 What’s Next on the Roadmap?
&lt;/h2&gt;

&lt;p&gt;This project gave me solid operational foundations, but real-world production setups go even further. Here is what I plan to build next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Production WSGI Server:&lt;/strong&gt; Replace Flask’s development server with &lt;strong&gt;Gunicorn&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Hardening:&lt;/strong&gt; Add a non-root &lt;code&gt;USER appuser&lt;/code&gt; in the Dockerfile.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Container Healthchecks:&lt;/strong&gt; Add Docker &lt;code&gt;HEALTHCHECK&lt;/code&gt; instructions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Pipeline:&lt;/strong&gt; Automate builds and test suites with GitHub Actions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration:&lt;/strong&gt; Expand to multi-container setups using &lt;strong&gt;Docker Compose&lt;/strong&gt; and eventually deploy to &lt;strong&gt;Kubernetes&lt;/strong&gt;!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💬 Over to You!
&lt;/h2&gt;

&lt;p&gt;If you're starting your DevOps journey, what was the first weird Docker error that made you pull your hair out? Was it socket permissions, port collisions, or dangling volumes? &lt;/p&gt;

&lt;p&gt;Drop a comment below—I’d love to hear your container war stories! 👇&lt;/p&gt;

</description>
      <category>docker</category>
      <category>webdev</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>Zero to Published: Building an Automated Release Pipeline with GitHub Actions, Mise, and DevPod</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 08 Sep 2026 20:58:18 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/zero-to-published-building-an-automated-release-pipeline-with-github-actions-mise-and-devpod-2ji5</link>
      <guid>https://dev.to/alanvarghese-dev/zero-to-published-building-an-automated-release-pipeline-with-github-actions-mise-and-devpod-2ji5</guid>
      <description>&lt;p&gt;We've all been there: you finish implementing a new feature, run a quick manual check in your terminal, zip up the build directory by hand, navigate to GitHub, click &lt;strong&gt;Releases&lt;/strong&gt;, upload your zip file, and write a release note on the fly.&lt;/p&gt;

&lt;p&gt;Then, ten minutes later, a user opens an issue: &lt;em&gt;"The release package is missing a critical config file,"&lt;/em&gt; or &lt;em&gt;"The build fails on Python 3.12 because of an untracked dependency mismatch."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Manual releases are slow, error-prone, and painful to reproduce. In modern software engineering, &lt;strong&gt;every step between writing code and shipping a release should be automated, deterministic, and protected by quality gates.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this article, I'll walk you through how I built an end-to-end automated release pipeline for a Python project hosted inside a multi-project monorepo (&lt;code&gt;alanvarghese-dev/lab&lt;/code&gt;). We’ll cover:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic Development Environments&lt;/strong&gt; using &lt;strong&gt;DevPod&lt;/strong&gt; and &lt;strong&gt;Mise&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quality Gates&lt;/strong&gt; with &lt;strong&gt;Ruff&lt;/strong&gt; (linting) and &lt;strong&gt;Pytest&lt;/strong&gt; (unit testing).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD Pipeline Orchestration&lt;/strong&gt; using &lt;strong&gt;GitHub Actions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Release Packaging &amp;amp; Publishing&lt;/strong&gt; triggered seamlessly by Git tags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-World Gotchas&lt;/strong&gt; encountered along the way (and how to solve them).&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture &amp;amp; Monorepo Layout
&lt;/h2&gt;

&lt;p&gt;Before jumping into the code, let’s look at the structure. This project resides as a subproject within a larger DevOps lab repository (&lt;code&gt;alanvarghese-dev/lab&lt;/code&gt;). This setup allows multiple DevOps experiments and pipelines to coexist without cluttering separate repositories.&lt;/p&gt;

&lt;p&gt;Here is how the repository is structured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/workspaces/ (Repository Root)
├── .github/
│   └── workflows/
│       └── automated-release.yml      # Root CI/CD workflow
│
└── automated-release-pipeline/        # Subproject Directory
    ├── .devcontainer/
    │   ├── Dockerfile                 # DevPod container specification
    │   └── .devcontainer.json         # DevContainer build context
    ├── tests/
    │   └── test_app.py                # Automated Pytest suite
    ├── app.py                         # Core application entrypoint
    ├── mise.toml                      # Declarative tool &amp;amp; version manager
    ├── pytest.ini                     # Pytest discovery &amp;amp; module search path
    ├── .gitignore                     # Clean repository boundaries
    └── dist/
        └── automated-release-pipeline.tar.gz  # Generated release artifact
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💡 &lt;strong&gt;Monorepo Tip:&lt;/strong&gt; Because the Git root is at &lt;code&gt;/workspaces&lt;/code&gt; while our application lives under &lt;code&gt;/workspaces/automated-release-pipeline/&lt;/code&gt;, our GitHub Actions workflow is defined at the root &lt;code&gt;.github/workflows/&lt;/code&gt; directory, while workflow steps target the subproject folder.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  📦 Step 1: Isolated Environment with DevPod &amp;amp; Mise
&lt;/h2&gt;

&lt;p&gt;One of the biggest friction points in developer velocity is the classic &lt;em&gt;"works on my machine"&lt;/em&gt; problem. To achieve 100% parity between local development and CI runners, we combine &lt;strong&gt;DevPod&lt;/strong&gt; (containerized development) with &lt;strong&gt;Mise&lt;/strong&gt; (modern polyglot tool manager).&lt;/p&gt;

&lt;h3&gt;
  
  
  1. DevPod Configuration
&lt;/h3&gt;

&lt;p&gt;Using a DevContainer specification based on Ubuntu 24.04, we bake &lt;code&gt;mise&lt;/code&gt; directly into the container image:&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="c"&gt;# .devcontainer/Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/devcontainers/base:ubuntu-24.04&lt;/span&gt;

&lt;span class="c"&gt;# Copy Mise binary directly from official image&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=jdxcode/mise /usr/local/bin/mise /usr/local/bin/&lt;/span&gt;

&lt;span class="c"&gt;# Auto-activate Mise in interactive shells&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate bash)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.bashrc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And configure the DevContainer manifest:&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.devcontainer/devcontainer.json&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;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"automated-release-pipeline"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dockerfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dockerfile"&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;span class="nl"&gt;"remoteUser"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vscode"&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;h3&gt;
  
  
  2. Declarative Tooling with &lt;code&gt;mise.toml&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Instead of relying on whatever Python version is globally installed on your host OS, &lt;code&gt;mise.toml&lt;/code&gt; locks down the exact runtimes, package managers, and CLI tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="c"&gt;# mise.toml&lt;/span&gt;
&lt;span class="nn"&gt;[tools]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"3.12"&lt;/span&gt;
&lt;span class="py"&gt;pipx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;"pipx:pytest"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;"pipx:ruff"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With one command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Mise downloads and configures Python 3.12, Pytest, and Ruff in an isolated sandbox.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Step 2: Application Code &amp;amp; Quality Gates
&lt;/h2&gt;

&lt;p&gt;To validate the release workflow, we have a clean Python application with corresponding unit tests and linter configurations.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Application (&lt;code&gt;app.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2 + 3 = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;add&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Unit Testing with Pytest (&lt;code&gt;tests/test_app.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/test_app.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add_negative_numbers&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Configuring Pytest (&lt;code&gt;pytest.ini&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;When tests reside in a subdirectory (&lt;code&gt;tests/&lt;/code&gt;) separate from the root source files, Python's import engine needs to know where to find top-level modules. We declare this cleanly in &lt;code&gt;pytest.ini&lt;/code&gt;:&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="c"&gt;# pytest.ini
&lt;/span&gt;&lt;span class="nn"&gt;[pytest]&lt;/span&gt;
&lt;span class="py"&gt;testpaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
&lt;span class="py"&gt;pythonpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. Local Verification Commands
&lt;/h3&gt;

&lt;p&gt;Before pushing any commit, we verify our quality gates locally using &lt;code&gt;mise exec&lt;/code&gt;:&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;# 1. Run the application&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; python app.py

&lt;span class="c"&gt;# 2. Run Ruff linter (code quality)&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; ruff check &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# 3. Run Pytest suite&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚡ Step 3: CI/CD Pipeline Orchestration with GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Now comes the core automation engine. We want GitHub Actions to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trigger automatically whenever a release tag (e.g. &lt;code&gt;v*&lt;/code&gt;) or commit is pushed.&lt;/li&gt;
&lt;li&gt;Spin up Mise and install our pinned toolset.&lt;/li&gt;
&lt;li&gt;Enforce &lt;strong&gt;Quality Gates&lt;/strong&gt; (Ruff + Pytest). If either fails, immediately halt the pipeline.&lt;/li&gt;
&lt;li&gt;Package the application assets into a clean &lt;code&gt;.tar.gz&lt;/code&gt; distribution archive.&lt;/li&gt;
&lt;li&gt;Upload the artifact to the GitHub workflow run.&lt;/li&gt;
&lt;li&gt;Publish a formal &lt;strong&gt;GitHub Release&lt;/strong&gt; with auto-generated changelogs and the release tarball attached.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  The Pipeline Architecture
&lt;/h3&gt;



&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["Developer pushes Git Tag (e.g., v1.0.0)"] --&amp;gt; B["GitHub Actions Triggered"]
    B --&amp;gt; C["Checkout Repo (actions/checkout@v4)"]
    C --&amp;gt; D["Setup Mise (jdx/mise-action@v3)"]
    D --&amp;gt; E["Install Dependencies (mise install)"]
    E --&amp;gt; F["Lint Gate (mise exec -- ruff check .)"]
    F --&amp;gt; G["Test Gate (mise exec -- pytest)"]
    G --&amp;gt; H["Build Release Archive (dist/automated-release-pipeline.tar.gz)"]
    H --&amp;gt; I["Upload CI Artifact (actions/upload-artifact@v4)"]
    I --&amp;gt; J["Publish GitHub Release (softprops/action-gh-release@v2)"]
    J --&amp;gt; K["🎉 Release Published with Tarball Attached"]

    F -.-&amp;gt;|Linter Error| L["❌ Halt Pipeline"]
    G -.-&amp;gt;|Test Failure| L&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  The GitHub Actions Workflow File (&lt;code&gt;.github/workflows/automated-release.yml&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Automated Release Pipeline&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
    &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;v*'&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;validate-and-release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./automated-release-pipeline&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout Repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Mise&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdx/mise-action@v3&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Tools via Mise&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise install&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Code Quality Check (Ruff)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise exec -- ruff check .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Test Suite (Pytest)&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise exec -- pytest&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Package Release Tarball&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;startsWith(github.ref, 'refs/tags/v')&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p dist&lt;/span&gt;
          &lt;span class="s"&gt;tar -czvf dist/automated-release-pipeline.tar.gz app.py mise.toml pytest.ini&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload Build Artifact to Workflow Run&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;startsWith(github.ref, 'refs/tags/v')&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/upload-artifact@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;automated-release-pipeline-dist&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;automated-release-pipeline/dist/automated-release-pipeline.tar.gz&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create GitHub Release&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;startsWith(github.ref, 'refs/tags/v')&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;softprops/action-gh-release@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;files&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;automated-release-pipeline/dist/automated-release-pipeline.tar.gz&lt;/span&gt;
          &lt;span class="na"&gt;generate_release_notes&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;h2&gt;
  
  
  🏷️ Step 4: The Tag-Driven Release Flow in Action
&lt;/h2&gt;

&lt;p&gt;With the workflow in place, cutting a new release is completely frictionless. Here is the exact end-to-end developer workflow:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Test and Stage Locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Verify all quality checks pass&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
mise &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; pytest

&lt;span class="c"&gt;# Stage and commit your changes&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: complete automated release pipeline setup"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Cut and Push an Annotated Release Tag
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create an annotated Git tag&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v1.0.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release v1.0.0"&lt;/span&gt;

&lt;span class="c"&gt;# Explicitly push the tag reference&lt;/span&gt;
git push origin refs/tags/v1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. The Result
&lt;/h3&gt;

&lt;p&gt;Once pushed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Actions detects &lt;code&gt;refs/tags/v1.0.0&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Tools are provisioned via &lt;code&gt;jdx/mise-action&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ruff and Pytest execute in seconds.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dist/automated-release-pipeline.tar.gz&lt;/code&gt; is built and uploaded.&lt;/li&gt;
&lt;li&gt;A GitHub Release &lt;code&gt;v1.0.0&lt;/code&gt; is published with the tarball attached and release notes automatically generated from commit history!&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🐛 Troubleshooting &amp;amp; War Stories
&lt;/h2&gt;

&lt;p&gt;Real engineering is never without bumps. Here are three common issues encountered during this project and how to fix them:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Pytest &lt;code&gt;ModuleNotFoundError: No module named 'app'&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Running &lt;code&gt;pytest&lt;/code&gt; from the root directory failed to import &lt;code&gt;app.py&lt;/code&gt; inside &lt;code&gt;tests/test_app.py&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; By default, pytest does not always add the current working directory to &lt;code&gt;sys.path&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Create a &lt;code&gt;pytest.ini&lt;/code&gt; file in the project root containing:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;  &lt;span class="nn"&gt;[pytest]&lt;/span&gt;
  &lt;span class="py"&gt;testpaths&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;tests&lt;/span&gt;
  &lt;span class="py"&gt;pythonpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Git Tag Push Error: &lt;code&gt;src refspec v1.0.0 does not match any&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Running &lt;code&gt;git push origin v1.0.0&lt;/code&gt; failed with a refspec mismatch error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; The tag had not been created as an annotated tag or was ambiguous in local refs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Verify the local tag exists with &lt;code&gt;git tag&lt;/code&gt;, create an annotated tag with &lt;code&gt;git tag -a v1.0.0 -m "Release v1.0.0"&lt;/code&gt;, and push explicitly using the full reference path:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  git push origin refs/tags/v1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Monorepo Path Context in GitHub Actions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Problem:&lt;/strong&gt; Actions failed when looking for &lt;code&gt;mise.toml&lt;/code&gt; at the repository root.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; The pipeline is inside &lt;code&gt;/workspaces/automated-release-pipeline/&lt;/code&gt;, but GitHub Actions defaults to the root of the Git repo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Set &lt;code&gt;defaults.run.working-directory: ./automated-release-pipeline&lt;/code&gt; in the GitHub Actions job configuration so that every &lt;code&gt;run&lt;/code&gt; step executes in the correct subdirectory.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔮 What's Next? (Future Roadmap)
&lt;/h2&gt;

&lt;p&gt;While this pipeline is rock solid for our Python application, here are great next-level enhancements for enterprise production systems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;strong&gt;Docker Image Builds &amp;amp; Container Registry&lt;/strong&gt;: Package the application into a minimal Docker container and push to GitHub Container Registry (GHCR).&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Automated Semantic Versioning&lt;/strong&gt;: Incorporate tools like &lt;code&gt;semantic-release&lt;/code&gt; or &lt;code&gt;commitizen&lt;/code&gt; to parse Conventional Commits and auto-bump versions without manual tagging.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Security Scanning (SAST)&lt;/strong&gt;: Add tools like Bandit and Trivy to scan dependencies and source code for CVEs during CI.&lt;/li&gt;
&lt;li&gt;[ ] &lt;strong&gt;Deployment Environments&lt;/strong&gt;: Add deployment stages (Dev -&amp;gt; Staging -&amp;gt; Prod) with approval gates using GitHub Environments.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;A release pipeline is much more than a simple build script. A true production-ready release pipeline connects:&lt;/p&gt;

&lt;p&gt;$$\text{Source Control} \longrightarrow \text{Tool Management} \longrightarrow \text{Quality Gates} \longrightarrow \text{Artifact Packaging} \longrightarrow \text{Distribution}$$&lt;/p&gt;

&lt;p&gt;By combining &lt;strong&gt;DevPod&lt;/strong&gt; for containerized dev environments, &lt;strong&gt;Mise&lt;/strong&gt; for unified tooling, &lt;strong&gt;Pytest &amp;amp; Ruff&lt;/strong&gt; for zero-defect quality gates, and &lt;strong&gt;GitHub Actions&lt;/strong&gt; for tag-driven releases, we eliminated manual errors and turned releases into a one-command celebration. 🎉&lt;/p&gt;




&lt;p&gt;💬 &lt;strong&gt;How do you manage releases in your projects? Are you using Mise, ASDF, or Docker for tool versioning? Let's discuss in the comments below!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>cicd</category>
      <category>githubactions</category>
      <category>git</category>
    </item>
    <item>
      <title>Building a Bulletproof Python CI Pipeline with GitHub Actions, DevPod &amp; Mise (+ the Real-World Bugs We Squashed Along the Way)</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 01 Sep 2026 16:07:35 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/building-a-bulletproof-python-ci-pipeline-with-github-actions-devpod-mise-the-real-world-bugs-1dog</link>
      <guid>https://dev.to/alanvarghese-dev/building-a-bulletproof-python-ci-pipeline-with-github-actions-devpod-mise-the-real-world-bugs-1dog</guid>
      <description>&lt;p&gt;A complete guide to building reproducible, containerized Python CI pipelines using DevPod, Mise, Ruff, pytest, and GitHub Actions—featuring hard-won lessons from real debugging sessions.&lt;/p&gt;

&lt;p&gt;Have you ever uttered the classic developer refrain: &lt;strong&gt;"Well, it works on my machine!"&lt;/strong&gt;? &lt;/p&gt;

&lt;p&gt;We have all been there. You write a clean Python script, write a few unit tests that pass locally, push to GitHub, and immediately get greeted by a glowing red ❌ in your CI pipeline. Or even worse: your CI passes with flying colors, only for production to blow up because your CI runner was secretly masking environment mismatches.&lt;/p&gt;

&lt;p&gt;In this guide, we will walk through building an end-to-end, production-ready Continuous Integration (CI) pipeline for a Python project from scratch. We combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🐳 &lt;strong&gt;DevPod &amp;amp; Docker&lt;/strong&gt;: For fully isolated, reproducible local container environments.&lt;/li&gt;
&lt;li&gt;⚙️ &lt;strong&gt;Mise-en-place (Mise)&lt;/strong&gt;: For declarative, deterministic tool versioning across local and CI.&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Ruff&lt;/strong&gt;: For blazingly fast linting and auto-formatting.&lt;/li&gt;
&lt;li&gt;🧪 &lt;strong&gt;pytest&lt;/strong&gt;: For rock-solid unit testing.&lt;/li&gt;
&lt;li&gt;🚀 &lt;strong&gt;GitHub Actions&lt;/strong&gt;: For automated quality gates on every push.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;More importantly, rather than showing only the "happy path," we will dissect the &lt;strong&gt;real-world edge cases and debugging hurdles&lt;/strong&gt; encountered along the way—including Python import path traps, monorepo workflow discovery quirks, toolchain vs. interpreter disconnects, and why you should always &lt;strong&gt;deliberately break your CI&lt;/strong&gt; to verify its integrity.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏛 The Architecture: How It All Fits Together
&lt;/h2&gt;

&lt;p&gt;A robust development lifecycle requires a clear separation of concerns. Developer ergonomics shouldn't bleed into project dependencies, and local environments should mirror CI as closely as possible.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;+-------------------------------------------------------------------------+
|                       LOCAL DEVELOPMENT (DevPod)                        |
|                                                                         |
|  Developer  ──&amp;gt;  DevPod / Docker Container                              |
|                         ├── Dotfiles (Personal Shell / Theme)           |
|                         └── Mise (Declared Tools in mise.toml)          |
|                                  ├── Python 3.12+                       |
|                                  ├── Ruff (Linter/Formatter)            |
|                                  └── pytest (Test Runner)               |
|                                                                         |
|  Local Quality Checks: ruff check .  &amp;amp;&amp;amp;  pytest -vv                     |
+-------------------------------------------------------------------------+
                                   │
                               git push
                                   ▼
+-------------------------------------------------------------------------+
|                       REMOTE CI (GitHub Actions)                        |
|                                                                         |
|  GitHub Actions Runner (Ubuntu Latest)                                  |
|         ├── 1. Checkout Repository                                      |
|         ├── 2. Install Mise (`jdx/mise-action`)                         |
|         ├── 3. `mise install` (Identical Tool Versions)                 |
|         ├── 4. Run Ruff (`mise exec -- ruff check .`)                   |
|         └── 5. Run pytest (`mise exec -- pytest -vv`)                   |
|                                                                         |
|  CI Verdict: PASS ✅ or FAIL ❌                                          |
+-------------------------------------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Separation of Concerns
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Responsibility&lt;/th&gt;
&lt;th&gt;Key Files&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dotfiles&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Developer ergonomics, shell prompt, aliases, personal editor config&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.zshrc&lt;/code&gt;, &lt;code&gt;.bashrc&lt;/code&gt;, personal Git config&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dev Container&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS-level base image, container runtime&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Dockerfile&lt;/code&gt;, &lt;code&gt;.devcontainer.json&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Mise&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Language versions, linters, package runners&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mise.toml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Application&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Business logic, module definitions&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;src/&lt;/code&gt;, &lt;code&gt;pytest.ini&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Test Suite&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Unit tests, edge case assertions&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tests/&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI / CD&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Automated validation, branch protection rules&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.github/workflows/github-actions-ci.yml&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📂 1. Setting Up the Project Structure
&lt;/h2&gt;

&lt;p&gt;Let's start with a clean directory layout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;github-actions-devpod-ci/
├── .devcontainer.json         # Dev container configuration
├── Dockerfile                 # Container image definition with Mise
├── .gitignore                 # Cache &amp;amp; environment ignores
├── mise.toml                  # Declarative tool specifications
├── pytest.ini                 # Python path configuration
├── src/
│   ├── __init__.py
│   └── main.py                # Application logic
└── tests/
    └── test_main.py           # Automated unit tests
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Application Code (&lt;code&gt;src/main.py&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;A simple arithmetic module with explicit error handling:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;b&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="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot divide by zero&lt;/span&gt;&lt;span class="sh"&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;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GitHub Actions DevPod CI Demo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2 + 3 =&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;add&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Test Suite (&lt;code&gt;tests/test_main.py&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subtract&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_add&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_subtract&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_multiply&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_divide&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_divide_by_zero&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&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="k"&gt;assert&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠 2. Deterministic Tool Management with Mise
&lt;/h2&gt;

&lt;p&gt;Instead of relying on whatever version of Python or linters happen to be installed on your host machine, we use &lt;strong&gt;&lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;Mise&lt;/a&gt;&lt;/strong&gt; to declare exact tooling requirements in &lt;code&gt;mise.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tools]&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;"pipx:ruff"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;"pipx:pytest"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;pipx&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Why Mise + pipx?
&lt;/h3&gt;

&lt;p&gt;Mise allows you to manage programming language runtimes (like &lt;code&gt;asdf&lt;/code&gt; or &lt;code&gt;nvm&lt;/code&gt;) and standalone CLI tools (via &lt;code&gt;pipx&lt;/code&gt;, &lt;code&gt;cargo&lt;/code&gt;, &lt;code&gt;npm&lt;/code&gt;, or standalone binaries) in a single declarative configuration file.&lt;/p&gt;

&lt;p&gt;When a developer joins your project or the CI runner fires up, running:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;installs everything in one shot. No manual virtualenv activation dance, no missing binary surprises.&lt;/p&gt;




&lt;h2&gt;
  
  
  🐳 3. Local Development with DevPod &amp;amp; Dev Containers
&lt;/h2&gt;

&lt;p&gt;To ensure every developer works in a clean, reproducible container, we define a lightweight &lt;code&gt;Dockerfile&lt;/code&gt; and &lt;code&gt;.devcontainer.json&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;Dockerfile&lt;/code&gt;
&lt;/h3&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; mcr.microsoft.com/devcontainers/base:ubuntu-24.04&lt;/span&gt;

&lt;span class="c"&gt;# Install Mise binary&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=jdxcode/mise /usr/local/bin/mise /usr/local/bin/&lt;/span&gt;

&lt;span class="c"&gt;# Activate Mise automatically in both bash and zsh shells&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate bash)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.bashrc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;.devcontainer.json&lt;/code&gt;
&lt;/h3&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;"build"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dockerfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dockerfile"&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;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;You can spin up this environment locally using &lt;a href="https://devpod.sh/" rel="noopener noreferrer"&gt;DevPod&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;devpod up &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--provider&lt;/span&gt; docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once inside the container, personal dotfiles give you your favorite shell customizations, while the project repository provides the exact tooling and application dependencies.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 4. The GitHub Actions CI Pipeline
&lt;/h2&gt;

&lt;p&gt;Now let's automate our checks. In a multi-project or monorepo workspace (e.g., &lt;code&gt;lab/github-actions-devpod-ci&lt;/code&gt;), our workflow file lives at &lt;code&gt;.github/workflows/github-actions-ci.yml&lt;/code&gt; at the repository root.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GitHub Actions DevPod CI&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;main&lt;/span&gt; &lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ci-pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-actions-devpod-ci&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Mise&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jdx/mise-action@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&lt;/span&gt;
          &lt;span class="na"&gt;install&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install Project Tools&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise install&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Ruff Linting&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise exec -- ruff check .&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run pytest Test Suite&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mise exec -- pytest -vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Workflow Highlights:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;actions/checkout@v4&lt;/code&gt;&lt;/strong&gt;: Pulls the repository code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;jdx/mise-action@v2&lt;/code&gt;&lt;/strong&gt;: Sets up Mise directly on the GitHub Actions runner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;defaults.run.working-directory&lt;/code&gt;&lt;/strong&gt;: Ensures every &lt;code&gt;run&lt;/code&gt; step executes inside the specific subproject folder where &lt;code&gt;mise.toml&lt;/code&gt; and &lt;code&gt;pytest.ini&lt;/code&gt; reside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mise exec -- &amp;lt;command&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Runs &lt;code&gt;ruff&lt;/code&gt; and &lt;code&gt;pytest&lt;/code&gt; using the exact tool versions managed by Mise.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🔍 The Debugging Field Guide: 7 Real-World Traps &amp;amp; How We Solved Them
&lt;/h2&gt;

&lt;p&gt;Building pipelines in documentation looks effortless; building them in real projects always exposes subtle edge cases. Here are the real bugs encountered during implementation and the architectural principles behind fixing them.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap #1: "Command Not Found: pytest" (Explicit vs. Implicit Tooling)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: After running &lt;code&gt;mise install&lt;/code&gt;, trying to run &lt;code&gt;pytest&lt;/code&gt; returned &lt;code&gt;command not found&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Root Cause&lt;/strong&gt;: We assumed pytest would be installed by default because Python was present. But &lt;code&gt;mise.toml&lt;/code&gt; only declared &lt;code&gt;python = "latest"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix&lt;/strong&gt;: Explicitly declare tool requirements in &lt;code&gt;mise.toml&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;  &lt;span class="py"&gt;"pipx:pytest"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Core Takeaway&lt;/strong&gt;: &lt;em&gt;Never rely on ambient system tools.&lt;/em&gt; If your build or test process requires a binary, declare it explicitly in your version control configuration.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Trap #2: Binary Exists vs. Module Exists (&lt;code&gt;which pytest&lt;/code&gt; vs. &lt;code&gt;python -m pytest&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: &lt;code&gt;which pytest&lt;/code&gt; showed &lt;code&gt;/home/vscode/.local/share/mise/installs/pipx-pytest/...&lt;/code&gt;, yet running &lt;code&gt;python -m pytest&lt;/code&gt; resulted in &lt;code&gt;No module named pytest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Root Cause&lt;/strong&gt;: Standalone tools installed via &lt;code&gt;pipx&lt;/code&gt; or Mise binary shims live in their own isolated virtual environment. The pytest executable is available on &lt;code&gt;$PATH&lt;/code&gt;, but it is not installed as an importable package inside the main Python interpreter environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix&lt;/strong&gt;: Standardize on executing the standalone test runner binary directly (&lt;code&gt;pytest -vv&lt;/code&gt; or &lt;code&gt;mise exec -- pytest&lt;/code&gt;), rather than assuming &lt;code&gt;python -m pytest&lt;/code&gt; inside an unbundled interpreter.&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Trap #3: &lt;code&gt;ModuleNotFoundError: No module named 'src'&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: Running &lt;code&gt;pytest&lt;/code&gt; threw:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ModuleNotFoundError: No module named 'src'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though &lt;code&gt;from src.main import add&lt;/code&gt; was written in &lt;code&gt;tests/test_main.py&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Diagnosis&lt;/strong&gt;: Running a one-liner proved Python could import the module fine when invoked from the root:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from src.main import add; print(add(2, 3))"&lt;/span&gt;
  &lt;span class="c"&gt;# Output: 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue was that &lt;code&gt;pytest&lt;/code&gt; does not automatically add the current working directory to &lt;code&gt;sys.path&lt;/code&gt; during test execution unless configured.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Bad Workaround (Avoid This!)&lt;/strong&gt;: Copying the &lt;code&gt;src/&lt;/code&gt; folder into &lt;code&gt;tests/&lt;/code&gt;. This makes tests pass temporarily but introduces duplicate code and destroys single-source-of-truth architecture.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Clean Architectural Solution&lt;/strong&gt;: Create a standard &lt;code&gt;pytest.ini&lt;/code&gt; in your project root:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;  &lt;span class="nn"&gt;[pytest]&lt;/span&gt;
  &lt;span class="py"&gt;pythonpath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This cleanly tells &lt;code&gt;pytest&lt;/code&gt; to treat the project root as part of the module search path.&lt;/p&gt;




&lt;h3&gt;
  
  
  Trap #4: The Relative Import Trap (&lt;code&gt;from .src.main import ...&lt;/code&gt;)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: When trying to fix the import error above, changing the import in &lt;code&gt;tests/test_main.py&lt;/code&gt; to:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;.src.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;  &lt;span class="c1"&gt;# ❌ INCORRECT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;resulted in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ModuleNotFoundError: No module named 'tests.src'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Root Cause&lt;/strong&gt;: A leading dot (&lt;code&gt;.&lt;/code&gt;) designates a relative import within the current package (&lt;code&gt;tests&lt;/code&gt;). Python searched for &lt;code&gt;tests.src&lt;/code&gt;, which does not exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution&lt;/strong&gt;: Use standard absolute imports from the project root:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;  &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;src.main&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;subtract&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ CORRECT
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Trap #5: Ruff Linting vs. Guesswork
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: Ruff flagged multi-line import formatting in test files.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Anti-Pattern&lt;/strong&gt;: Spending 15 minutes manually reordering lines and guessing what the linter expects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution&lt;/strong&gt;: Let Ruff fix safe formatting and import sorting automatically:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="c"&gt;# Automatically fix safe rule violations&lt;/span&gt;
  ruff check &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fix&lt;/span&gt;

  &lt;span class="c"&gt;# Verify clean state&lt;/span&gt;
  ruff check &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Trap #6: Monorepo Workflow Discovery in GitHub Actions
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: You push commits containing &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; inside a nested subproject (&lt;code&gt;my-repo/subproject/.github/workflows/ci.yml&lt;/code&gt;), but GitHub Actions never triggers!&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Root Cause&lt;/strong&gt;: GitHub Actions &lt;strong&gt;only&lt;/strong&gt; scans the root directory of the repository for workflow files (&lt;code&gt;.github/workflows/*.yml&lt;/code&gt;). Subdirectory workflow files are completely ignored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Solution&lt;/strong&gt;: Place your workflow file at the repository root and use &lt;code&gt;defaults.run.working-directory&lt;/code&gt;:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Located at: .github/workflows/github-actions-ci.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ci-pipeline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;defaults&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;working-directory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github-actions-devpod-ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Trap #7: Git Index &amp;amp; Monorepo History Mismatches
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Symptom&lt;/strong&gt;: When staging subproject changes in a repository with existing commits, Git reported &lt;code&gt;No commits yet&lt;/code&gt; or threw index conflicts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Fix&lt;/strong&gt;: Never delete &lt;code&gt;.git&lt;/code&gt; in frustration! Instead, inspect remote state and align:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  git status
  git remote &lt;span class="nt"&gt;-v&lt;/span&gt;
  git log &lt;span class="nt"&gt;--oneline&lt;/span&gt; &lt;span class="nt"&gt;--all&lt;/span&gt; &lt;span class="nt"&gt;--max-count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
  git fetch origin
  git reset origin/main
  git restore &amp;lt;unmodified-sibling-folders&amp;gt;
  git add &amp;lt;project-folder&amp;gt;
  git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add CI pipeline"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💥 5. Validating CI with Intentional Failure Injection
&lt;/h2&gt;

&lt;p&gt;Here is an uncomfortable truth: &lt;strong&gt;A CI pipeline that only ever passed is not a proven CI pipeline.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;How do you know your pipeline isn't returning a false positive because of an incorrect exit code or a misconfigured test glob?&lt;/p&gt;

&lt;p&gt;To prove our CI pipeline actually guards the codebase, we performed a deliberate &lt;strong&gt;Failure Injection Test&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Inject a Failing Assertion
&lt;/h3&gt;

&lt;p&gt;In &lt;code&gt;tests/test_main.py&lt;/code&gt;, we temporarily added:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_ci_failure&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Intentionally broken test: 2 + 2 != 5
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;add&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Push to GitHub &amp;amp; Inspect the Run
&lt;/h3&gt;

&lt;p&gt;GitHub Actions immediately triggered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;✅ &lt;strong&gt;Checkout repository&lt;/strong&gt;: PASSED&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Install Mise&lt;/strong&gt;: PASSED&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Install Project Tools&lt;/strong&gt;: PASSED&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;Run Ruff Linting&lt;/strong&gt;: PASSED&lt;/li&gt;
&lt;li&gt;❌ &lt;strong&gt;Run pytest Test Suite&lt;/strong&gt;: &lt;strong&gt;FAILED&lt;/strong&gt; (Exit code 1)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FAILED tests/test_main.py::test_ci_failure - assert 4 == 5
========================= 1 failed, 5 passed in 0.04s =========================
Error: Process completed with exit code 1.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Revert and Observe Recovery
&lt;/h3&gt;

&lt;p&gt;After deleting the broken test and pushing again, the pipeline turned green (5/5 tests passed).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Rule of Thumb&lt;/strong&gt;: Before shipping any CI configuration, always break a test or lint rule on purpose to ensure the pipeline halts execution with a non-zero exit code.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🧭 The 10-Step Systematic Troubleshooting Protocol
&lt;/h2&gt;

&lt;p&gt;When debugging containerized environments, Python import errors, or CI runner failures, don't guess. Follow this structured protocol:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 1. Read the full error message and traceback (don't skim just the last line).
 2. Identify the exact command and working directory that failed.
 3. Determine which layer is failing (Host, Docker, DevPod, Mise, Python, Git, or CI).
 4. Verify binary paths (`which &amp;lt;tool&amp;gt;`, `mise which &amp;lt;tool&amp;gt;`).
 5. Verify runtime versions (`&amp;lt;tool&amp;gt; --version`, `python --version`).
 6. Isolate and test the smallest reproducible unit (e.g. `python -c "from src.main import ..."`).
 7. Make exactly ONE change at a time.
 8. Re-run the command under identical conditions.
 9. Confirm the fix preserves architectural clean boundaries (no copy-pasting code into test dirs!).
10. Proceed only after the root cause is resolved and verified.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏁 Summary &amp;amp; Key Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Explicit is better than implicit&lt;/strong&gt;: Declare all runtimes and tools in &lt;code&gt;mise.toml&lt;/code&gt; rather than assuming host availability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decouple your layers&lt;/strong&gt;: Keep personal dotfiles, container configurations, tool definitions, and application code in their proper places.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use &lt;code&gt;pytest.ini&lt;/code&gt; for import path resolution&lt;/strong&gt;: Avoid import path hacks or relative submodule mangling.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind monorepo pathing in GitHub Actions&lt;/strong&gt;: Workflows must live in &lt;code&gt;.github/workflows/&lt;/code&gt; at the repository root, but commands can target subdirectories via &lt;code&gt;working-directory&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always test failure modes&lt;/strong&gt;: A quality gate is only reliable when it has been observed catching real errors.&lt;/li&gt;
&lt;/ol&gt;




&lt;h3&gt;
  
  
  Useful Commands Cheat Sheet
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Mise Management&lt;/span&gt;
mise &lt;span class="nb"&gt;install&lt;/span&gt;                &lt;span class="c"&gt;# Install all tools in mise.toml&lt;/span&gt;
mise &lt;span class="nb"&gt;ls&lt;/span&gt;                     &lt;span class="c"&gt;# List installed tools and active versions&lt;/span&gt;
mise which python           &lt;span class="c"&gt;# Inspect active Python binary path&lt;/span&gt;

&lt;span class="c"&gt;# Local Quality Verification&lt;/span&gt;
ruff check &lt;span class="nb"&gt;.&lt;/span&gt;                &lt;span class="c"&gt;# Lint checks&lt;/span&gt;
ruff check &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fix&lt;/span&gt;          &lt;span class="c"&gt;# Auto-fix safe lint errors&lt;/span&gt;
pytest &lt;span class="nt"&gt;-vv&lt;/span&gt;                  &lt;span class="c"&gt;# Verbose test run&lt;/span&gt;
pytest &lt;span class="nt"&gt;--collect-only&lt;/span&gt; &lt;span class="nt"&gt;-vv&lt;/span&gt;   &lt;span class="c"&gt;# Inspect test discovery without execution&lt;/span&gt;

&lt;span class="c"&gt;# Python One-Liner Sanity Check&lt;/span&gt;
python &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from src.main import add; print(add(2, 3))"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;a href="https://github.com/alanvarghese-dev/lab" rel="noopener noreferrer"&gt;[github]&lt;/a&gt;&lt;br&gt;
&lt;a href="https://www.linkedin.com/in/alanvarghese-dev/" rel="noopener noreferrer"&gt;[linkedin]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Have you experienced tricky import bugs or CI configuration headaches in your Python projects? How do you manage tool versioning across your team? Drop your thoughts and experiences in the comments below!&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>python</category>
      <category>docker</category>
    </item>
    <item>
      <title>From Zero to Green Build: How I Built a Jenkins + DevPod CI Pipeline with Mise (+ 10 Real Errors That Taught Me Everything)</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 01 Sep 2026 15:50:41 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/from-zero-to-green-build-how-i-built-a-jenkins-devpod-ci-pipeline-with-mise-10-real-errors-3p9j</link>
      <guid>https://dev.to/alanvarghese-dev/from-zero-to-green-build-how-i-built-a-jenkins-devpod-ci-pipeline-with-mise-10-real-errors-3p9j</guid>
      <description>&lt;p&gt;A complete hands-on guide to building an isolated, reproducible CI pipeline with Jenkins Controller, SSH-connected DevPod agents, and Mise runtime management.&lt;/p&gt;

&lt;p&gt;Continuous Integration (CI) setups frequently suffer from a classic problem: &lt;strong&gt;the "Works on My Machine" dilemma&lt;/strong&gt;. Local development happens in nicely customized containers or modern toolchains, while CI build servers either drift into giant "snowflakes" full of globally installed packages, or run bloated custom VMs that take forever to configure and maintain.&lt;/p&gt;

&lt;p&gt;In this project, I set out to build a modern, isolated, and fully reproducible CI lab environment:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jenkins Controller&lt;/strong&gt; handles scheduling and orchestration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;DevPod (Containers via SSH)&lt;/strong&gt; provides isolated, reproducible build agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mise&lt;/strong&gt; acts as a declarative polyglot toolchain manager (&lt;code&gt;mise.toml&lt;/code&gt;) to deterministically manage runtimes (Python, Java 21, Node.js, and Jenkins CLI) without polluting global paths.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Along the way, things did &lt;em&gt;not&lt;/em&gt; work on the first try. I hit subtle shell differences, monorepo path traps, SSH agent quirks, and environment activation edge cases.&lt;/p&gt;

&lt;p&gt;Here is the complete walkthrough of how the architecture works, how every file is configured, and the &lt;strong&gt;10 real-world errors&lt;/strong&gt; that taught me foundational DevOps lessons.&lt;/p&gt;




&lt;h2&gt;
  
  
  🏗️ Architecture &amp;amp; High-Level Flow
&lt;/h2&gt;

&lt;p&gt;The goal was simple: separate the &lt;strong&gt;orchestrator&lt;/strong&gt; from the &lt;strong&gt;execution environment&lt;/strong&gt;. The Jenkins Controller should never execute heavy compilation or test tasks directly; it should delegate build execution to an ephemeral DevPod container agent over SSH.&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    subgraph GitHub["GitHub Repository (alanvarghese-dev/lab)"]
        Repo["Source Code &amp;amp; Jenkinsfile"]
    end

    subgraph Jenkins_Master["Jenkins Controller"]
        JC["Jenkins Controller\n(Scheduler &amp;amp; Orchestrator)"]
    end

    subgraph DevPod_Agent["DevPod Build Agent (Container)"]
        Agent["Jenkins Agent Node\n(Label: 'devpod')"]
        Mise["Mise Runtime Manager\n(mise.toml)"]
        Python["Python Runtime\n(Latest via Mise)"]
        Pytest["pytest\n(Test Suite Runner)"]
        App["Application Build\n(app.py / test_app.py)"]
    end

    Repo --&amp;gt;|SCM Trigger / Poll| JC
    JC --&amp;gt;|SSH Connection| Agent
    Agent --&amp;gt; Mise
    Mise --&amp;gt; Python
    Python --&amp;gt; Pytest
    Pytest --&amp;gt; App&lt;/code&gt;&lt;/pre&gt;



&lt;h3&gt;
  
  
  Complete End-to-End Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GitHub (alanvarghese-dev/lab/jenkins-pipeline)
   ↓
Jenkins Controller (Orchestrates Job &amp;amp; evaluates Jenkinsfile)
   ↓ [SSH]
DevPod Jenkins Agent (Container with label: 'devpod')
   ↓
Mise (Resolves toolchain from mise.toml)
   ↓
Python &amp;amp; pip (Installs dependencies from requirements.txt)
   ↓
pytest (Executes unit test suite)
   ↓
Build Status: SUCCESS ✅
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🛠️ The Tech Stack
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;CI Orchestrator&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://www.jenkins.io/" rel="noopener noreferrer"&gt;Jenkins&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Job orchestration, pipeline stage execution, reporting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Build Agent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://devpod.sh/" rel="noopener noreferrer"&gt;DevPod&lt;/a&gt; (Docker)&lt;/td&gt;
&lt;td&gt;Isolated, disposable containerized build execution environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Base Image&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcr.microsoft.com/devcontainers/base:ubuntu-24.04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clean, standardized Linux environment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Tool Version Manager&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;a href="https://mise.jdx.dev/" rel="noopener noreferrer"&gt;Mise&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Polyglot runtime versioning (Python, Java 21, Node.js, Jenkins CLI)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;App Runtime &amp;amp; Tests&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python 3 + &lt;code&gt;pytest&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Application code (&lt;code&gt;app.py&lt;/code&gt;) &amp;amp; unit tests (&lt;code&gt;test_app.py&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Transport&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;td&gt;Controller-to-Agent communication channel&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  📁 Repository Structure
&lt;/h2&gt;

&lt;p&gt;The project lives in a monorepo setup (&lt;code&gt;alanvarghese-dev/lab&lt;/code&gt;) designed to house multiple DevOps learning projects side by side:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lab/
└── jenkins-pipeline/
    ├── .devcontainer.json      # Devcontainer build definition
    ├── Dockerfile              # DevPod agent image with Mise
    ├── Jenkinsfile             # Declarative Jenkins CI pipeline
    ├── mise.toml               # Tool versions (Java, Python, Node, CLI)
    ├── app.py                  # Sample Python application
    ├── test_app.py             # Pytest test suite
    ├── requirements.txt        # Python dependencies (pytest)
    └── README.md               # Project documentation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚙️ Step-by-Step Configuration
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Devcontainer Agent Image (&lt;code&gt;Dockerfile&lt;/code&gt; &amp;amp; &lt;code&gt;.devcontainer.json&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;To build our DevPod agent with Mise pre-baked, we copy the binary directly from the official Mise multi-stage Docker image:&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="c"&gt;# Dockerfile&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; mcr.microsoft.com/devcontainers/base:ubuntu-24.04&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=jdxcode/mise /usr/local/bin/mise /usr/local/bin/&lt;/span&gt;

&lt;span class="c"&gt;# Activate mise in interactive shells&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate bash)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.bashrc &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'eval "$(mise activate zsh)"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/vscode/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And configure the &lt;code&gt;.devcontainer.json&lt;/code&gt;:&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;"build"&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="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dockerfile"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dockerfile"&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;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;h3&gt;
  
  
  2. Declarative Toolchain (&lt;code&gt;mise.toml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Instead of installing packages globally via &lt;code&gt;apt-get&lt;/code&gt; or installing random pythons across system directories, we declare all required tooling in &lt;code&gt;mise.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tools]&lt;/span&gt;
&lt;span class="py"&gt;"aqua:jenkins-zh/jenkins-cli"&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;python&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;span class="py"&gt;java&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"21"&lt;/span&gt;
&lt;span class="py"&gt;node&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"latest"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because this file is checked into version control, any developer opening the container with DevPod or any CI agent running Jenkins gets the exact same runtime versions.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Application &amp;amp; Pytest Suite
&lt;/h3&gt;

&lt;p&gt;Keep the application minimal so you can focus 100% on pipeline predictability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from Jenkins + Devpod!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# test_app.py
&lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_hello&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello from Jenkins + Devpod!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# requirements.txt
pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  4. The Declarative Jenkins Pipeline (&lt;code&gt;Jenkinsfile&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Here is the complete &lt;code&gt;Jenkinsfile&lt;/code&gt;. Notice two critical patterns:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;dir('jenkins-pipeline')&lt;/code&gt;&lt;/strong&gt;: Because this is a monorepo, we explicitly change into the project directory so Mise can find &lt;code&gt;mise.toml&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;mise exec python -- ...&lt;/code&gt;&lt;/strong&gt;: We avoid brittle shell activation scripts and let Mise directly wrap command execution.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;label&lt;/span&gt; &lt;span class="s1"&gt;'devpod'&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Checkout'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&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;checkout&lt;/span&gt; &lt;span class="n"&gt;scm&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Setup Python'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&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;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jenkins-pipeline'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'''
                        set -e
                        echo "Project directory:" &amp;amp;&amp;amp; pwd
                        echo "Mise configuration:" &amp;amp;&amp;amp; ls -la mise.toml
                        mise install
                        echo "Python version:"
                        mise exec python -- python --version
                    '''&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Install Dependencies'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&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;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jenkins-pipeline'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'''
                        set -e
                        mise exec python -- python -m pip install -r requirements.txt
                    '''&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Test'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&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;dir&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'jenkins-pipeline'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
                    &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'''
                        set -e
                        mise exec python -- python -m pytest
                    '''&lt;/span&gt;
                &lt;span class="o"&gt;}&lt;/span&gt;
            &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;success&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Python CI build successful! 🚀'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;failure&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'Python CI build failed! ❌'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💥 10 Real Errors Encountered &amp;amp; What They Taught Me
&lt;/h2&gt;

&lt;p&gt;Building CI pipelines is 10% writing code and 90% debugging subtle environment mismatches. Here are the 10 errors I hit during this lab and the lessons behind them:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. DevPod Container Created, but Features Failed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; The container was created in Docker, but DevPod reported connection and tunneling errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Container creation and DevPod connectivity are distinct stages. Always ensure:
&lt;code&gt;Container Running&lt;/code&gt; ➔ &lt;code&gt;DevPod SSH Access&lt;/code&gt; ➔ &lt;code&gt;Shell Available&lt;/code&gt; before troubleshooting build tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. "No SSH session / exited"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; DevPod SSH tunneling abruptly dropped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; This was an infrastructure/connection issue, not a Jenkins or Python failure. Once &lt;code&gt;devpod ssh&lt;/code&gt; succeeded reliably from the terminal, the underlying transport was proven stable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Dotfiles Were Not Applied
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; The agent container spun up without customized shell dotfiles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; DevPod’s automated dotfiles mechanism requires setup scripts to be located in conventional locations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. "Setup not found"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; DevPod searched for bootstrap files (&lt;code&gt;install.sh&lt;/code&gt;, &lt;code&gt;bootstrap.sh&lt;/code&gt;, &lt;code&gt;setup.sh&lt;/code&gt;) and couldn't find them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Automation tools depend on strict conventions. Always verify standard entry points when integrating dotfiles repositories into devcontainers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. GitHub SSH Authentication Confusion
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Output showed: &lt;code&gt;Hi username! You've successfully authenticated, but GitHub does not provide shell access.&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; This is actually a &lt;strong&gt;success&lt;/strong&gt; message from GitHub confirming that your SSH public key is valid. GitHub explicitly denies interactive PTY shells, so don't mistake this for an authentication failure!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  6. &lt;code&gt;java: command not found&lt;/code&gt; (The Non-Interactive Shell Trap)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Java 21 was installed inside DevPod, but Jenkins reported &lt;code&gt;java: command not found&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; &lt;strong&gt;Installed on disk ≠ Available to the Jenkins process.&lt;/strong&gt; When you log in via &lt;code&gt;devpod ssh&lt;/code&gt;, your &lt;code&gt;.zshrc&lt;/code&gt; / &lt;code&gt;.bashrc&lt;/code&gt; loads your PATH. Jenkins launches a non-interactive &lt;code&gt;/bin/sh&lt;/code&gt; shell that skips interactive rc files.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Interactive Login:   .zshrc / .bashrc loaded ➔ PATH configured ➔ Tools work
Jenkins Non-Login:   /bin/sh launched        ➔ Minimal PATH    ➔ Command not found
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  7. "Python is installed but not activated"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Mise reported that Python was installed on the machine, but not active.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Mise relies on discovering a valid &lt;code&gt;mise.toml&lt;/code&gt; in the current directory or parent tree. If you are in a directory without a config, Mise will not activate the tool by default.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  8. &lt;code&gt;Syntax error: "(" unexpected&lt;/code&gt; on &lt;code&gt;eval "$(mise activate bash)"&lt;/code&gt;
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Attempting to evaluate Mise activation inside a Jenkins &lt;code&gt;sh&lt;/code&gt; step blew up with a syntax error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Jenkins defaults to &lt;code&gt;/bin/sh&lt;/code&gt; (often Debian/Ubuntu &lt;code&gt;dash&lt;/code&gt;), which does not support certain Bash/Zsh process substitution or subshell syntax.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solution:&lt;/strong&gt; Instead of hacking shell startup scripts, use &lt;code&gt;mise exec &amp;lt;tool&amp;gt; -- &amp;lt;command&amp;gt;&lt;/code&gt; directly in pipeline steps!&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  9. Mise Could Not Find Python in Monorepo Root
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; &lt;code&gt;mise exec -- python --version&lt;/code&gt; failed in Jenkins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; Jenkins checked out the entire monorepo (&lt;code&gt;lab/&lt;/code&gt;), and executed commands at the root. The &lt;code&gt;mise.toml&lt;/code&gt; was inside &lt;code&gt;lab/jenkins-pipeline/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fix:&lt;/strong&gt; Use Jenkins' &lt;code&gt;dir('jenkins-pipeline') { ... }&lt;/code&gt; block so the working directory matches where &lt;code&gt;mise.toml&lt;/code&gt; lives.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  10. Git "Does not have a commit checked out"
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Symptom:&lt;/strong&gt; Git commands failed unexpectedly inside the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; Commands ran from &lt;code&gt;/workspaces&lt;/code&gt; instead of &lt;code&gt;/workspaces/jenkins-pipeline&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lesson:&lt;/strong&gt; Always verify your working directory before running Git operations: &lt;code&gt;pwd &amp;amp;&amp;amp; git status&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 The 5-Step CI Troubleshooting Checklist
&lt;/h2&gt;

&lt;p&gt;Whenever your Jenkins build fails with a "command not found" or tool version issue, run through this checklist before touching code:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A["1. Is the tool installed?\n(mise ls)"] --&amp;gt;|Yes| B["2. Where is the binary?\n(mise which &amp;lt;tool&amp;gt;)"]
    B --&amp;gt; C["3. Is mise.toml in the directory?\n(ls -la mise.toml)"]
    C --&amp;gt; D["4. Is Jenkins in the right folder?\n(pwd)"]
    D --&amp;gt; E["5. Can Mise execute it?\n(mise exec &amp;lt;tool&amp;gt; -- &amp;lt;cmd&amp;gt;)"]
    A --&amp;gt;|No| F["Run: mise install"]&lt;/code&gt;&lt;/pre&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is the tool installed?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Where is the binary located?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   mise which python
   mise which java
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Is &lt;code&gt;mise.toml&lt;/code&gt; present in the current folder?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-la&lt;/span&gt; mise.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What is Jenkins' current working directory?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Can Mise execute the tool directly?&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   mise &lt;span class="nb"&gt;exec &lt;/span&gt;python &lt;span class="nt"&gt;--&lt;/span&gt; python &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💡 Key Architectural Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure First, Application Second:&lt;/strong&gt; Get your container running, SSH passing, and agent labeled before spending time configuring application test suites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate Orchestration from Execution:&lt;/strong&gt; Keep the Jenkins Controller lean. Run builds inside disposable agents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embrace Deterministic Toolchains:&lt;/strong&gt; Using &lt;code&gt;mise.toml&lt;/code&gt; ensures that both local devcontainer sessions and remote CI builds use the exact same runtime versions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never Rely on Interactive Shell Environments in CI:&lt;/strong&gt; Avoid complex &lt;code&gt;.bashrc&lt;/code&gt; / &lt;code&gt;.zshrc&lt;/code&gt; sourcing in CI. Use explicit CLI wrappers like &lt;code&gt;mise exec&lt;/code&gt; to execute binaries cleanly.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🗺️ What's Next on the DevOps Roadmap?
&lt;/h2&gt;

&lt;p&gt;This Jenkins + DevPod CI lab is the foundation of my broader homelab DevOps monorepo (&lt;code&gt;alanvarghese-dev/lab&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;lab/
├── jenkins-pipeline/    # ✅ Jenkins + DevPod SSH CI Pipeline (Completed!)
├── docker/              # 🔜 Multi-stage builds &amp;amp; container optimization
├── kubernetes/          # 🔜 K8s deployment manifests &amp;amp; Helm charts
├── terraform/           # 🔜 Infrastructure as Code (IaC)
└── gitops/              # 🔜 GitOps automated delivery with ArgoCD / Flux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💬 Discussion
&lt;/h2&gt;

&lt;p&gt;Have you tried using DevPod or containerized SSH agents with Jenkins? How do you handle tool versioning in your CI runners? Let me know in the comments below!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alanvarghese-dev/lab" rel="noopener noreferrer"&gt;[github]&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.linkedin.com/in/alanvarghese-dev/" rel="noopener noreferrer"&gt;[linkedin]&lt;/a&gt;&lt;/p&gt;

</description>
      <category>jenkins</category>
      <category>linux</category>
      <category>devops</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Automate Your Linux Updates Like a Pro (with Rollbacks!) 🚀</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 01 Sep 2026 15:27:22 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/automate-your-linux-updates-like-a-pro-with-rollbacks-ig0</link>
      <guid>https://dev.to/alanvarghese-dev/automate-your-linux-updates-like-a-pro-with-rollbacks-ig0</guid>
      <description>&lt;p&gt;Running &lt;code&gt;sudo apt-get update &amp;amp;&amp;amp; sudo apt-get upgrade -y&lt;/code&gt; is fine for your local dev machine. But when you're managing multiple Ubuntu or Debian servers, manual updates are a recipe for disaster. &lt;/p&gt;

&lt;p&gt;One day, a kernel update will break a driver, or a library conflict will brick a service. If you don't have a history of what was installed or a way to roll back, you're in for a long night of troubleshooting.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;System Update Manager&lt;/strong&gt;, a production-ready Bash tool that treats system updates like a CI/CD pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem with "Yolo-Upgrading"
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No Audit Trail&lt;/strong&gt;: Did that server crash because of an update 2 hours ago or a config change?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Point of No Return"&lt;/strong&gt;: Once &lt;code&gt;apt&lt;/code&gt; finishes, knowing exactly which versions were upgraded is a pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency Issues&lt;/strong&gt;: Running multiple update scripts at once can lock the dpkg database.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  My Solution: System Update Manager
&lt;/h2&gt;

&lt;p&gt;This isn't just a wrapper for &lt;code&gt;apt&lt;/code&gt;. It’s a full management suite designed for reliability.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features 🛠️
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;SQLite History Tracking&lt;/strong&gt;: Every update attempt is logged in a local database. You can see the start time, duration, exit codes, and exactly which packages changed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-update Snapshots&lt;/strong&gt;: Before touching a single byte, it snapshots your package list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated Rollback Scripts&lt;/strong&gt;: If an update fails, the tool generates a standalone &lt;code&gt;.sh&lt;/code&gt; script that uses &lt;code&gt;apt-get install --allow-downgrades&lt;/code&gt; to bring your system back to its previous state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Automation&lt;/strong&gt;: It ships with Systemd timer and Cron configurations out of the box.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock Management&lt;/strong&gt;: Uses file-based locking to ensure only one instance is running.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;The script follows a strict lifecycle for every update:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pre-flight Check&lt;/strong&gt;: Validates root privileges and disk space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locking&lt;/strong&gt;: Acquires a lock file to prevent concurrent runs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snapshot&lt;/strong&gt;: Records current package versions in SQLite.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Update&lt;/strong&gt;: Executes &lt;code&gt;apt-get update&lt;/code&gt; and &lt;code&gt;upgrade&lt;/code&gt; with configurable retries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recovery&lt;/strong&gt;: If a failure occurs, it immediately generates a rollback script.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logging&lt;/strong&gt;: Records the outcome and duration.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  A Peek at the Status UI
&lt;/h3&gt;

&lt;p&gt;I wanted the CLI to be readable at a glance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;╔══════════════════════════════════════════════════════╗
║          SYSTEM UPDATE MANAGER - STATUS              ║
╠══════════════════════════════════════════════════════╣
║ Status: SUCCESS                                     ║
║ Start Time:  2024-03-24 03:00:00                    ║
║ End Time:    2024-03-24 03:05:23                    ║
║ Exit Code:   0                                      ║
╚══════════════════════════════════════════════════════╝
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;If you want to try it out (or use it in your own homelab/prod environment), here is the quick start:&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;# Clone it&lt;/span&gt;
git clone https://github.com/yourusername/automated_system_update_manager.git
&lt;span class="nb"&gt;cd &lt;/span&gt;automated_system_update_manager

&lt;span class="c"&gt;# Check for updates without changing anything&lt;/span&gt;
./update_manager.sh &lt;span class="nt"&gt;--check&lt;/span&gt;

&lt;span class="c"&gt;# Run a manual install&lt;/span&gt;
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./update_manager.sh &lt;span class="nt"&gt;--install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Automation via Systemd
&lt;/h3&gt;

&lt;p&gt;The cleanest way to run this is via a Systemd timer. It’s more modern than Cron and integrates perfectly with &lt;code&gt;journalctl&lt;/code&gt;.&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 cp &lt;/span&gt;systemd/update-manager.&lt;span class="k"&gt;*&lt;/span&gt; /etc/systemd/system/
&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; update-manager.timer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your system will update daily at 3 AM, and you can sleep soundly knowing there's a rollback script waiting if things go south.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Building this project reminded me that &lt;strong&gt;Bash is still a superpower&lt;/strong&gt;. By combining simple Unix tools like &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, and &lt;code&gt;sqlite3&lt;/code&gt;, you can build incredibly robust infrastructure tools without the overhead of a heavy Python or Go binary.&lt;/p&gt;

&lt;p&gt;What do you use to manage updates on your servers? Let me know in the comments! 👇&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>linux</category>
      <category>bash</category>
    </item>
    <item>
      <title>From 'src refspec' Errors to Clean PRs: The Practical Git Branching Workflow You Need</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Thu, 20 Aug 2026 04:38:46 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/from-src-refspec-errors-to-clean-prs-the-practical-git-branching-workflow-you-need-4job</link>
      <guid>https://dev.to/alanvarghese-dev/from-src-refspec-errors-to-clean-prs-the-practical-git-branching-workflow-you-need-4job</guid>
      <description>&lt;p&gt;We’ve all been there: pushing straight to &lt;code&gt;main&lt;/code&gt;, writing commit messages like &lt;code&gt;"fix stuff"&lt;/code&gt;, and dreading the moment someone mentions &lt;em&gt;merge conflicts&lt;/em&gt; or &lt;em&gt;branch topology&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;When you're building solo, you can get away with &lt;code&gt;git add .&lt;/code&gt; and &lt;code&gt;git push origin main&lt;/code&gt;. But the moment you collaborate with a team, work in an open-source project, or maintain production software, messy Git habits quickly turn into broken builds and lost code.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk you through a practical, lightweight Git branching workflow modeled on real-world team practices (often called &lt;strong&gt;Git Flow Light&lt;/strong&gt;). We'll set up a project, create feature branches, use the GitHub CLI (&lt;code&gt;gh&lt;/code&gt;) for Pull Requests, intentionally create and resolve a merge conflict, and cover the most common Git errors you will face along the way.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧠 1. The Mental Model: What's Actually Happening in Git?
&lt;/h2&gt;

&lt;p&gt;Before typing commands into the terminal, let’s demystify two core Git mechanics that make everything click:&lt;/p&gt;

&lt;h3&gt;
  
  
  The Three-Tree Architecture
&lt;/h3&gt;

&lt;p&gt;Git doesn’t just copy your files to the cloud; it manages changes across three distinct zones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ Working Directory ]  ---&amp;gt;  `git add`  ---&amp;gt;  [ Staging Area (Index) ]  ---&amp;gt;  `git commit`  ---&amp;gt;  [ Repository History ]
 (Live files on disk)                           (The box you're packing)                             (Committed snapshots)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Working Directory:&lt;/strong&gt; The actual physical files you are editing in VS Code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Staging Area (Index):&lt;/strong&gt; The draft snapshot. It holds a &lt;em&gt;copy&lt;/em&gt; of your staged changes, not a live reference.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repository (&lt;code&gt;.git&lt;/code&gt;):&lt;/strong&gt; The permanent history of committed snapshots.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Branches Are Just Cheap Pointers
&lt;/h3&gt;

&lt;p&gt;In Git, creating a branch does &lt;strong&gt;not&lt;/strong&gt; duplicate your entire codebase or take up disk space. A branch is merely a 40-character text file containing the commit SHA it points to.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When you create a branch, Git creates a new pointer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HEAD&lt;/code&gt; simply indicates which pointer your working directory is currently tracking.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(develop) ─── [ C1 ] ─── [ C2 ] ─── [ C3 ] (HEAD -&amp;gt; feature/todo-ui)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🌳 2. The Branching Strategy (Git Flow Light)
&lt;/h2&gt;

&lt;p&gt;For most teams and personal projects, a 3-tier structure provides the perfect balance between stability and agility without the over-engineering of full Git Flow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Production-ready releases&lt;/td&gt;
&lt;td&gt;Only receives tested code from &lt;code&gt;develop&lt;/code&gt;. Tagged with version numbers (e.g., &lt;code&gt;v1.0.0&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;develop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Integration branch&lt;/td&gt;
&lt;td&gt;Where all finished features land and are tested together.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;feature/*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Isolated task or component&lt;/td&gt;
&lt;td&gt;Forked from &lt;code&gt;develop&lt;/code&gt;, merged back via Pull Request, and deleted after merge.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  🚀 3. Step-by-Step Workflow in Action
&lt;/h2&gt;

&lt;p&gt;Let’s trace a real project scenario: setting up a clean repository and building a small Todo App.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Initialize and Configure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Initialize git in your project directory&lt;/span&gt;
git init

&lt;span class="c"&gt;# Configure identity (if not already set globally)&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.name &lt;span class="s2"&gt;"Your Name"&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; user.email &lt;span class="s2"&gt;"your.email@example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Establish &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;develop&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Always make sure you have an initial commit before trying to manage branches:&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;# Create the initial files&lt;/span&gt;
&lt;span class="nb"&gt;touch &lt;/span&gt;index.html style.css app.js

&lt;span class="c"&gt;# Stage and commit baseline&lt;/span&gt;
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: initial project structure"&lt;/span&gt;

&lt;span class="c"&gt;# Ensure main is the active branch&lt;/span&gt;
git branch &lt;span class="nt"&gt;-M&lt;/span&gt; main

&lt;span class="c"&gt;# Connect to GitHub remote&lt;/span&gt;
git remote add origin https://github.com/your-username/todo-app.git

&lt;span class="c"&gt;# Push main and set upstream&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main

&lt;span class="c"&gt;# Create and switch to develop branch&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; develop
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin develop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Work on an Isolated Feature Branch
&lt;/h3&gt;

&lt;p&gt;Never write code directly on &lt;code&gt;develop&lt;/code&gt; or &lt;code&gt;main&lt;/code&gt;. Whenever you start a new task, fork a feature branch from &lt;code&gt;develop&lt;/code&gt;:&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;# Ensure you are on latest develop&lt;/span&gt;
git checkout develop
git pull origin develop

&lt;span class="c"&gt;# Cut a new feature branch&lt;/span&gt;
git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/add-task-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make your code updates in &lt;code&gt;index.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"UTF-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;To-Do List&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"style.css"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"app"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My To-Do List&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"text"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"new-task"&lt;/span&gt; &lt;span class="na"&gt;placeholder=&lt;/span&gt;&lt;span class="s"&gt;"Add a new task..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"add-btn"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"task-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"app.js"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stage and commit with a descriptive message:&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;# Check what changed&lt;/span&gt;
git status

&lt;span class="c"&gt;# Stage &amp;amp; commit&lt;/span&gt;
git add index.html
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat(ui): add basic task input structure and markup"&lt;/span&gt;

&lt;span class="c"&gt;# Push feature branch to remote&lt;/span&gt;
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin feature/add-task-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  📬 4. Pull Requests as Quality Gates (Using GitHub CLI)
&lt;/h2&gt;

&lt;p&gt;Instead of manually clicking through the GitHub web UI, you can streamline the review process using the &lt;strong&gt;GitHub CLI (&lt;code&gt;gh&lt;/code&gt;)&lt;/strong&gt;:&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the PR to &lt;code&gt;develop&lt;/code&gt;:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh &lt;span class="nb"&gt;pr &lt;/span&gt;create &lt;span class="nt"&gt;--base&lt;/span&gt; develop &lt;span class="nt"&gt;--head&lt;/span&gt; feature/add-task-ui &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"feat: Add Task UI"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Implements initial HTML structure for task input and listing."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Merging and Cleaning Up:
&lt;/h3&gt;

&lt;p&gt;Once the PR passes checks (or code review):&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;# Merge with a merge commit to preserve branch topology and delete the remote branch&lt;/span&gt;
gh &lt;span class="nb"&gt;pr &lt;/span&gt;merge &lt;span class="nt"&gt;--merge&lt;/span&gt; &lt;span class="nt"&gt;--delete-branch&lt;/span&gt;

&lt;span class="c"&gt;# Switch back locally and sync&lt;/span&gt;
git checkout develop
git pull origin develop

&lt;span class="c"&gt;# Clean up the local feature branch&lt;/span&gt;
git branch &lt;span class="nt"&gt;-d&lt;/span&gt; feature/add-task-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  💥 5. Merge Conflicts: Demystified &amp;amp; Conquered
&lt;/h2&gt;

&lt;p&gt;Merge conflicts occur when Git cannot automatically reconcile two different changes made to the same lines of a file across different branches.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Conflict Setup Scenario
&lt;/h3&gt;

&lt;p&gt;Imagine two developers (or two branches) modify the heading in &lt;code&gt;index.html&lt;/code&gt; at the same time:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch &lt;code&gt;develop&lt;/code&gt; has:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My Daily To-Do List&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branch &lt;code&gt;feature/update-ui&lt;/code&gt; has:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My To-Do Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you attempt to merge &lt;code&gt;feature/update-ui&lt;/code&gt; into &lt;code&gt;develop&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout develop
git merge feature/update-ui
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git halts the process and warns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CONFLICT (content): Merge conflict in index.html
Automatic merge failed; fix conflicts and then commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Anatomy of Conflict Markers
&lt;/h3&gt;

&lt;p&gt;Open &lt;code&gt;index.html&lt;/code&gt;, and you will see Git's conflict markers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;&lt;/span&gt;&lt;span class="err"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="na"&gt;HEAD&lt;/span&gt; &lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="na"&gt;Current&lt;/span&gt; &lt;span class="na"&gt;change&lt;/span&gt; &lt;span class="na"&gt;on&lt;/span&gt; &lt;span class="na"&gt;develop&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;
        &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="na"&gt;h1&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;My Daily To-Do List&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
=======
        &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My To-Do Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/update-ui (Incoming change)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to Resolve It:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decide on the correct code&lt;/strong&gt; (e.g., combine or pick one version).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Delete the marker lines&lt;/strong&gt; (&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt;, &lt;code&gt;=======&lt;/code&gt;, &lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save the file:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;   &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;My Daily To-Do Tasks&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stage and commit the resolution:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   git add index.html
   git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix(merge): resolve heading text conflict between develop and feature/update-ui"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  🏷️ 6. Releasing to Production (&lt;code&gt;main&lt;/code&gt;) &amp;amp; Tagging
&lt;/h2&gt;

&lt;p&gt;Once &lt;code&gt;develop&lt;/code&gt; contains a stable set of merged features and has been tested, merge it into &lt;code&gt;main&lt;/code&gt; and tag the release:&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;# Switch to main and merge develop&lt;/span&gt;
git checkout main
git pull origin main
git merge develop &lt;span class="nt"&gt;--no-ff&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore(release): merge develop into main for v1.0.0"&lt;/span&gt;

&lt;span class="c"&gt;# Tag the milestone&lt;/span&gt;
git tag &lt;span class="nt"&gt;-a&lt;/span&gt; v1.0.0 &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Release version 1.0.0 - Initial working Todo App"&lt;/span&gt;

&lt;span class="c"&gt;# Push main and tags to remote&lt;/span&gt;
git push origin main
git push origin v1.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  ⚠️ 7. The Wall of Common Git Errors (&amp;amp; Exact Fixes)
&lt;/h2&gt;

&lt;p&gt;Save this cheat sheet for when terminal errors strike:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Error Message&lt;/th&gt;
&lt;th&gt;Why It Happened&lt;/th&gt;
&lt;th&gt;How to Fix It&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;fatal: not a git repository&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You ran a git command outside the project folder.&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;cd&lt;/code&gt; into your actual repository root directory.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src refspec main does not match any&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;You tried to push &lt;code&gt;main&lt;/code&gt; before creating at least one commit.&lt;/td&gt;
&lt;td&gt;Create an initial commit: &lt;code&gt;git commit --allow-empty -m "init"&lt;/code&gt; then push.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;base ref must be a branch&lt;/code&gt; / &lt;code&gt;base sha can't be blank&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Opening a PR to &lt;code&gt;--base develop&lt;/code&gt; when &lt;code&gt;develop&lt;/code&gt; hasn't been pushed to remote.&lt;/td&gt;
&lt;td&gt;Push the base branch first: &lt;code&gt;git push -u origin develop&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;no commits between dev and feat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The feature branch has no unique commits ahead of &lt;code&gt;develop&lt;/code&gt;.&lt;/td&gt;
&lt;td&gt;Make changes and commit them on the feature branch first.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;CONFLICT (content): Merge conflict in &amp;lt;file&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Concurrent edits to the exact same lines across branches.&lt;/td&gt;
&lt;td&gt;Open the file, remove &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt;/&lt;code&gt;=======&lt;/code&gt;/&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&lt;/code&gt;, pick the final code, &lt;code&gt;git add .&lt;/code&gt;, and &lt;code&gt;git commit&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;default branch is invalid&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tried to change default branch to &lt;code&gt;main&lt;/code&gt; before &lt;code&gt;main&lt;/code&gt; exists remotely.&lt;/td&gt;
&lt;td&gt;Push &lt;code&gt;main&lt;/code&gt; to remote first: &lt;code&gt;git push -u origin main&lt;/code&gt;, then update repo settings.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;git diff&lt;/code&gt; shows nothing after editing&lt;/td&gt;
&lt;td&gt;You forgot to save the file in your code editor.&lt;/td&gt;
&lt;td&gt;Hit &lt;code&gt;Ctrl+S&lt;/code&gt; / &lt;code&gt;Cmd+S&lt;/code&gt; in your editor, then re-run &lt;code&gt;git diff&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  💡 Pro Tips for a Smoother Git Life
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;git status&lt;/code&gt; is your reflex:&lt;/strong&gt; Run it before &lt;code&gt;git add&lt;/code&gt; and before &lt;code&gt;git commit&lt;/code&gt;. Always know what is staged versus unstaged.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use visual history logs:&lt;/strong&gt; Run &lt;code&gt;git log --oneline --graph --all&lt;/code&gt; to see a ASCII branch tree of your entire repository history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep branches short-lived:&lt;/strong&gt; Large, long-living feature branches are conflict magnets. Break work into small, bite-sized PRs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prune stale remote references:&lt;/strong&gt; Run &lt;code&gt;git fetch --prune&lt;/code&gt; periodically to remove local references to branches that were deleted on GitHub.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write semantic commit messages:&lt;/strong&gt; &lt;code&gt;feat:&lt;/code&gt;, &lt;code&gt;fix:&lt;/code&gt;, &lt;code&gt;docs:&lt;/code&gt;, &lt;code&gt;refactor:&lt;/code&gt;. Your future self and teammates will thank you.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💬 Over to You!
&lt;/h2&gt;

&lt;p&gt;What is your team's favorite Git workflow? Have you ever had a legendary merge conflict disaster? Drop your thoughts or questions in the comments below! 👇&lt;/p&gt;

</description>
      <category>git</category>
      <category>devops</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Build a Lightweight File Integrity Monitor with Bash, SQLite, and Docker</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Tue, 24 Mar 2026 10:57:51 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/build-a-lightweight-file-integrity-monitor-with-bash-sqlite-and-docker-1al5</link>
      <guid>https://dev.to/alanvarghese-dev/build-a-lightweight-file-integrity-monitor-with-bash-sqlite-and-docker-1al5</guid>
      <description>&lt;p&gt;In the world of server security, &lt;strong&gt;File Integrity Monitoring (FIM)&lt;/strong&gt; is a critical layer of defense. It's the "silent alarm" that tells you when a configuration file, a system binary, or a sensitive database has been tampered with.&lt;/p&gt;

&lt;p&gt;While there are enterprise grade tools like Tripwire or OSSEC, sometimes you need something lightweight, transparent, and easy to deploy.&lt;/p&gt;

&lt;p&gt;In this post, I’ll walk you through a project I built: a &lt;strong&gt;Bash based File Integrity Checker&lt;/strong&gt; that uses &lt;strong&gt;SQLite&lt;/strong&gt; for baseline storage and &lt;strong&gt;Docker&lt;/strong&gt; for a fully isolated testing environment.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 The Core Concept: Baseline vs. Reality
&lt;/h2&gt;

&lt;p&gt;The tool works on a simple but powerful principle:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Initialize (&lt;code&gt;--init&lt;/code&gt;)&lt;/strong&gt;: Scan your critical files, calculate their &lt;strong&gt;SHA-256 hashes&lt;/strong&gt;, and store them in a persistent SQLite database. This is your "known-good" state.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check (&lt;code&gt;--check&lt;/code&gt;)&lt;/strong&gt;: Periodically re-scan those same files. If a single bit has changed, the hashes won't match, and an alert is triggered.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  🛠️ The Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bash&lt;/strong&gt;: The engine. It handles the file traversal, hashing logic, and alerting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQLite3&lt;/strong&gt;: Instead of messy text files, I used SQLite to store the baseline. It’s fast, structured, and handles hundreds of files with ease.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker &amp;amp; Docker Compose&lt;/strong&gt;: To make testing easy, I containerized the entire app.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MailHog&lt;/strong&gt;: A "superpower" for development. It's a mock SMTP server that catches all outgoing alert emails so you can verify them in a web UI without spamming your real inbox.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🔍 A Deep Dive into the Code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Hashing Logic
&lt;/h3&gt;

&lt;p&gt;We use &lt;code&gt;sha256sum&lt;/code&gt; to ensure high cryptographic security. Even a tiny change to a file (like adding a space) results in a completely different hash.&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;# Calculate hash and store in database&lt;/span&gt;
&lt;span class="nv"&gt;current_hash&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$file_path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
sqlite3 &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DB_PATH&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"INSERT INTO file_hashes (file_path, hash) VALUES ('&lt;/span&gt;&lt;span class="nv"&gt;$file_path&lt;/span&gt;&lt;span class="s2"&gt;', '&lt;/span&gt;&lt;span class="nv"&gt;$current_hash&lt;/span&gt;&lt;span class="s2"&gt;');"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Multi Layered Alerting
&lt;/h3&gt;

&lt;p&gt;One of the biggest challenges in shell scripting is ensuring email delivery. My script uses a "fallback" strategy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary&lt;/strong&gt;: &lt;code&gt;ssmtp&lt;/code&gt; (sendmail) via the local system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secondary&lt;/strong&gt;: &lt;code&gt;curl&lt;/code&gt; to talk directly to the &lt;strong&gt;MailHog API&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Last Resort&lt;/strong&gt;: Standard &lt;code&gt;mail&lt;/code&gt; command.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🐳 The Docker Testing Workflow
&lt;/h2&gt;

&lt;p&gt;Testing a security tool can be nerve wracking you don't want to accidentally modify your host's &lt;code&gt;/etc/&lt;/code&gt; files! That’s where Docker shines. &lt;/p&gt;

&lt;p&gt;I created a &lt;code&gt;docker-compose.yml&lt;/code&gt; that spins up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;strong&gt;Ubuntu-based checker&lt;/strong&gt; container with mock system files.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;MailHog&lt;/strong&gt; container for email visualization.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  How to test a "Malicious" change:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Start the environment:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create the baseline:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&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; file_integrity_checker bash /app/file_integrity_checker.sh &lt;span class="nt"&gt;--init&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Simulate a hack:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&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; file_integrity_checker bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"echo 'MALICIOUS_CODE' &amp;gt;&amp;gt; /etc/myapp/config/app.yaml"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run the check:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&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; file_integrity_checker bash /app/file_integrity_checker.sh &lt;span class="nt"&gt;--check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;See the alert:&lt;/strong&gt; Open &lt;code&gt;http://localhost:8025&lt;/code&gt; and watch the security alert arrive in your MailHog inbox!&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  💡 Key Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Volume Mounts &amp;amp; Permissions&lt;/strong&gt;: When mounting files from macOS to a Linux container, file permissions (like the execute bit) can be tricky. I learned that executing via &lt;code&gt;bash &amp;lt;script&amp;gt;&lt;/code&gt; is often more robust than relying on the &lt;code&gt;+x&lt;/code&gt; bit in a shared volume.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Architecture Matters&lt;/strong&gt;: Adding &lt;code&gt;platform: linux/amd64&lt;/code&gt; to the &lt;code&gt;docker-compose.yml&lt;/code&gt; was essential for ensuring the MailHog image (which is AMD64 only) runs smoothly on modern ARM64 chips like the Apple M1/M2.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  🏁 Conclusion
&lt;/h2&gt;

&lt;p&gt;Building your own security tools is one of the best ways to understand how systems work. This project taught me about hashing, database persistence in shell, and the power of Docker for creating reproducible security labs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's next for this project?&lt;/strong&gt; I'm looking into adding Slack/Discord webhook support and real time monitoring via &lt;code&gt;inotify&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading! If you have questions about Bash security or Docker setups, let's chat in the comments! 🛡️&lt;/p&gt;

</description>
      <category>security</category>
      <category>bash</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>Monitoring Remote Network Health: Building a Lightweight Connectivity Tool with Bash and Docker</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Mon, 09 Mar 2026 12:26:06 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/monitoring-remote-network-health-building-a-lightweight-connectivity-tool-with-bash-and-docker-3eok</link>
      <guid>https://dev.to/alanvarghese-dev/monitoring-remote-network-health-building-a-lightweight-connectivity-tool-with-bash-and-docker-3eok</guid>
      <description>&lt;p&gt;Have you ever needed to verify if your remote servers have outbound internet access? Whether you're managing a cluster of web servers or a set of edge devices, ensuring they can "talk to the world" is a fundamental task.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through a lightweight &lt;strong&gt;Network Connectivity Monitoring Tool&lt;/strong&gt; I built using Bash. It's simple, portable, and comes with a Docker-based test environment to get you started safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;Manually SSH-ing into dozens of servers to run a &lt;code&gt;ping&lt;/code&gt; command is tedious and error-prone. I needed a way to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Check connectivity from &lt;em&gt;multiple&lt;/em&gt; servers at once.&lt;/li&gt;
&lt;li&gt; Support non-standard SSH ports.&lt;/li&gt;
&lt;li&gt; Log results for historical tracking.&lt;/li&gt;
&lt;li&gt; Get a quick summary of which hosts are "Reachable" vs "Unreachable."&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Solution: A Bash-Powered Monitor
&lt;/h2&gt;

&lt;p&gt;The core of this tool is a Bash script that uses SSH to execute a ping command on remote targets. Here's a look at the key features and how it works.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Robust Scripting with &lt;code&gt;set -euo pipefail&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;To make the script reliable, I used strict error handling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-e&lt;/code&gt;: Exit immediately if a command fails.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-u&lt;/code&gt;: Treat unset variables as errors.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-o pipefail&lt;/code&gt;: Ensure that if any part of a pipeline fails, the whole pipeline fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Intelligent Connectivity Checks
&lt;/h3&gt;

&lt;p&gt;The script doesn't just check if the server is "up"; it distinguishes between an &lt;strong&gt;SSH failure&lt;/strong&gt; (can't connect to the box) and a &lt;strong&gt;Network failure&lt;/strong&gt; (connected to the box, but the box can't reach the internet).&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;# A snippet of the core logic&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;ssh &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;StrictHostKeyChecking&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;no &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;BatchMode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;yes&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ssh_target&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
       &lt;span class="s2"&gt;"ping -c 3 8.8.8.8 &amp;gt; /dev/null 2&amp;gt;&amp;amp;1"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;log_result &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"REACHABLE"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ✓ &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt; - REACHABLE"&lt;/span&gt;
&lt;span class="k"&gt;else
    &lt;/span&gt;log_result &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"UNREACHABLE"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$TIMESTAMP&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"  ✗ &lt;/span&gt;&lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="s2"&gt; - UNREACHABLE (network issue)"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Automated Logging &amp;amp; Summaries
&lt;/h3&gt;

&lt;p&gt;Every run generates a timestamped log in &lt;code&gt;connectivity_log.txt&lt;/code&gt;, making it easy to spot patterns over time. At the end of each run, you get a clean summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;========================================
  Summary
========================================
  Total hosts checked: 3
  Reachable:          2
  Unreachable:        1
========================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Testing Safely with Docker
&lt;/h2&gt;

&lt;p&gt;One of the best parts of this project is the included &lt;strong&gt;Test Lab&lt;/strong&gt;. Using &lt;code&gt;docker-compose&lt;/code&gt;, you can spin up three Ubuntu-based SSH servers locally to test the script without risking your production environment.&lt;/p&gt;

&lt;p&gt;I've included a &lt;code&gt;setup.sh&lt;/code&gt; script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts the containers.&lt;/li&gt;
&lt;li&gt;Installs &lt;code&gt;ping&lt;/code&gt; (not present in minimal Ubuntu images).&lt;/li&gt;
&lt;li&gt;Configures SSH keys for passwordless login.&lt;/li&gt;
&lt;li&gt;Validates the environment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Try It Out
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Clone the project.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Generate an SSH key&lt;/strong&gt; if you haven't already: &lt;code&gt;ssh-keygen -t rsa&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Launch the test lab&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Run the monitor&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./monitor_connectivity.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;While building this, I ran into a few classic Bash "gotchas":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Loops vs &lt;code&gt;set -e&lt;/code&gt;&lt;/strong&gt;: Using a &lt;code&gt;while read&lt;/code&gt; loop with &lt;code&gt;set -e&lt;/code&gt; can cause the script to exit prematurely when it hits the end of a file. Switching to a &lt;code&gt;for&lt;/code&gt; loop with &lt;code&gt;grep&lt;/code&gt; solved this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker Ephemerality&lt;/strong&gt;: Since containers reset on restart, I automated the SSH key injection into the &lt;code&gt;setup.sh&lt;/code&gt; script to ensure a smooth developer experience.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Sometimes, the simplest tools are the most effective. This Bash script provides a no-nonsense way to monitor network health across your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check out the full source code here!&lt;/strong&gt; &lt;a href="https://github.com/alanvarghese-dev/Bash_Scripting/tree/main/network_connectivity_monitoring_tool" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What are your favorite "mini-tools" for server management? Let me know in the comments!&lt;/p&gt;

</description>
      <category>bash</category>
      <category>devops</category>
      <category>automation</category>
      <category>linux</category>
    </item>
    <item>
      <title>How I Built a Lightweight Cron Job Health Monitor with Bash and Docker</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Sun, 08 Mar 2026 18:16:58 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/how-i-built-a-lightweight-cron-job-health-monitor-with-bash-and-docker-16mh</link>
      <guid>https://dev.to/alanvarghese-dev/how-i-built-a-lightweight-cron-job-health-monitor-with-bash-and-docker-16mh</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tired of silent cron failures? Here's a lightweight Bash-based solution to monitor and alert on your scheduled tasks across multiple servers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h1&gt;
  
  
  Why Your Cron Jobs Need Monitoring
&lt;/h1&gt;

&lt;p&gt;We’ve all been there. You set up a "mission-critical" backup or data sync as a cron job, and then you forget about it. Six months later, you realize it hasn't run in weeks because of a silent failure, a disk space issue, or an SSH key change.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cron is great for execution, but it's terrible at visibility.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In this post, I'll walk through a lightweight, Bash-powered monitoring solution I built to keep tabs on cron jobs across multiple servers without needing a heavy agent like Zabbix or Datadog.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛠 The Architecture
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discover&lt;/strong&gt; jobs automatically from remote servers via SSH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor&lt;/strong&gt; their last execution time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alert&lt;/strong&gt; via Slack or Email if a job is "missed" or overdue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test&lt;/strong&gt; everything locally using Docker.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────┐
│        Monitoring Host        │
│  (Bash + cron_health_monitor) │
└───────────────┬───────────────┘
                │
        ┌───────┴───────┐
        ▼               ▼
  ┌──────────┐    ┌──────────┐
  │ Server A │    │ Server B │
  │ (Docker) │    │ (Docker) │
  └──────────┘    └──────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🚀 Key Features
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Automated SSH Discovery
&lt;/h3&gt;

&lt;p&gt;The script scans &lt;code&gt;crontab -l&lt;/code&gt; on remote servers, parses the schedules, and automatically adds them to its tracking list. No more manual entry for every single task.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. State-Based Tracking
&lt;/h3&gt;

&lt;p&gt;Instead of checking logs (which can be messy), the monitor looks at "last run" timestamps. Jobs can report their own completion via a simple CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./cron_health_monitor.sh record backup_job server1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Dockerized Testing Environment
&lt;/h3&gt;

&lt;p&gt;To ensure the monitor works before deploying it to production, I included a &lt;code&gt;docker-compose.yml&lt;/code&gt; that spins up three Ubuntu servers. This allows you to simulate real-world cron failures in a safe sandbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧩 The "Aha!" Moments (and Bugs)
&lt;/h2&gt;

&lt;p&gt;While building this, I ran into a few classic engineering hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The Date Dilemma&lt;/strong&gt;: I initially used BSD-style &lt;code&gt;date&lt;/code&gt; commands (macOS default), which broke completely on the Linux target servers. I had to switch to the more universal GNU &lt;code&gt;date -d&lt;/code&gt; syntax.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Docker Networking&lt;/strong&gt;: On macOS, connecting to &lt;code&gt;localhost&lt;/code&gt; inside a container can be tricky. Switching to &lt;code&gt;127.0.0.1&lt;/code&gt; fixed several "Host not found" errors.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Silent Failures&lt;/strong&gt;: I learned that if the SSH connection fails, the script should alert on the &lt;em&gt;connection&lt;/em&gt; failure, not just the missing cron job.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🛠 Quick Start
&lt;/h2&gt;

&lt;p&gt;If you want to try it out:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Clone &amp;amp; Setup&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ./setup.sh &lt;span class="c"&gt;# Generates keys &amp;amp; starts Docker test servers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Discover Jobs&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ./cron_health_monitor.sh discover
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Run Health Check&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   ./cron_health_monitor.sh check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  💡 Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Bash is incredibly powerful for infrastructure glue code. By combining standard tools like &lt;code&gt;ssh&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, and &lt;code&gt;grep&lt;/code&gt;, you can build a monitoring system that is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Zero-agent&lt;/strong&gt;: Nothing to install on target servers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-overhead&lt;/strong&gt;: Runs in milliseconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable&lt;/strong&gt;: Works on almost any Linux distro.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check out the full source code and my "bug log" in the repository!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/alanvarghese-dev/Bash_Scripting/tree/main/cron_job_health_monitor" rel="noopener noreferrer"&gt;https://github.com/alanvarghese-dev/Bash_Scripting/tree/main/cron_job_health_monitor&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What are you using to monitor your legacy cron jobs? Let's discuss in the comments!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>bash</category>
      <category>linux</category>
    </item>
    <item>
      <title>Stop SSH-ing One by One: Building a Parallel Command Executor in Bash</title>
      <dc:creator>Alan Varghese</dc:creator>
      <pubDate>Thu, 05 Mar 2026 20:29:19 +0000</pubDate>
      <link>https://dev.to/alanvarghese-dev/stop-ssh-ing-one-by-one-building-a-parallel-command-executor-in-bash-55m1</link>
      <guid>https://dev.to/alanvarghese-dev/stop-ssh-ing-one-by-one-building-a-parallel-command-executor-in-bash-55m1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Learn how to build a robust, multi-server SSH command runner using Bash, Docker, and parallel processing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As developers or system administrators, we've all been there: You need to check the disk space, uptime, or service status on 10 different servers.&lt;/p&gt;

&lt;p&gt;The "manual" way is painful:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ssh user@server1&lt;/code&gt; -&amp;gt; &lt;code&gt;df -h&lt;/code&gt; -&amp;gt; &lt;code&gt;exit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh user@server2&lt;/code&gt; -&amp;gt; &lt;code&gt;df -h&lt;/code&gt; -&amp;gt; &lt;code&gt;exit&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;...repeat 8 more times. 😫&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sure, tools like &lt;strong&gt;Ansible&lt;/strong&gt; exist, but sometimes you just want a lightweight, zero-dependency script to fire off a quick command and see what's happening &lt;em&gt;right now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;In this post, I'll walk you through how I built a &lt;strong&gt;Multi-Server SSH Executor&lt;/strong&gt; using pure Bash. We'll explore parallel processing, robust file parsing, and how to simulate a server cluster locally using Docker.&lt;/p&gt;

&lt;h2&gt;
  
  
  🎯 The Goal
&lt;/h2&gt;

&lt;p&gt;We want a script that takes a command (e.g., &lt;code&gt;uptime&lt;/code&gt;) and runs it on a list of servers defined in a config file.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Parallel Execution:&lt;/strong&gt; Use threading (background processes) so checking 10 servers takes as long as the slowest one, not the sum of all of them.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Robust Config Parsing:&lt;/strong&gt; Handle comments, weird whitespace, and different ports/users.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Local Testing Ground:&lt;/strong&gt; A way to test this without buying 5 VPS instances (spoiler: we use Docker).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🏗️ The Architecture
&lt;/h2&gt;

&lt;p&gt;The project consists of three main parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;servers.conf&lt;/code&gt;: A simple file defining our target servers.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;multi_ssh.sh&lt;/code&gt;: The brains of the operation.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;docker-compose.yml&lt;/code&gt;: A simulated lab environment with 4 SSH-enabled containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  1. The Configuration
&lt;/h3&gt;

&lt;p&gt;I wanted a simple format that's easy to read but flexible:&lt;br&gt;
&lt;code&gt;name:hostname:port:username&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Production Servers
web01:192.168.1.10:22:admin
db01:192.168.1.20:22:dbadmin

# Docker Lab (Localhost mapped ports)
web1:localhost:2221:root
web2:localhost:2222:root
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The Simulation (Docker Lab)
&lt;/h3&gt;

&lt;p&gt;Testing SSH scripts on production servers is... brave. Instead, I used &lt;code&gt;docker-compose&lt;/code&gt; to spin up lightweight Ubuntu containers running &lt;code&gt;sshd&lt;/code&gt;.&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;web1&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;rastasheep/ubuntu-sshd:18.04&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;2221:22"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;web2&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;rastasheep/ubuntu-sshd:18.04&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;2222:22"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now I have "real" servers running on localhost ports 2221, 2222, etc.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚡ The "Secret Sauce": Parallelism in Bash
&lt;/h2&gt;

&lt;p&gt;The core challenge is running commands simultaneously. In Bash, we do this by putting a command in the background with &lt;code&gt;&amp;amp;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is the simplified logic:&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;# Loop through servers&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;server &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;servers&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c"&gt;# Run SSH in the background&lt;/span&gt;
    ssh &lt;span class="nv"&gt;$user&lt;/span&gt;@&lt;span class="nv"&gt;$host&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$command&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"/tmp/result_&lt;/span&gt;&lt;span class="nv"&gt;$server&lt;/span&gt;&lt;span class="s2"&gt;.txt"&lt;/span&gt; &amp;amp;

    &lt;span class="c"&gt;# Save the Process ID (PID)&lt;/span&gt;
    pids+&lt;span class="o"&gt;=(&lt;/span&gt;&lt;span class="nv"&gt;$!&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Wait for all background jobs to finish&lt;/span&gt;
&lt;span class="nb"&gt;wait&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple trick reduces execution time from &lt;strong&gt;(N * Timeout)&lt;/strong&gt; to &lt;strong&gt;(Max(Timeout))&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧠 Lessons Learned &amp;amp; "Gotchas"
&lt;/h2&gt;

&lt;p&gt;Writing the script revealed a few common Bash pitfalls that I had to fix to make it production-ready.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 1: &lt;code&gt;for&lt;/code&gt; loops vs. &lt;code&gt;while read&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Initially, I used a &lt;code&gt;for&lt;/code&gt; loop to read lines from the config file.&lt;br&gt;
&lt;strong&gt;The Trap:&lt;/strong&gt; If a line has spaces (like a description), &lt;code&gt;for&lt;/code&gt; splits it into multiple items.&lt;br&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Use a &lt;code&gt;while&lt;/code&gt; loop with a custom Internal Field Separator (&lt;code&gt;IFS&lt;/code&gt;).&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;# Robust way to read lines&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nv"&gt;IFS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt; &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; name &lt;span class="nb"&gt;hostname &lt;/span&gt;port username &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="s2"&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;do&lt;/span&gt;
    &lt;span class="c"&gt;# Process server...&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt; &amp;lt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$config_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note the &lt;code&gt;|| [[ -n "$name" ]]&lt;/code&gt; part—this ensures we don't skip the last line if the file doesn't end with a newline character!&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Lesson 2: Race Conditions &amp;amp; Temp Files
&lt;/h3&gt;

&lt;p&gt;When running parallel jobs, you can't just write to &lt;code&gt;output.txt&lt;/code&gt;. Multiple processes will write at the same time, garbling the text.&lt;br&gt;
&lt;strong&gt;The Fix:&lt;/strong&gt; Give each process its own temporary file (e.g., &lt;code&gt;/tmp/ssh_result_web1.txt&lt;/code&gt;), let them finish, and &lt;em&gt;then&lt;/em&gt; aggregate the results sequentially.&lt;/p&gt;

&lt;p&gt;I used &lt;code&gt;mktemp&lt;/code&gt; to ensure my temporary files never collided with other running instances of the script.&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="nv"&gt;SERVERS_LIST_TMP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;mktemp&lt;/span&gt; /tmp/ssh_multi_servers.XXXXXX&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Lesson 3: SSH is picky
&lt;/h3&gt;

&lt;p&gt;Running SSH non-interactively requires specific flags to avoid hanging:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;-o BatchMode=yes&lt;/code&gt;: Fail instead of asking for a password.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-o ConnectTimeout=X&lt;/code&gt;: Don't wait forever if a server is down.&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;-o StrictHostKeyChecking=no&lt;/code&gt;: Crucial for automated environments where IPs might change (like Docker containers).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🚀 The Result
&lt;/h2&gt;

&lt;p&gt;Running &lt;code&gt;./multi_ssh.sh "df -h"&lt;/code&gt; gives me a beautiful, color-coded summary of disk space across my entire fleet in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  📥 Try It Yourself
&lt;/h2&gt;

&lt;p&gt;I've open-sourced this tool along with the setup script that automatically generates SSH keys and configures the Docker containers for you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Docker &amp;amp; Docker Compose&lt;/li&gt;
&lt;li&gt;  &lt;code&gt;sshpass&lt;/code&gt; (for the initial setup script)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Installation:&lt;/strong&gt;&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 https://github.com/alanvarghese-dev/Bash_Scripting/tree/main/ssh_multi_server_executor.git

&lt;span class="nb"&gt;cd &lt;/span&gt;ssh-multi-server-executor
./ssh_install.sh  &lt;span class="c"&gt;# Sets up the Docker lab&lt;/span&gt;
./multi_ssh.sh &lt;span class="s2"&gt;"uptime"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me know in the comments if you prefer Bash for these tasks or if you stick to heavier tools like Ansible!&lt;/p&gt;

&lt;p&gt;Happy scripting! 💻✨&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>devops</category>
      <category>automation</category>
    </item>
  </channel>
</rss>
