<?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: marlinekhavele</title>
    <description>The latest articles on Forem by marlinekhavele (@khavelemarline).</description>
    <link>https://forem.com/khavelemarline</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%2F373307%2F54c3c286-f37b-4911-a903-70dbd7295f1d.jpg</url>
      <title>Forem: marlinekhavele</title>
      <link>https://forem.com/khavelemarline</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/khavelemarline"/>
    <language>en</language>
    <item>
      <title>How I Built a Real Time DDoS Detection Engine from Scratch</title>
      <dc:creator>marlinekhavele</dc:creator>
      <pubDate>Wed, 29 Apr 2026 21:40:20 +0000</pubDate>
      <link>https://forem.com/khavelemarline/how-i-built-a-real-time-ddos-detection-engine-from-scratch-1bei</link>
      <guid>https://forem.com/khavelemarline/how-i-built-a-real-time-ddos-detection-engine-from-scratch-1bei</guid>
      <description>&lt;p&gt;As a beginner Imagine you run a cloud storage platform. Thousands of users upload files, share documents, and log in every day. Then one afternoon, traffic suddenly spikes  thousands of requests per second hammering your server from a single IP address. Your server slows down. Legitimate users can't log in. You're under attack.&lt;/p&gt;

&lt;p&gt;The traditional answer is Fail2Ban a tool that watches logs and blocks IPs. But what if you had to build that yourself, from first principles? That's exactly what this project is a custom anomaly detection daemon that watches HTTP traffic in real time, learns what "normal" looks like, and automatically blocks attackers via iptables.&lt;br&gt;
No Fail2Ban. No rate limiting libraries. Just Python, math, and Linux.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What the System Does&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; receives all incoming HTTP requests and writes each one as a JSON log line&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;detector daemon&lt;/strong&gt; tails that log file continuously, line by line&lt;/li&gt;
&lt;li&gt;For every request, it asks:"&lt;em&gt;Is this IP behaving abnormally compared to recent history&lt;/em&gt;?"&lt;/li&gt;
&lt;li&gt;If yes, it adds an &lt;strong&gt;iptables DROP rule&lt;/strong&gt;to block that IP at the kernel level&lt;/li&gt;
&lt;li&gt;It sends a &lt;strong&gt;Slack alert&lt;/strong&gt;so the team knows what happened&lt;/li&gt;
&lt;li&gt;After a timeout (10 minutes, 30 minutes, 2 hours, or permanent depending on repeat offenders), it automatically lifts the ban&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;live web dashboard&lt;/strong&gt; shows banned IPs, traffic rates, and system health in real time&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Reading the Logs  The Monitor&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Before we can detect anything, we need to read the Nginx access log. Nginx writes one line per HTTP request. We configure it to write in JSON format so parsing is clean:&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;"source_ip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.2.3.4"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2025-01-01T12:00:00+00:00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GET"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"path"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/index.php"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"response_size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1024&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;The monitor runs as a background thread and tails this file continuously. Here's the core idea:&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;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# nothing new yet, wait a moment
&lt;/span&gt;        &lt;span class="k"&gt;continue&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# turn JSON into a LogEntry object
&lt;/span&gt;    &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# hand it off to the detector
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why a queue&lt;/strong&gt;? The monitor's job is purely I/O reading lines as fast as they arrive. The detector's job is CPU work running math on each entry. Separating them with a queue means they run independently. If detection is briefly slow, the queue buffers entries and nothing is lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Handling log rotation&lt;/strong&gt;: Nginx periodically creates a new log file. If we don't handle this, our tail would keep reading the old file and miss all new traffic. We detect rotation by watching the file's &lt;strong&gt;inode&lt;/strong&gt;  a unique ID the operating system assigns to each file. If the inode changes, we reopen the file from the beginning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 2: The Sliding Window  Counting Requests Without Gaps&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The most important question is: "&lt;em&gt;How many requests has this IP sent in the last 60 seconds?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The naive approach is to keep a counter per IP and reset it every minute. But that creates a blind spot if an attacker sends 1000 requests at 00:59 and 1000 more at 01:01, each resets within its own minute and you never see the true burst.&lt;/p&gt;

