<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Mordecai </title>
    <description>The latest articles on Forem by Mordecai  (@mordecai_amehson).</description>
    <link>https://forem.com/mordecai_amehson</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1155526%2Fdf80c2f5-f4a3-48e8-9618-ca2b38088c96.jpg</url>
      <title>Forem: Mordecai </title>
      <link>https://forem.com/mordecai_amehson</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mordecai_amehson"/>
    <language>en</language>
    <item>
      <title>Understanding How Containers Communicate in Docker and Kubernetes</title>
      <dc:creator>Mordecai </dc:creator>
      <pubDate>Thu, 07 May 2026 15:44:49 +0000</pubDate>
      <link>https://forem.com/mordecai_amehson/understanding-how-containers-communicate-in-docker-and-kubernetes-17b4</link>
      <guid>https://forem.com/mordecai_amehson/understanding-how-containers-communicate-in-docker-and-kubernetes-17b4</guid>
      <description>&lt;p&gt;&lt;em&gt;A beginner-friendly guide to Docker networking&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;When you run an application in Docker, it doesn't automatically know how to reach other services. A container is isolated by default — it has its own network namespace, its own IP address, and its own view of the world. For two services to talk, you have to explicitly &lt;em&gt;connect&lt;/em&gt; them.&lt;br&gt;
We would be exploring possible scenarios for communication among containers.&lt;/p&gt;

&lt;p&gt;This is something I learned while building &lt;a href="https://dev.to/mordecai_amehson/swiftdeploy-a-tool-that-writes-its-own-infrastructure-170d"&gt;SwiftDeploy&lt;/a&gt;. My Go API and Nginx were in separate containers and I didn't have full understanding of ow they communicated with each other.&lt;/p&gt;


&lt;h2&gt;
  
  
  Scenario 1: Two Containers Talking to Each Other
&lt;/h2&gt;

&lt;p&gt;This is the most common scenario — an API and a database, or a frontend and a backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wrong way&lt;/strong&gt; is to use &lt;code&gt;localhost&lt;/code&gt;. If your API tries to connect to &lt;code&gt;localhost:5432&lt;/code&gt; for PostgreSQL, it won't work. Inside a container, &lt;code&gt;localhost&lt;/code&gt; refers to the container itself — not your host machine, not another container.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The right way&lt;/strong&gt; is to use a Docker network. When two containers join the same network, they can reach each other by &lt;strong&gt;service name&lt;/strong&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;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;myapp-net&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;myapp-net&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&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;myapp-net&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the API can connect to the database using &lt;code&gt;database:5432&lt;/code&gt; — Docker's internal DNS resolves the service name to the container's IP automatically.&lt;/p&gt;

&lt;p&gt;In SwiftDeploy, Nginx reaches the API using &lt;code&gt;api:3000&lt;/code&gt; — not &lt;code&gt;localhost:3000&lt;/code&gt;. That's why it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it works under the hood:&lt;/strong&gt;&lt;br&gt;
Docker creates a virtual bridge network. Every container on that network gets an internal IP (like &lt;code&gt;172.18.0.2&lt;/code&gt;). Docker runs an internal DNS server that maps service names to these IPs. When the API says "connect to &lt;code&gt;database&lt;/code&gt;", Docker's DNS resolves it to &lt;code&gt;172.18.0.3&lt;/code&gt; or whatever IP the database got.&lt;/p&gt;


&lt;h2&gt;
  
  
  Scenario 2: Container Talking to the Host Machine
&lt;/h2&gt;

&lt;p&gt;Sometimes a container needs to reach something running directly on your laptop — like a local development server or a database running outside Docker.&lt;/p&gt;

&lt;p&gt;You can't use &lt;code&gt;localhost&lt;/code&gt; from inside a container to reach the host. Instead use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;On Mac/Windows:&lt;/strong&gt; &lt;code&gt;host.docker.internal&lt;/code&gt; — Docker provides this hostname automatically&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On Linux:&lt;/strong&gt; &lt;code&gt;172.17.0.1&lt;/code&gt; — the default Docker bridge gateway IP
&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="c1"&gt;# Inside a container on Linux
&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;172.17.0.1:5432&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Inside a container on Mac/Windows
&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host.docker.internal:5432&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;p&gt;Alternatively, use &lt;code&gt;--network host&lt;/code&gt; when running the container — this removes the network isolation entirely and the container shares the host's network stack. &lt;code&gt;localhost&lt;/code&gt; works again but you lose isolation.&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;--network&lt;/span&gt; host my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Scenario 3: Two Different Applications on the Same Machine (No Docker)
&lt;/h2&gt;

&lt;p&gt;When two regular applications run on the same machine — no containers — they communicate through &lt;strong&gt;localhost&lt;/strong&gt; and &lt;strong&gt;ports&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Application A listens on port 8000:&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="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;8000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application B connects to it:&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/api&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;p&gt;The operating system routes the traffic internally — it never leaves the machine. This is fast but means both apps must be on the same machine.&lt;/p&gt;




&lt;h2&gt;
  
  
  Scenario 4: One Container, One Regular Application
&lt;/h2&gt;

&lt;p&gt;This is the reverse proxy pattern — exactly what SwiftDeploy uses.&lt;/p&gt;

&lt;p&gt;Nginx runs in a container. The API runs as a regular process on the host. How does Nginx reach the API?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option 1 — Port mapping:&lt;/strong&gt;&lt;br&gt;
Map the API's host port into the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# API runs on host port 3000&lt;/span&gt;
&lt;span class="c"&gt;# Nginx container uses host.docker.internal:3000 to reach it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option 2 — Host network mode:&lt;/strong&gt;&lt;br&gt;
Run Nginx with &lt;code&gt;--network host&lt;/code&gt;. Now it can use &lt;code&gt;localhost:3000&lt;/code&gt; directly.&lt;/p&gt;

&lt;p&gt;In SwiftDeploy both the API and Nginx run in containers on the same Docker network — so they use service name discovery instead. But the pattern above is common in development setups.&lt;/p&gt;


&lt;h2&gt;
  
  
  Scenario 5: Kubernetes — How Pods Communicate
&lt;/h2&gt;

&lt;p&gt;In Kubernetes, containers run inside &lt;strong&gt;pods&lt;/strong&gt;. Communication works at two levels:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Within a pod — containers share localhost:&lt;/strong&gt;&lt;br&gt;
If two containers are in the same pod they share a network namespace. They communicate on &lt;code&gt;localhost&lt;/code&gt; just like two processes on the same machine.&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="c1"&gt;# Both containers in this pod share localhost&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&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;api&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="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&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;sidecar&lt;/span&gt;
      &lt;span class="c1"&gt;# can reach api at localhost:8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Between pods — use Services:&lt;/strong&gt;&lt;br&gt;
Pods get dynamic IPs that change when they restart. You never hardcode a pod IP. Instead you create a &lt;strong&gt;Service&lt;/strong&gt; — a stable DNS name that routes to whatever pods match a label selector.&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;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&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;api-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api&lt;/span&gt;          &lt;span class="c1"&gt;# routes to pods with this label&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="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now any pod in the cluster can reach the API at &lt;code&gt;api-service:80&lt;/code&gt; — Kubernetes DNS resolves it to the right pod IP automatically. Even if the pod restarts and gets a new IP, the Service name stays the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ClusterIP vs NodePort vs LoadBalancer:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ClusterIP&lt;/strong&gt; — only accessible inside the cluster (like Docker's internal network)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;NodePort&lt;/strong&gt; — exposes the service on every node's IP at a specific port&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoadBalancer&lt;/strong&gt; — provisions a cloud load balancer with a public IP&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary Table
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;How they communicate&lt;/th&gt;
&lt;th&gt;Key tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container ↔ Container&lt;/td&gt;
&lt;td&gt;Service name on shared network&lt;/td&gt;
&lt;td&gt;Docker network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container → Host&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;host.docker.internal&lt;/code&gt; or &lt;code&gt;172.17.0.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Docker bridge gateway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;App ↔ App (no Docker)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:port&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;OS network stack&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container ↔ App&lt;/td&gt;
&lt;td&gt;Port mapping or host network&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ports:&lt;/code&gt; in compose&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod ↔ Pod (Kubernetes)&lt;/td&gt;
&lt;td&gt;Service DNS name&lt;/td&gt;
&lt;td&gt;Kubernetes Service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pod ↔ Pod (same pod)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Shared network namespace&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  What I Learned Building SwiftDeploy
&lt;/h2&gt;