&lt;p&gt;The correct approach is a &lt;strong&gt;sliding window&lt;/strong&gt;: track the exact timestamp of every request, and at any moment count only those within the last 60 seconds.&lt;/p&gt;

&lt;p&gt;We use Python's &lt;code&gt;collections.deque&lt;/code&gt; for this:&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;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SlidingWindowCounter&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;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;window_seconds&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;   &lt;span class="c1"&gt;# stores arrival times
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# new request arrives at the right end
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evict_and_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;

        &lt;span class="c1"&gt;# Remove expired entries from the LEFT (oldest end)
&lt;/span&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_timestamps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# everything left is within the window
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evict_and_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window_seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why a deque and not a list?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A regular Python list is slow at removing from the front it has to shift every element left, which is O(n). A &lt;code&gt;deque&lt;/code&gt; (double ended queue) removes from either end in O(1). Since we always append new entries to the right and evict old entries from the left, a deque is the perfect data structure.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;eviction logic&lt;/strong&gt;: We don't scan the whole deque looking for expired entries. We only look at the leftmost entry (index 0) the oldest one. If it's older than 60 seconds, we pop it and check the next one. Because entries are always appended in time order, once we find an entry that's within the window, everything to its right is also within the window. We stop there.&lt;/p&gt;

&lt;p&gt;In steady traffic, this evicts 0 or 1 entries per call effectively instant.&lt;/p&gt;

&lt;p&gt;We maintain &lt;strong&gt;one counter per IP&lt;/strong&gt; and one &lt;strong&gt;global counter&lt;/strong&gt; for all traffic combined.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 3: Learning What "Normal" Looks Like The Rolling Baseline&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The sliding window tells us the current rate. But we can't know if that rate is suspicious without knowing what's normal for this server.&lt;/p&gt;