&lt;p&gt;When setting up SwiftDeploy, since both Nginx and the Go API were running in separate containers, Nginx used Docker service discovery (api:3000) rather than localhost to communicate with the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Wrong — localhost doesn't reach another container&lt;/span&gt;
&lt;span class="k"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;# Right — use the service name&lt;/span&gt;
&lt;span class="k"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://api:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How SwiftDeploy Was Structured
&lt;/h2&gt;

&lt;p&gt;SwiftDeploy used multiple containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Client
   ↓
Nginx Container
   ↓
API Container

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Later, I added:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;OPA container&lt;br&gt;
observability and metrics&lt;br&gt;
policy evaluation&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;All of these containers needed controlled communication.&lt;/p&gt;

&lt;p&gt;The important design decision was:&lt;/p&gt;

&lt;p&gt;only Nginx was publicly exposed&lt;br&gt;
internal services stayed inside the Docker network&lt;/p&gt;

&lt;p&gt;That separation was intentional for both architecture and security reasons.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;One word change. That's how important Docker networking is to understand. Once I put both containers on the same named network and used the service name, everything worked.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The mental model that helped me most: each container is like a separate &lt;em&gt;computer&lt;/em&gt;. To connect two computers you need a &lt;em&gt;network&lt;/em&gt;. Docker networks are that network, and service names are like &lt;em&gt;hostnames&lt;/em&gt;.
&lt;/h2&gt;




&lt;p&gt;&lt;em&gt;Read my SwiftDeploy project writeup here: &lt;a href="https://dev.to/mordecai_amehson/swiftdeploy-a-tool-that-writes-its-own-infrastructure-170d"&gt;https://dev.to/mordecai_amehson/swiftdeploy-a-tool-that-writes-its-own-infrastructure-170d&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;




</description>
      <category>docker</category>
      <category>kubernetes</category>
      <category>containers</category>
      <category>devops</category>
    </item>
    <item>
      <title>SwiftDeploy: A Tool That Writes Its Own Infrastructure</title>
      <dc:creator>Mordecai </dc:creator>
      <pubDate>Thu, 07 May 2026 03:39:01 +0000</pubDate>
      <link>https://forem.com/mordecai_amehson/swiftdeploy-a-tool-that-writes-its-own-infrastructure-170d</link>
      <guid>https://forem.com/mordecai_amehson/swiftdeploy-a-tool-that-writes-its-own-infrastructure-170d</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzm4vc5m3pi51b180aen5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzm4vc5m3pi51b180aen5.png" alt=" " width="800" height="729"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What is SwiftDeploy?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Most DevOps work involves writing config files manually — nginx.conf, docker-compose.yml, environment variables. SwiftDeploy flips this. You describe what you want in one file (manifest.yaml) and the tool generates everything else automatically.&lt;br&gt;
The manifest is the single source of truth. Every generated file derives from it. Change the manifest, regenerate, everything updates consistently.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 1: The Design — A Tool That Writes Its Own Files
&lt;/h2&gt;

&lt;p&gt;The core idea is template substitution. I created two template files with placeholder variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;templates/nginx.conf.tmpl&lt;/span&gt;      &lt;span class="s"&gt;→&lt;/span&gt; &lt;span class="s"&gt;contains&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;NGINX_PORT&lt;/span&gt;&lt;span class="err"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;SERVICE_PORT&lt;/span&gt;&lt;span class="err"&gt;}}&lt;/span&gt;
&lt;span class="nc"&gt;templates/docker-compose&lt;/span&gt;&lt;span class="s"&gt;.yml.tmpl&lt;/span&gt; &lt;span class="s"&gt;→&lt;/span&gt; &lt;span class="s"&gt;contains&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;SERVICE_IMAGE&lt;/span&gt;&lt;span class="err"&gt;}}&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;SERVICE_MODE&lt;/span&gt;&lt;span class="err"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run swiftdeploy init, the CLI reads the manifest and uses sed to replace every placeholder with the real value:&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;sed&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"s|{{NGINX_PORT}}|8080|g"&lt;/span&gt; templates/nginx.conf.tmpl &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; nginx.conf

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Nothing is hardcoded&lt;br&gt;
Change the manifest, run init, get fresh configs&lt;br&gt;
The grader can delete generated files, run init, and verify everything regenerates correctly&lt;/p&gt;

&lt;p&gt;The API is written in Go — a single binary that compiles down to 11.9MB. No runtime dependencies, fast startup, well within the 300MB image size limit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 2: The Guardrails — OPA Policy Engine
&lt;/h2&gt;

&lt;p&gt;Before deploying or promoting, SwiftDeploy asks OPA: "is this allowed?"&lt;br&gt;
OPA (Open Policy Agent) is a separate container that makes yes/no decisions based on rules you write in a language called Rego. The key principle is the CLI never makes the decision itself — it just asks OPA and surfaces the answer.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Why isolate decisions in OPA?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you hardcode thresholds in the CLI:&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="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$DISK_GB&lt;/span&gt; &lt;span class="nt"&gt;-lt&lt;/span&gt; 10 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Changing a threshold means editing the CLI code, testing it, redeploying. With OPA, you edit a policy file and restart OPA. The CLI doesn't change.&lt;br&gt;
Infrastructure policy&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;infrastructure&lt;/span&gt;

&lt;span class="ow"&gt;default&lt;/span&gt; &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;disk_free_gb&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_disk_free_gb&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_load&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_cpu_load&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds live in a separate JSON file — not hardcoded in the Rego. Change thresholds.json, restart OPA, new limits apply immediately.&lt;br&gt;
Canary safety policy&lt;br&gt;
Before promoting canary to stable, the CLI scrapes /metrics and sends the data to OPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rego"&gt;&lt;code&gt;&lt;span class="ow"&gt;package&lt;/span&gt; &lt;span class="n"&gt;canary&lt;/span&gt;

&lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="n"&gt;if&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_error_rate&lt;/span&gt;
    &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;p99_latency_ms&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;thresholds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_p99_latency_ms&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If error rate exceeds 1% or P99 latency exceeds 500ms, promotion is blocked with a clear message.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 3: The Chaos — What Happens When Things Break
&lt;/h2&gt;

&lt;p&gt;The API has a /chaos endpoint (canary mode only) that simulates degraded behaviour:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Inject 80% error rate
&lt;span class="go"&gt;curl -X POST http://localhost:8080/chaos \
  -d '{"mode": "error", "rate": 0.8}'

&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Try to promote — gets blocked
&lt;span class="go"&gt;./swiftdeploy promote stable
&lt;/span&gt;&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;→ Error rate 80.00% exceeds maximum 1.00%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran this during testing and it worked exactly as designed. The canary policy caught the degraded state and blocked promotion. Once I recovered chaos and restarted the API to reset metrics, the promotion succeeded.&lt;br&gt;
This is the value of the policy gate — it prevents you from accidentally promoting a broken canary to production.&lt;/p&gt;
&lt;h2&gt;
  
  
  Part 4: Live Metrics and Audit
&lt;/h2&gt;

&lt;p&gt;The API exposes /metrics in Prometheus format:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight prometheus"&gt;&lt;code&gt;&lt;span class="n"&gt;http_requests_total&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="na"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"200"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;
&lt;span class="n"&gt;http_request_duration_seconds_p99&lt;/span&gt; &lt;span class="mf"&gt;0.0034&lt;/span&gt;
&lt;span class="n"&gt;app_mode&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="n"&gt;chaos_active&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;swiftdeploy status scrapes this every 3 seconds and shows a live dashboard. Every scrape appends to history.jsonl. swiftdeploy audit then parses this file and generates audit_report.md — a markdown table showing mode changes, error rates, and policy violations over time.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Timing matters with containers — OPA needs to start before the policy check runs. I had to start OPA first, wait 4 seconds, run the check, then bring up the rest of the stack.&lt;/li&gt;
&lt;li&gt;Metrics are cumulative — when testing chaos, errors accumulate in the counter. Restarting the API resets the counter. In production you'd use a sliding window.&lt;/li&gt;
&lt;li&gt;Generated files don't belong in git — they're derived from the manifest. Anyone cloning the repo runs swiftdeploy init to get fresh configs.&lt;/li&gt;
&lt;li&gt;Go was the right choice — the API image is 11.9MB. A Python equivalent would be 200MB+. Single binary, no dependencies, instant startup.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;Full source code: &lt;a href="https://github.com/Hacker-Dark/swiftdeploy" rel="noopener noreferrer"&gt;https://github.com/Hacker-Dark/swiftdeploy&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
    </item>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine from Scratch</title>
      <dc:creator>Mordecai </dc:creator>
      <pubDate>Mon, 27 Apr 2026 03:12:54 +0000</pubDate>
      <link>https://forem.com/mordecai_amehson/how-i-built-a-real-time-ddos-detection-engine-from-scratch-cll</link>
      <guid>https://forem.com/mordecai_amehson/how-i-built-a-real-time-ddos-detection-engine-from-scratch-cll</guid>
      <description>&lt;p&gt;Imagine you run a cloud storage platform serving thousands of users. One day, an attacker floods your server with millions of requests per second. Your server crashes. Real users can't access their files. You lose money and trust.&lt;br&gt;