&lt;p&gt;This is the baseline. It answers: "Historically, how many requests per second does this server receive?"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building the baseline&lt;/strong&gt;&lt;br&gt;
Every second, we take a snapshot of the global window count and store 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="c1"&gt;# maxlen=1800 means we keep 30 minutes of history (30 * 60 = 1800 seconds)
&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_per_second_counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxlen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Every second:
&lt;/span&gt;&lt;span class="n"&gt;current_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evict_and_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_per_second_counts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;current_count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;The maxlen=1800&lt;/code&gt; is doing something clever: when the deque is full and we append a new entry, Python &lt;strong&gt;automatically drops the oldest entry from the left&lt;/strong&gt;. We get a rolling 30 minute window with zero manual cleanup code.&lt;br&gt;
&lt;strong&gt;Computing mean and standard deviation&lt;/strong&gt;&lt;br&gt;
Every 60 seconds we recalculate from the stored history:&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;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_per_second_counts&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&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;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Apply floors to prevent false positives during idle periods
&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;   &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# assume at least 1 req/s as baseline
&lt;/span&gt;&lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# assume at least 0.5 req/s variation
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why floor values?&lt;/strong&gt; If the server is idle at 3am with zero traffic, mean=0 and stddev=0. Then a single request creates an infinite z score and triggers a false alarm. The floors prevent this  they say "even in the quietest period, assume some baseline activity."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Per-hour slots&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Traffic at 3am looks different from traffic at 3pm. We store separate baseline stats per hour of the day&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;current_hour&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;
&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_hour_stats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;current_hour&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BaselineStats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When looking up the baseline, we prefer the current hour's stats if they have enough samples. This means the detector naturally adapts to day/night traffic patterns without any manual configuration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 4: Making the Decision Z Score and the 5x Rule&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;current rate&lt;/strong&gt; for an IP (from the sliding window)&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;baseline mean and stddev&lt;/strong&gt; (from the rolling window)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We combine these into a &lt;strong&gt;z score&lt;/strong&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="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_rate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;baseline_mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;baseline_stddev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The z score measures how many standard deviations above normal the current rate is. A z score of 3.0 means the rate is 3 standard deviations above the mean statistically, this happens by chance less than 0.3% of the time under normal conditions.&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;_check_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ip_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ip_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;zscore&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_rate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stddev&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AnomalyEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&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;z-score &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zscore&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; 3.0&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ip_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;AnomalyEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="o"&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;rate &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/s &amp;gt; 5x baseline&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;p&gt;&lt;strong&gt;Why two rules&lt;/strong&gt;? They catch different scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Z score&lt;/strong&gt; catches relative anomalies. If baseline is 10 req/s and someone hits 40 req/s, z score fires because that's unusual relative to history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5x rule&lt;/strong&gt; catches absolute spikes even when the baseline is tiny. If baseline is 0.1 req/s (idle server) and someone hits 0.8 req/s, the z-score might not fire (stddev is also tiny), but 8x &amp;gt; 5x catches it immediately.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Error surge tightening&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There's a sneaky attack pattern: low and slow scanning. An attacker sends just a few requests per second  below detection thresholds but most of them return 404 errors because they're probing for vulnerabilities (/&lt;code&gt;wp-admin&lt;/code&gt;, /&lt;code&gt;.env&lt;/code&gt;, /&lt;code&gt;phpMyAdmin&lt;/code&gt;, etc.).&lt;br&gt;
We detect this separately: if an IP's 4xx/5xx error rate is 3x higher than normal, we &lt;strong&gt;tighten the detection thresholds by 30%&lt;/strong&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error_surge_detected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;zscore_threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;   &lt;span class="c1"&gt;# → 2.1 (easier to trigger)
&lt;/span&gt;    &lt;span class="n"&gt;rate_multiplier&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;   &lt;span class="c1"&gt;# → 3.5 (easier to trigger)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This catches the scanner without blocking every IP that occasionally gets a 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5: Blocking the IP with iptables&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When an anomaly fires, we need to block the IP immediately. We use iptables  the Linux kernel's built-in firewall.&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;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;block_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sudo&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;iptables&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;-I&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;INPUT&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;1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="c1"&gt;# INSERT at position 1 (top of the chain)
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;              &lt;span class="c1"&gt;# source IP to match
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&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;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;           &lt;span class="c1"&gt;# silently discard matching packets
&lt;/span&gt;    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking this down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;I INPUT 1&lt;/code&gt; inserts our rule at the top of the INPUT chain, so it's checked before any other rules. The attacker's packets are dropped immediately.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;j DROP&lt;/code&gt; silently discard. We don't send any response back (as opposed to &lt;code&gt;-j REJECT&lt;/code&gt; which sends an ICMP error). This is intentional: the attacker gets no feedback that they're blocked.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To verify blocks are active, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-L&lt;/span&gt; INPUT &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--line-numbers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The backoff schedule escalating punishment&lt;/strong&gt;&lt;br&gt;
Not all attacks are equal. A first time offender might get an automatic unban after 10 minutes. A repeat offender escalates through longer bans&lt;/p&gt;

&lt;p&gt;We track how many times each IP has been banned across its entire history. When re banning, we move to the next duration:&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;ban_durations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;   &lt;span class="c1"&gt;# seconds; -1 = permanent
&lt;/span&gt;
&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_ban_history&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="n"&gt;ip&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="c1"&gt;# how many times banned before
&lt;/span&gt;&lt;span class="n"&gt;duration&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ban_durations&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ban_durations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Auto-unban runs in a background thread that wakes every 30 seconds, checks for expired bans, removes the iptables rule, and sends a Slack notification.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 6: The Audit Log&lt;/strong&gt;&lt;br&gt;
Every significant event  ban, unban, baseline recalculation is written to a structured log file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[2025-01-01T12:00:00+00:00] BAN 1.2.3.4 | IP z-score 4.5 &amp;gt; 3.0 | rate=42.300/s | baseline=5.000/0.500 | 600s
[2025-01-01T12:10:00+00:00] UNBAN 1.2.3.4 | expired | rate=0.000/s | baseline=5.000/0.500 |
[2025-01-01T12:01:00+00:00] BASELINE_RECALC GLOBAL | samples=1800 hour=12 | rate=5.200/s | baseline=5.100/0.480 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a full timeline of what happened and why  invaluable for post incident analysis.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 7: The Live Dashboard&lt;/strong&gt;&lt;br&gt;
The dashboard is served by FastAPI and auto refreshes every 3 seconds. It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current global req/s vs baseline mean&lt;/li&gt;
&lt;li&gt;Number of currently banned IPs&lt;/li&gt;
&lt;li&gt;CPU and memory usage&lt;/li&gt;
&lt;li&gt;Daemon uptime&lt;/li&gt;
&lt;li&gt;Table of all active bans with time remaining&lt;/li&gt;
&lt;li&gt;Top 10 source IPs by request rate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The frontend is plain JavaScript polling &lt;code&gt;/api/metrics&lt;/code&gt;. No React, no build step  just a &lt;code&gt;fetch()&lt;/code&gt; on a timer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Putting It All Together&lt;/strong&gt;&lt;br&gt;
The main loop is intentionally simple:&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;while&lt;/span&gt; &lt;span class="n"&gt;running&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;maybe_recalculate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;    &lt;span class="c1"&gt;# recalc baseline every 60s
&lt;/span&gt;
    &lt;span class="c1"&gt;# Drain the log queue in batches
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_nowait&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;blocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_banned&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;source_ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;               &lt;span class="c1"&gt;# already blocked, skip
&lt;/span&gt;
        &lt;span class="n"&gt;tracker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# update sliding windows
&lt;/span&gt;        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# check for anomaly
&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;PER_IP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blocker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ban&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_ban&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;audit_log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;GLOBAL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_global_alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four threads run concurrently:&lt;br&gt;
&lt;strong&gt;Main thread&lt;/strong&gt;: the detection loop above&lt;br&gt;
&lt;strong&gt;LogMonitor thread&lt;/strong&gt;: tails the log file, feeds the queue&lt;br&gt;
&lt;strong&gt;Unbanner thread&lt;/strong&gt;: wakes every 30s to release expired bans&lt;br&gt;
&lt;strong&gt;Dashboard thread&lt;/strong&gt;: serves the FastAPI web UI&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Lessons&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use the right data structure&lt;/strong&gt;. A &lt;code&gt;deque&lt;/code&gt;makes the sliding window O(1). The wrong choice (a list, a dict of minute buckets) would have made it O(n) or introduced measurement gaps.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't hardcode thresholds&lt;/strong&gt;. Every threshold in this system lives in &lt;code&gt;config.yaml&lt;/code&gt;. Tuning detection sensitivity is a matter of editing one file, not hunting through code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The baseline needs a warmup period&lt;/strong&gt;. The system needs 30 minutes of real traffic before the baseline is trustworthy. Floor values prevent false alarms during warmup.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Two detection rules are better than one&lt;/strong&gt;. Z score and the 5x multiplier cover different attack shapes. Neither alone is sufficient.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Be careful with iptables in Docker&lt;/strong&gt;. The detector container needs &lt;code&gt;cap_add:&lt;/code&gt; &lt;code&gt;[NET_ADMIN]&lt;/code&gt; and must run as root. Rules applied inside the container affect the host's iptables chain  which is exactly what we want, but worth understanding before you do it.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Nginx (JSON logs) → shared Docker volume
                                        ↓
                               Detection daemon
                                  ├── monitor.py    (tail logs)
                                  ├── baseline.py   (rolling stats)
                                  ├── detector.py   (z-score logic)
                                  ├── blocker.py    (iptables)
                                  ├── unbanner.py   (backoffreleases)
                                  ├── notifier.py   (Slack)
                                  └── dashboard.py  (FastAPI UI)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>backend</category>
      <category>devops</category>
      <category>devsecops</category>
    </item>
    <item>
      <title>Deploying a FastAPI Application on Digital Ocean Droplet with Nginx and CI/CD</title>
      <dc:creator>marlinekhavele</dc:creator>
      <pubDate>Tue, 16 Sep 2025 09:24:50 +0000</pubDate>
      <link>https://forem.com/khavelemarline/deploying-a-fastapi-application-on-digital-ocean-droplet-with-nginx-and-cicd-4nel</link>
      <guid>https://forem.com/khavelemarline/deploying-a-fastapi-application-on-digital-ocean-droplet-with-nginx-and-cicd-4nel</guid>
      <description>&lt;p&gt;Deploying a FastAPI application with Nginx on Digital Ocean droplet  can be challenging especially for beginners. This guide will walk you through setting up an Droplet, installing necessary dependencies, configuring Nginx as a reverse proxy, and setting up a CI/CD pipeline with GitHub Actions.&lt;br&gt;