This is a DDoS attack — Distributed Denial of Service. The goal of this project was to build a tool that detects these attacks automatically and blocks them before they cause damage.&lt;br&gt;
No off-the-shelf tools. No Fail2Ban. Pure Python, built from scratch.&lt;/p&gt;

&lt;p&gt;What the Tool Does&lt;br&gt;
Here's the full picture of what I built:&lt;br&gt;
Nginx (logs every request as JSON)&lt;br&gt;
         ↓&lt;br&gt;
Detector daemon reads logs in real time&lt;br&gt;
         ↓&lt;br&gt;
Sliding window tracks request rates&lt;br&gt;
         ↓&lt;br&gt;
Baseline learns what normal traffic looks like&lt;br&gt;
         ↓&lt;br&gt;
Anomaly detector compares current rate to baseline&lt;br&gt;
         ↓&lt;br&gt;
If anomalous → block IP with iptables + send Slack alert&lt;br&gt;
         ↓&lt;br&gt;
Auto-unban after cooldown period&lt;br&gt;
Everything runs continuously as a background service on a Linux server.&lt;/p&gt;

&lt;p&gt;Part 1: Reading Nginx Logs in Real Time&lt;br&gt;
The first challenge was getting the tool to watch incoming traffic live. Nginx was configured to write every HTTP request as a JSON line to a log file:&lt;br&gt;
json{&lt;br&gt;
  "source_ip": "102.91.99.217",&lt;br&gt;
  "timestamp": "2026-04-27T02:31:00+00:00",&lt;br&gt;
  "method": "GET",&lt;br&gt;
  "path": "/",&lt;br&gt;
  "status": 200,&lt;br&gt;
  "response_size": 6674&lt;br&gt;
}&lt;br&gt;
To read this in real time, I used a technique called log tailing — the same thing tail -f does in Linux. The program opens the file, jumps to the end, and then sits in a loop reading new lines as they appear:&lt;br&gt;
pythonwith open(log_path, "r") as f:&lt;br&gt;
    f.seek(0, 2)  # jump to end of file&lt;br&gt;
    while True:&lt;br&gt;
        line = f.readline()&lt;br&gt;
        if not line:&lt;br&gt;
            time.sleep(0.1)  # wait for new data&lt;br&gt;
            continue&lt;br&gt;
        yield parse_line(line)  # process the line&lt;br&gt;
Every time Nginx writes a new request, the detector picks it up within 100 milliseconds.&lt;/p&gt;

&lt;p&gt;Part 2: The Sliding Window&lt;br&gt;
Now that we're reading requests in real time, we need to know how fast each IP is sending requests. This is where the sliding window comes in.&lt;br&gt;
A sliding window answers the question: "How many requests has this IP sent in the last 60 seconds?"&lt;br&gt;
The naive approach would be a counter that resets every minute. But that's inaccurate — an attacker could send 1000 requests in the last 10 seconds of one minute and the first 10 seconds of the next, and the counter would never catch it.&lt;br&gt;
Instead, I used Python's collections.deque — a double-ended queue that lets us add to one end and remove from the other efficiently.&lt;br&gt;
Here's how it works:&lt;br&gt;
pythonfrom collections import deque&lt;br&gt;
import time&lt;/p&gt;