&lt;strong&gt;Prerequisites&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A Digital Ocean account.&lt;/li&gt;
&lt;li&gt;A FastAPI application in a GitHub repository.&lt;/li&gt;
&lt;li&gt;Basic familiarity with the command line and SSH.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Part 1: Setting Up Your Digital Ocean Droplet&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Create a Droplet&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Log in to your Digital Ocean dashboard and click "Create" -&amp;gt; "Droplets".&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Ubuntu&lt;/strong&gt; (a recent LTS version like 22.04 or 24.04) as your distribution.&lt;/li&gt;
&lt;li&gt;Select a plan. The &lt;strong&gt;Basic Shared CPU&lt;/strong&gt; plan is a great starting point.&lt;/li&gt;
&lt;li&gt;Choose a datacenter region closest to you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authentication&lt;/strong&gt;: For security, &lt;strong&gt;highly prefer SSH keys&lt;/strong&gt; over a password. Add your SSH public key. If you haven't set one up, &lt;a href="https://docs.digitalocean.com/products/droplets/how-to/add-ssh-keys/" rel="noopener noreferrer"&gt;follow this guide.&lt;/a&gt; &lt;/li&gt;
&lt;li&gt;Finalize and create the Droplet.&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access Your Droplet&lt;/strong&gt;:&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;Once created, find the Droplet's public IP address in your dashboard.&lt;/li&gt;
&lt;li&gt;Open your terminal and connect:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh root@your_droplet_ip_address
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Part 2: Server Setup and Application Installation&lt;/strong&gt;&lt;br&gt;
Once logged in as &lt;code&gt;root&lt;/code&gt;, run these commands to prepare your server.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; Create a Non-root User (Security Best Practice):
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;adduser deployer
usermod -aG sudo deployer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This creates a new user named &lt;code&gt;deployer&lt;/code&gt; and adds it to the &lt;code&gt;sudo&lt;/code&gt; group.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Copy your SSH key&lt;/strong&gt; to the new user to allow password less login:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rsync --archive --chown=deployer:deployer ~/.ssh /home/deployer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Now, you can disconnect and log in as the new user: ssh &lt;code&gt;deployer@your_droplet_ip_address&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Update the System&lt;/strong&gt;:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt; &lt;strong&gt;Install Python, Pip, and Pipenv&lt;/strong&gt;:
We will use &lt;code&gt;pipenv&lt;/code&gt; for managing your application's virtual environment and dependencies.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install python3-pip -y
pip3 install pipenv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Install Nginx&lt;/strong&gt;:
Nginx will act as a reverse proxy, handling client requests and passing them to your FastAPI app.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt install nginx -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set Up Firewall (UFW)&lt;/strong&gt;:
Configure the firewall to allow SSH, HTTP, and HTTPS traffic.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ufw allow 'OpenSSH'
sudo ufw allow 'Nginx Full'
sudo ufw enable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;Part 3: Configuring Nginx as a Reverse Proxy&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Create an Nginx Configuration File&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new file for your site in &lt;code&gt;/etc/nginx/sites-available/&lt;/code&gt;.
&lt;strong&gt;Paste the following configuration&lt;/strong&gt;:
Replace &lt;code&gt;your_droplet_ip_address&lt;/code&gt;with your actual IP or domain name if you have one.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;server {
    listen 80;
    server_name your_droplet_ip_address;

    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;This tells Nginx to listen on port 80 and forward all traffic to the FastAPI application running on &lt;code&gt;127.0.0.1:8000&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Enable the Configuration&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a symbolic link to the &lt;code&gt;sites-enabled&lt;/code&gt;directory:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo ln -s /etc/nginx/sites-available/fastapi_app /etc/nginx/sites-enabled/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;Test the Nginx configuration for syntax errors:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nginx -t
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If successful, restart Nginx:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl restart nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Part 4: Deploying the Application Manually&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Clone Your Repository&lt;/strong&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 clone https://github.com/your_username/your_repo_name.git /home/deployer/app
cd /home/deployer/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Install Dependencies with Pipenv&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Run the Application&lt;/strong&gt;:&lt;br&gt;
Let's test if everything works. Run your app inside the Pipenv shell&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pipenv run uvicorn main:app --host 0.0.0.0 --port 8000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Visit &lt;code&gt;http://your_droplet_ip_address&lt;/code&gt; in your browser. You should see your FastAPI app running behind Nginx! Press &lt;code&gt;Ctrl+C&lt;/code&gt; to stop the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Part 5: Automating Deployments with GitHub Actions CI/CD&lt;/strong&gt;&lt;br&gt;
We'll create a workflow that automatically deploys our app when we push to the &lt;code&gt;main&lt;/code&gt; branch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create GitHub Secrets&lt;/strong&gt;:&lt;br&gt;
In your GitHub repository, go to &lt;strong&gt;Settings&lt;/strong&gt; &amp;gt; &lt;strong&gt;Secrets and variables&lt;/strong&gt; &amp;gt; &lt;strong&gt;Actions&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create two new secrets:
&lt;code&gt;HOST&lt;/code&gt;: Your Droplet's IP address.
&lt;code&gt;SSH_PRIVATE_KEY&lt;/code&gt;: The entire contents of your private SSH key (the one that matches the public key you added to Digital Ocean).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Create the Workflow File&lt;/strong&gt;:&lt;br&gt;
In your repo, create the directory &lt;code&gt;.github/workflows/&lt;/code&gt; and a file inside it named &lt;code&gt;deploy.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Configure the Deployment Workflow&lt;/strong&gt;:&lt;br&gt;
Paste the following YAML configuration into &lt;code&gt;.github/workflows/deploy.yml&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;name: Deploy to DigitalOcean Droplet

on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout code
      uses: actions/checkout@v4

    - name: Deploy to Droplet
      uses: appleboy/ssh-action@v1.0.0
      with:
        host: ${{ secrets.HOST }}
        username: deployer
        key: ${{ secrets.SSH_PRIVATE_KEY }}
        script: |
          cd /home/deployer/app
          git pull origin main
          pipenv install
          sudo systemctl restart fastapi.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create a Systemd Service&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The CI/CD script above tries to run &lt;code&gt;sudo systemctl restart fastapi.service&lt;/code&gt;. We need to create this service to run our app in the background and restart on reboot.&lt;br&gt;
On your Droplet,create a service file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/systemd/system/fastapi.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following configuration, adjusting paths if necessary (&lt;code&gt;User&lt;/code&gt;, &lt;code&gt;WorkingDirectory&lt;/code&gt;, and the command):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Unit]
Description=FastAPI Application
After=network.target

[Service]
User=deployer
Group=www-data
WorkingDirectory=/home/deployer/app
Environment="PATH=/home/deployer/.local/bin"
ExecStart=/usr/local/bin/pipenv run uvicorn main:app --host 0.0.0.0 --port 8000

[Install]
WantedBy=multi-user.target
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enable and start the service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl enable fastapi.service
sudo systemctl start fastapi.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can check its status with: &lt;code&gt;sudo systemctl status fastapi.service&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You're Live!&lt;/strong&gt;&lt;br&gt;
Your automated pipeline is now set up. The next time you push code to the &lt;code&gt;main&lt;/code&gt; branch on GitHub, the GitHub Action will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;SSH into your Droplet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git pull&lt;/code&gt; the latest code.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pipenv install&lt;/code&gt; any new dependencies.&lt;/li&gt;
&lt;li&gt;Restart the &lt;code&gt;fastapi.service&lt;/code&gt; to apply the changes.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can now visit your Droplet's IP address in a web browser, and you will see your automatically deployed FastAPI application!&lt;/p&gt;

</description>
      <category>nginx</category>
      <category>fastapi</category>
      <category>githubactions</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>Terraform: UP&amp; Running</title>
      <dc:creator>marlinekhavele</dc:creator>
      <pubDate>Mon, 15 Sep 2025 06:52:22 +0000</pubDate>
      <link>https://forem.com/khavelemarline/terraform-up-running-31e3</link>
      <guid>https://forem.com/khavelemarline/terraform-up-running-31e3</guid>
      <description>&lt;p&gt;As programmers, &lt;code&gt;git&lt;/code&gt; is our safety net. &lt;code&gt;git&lt;/code&gt; commit is a snapshot of our application's truth. &lt;code&gt;git log&lt;/code&gt; tells a story of what changed, when, and why. We can experiment with git branch and, if things go wrong, revert to a known good state with &lt;code&gt;git revert&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Now, imagine your cloud infrastructure the servers, databases, and networks on AWS, Azure, or Google Cloud is governed by the same principles. No more frantic note taking, trying to remember which checkbox you clicked in a web console. No more "works on my machine" extended to "works in my cloud account." This is the promise of Terraform&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is Terraform?&lt;/strong&gt;&lt;br&gt;