&lt;p&gt;ip_window = deque()  # stores timestamps of recent requests&lt;/p&gt;

&lt;p&gt;def record_request(ip):&lt;br&gt;
    now = time.time()&lt;br&gt;
    cutoff = now - 60  # 60 second window&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Add current timestamp
ip_window.append(now)

# Evict timestamps older than 60 seconds from the left
while ip_window and ip_window[0] &amp;lt; cutoff:
    ip_window.popleft()

# Rate = number of requests in window / window size
rate = len(ip_window) / 60
return rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Every time a request comes in, we add its timestamp. Every time we check the rate, we first remove any timestamps older than 60 seconds from the left side of the deque. The rate is simply the count of remaining timestamps divided by 60.&lt;br&gt;
This gives us an accurate, always up-to-date requests-per-second count for every IP on the server.&lt;/p&gt;

&lt;p&gt;Part 3: The Baseline — Teaching the Tool What "Normal" Looks Like&lt;br&gt;
Knowing the current rate isn't enough. We need to know if that rate is unusual.&lt;br&gt;
At 3am, 5 requests per second might be suspicious. At noon, it might be completely normal. The tool needs to learn from actual traffic patterns — not from hardcoded values.&lt;br&gt;
This is the rolling baseline. Here's how it works:&lt;/p&gt;

&lt;p&gt;Every second, we record how many requests the server received that second&lt;br&gt;
We keep a 30-minute history of these per-second counts&lt;br&gt;
Every 60 seconds, we calculate the mean (average) and standard deviation of these counts&lt;/p&gt;

&lt;p&gt;pythonsamples = [count for _, count in rolling_window]&lt;/p&gt;

&lt;p&gt;mean = sum(samples) / len(samples)&lt;br&gt;
variance = sum((x - mean) ** 2 for x in samples) / len(samples)&lt;br&gt;
stddev = math.sqrt(variance)&lt;br&gt;
The mean tells us what a typical second looks like. The standard deviation tells us how much variation is normal.&lt;br&gt;
I also maintain per-hour slots — separate baselines for each hour of the day. If the current hour has enough data (at least 5 samples), I prefer that over the general baseline. This means the tool naturally adapts to rush hours vs quiet hours.&lt;br&gt;
To prevent the tool from failing on a fresh start with no data, I set floor values:&lt;/p&gt;

&lt;p&gt;Minimum mean: 0.1 req/s&lt;br&gt;
Minimum stddev: 0.1&lt;/p&gt;

&lt;p&gt;Part 4: Detecting Anomalies&lt;br&gt;
With a baseline established, detection becomes a statistical question: "Is this IP's current rate unusually high compared to normal?"&lt;br&gt;
I use two detection methods — whichever fires first:&lt;br&gt;
Method 1: Z-Score&lt;br&gt;
The z-score measures how many standard deviations above the mean a value is:&lt;br&gt;
pythonz_score = (current_rate - baseline_mean) / baseline_stddev&lt;br&gt;
If the z-score exceeds 1.5, the IP is anomalous. A z-score of 1.5 means the rate is 1.5 standard deviations above normal — statistically unusual.&lt;br&gt;
For example:&lt;/p&gt;

&lt;p&gt;Baseline mean: 1.0 req/s&lt;br&gt;
Baseline stddev: 0.5&lt;br&gt;
Current rate: 2.5 req/s&lt;br&gt;
Z-score: (2.5 - 1.0) / 0.5 = 3.0 → anomalous!&lt;/p&gt;

&lt;p&gt;Method 2: Rate Multiplier&lt;br&gt;
Sometimes the stddev is very small and the z-score math doesn't capture obvious spikes. So I also check if the rate is more than 1.5x the baseline mean:&lt;br&gt;
pythonif current_rate &amp;gt; 1.5 * baseline_mean:&lt;br&gt;
    # flag as anomalous&lt;br&gt;
Error Rate Tightening&lt;br&gt;
If an IP is sending a lot of 4xx or 5xx errors (bad requests, unauthorized attempts), I automatically tighten the thresholds by 30%. An IP probing for vulnerabilities gets less tolerance.&lt;/p&gt;