Terraform is an &lt;code&gt;Infrastructure as Code (IaC)&lt;/code&gt; tool created by HashiCorp. It allows you to define and provision your entire infrastructure using a declarative configuration language.&lt;/p&gt;

&lt;p&gt;Think of it like this: Instead of manually clicking buttons in the AWS console to create a server, you write a configuration file that describes that server. You then tell Terraform to make the real world infrastructure match your description. It's version control for your infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Console Clicking Problem&lt;/strong&gt;&lt;br&gt;
You have felt the pain. You need a new staging environment. You spend an hour carefully clicking through the AWS console, configuring a VPC, subnets, security groups, EC2 instances, and a load balancer. It works yeeei!&lt;/p&gt;

&lt;p&gt;Two weeks later  you need to create an identical environment for a new client. You can't remember the exact steps. Was the ingress rule on the security group for port &lt;code&gt;8080&lt;/code&gt; or &lt;code&gt;8000&lt;/code&gt;? Which &lt;code&gt;AMI ID&lt;/code&gt; did I use? The knowledge of how to build the system is trapped in your head and a series of irreversible clicks.&lt;/p&gt;

&lt;p&gt;Terraform solves this by capturing that knowledge in code.The Same Changes, The Same Knowledge&lt;/p&gt;

&lt;p&gt;The core beauty of Terraform is that the same changes we do locally are the same knowledge we are using to build our infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write Code&lt;/strong&gt;: You define your infrastructure in files with a&lt;code&gt;.tf&lt;/code&gt; extension using HashiCorp Configuration Language (HCL), which is both human and machine readable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Plan&lt;/strong&gt;: You run &lt;code&gt;terraform plan&lt;/code&gt;. Terraform reads your code, compares it to the current state of your infrastructure, and generates an execution plan. This is like a dry run it shows you exactly what will be created, changed, or destroyed before it happens. This is your ultimate safety check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Apply&lt;/strong&gt;: You run &lt;code&gt;terraform apply&lt;/code&gt;. Terraform executes the plan, making API calls to the cloud provider to build the infrastructure you described.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.tf&lt;/code&gt;files you write become the single source of truth. They are the knowledge.&lt;/p&gt;

&lt;p&gt;They can be:&lt;br&gt;
&lt;strong&gt;Version Controlled&lt;/strong&gt;: Committed to git, alongside your   application code.&lt;br&gt;
&lt;strong&gt;Reviewed&lt;/strong&gt;: Peer reviewed in pull requests.&lt;br&gt;
&lt;strong&gt;Reused&lt;/strong&gt;: Used to create identical dev, staging, and production environments.&lt;br&gt;
&lt;strong&gt;Shared&lt;/strong&gt;: Onboard new team members by giving them the code, not a 50 page manual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Manual Click Way:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to EC2 Dashboard.&lt;/li&gt;
&lt;li&gt;Click "Launch Instance".&lt;/li&gt;
&lt;li&gt;Choose "Amazon Linux 2 AMI".&lt;/li&gt;
&lt;li&gt;Choose "t2.micro".&lt;/li&gt;
&lt;li&gt;Click "Review and Launch", then "Launch".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The Terraform Way:&lt;/strong&gt;&lt;br&gt;
You create a file named &lt;code&gt;main.tf&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;# Configure the AWS Provider
provider "aws" {
  region = "us-east-1"
}

# Declare the resource you want to create
resource "aws_instance" "my_web_server" {
  ami           = "ami-0c02fb55956c7d316" # Amazon Linux 2
  instance_type = "t2.micro"

  tags = {
    Name = "MyWebServer"
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, you execute the plan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform init    # Initializes Terraform and downloads the AWS provider
terraform plan    # Shows the execution plan
terraform apply   # Creates the EC2 instance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This code is now documentation. It's repeatable. It's shareable. If you want a second server, you copy the block, change the &lt;code&gt;Name tag&lt;/code&gt;, and run &lt;code&gt;apply&lt;/code&gt; again. The knowledge is no longer in your head; it's in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Core Benefits&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Visibility &amp;amp; Collaboration&lt;/strong&gt;: Everyone on the team can see the infrastructure design and propose changes via code reviews.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistency &amp;amp; Reliability&lt;/strong&gt;: Eliminates manual error and ensures environments are identical.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Speed &amp;amp; Efficiency&lt;/strong&gt;: Provisioning a complex infrastructure that took days can now be done in minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lifecycle Management&lt;/strong&gt;: Terraform isn't just for creation. It manages the entire lifecycle updates, scaling, and, crucially, clean destruction (&lt;code&gt;terraform destroy&lt;/code&gt;), which is perfect for tearing down test environments to save costs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Getting started is straightforward:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install Terraform&lt;/strong&gt; on your machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Configure Credentials&lt;/strong&gt; for your cloud provider (e.g AWS CLI).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write a&lt;/strong&gt; &lt;code&gt;.tf&lt;/code&gt; file defining a simple resource (like the EC2 instance above).&lt;/li&gt;
&lt;li&gt;Run &lt;code&gt;terraform init&lt;/code&gt;, &lt;code&gt;plan&lt;/code&gt;, and &lt;code&gt;apply&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You have just taken the first step out of the console and into a world of codified, version controlled, and reliable infrastructure management.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>terraform</category>
      <category>devops</category>
      <category>aws</category>
    </item>
  </channel>
</rss>