&lt;p&gt;Part 5: Blocking with iptables&lt;br&gt;
When an IP is flagged as anomalous, we block it at the kernel level using iptables. This is more powerful than blocking at the application level because the packets are dropped before they even reach Nginx.&lt;br&gt;
pythonimport subprocess&lt;/p&gt;

&lt;p&gt;def ban_ip(ip):&lt;br&gt;
    subprocess.run([&lt;br&gt;
        "iptables", "-I", "INPUT", "-s", ip, "-j", "DROP"&lt;br&gt;
    ])&lt;br&gt;
The -I INPUT inserts the rule at the top of the INPUT chain. -j DROP silently drops all packets from that IP. The attacker's requests never even reach the server.&lt;br&gt;
You can verify bans are active with:&lt;br&gt;
bashsudo iptables -L INPUT -n&lt;br&gt;
Auto-Unban with Backoff Schedule&lt;br&gt;
Permanent bans aren't always appropriate — the IP might be a legitimate user who got flagged by mistake. So I implemented an automatic unban system with a backoff schedule:&lt;/p&gt;

&lt;p&gt;First offence: banned for 10 minutes&lt;br&gt;
Second offence: banned for 30 minutes&lt;br&gt;
Third offence: banned for 2 hours&lt;br&gt;
Fourth offence: permanently banned&lt;/p&gt;

&lt;p&gt;Each time an IP is unbanned, a Slack notification is sent with the next ban duration if they reoffend.&lt;/p&gt;

&lt;p&gt;Part 6: Slack Alerts&lt;br&gt;
Every ban and unban sends an immediate Slack notification via webhook:&lt;br&gt;
pythonrequests.post(webhook_url, json={&lt;br&gt;
    "text": f"🚨 IP BANNED: {ip}\nRate: {rate} req/s\nBaseline: {mean} req/s\nDuration: {duration}"&lt;br&gt;
})&lt;br&gt;
The alert includes the condition that fired, the current rate, the baseline, and the ban duration — everything needed to understand what happened without digging through logs.&lt;/p&gt;

&lt;p&gt;Part 7: The Live Dashboard&lt;br&gt;
The tool serves a web dashboard on port 8080 that refreshes every 3 seconds showing:&lt;/p&gt;

&lt;p&gt;Global requests per second&lt;br&gt;
Current baseline mean and stddev&lt;br&gt;
List of banned IPs with reasons&lt;br&gt;
Top 10 source IPs&lt;br&gt;
CPU and memory usage&lt;/p&gt;

&lt;p&gt;Built with pure Python's http.server — no frameworks needed.&lt;/p&gt;

&lt;p&gt;What I Learned&lt;br&gt;
Building this from scratch taught me things no tutorial ever could:&lt;/p&gt;

&lt;p&gt;Statistical anomaly detection is surprisingly approachable once you understand z-scores&lt;br&gt;
deque is one of the most useful Python data structures for time-based problems&lt;br&gt;
iptables is incredibly powerful — blocking at kernel level is orders of magnitude more efficient than application-level blocking&lt;br&gt;
Baselines must be dynamic — hardcoded thresholds always fail in production because traffic patterns change by hour, day, and season&lt;br&gt;
A daemon is not a cron job — continuous processing requires careful thought about memory, threading, and graceful shutdown&lt;/p&gt;

&lt;p&gt;The Stack&lt;/p&gt;

&lt;p&gt;Python 3.12 — detector daemon&lt;br&gt;
Docker + Docker Compose — Nextcloud and Nginx deployment&lt;br&gt;
Nginx — reverse proxy with JSON access logging&lt;br&gt;
iptables — kernel-level IP blocking&lt;br&gt;
Slack webhooks — real-time alerts&lt;br&gt;
systemd — keeps the daemon running persistently&lt;/p&gt;

&lt;p&gt;Repository&lt;br&gt;
The full source code is available at:&lt;br&gt;
&lt;a href="https://github.com/Hacker-Dark/hng-stage3-devops" rel="noopener noreferrer"&gt;https://github.com/Hacker-Dark/hng-stage3-devops&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built as part of the HNG DevOps Internship Stage 3 task.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
