<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: anitaalicloud</title>
    <description>The latest articles on DEV Community by anitaalicloud (@anitaalicloud).</description>
    <link>https://dev.to/anitaalicloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3901516%2F3aeb27ec-d326-4295-bcd7-bc103e1aa263.png</url>
      <title>DEV Community: anitaalicloud</title>
      <link>https://dev.to/anitaalicloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anitaalicloud"/>
    <language>en</language>
    <item>
      <title>Containerizing a Node.js API with Docker and Automating Deployments to Amazon ECR with GitHub Action</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Fri, 10 Jul 2026 09:55:07 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/containerizing-a-nodejs-api-with-docker-and-automating-deployments-to-amazon-ecr-with-github-action-1f11</link>
      <guid>https://dev.to/anitaalicloud/containerizing-a-nodejs-api-with-docker-and-automating-deployments-to-amazon-ecr-with-github-action-1f11</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In my previous article, I walked through building a lightweight Node.js REST API and deploying it live on an AWS EC2 instance with Nginx and PM2. If you haven't read that yet, I'd recommend starting there — &lt;a href="https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8"&gt;https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8&lt;/a&gt; — as this article picks up right where that one left off.&lt;/p&gt;

&lt;p&gt;Here, we take the same API further. We containerize it using a multi stage Dockerfile, create a private Amazon ECR repository to store our images, and wire up a GitHub Actions CI pipeline that automatically builds, scans, and pushes a versioned Docker image every time we push to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;By the end, every push to your repository will trigger a pipeline that produces a clean, security scanned, semantically versioned Docker image sitting in ECR  with zero manual steps.&lt;/p&gt;




&lt;h2&gt;
  
  
  What We Are Adding
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multi-stage Dockerfile&lt;/td&gt;
&lt;td&gt;Containerize the API with a lean, secure production image&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon ECR&lt;/td&gt;
&lt;td&gt;Private registry to store and version our Docker images&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GitHub Actions CI&lt;/td&gt;
&lt;td&gt;Automate the build, scan, and push on every push to &lt;code&gt;main&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Trivy security scan&lt;/td&gt;
&lt;td&gt;Gate the pipeline — no vulnerable image reaches ECR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic versioning&lt;/td&gt;
&lt;td&gt;Every image build gets a unique, traceable tag&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Node.js API from Article 1- &lt;a href="https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8"&gt;https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8&lt;/a&gt; already on GitHub&lt;/li&gt;
&lt;li&gt;Docker installed locally&lt;/li&gt;
&lt;li&gt;An AWS account with an IAM user (not root)&lt;/li&gt;
&lt;li&gt;A public GitHub repository&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Step 1: The Multi Stage Dockerfile
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why Multi Stage?
&lt;/h3&gt;

&lt;p&gt;A standard Dockerfile builds everything in one layer, dev tools, build dependencies, and runtime all end up in the final image. A multi-stage build separates concerns: the first stage installs and builds, the second stage starts clean and copies only what is needed to run the app. The result is a smaller, more secure production image.&lt;/p&gt;

&lt;p&gt;Create a &lt;code&gt;Dockerfile&lt;/code&gt; in the root of your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ─── Stage 1: Build ───────────────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;

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

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

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

&lt;span class="c"&gt;# ─── Stage 2: Production ──────────────────────────────────────&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;

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

&lt;span class="c"&gt;# Run as a non-root user for security&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/index.js .&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/package.json .&lt;/span&gt;

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

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

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

&lt;/div&gt;



&lt;p&gt;A few important details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;npm ci&lt;/code&gt; reads from &lt;code&gt;package-lock.json&lt;/code&gt; to install exact dependency versions, making builds reproducible across environments&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--from=builder&lt;/code&gt; copies only the files the app needs to run. Nothing else from the build stage makes it into the final image. Running as &lt;code&gt;appuser&lt;/code&gt; instead of root reduces the attack surface if the container is ever compromised&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Testing the Build Locally
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--target&lt;/span&gt; production &lt;span class="nt"&gt;-t&lt;/span&gt; personal-api:local &lt;span class="nb"&gt;.&lt;/span&gt;
docker run &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 personal-api:local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a second terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/
curl http://localhost:3000/health
curl http://localhost:3000/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify the running container:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vkjthmf1uqxddyy3emo.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F6vkjthmf1uqxddyy3emo.png" alt=" " width="796" height="24"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Push the Dockerfile to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add Dockerfile
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"docker: add multi-stage Dockerfile"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 2: Create the Amazon ECR Repository
&lt;/h2&gt;

&lt;p&gt;ECR is AWS's private container registry. This is where our pipeline will push versioned images.&lt;/p&gt;

&lt;h3&gt;
  
  
  Via AWS Console
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Log into AWS as your &lt;strong&gt;IAM user&lt;/strong&gt; (not root)&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;ECR → Create repository&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Make sure you are in the correct region — &lt;code&gt;eu-north-1&lt;/code&gt; (Stockholm) in my case&lt;/li&gt;
&lt;li&gt;Name the repository &lt;code&gt;personal-api&lt;/code&gt;, visibility: &lt;strong&gt;Private&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create repository&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the repository URI — you will need this shortly:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   123456789012.dkr.ecr.eu-north-1.amazonaws.com/personal-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Via AWS CLI
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr create-repository &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--repository-name&lt;/span&gt; personal-api &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--region&lt;/span&gt; eu-north-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6fqvxd8s30emddm9yjt.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv6fqvxd8s30emddm9yjt.png" alt=" " width="799" height="399"&gt;&lt;/a&gt;&lt;br&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcdwd5ptl8wv7wfaa186.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqcdwd5ptl8wv7wfaa186.png" alt=" " width="800" height="75"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 3: Set Up the IAM User
&lt;/h2&gt;

&lt;p&gt;Never use root AWS credentials in a CI pipeline. Create a scoped IAM user with only the permissions the pipeline needs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;IAM → Users → Create user&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it &lt;code&gt;devops-user&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Attach the policy: &lt;code&gt;AmazonEC2ContainerRegistryFullAccess&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Go to &lt;strong&gt;Security credentials → Create access key&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;CLI&lt;/strong&gt;, then copy both the &lt;strong&gt;Access Key ID&lt;/strong&gt; and &lt;strong&gt;Secret Access Key&lt;/strong&gt; — you only see the secret key once&lt;/li&gt;
&lt;/ol&gt;


&lt;h2&gt;
  
  
  Step 4: Add GitHub Secrets
&lt;/h2&gt;

&lt;p&gt;The pipeline uses your AWS credentials to authenticate with ECR. Store them as GitHub secrets so they are never exposed in your code.&lt;/p&gt;

&lt;p&gt;Go to your repo → &lt;strong&gt;Settings → Secrets and variables → Actions → New repository secret&lt;/strong&gt; and add:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Secret&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your IAM access key ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your IAM secret access key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;AWS_REGION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;eu-north-1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ECR_REPOSITORY_URI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Your full ECR URI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rbgog49q2uuewodjf2o.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1rbgog49q2uuewodjf2o.png" alt=" " width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Step 5: The GitHub Actions CI Pipeline
&lt;/h2&gt;

&lt;p&gt;Create the file &lt;code&gt;.github/workflows/ci.yml&lt;/code&gt; in your repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CI — Build, Scan &amp;amp; Push to ECR&lt;/span&gt;

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

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;ECR_REPOSITORY_URI&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ECR_REPOSITORY_URI }}&lt;/span&gt;
  &lt;span class="na"&gt;AWS_REGION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_REGION }}&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build-scan-push&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;Build, Scan &amp;amp; Push&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# ── Phase 1: Build ─────────────────────────────────────────&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&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;Generate semantic version&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;version&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;MAJOR=1&lt;/span&gt;
          &lt;span class="s"&gt;MINOR=0&lt;/span&gt;
          &lt;span class="s"&gt;PATCH=${{ github.run_number }}&lt;/span&gt;
          &lt;span class="s"&gt;echo "VERSION=v${MAJOR}.${MINOR}.${PATCH}" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;Configure AWS credentials&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;aws-access-key-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_ACCESS_KEY_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-secret-access-key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SECRET_ACCESS_KEY }}&lt;/span&gt;
          &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.AWS_REGION }}&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;Login to Amazon ECR&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/amazon-ecr-login@v2&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;Build Docker image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;docker build \&lt;/span&gt;
            &lt;span class="s"&gt;--target production \&lt;/span&gt;
            &lt;span class="s"&gt;-t ${{ env.ECR_REPOSITORY_URI }}:${{ steps.version.outputs.VERSION }} \&lt;/span&gt;
            &lt;span class="s"&gt;-t ${{ env.ECR_REPOSITORY_URI }}:latest \&lt;/span&gt;
            &lt;span class="s"&gt;.&lt;/span&gt;

      &lt;span class="c1"&gt;# ── Phase 2: Scan ──────────────────────────────────────────&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;Scan image with Trivy&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aquasecurity/trivy-action@master&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;image-ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.ECR_REPOSITORY_URI }}:${{ steps.version.outputs.VERSION }}&lt;/span&gt;
          &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;table&lt;/span&gt;
          &lt;span class="na"&gt;exit-code&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
          &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;CRITICAL&lt;/span&gt;
          &lt;span class="na"&gt;ignore-unfixed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="c1"&gt;# ── Phase 3: Push ──────────────────────────────────────────&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;Push image to ECR&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ env.ECR_REPOSITORY_URI }}:${{ steps.version.outputs.VERSION }}&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ env.ECR_REPOSITORY_URI }}:latest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Understanding the Three Pipeline Phases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Phase 1 — Build
&lt;/h3&gt;

&lt;p&gt;The pipeline checks out the code, generates a semantic version tag, authenticates with AWS using the secrets you added, logs into ECR, and builds the Docker image.&lt;/p&gt;

&lt;p&gt;The version tag follows the format &lt;code&gt;v{MAJOR}.{MINOR}.{PATCH}&lt;/code&gt;. Major and minor are set manually. &lt;code&gt;PATCH&lt;/code&gt; is set to &lt;code&gt;github.run_number&lt;/code&gt; — a counter GitHub increments automatically on every pipeline run. So your images end up tagged like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v1.0.1  ← first push
v1.0.2  ← second push
v1.0.3  ← third push
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each build also gets a &lt;code&gt;latest&lt;/code&gt; tag pointing to the most recent successful push. This means you always have a traceable history of every image ever built, and rolling back is as simple as pulling a previous version tag from ECR.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2 — Scan
&lt;/h3&gt;

&lt;p&gt;Before any image reaches ECR, Trivy scans it for known security vulnerabilities. Setting &lt;code&gt;exit-code: 1&lt;/code&gt; means the pipeline fails immediately if any &lt;code&gt;CRITICAL&lt;/code&gt; vulnerabilities are found — the image is discarded and never pushed to the registry.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ignore-unfixed: true&lt;/code&gt; skips vulnerabilities that have no available fix yet. This keeps the pipeline practical rather than blocking builds over issues that cannot be resolved at this point in time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3 — Push
&lt;/h3&gt;

&lt;p&gt;This step only runs if the Trivy scan passes. It pushes both tags — the semantic version and &lt;code&gt;latest&lt;/code&gt; — to ECR. After a successful run, you can verify the pushed image in &lt;strong&gt;AWS Console → ECR → personal-api → Images&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Triggering the Pipeline
&lt;/h2&gt;

&lt;p&gt;Push the workflow file to GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add .github/workflows/ci.yml
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"ci: add GitHub Actions CI pipeline"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go to your repo → &lt;strong&gt;Actions&lt;/strong&gt; tab and watch the pipeline run through all three phases.&lt;/p&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdf2oc1t920xfhc95dwh.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpdf2oc1t920xfhc95dwh.png" alt=" " width="800" height="269"&gt;&lt;/a&gt;&lt;/p&gt;

&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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwt1v058e3leuw2eo5dwz.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.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwt1v058e3leuw2eo5dwz.png" alt=" " width="800" height="289"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nodejs-api-ci/
├── .github/
│   └── workflows/
│       └── ci.yml          # CI pipeline
├── Dockerfile               # Multi-stage build
├── index.js                 # Express API
├── package.json
├── package-lock.json
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage Dockerfiles produce smaller, cleaner images.&lt;/strong&gt; The production stage only contains what is needed to run — nothing from the build environment leaks through.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never use root AWS credentials in CI.&lt;/strong&gt; A scoped IAM user with only ECR permissions limits the blast radius if credentials are ever compromised.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never hardcode credentials.&lt;/strong&gt; GitHub secrets keep sensitive values out of your codebase entirely — the workflow file is public, your secrets are not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan before you push.&lt;/strong&gt; Trivy as a pipeline gate means only clean images reach your registry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Semantic versioning makes every build traceable.&lt;/strong&gt; If something breaks in production, you know exactly which image is running and can roll back to any previous version instantly.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Live Project
&lt;/h2&gt;

&lt;p&gt;📦 &lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;code&gt;https://github.com/AnitaAliCloud/nodejs-api-ci&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;🐳 &lt;strong&gt;ECR Repository:&lt;/strong&gt; &lt;code&gt;devopsprod.duckdns.org&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This is Part 2 of my DevOps series. Read Part 1 here: &lt;a href="https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8"&gt;https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>githubactions</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Build and Deploy a Node.js REST API on AWS EC2 with PM2</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Tue, 16 Jun 2026 11:34:09 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8</link>
      <guid>https://dev.to/anitaalicloud/how-to-build-and-deploy-a-nodejs-rest-api-on-aws-ec2-with-pm2-cg8</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Knowing how to get an API from your local machine to a live, publicly accessible server is one of the most practical skills in a developer or DevOps engineer's toolkit. In this guide, I'll walk you through the full process, from writing a minimal Express API to deploying it on AWS EC2 with PM2 keeping it alive persistently.&lt;/p&gt;

&lt;p&gt;By the end, you'll have a running API accessible from any browser or HTTP client, with no manual restarts required.&lt;/p&gt;




&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;An AWS account (free tier works)&lt;/li&gt;
&lt;li&gt;Node.js installed locally&lt;/li&gt;
&lt;li&gt;Basic familiarity with the Linux command line&lt;/li&gt;
&lt;li&gt;A GitHub account&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What We're Building
&lt;/h2&gt;

&lt;p&gt;A lightweight three-endpoint JSON API:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Response&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "message": "API is running" }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{ "message": "healthy" }&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /me&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;Name&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, and &lt;code&gt;GitHub link&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;All endpoints return &lt;code&gt;Content-Type: application/json&lt;/code&gt;, HTTP 200, and respond in well under 500ms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Writing the API
&lt;/h2&gt;

&lt;p&gt;On your local machine, initialize a new Node.js project and install Express:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;personal-api
&lt;span class="nb"&gt;cd &lt;/span&gt;personal-api
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;index.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;API is running&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/health&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;healthy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/me&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Your Full Name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;you@example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;github&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://github.com/yourusername&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Server running on port &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;Test it locally:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
curl http://localhost:3000/
curl http://localhost:3000/health
curl http://localhost:3000/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three should return the expected JSON with a 200 status. ✅&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 2: Push to GitHub
&lt;/h2&gt;

&lt;p&gt;Create a public repository and push your code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
git add &lt;span class="nb"&gt;.&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Initial commit: personal API"&lt;/span&gt;
git remote add origin https://github.com/YOUR_USERNAME/personal-api.git
git push &lt;span class="nt"&gt;-u&lt;/span&gt; origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Provision an AWS EC2 Instance
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Launch a &lt;strong&gt;t2.micro&lt;/strong&gt; (free tier eligible) instance with &lt;strong&gt;Ubuntu 22.04 LTS&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Create or reuse a key pair for SSH&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;Security Group&lt;/strong&gt;, open inbound traffic on:

&lt;ul&gt;
&lt;li&gt;Port &lt;strong&gt;22&lt;/strong&gt; (SSH)&lt;/li&gt;
&lt;li&gt;Port &lt;strong&gt;80&lt;/strong&gt; (HTTP)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&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%2Fuecnizgz5od0kmm3diac.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%2Fuecnizgz5od0kmm3diac.png" alt=" " width="800" height="145"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; your-key.pem ubuntu@YOUR_EC2_PUBLIC_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 4: Set Up the Server Environment
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Install Node.js
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://deb.nodesource.com/setup_20.x | &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; bash -
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; nodejs
node &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Clone the Repo and Install Dependencies
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~
git clone https://github.com/YOUR_USERNAME/personal-api.git
&lt;span class="nb"&gt;cd &lt;/span&gt;personal-api
npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node index.js
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 5: Keep the App Running with PM2
&lt;/h2&gt;

&lt;p&gt;If you start the app with &lt;code&gt;node index.js&lt;/code&gt; and close your SSH session, the process dies. PM2 solves this — it's a production process manager that restarts your app on crashes and survives server reboots.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install PM2
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; pm2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Start the App
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 start index.js &lt;span class="nt"&gt;--name&lt;/span&gt; personal-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable PM2 on Boot
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 startup
&lt;span class="c"&gt;# Run the command it outputs, then:&lt;/span&gt;
pm2 save
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From now on, the app restarts automatically after any reboot or crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pm2 list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&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%2Fwybr7i6aqt31vi4hqncg.png" alt=" " width="800" height="575"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Step 6: Configure Nginx as a Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;The app runs on port 3000 internally. Rather than exposing that port to the public, we use Nginx to listen on port 80 and forward requests to the app. This is the standard production pattern.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install Nginx
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;nginx &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;nginx
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Site Config
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;nano /etc/nginx/sites-available/personal-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste the following (replace &lt;code&gt;YOUR_EC2_PUBLIC_IP&lt;/code&gt; with your actual IP or domain):&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;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;YOUR_EC2_PUBLIC_IP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_http_version&lt;/span&gt; &lt;span class="mf"&gt;1.1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Upgrade&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Connection&lt;/span&gt; &lt;span class="s"&gt;'upgrade'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_cache_bypass&lt;/span&gt; &lt;span class="nv"&gt;$http_upgrade&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enable and Reload
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo ln&lt;/span&gt; &lt;span class="nt"&gt;-s&lt;/span&gt; /etc/nginx/sites-available/personal-api /etc/nginx/sites-enabled/
&lt;span class="nb"&gt;sudo &lt;/span&gt;nginx &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl reload nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This should be your expected result.&lt;br&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%2Fa8faq5k22g3dzf7rn5gc.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%2Fa8faq5k22g3dzf7rn5gc.png" alt=" " width="800" height="35"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Test the Live Deployment
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://YOUR_EC2_PUBLIC_IP/
curl http://YOUR_EC2_PUBLIC_IP/health
curl http://YOUR_EC2_PUBLIC_IP/me
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three return the correct JSON over HTTP. ✅&lt;/p&gt;

&lt;h2&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%2Fuwfpzfsltow93twi6l57.png" alt=" " width="800" height="66"&gt;
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Final Project Structure
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;personal-api/
├── index.js        # Express API
├── package.json
└── README.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never expose your app port directly.&lt;/strong&gt; Nginx as a reverse proxy adds a layer of control and is the correct production pattern — it also makes it easy to host multiple apps on one server later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PM2 is non-negotiable for Node.js in production.&lt;/strong&gt; Without a process manager, one crash or reboot silently kills your service.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Always test locally first.&lt;/strong&gt; It's much faster to debug on your machine than on a remote server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EC2 security groups are your firewall.&lt;/strong&gt; By default, everything is blocked — be deliberate about what you open.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Live Demo
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repo:&lt;/strong&gt; &lt;code&gt;https://github.com/AnitaAliCloud/hng-stage1-api&lt;/code&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>express</category>
      <category>aws</category>
      <category>devops</category>
    </item>
    <item>
      <title>Containerizing and Deploying a Microservice App with Docker and CI/CD</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Mon, 01 Jun 2026 17:17:41 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/from-deployment-to-automation-containerizing-a-microservice-application-1ho</link>
      <guid>https://dev.to/anitaalicloud/from-deployment-to-automation-containerizing-a-microservice-application-1ho</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In this project, I worked on a microservice based application and focused on making it production ready using Docker and CI/CD pipelines.&lt;/p&gt;

&lt;p&gt;Instead of just running the application locally, the goal was to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Understand how multiple services interact&lt;/li&gt;
&lt;li&gt;Package them using Docker&lt;/li&gt;
&lt;li&gt;Orchestrate them with Docker Compose&lt;/li&gt;
&lt;li&gt;Automate testing, security checks, and deployment using GitHub Actions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article explains how to &lt;strong&gt;run, test, and deploy the project from scratch&lt;/strong&gt;.&lt;/p&gt;




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

&lt;p&gt;GitHub Repo: &lt;a href="https://github.com/AnitaAliCloud/hng14-stage2-devops" rel="noopener noreferrer"&gt;https://github.com/AnitaAliCloud/hng14-stage2-devops&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  System Architecture
&lt;/h2&gt;

&lt;p&gt;The system consists of four main components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Frontend (Node.js) — user interface for submitting jobs
&lt;/li&gt;
&lt;li&gt;Backend API (FastAPI) — handles job creation and status
&lt;/li&gt;
&lt;li&gt;Worker Service (Python) — processes jobs asynchronously
&lt;/li&gt;
&lt;li&gt;Redis — message broker between services
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flow Diagram:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend → API → Redis Queue → Worker → Redis → API → Frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Before running this project, ensure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker installed → &lt;a href="https://docs.docker.com/get-docker/" rel="noopener noreferrer"&gt;https://docs.docker.com/get-docker/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Compose installed&lt;/li&gt;
&lt;li&gt;Git installed&lt;/li&gt;
&lt;li&gt;(Optional) Node.js and Python for local debugging&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How to Run the Project Locally
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Clone the repository
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/YOUR_USERNAME/YOUR_REPO.git
&lt;span class="nb"&gt;cd &lt;/span&gt;YOUR_REPO
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Set up environment variables
&lt;/h3&gt;

&lt;p&gt;Create a &lt;code&gt;.env&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update values if needed.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Start all services with Docker Compose
&lt;/h3&gt;



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

&lt;/div&gt;






&lt;h3&gt;
  
  
  4. Access the application
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Frontend: &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API: &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Redis: internal only (not exposed)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Running Health Checks
&lt;/h2&gt;

&lt;p&gt;Each service includes a health endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt; → API health status&lt;/li&gt;
&lt;li&gt;Docker health checks ensure services restart automatically if unhealthy&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  CI/CD Pipeline (GitHub Actions)
&lt;/h2&gt;

&lt;p&gt;The project includes an automated pipeline:&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipeline stages:
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Lint (flake8, eslint, hadolint)&lt;/li&gt;
&lt;li&gt;Test (unit tests with pytest)&lt;/li&gt;
&lt;li&gt;Build Docker images&lt;/li&gt;
&lt;li&gt;Security scan (Trivy)&lt;/li&gt;
&lt;li&gt;Integration tests&lt;/li&gt;
&lt;li&gt;Deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  CI/CD Workflow Screenshot:
&lt;/h3&gt;

&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%2Fgqbt6pgmbnfp4gzr79ms.jpeg" 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%2Fgqbt6pgmbnfp4gzr79ms.jpeg" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Running Tests Locally
&lt;/h2&gt;

&lt;p&gt;To run tests manually:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;For coverage report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Security Checks
&lt;/h2&gt;

&lt;p&gt;The pipeline includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker image scanning with Trivy&lt;/li&gt;
&lt;li&gt;Failing builds on critical vulnerabilities&lt;/li&gt;
&lt;li&gt;No secrets stored in images or repository&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Containers not starting?
&lt;/h3&gt;

&lt;p&gt;Check logs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;






&lt;h3&gt;
  
  
  Port already in use?
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;lsof &lt;span class="nt"&gt;-i&lt;/span&gt; :3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;This project helped reinforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How microservices communicate in real systems&lt;/li&gt;
&lt;li&gt;Why containerization improves reliability&lt;/li&gt;
&lt;li&gt;Importance of CI/CD automation&lt;/li&gt;
&lt;li&gt;How infrastructure impacts application behavior&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;This project demonstrates how a multi-service application can be containerized, tested, and deployed in a structured and repeatable way.&lt;/p&gt;

&lt;p&gt;Beyond just writing code, the focus was on building a system that can be reliably shipped and maintained.&lt;/p&gt;




&lt;h2&gt;
  
  
  Useful Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GitHub Repository: &lt;a href="https://github.com/AnitaAliCloud/hng14-stage2-devops" rel="noopener noreferrer"&gt;https://github.com/AnitaAliCloud/hng14-stage2-devops&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CI/CD Pipeline: (GitHub Actions tab in repo)&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>cicd</category>
      <category>devops</category>
      <category>docker</category>
      <category>microservices</category>
    </item>
    <item>
      <title>Building and Deploying a Simple REST API on a Linux VPS with Nginx</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Mon, 01 Jun 2026 10:55:08 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/building-and-deploying-a-simple-rest-api-on-a-linux-vps-with-nginx-4l10</link>
      <guid>https://dev.to/anitaalicloud/building-and-deploying-a-simple-rest-api-on-a-linux-vps-with-nginx-4l10</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I recently built and deployed a simple REST API on a Linux server to strengthen my understanding of how backend services are hosted and managed in real world environments.&lt;/p&gt;

&lt;p&gt;The goal was not to build a complex application, but to understand how an API runs in production from server setup to deployment and reverse proxy configuration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I created a lightweight REST API with three endpoints:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; → confirms the API is running
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/health&lt;/code&gt; → checks service health
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/me&lt;/code&gt; → returns basic profile information in JSON format
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each endpoint returns a JSON response and is optimized for fast response times.&lt;/p&gt;




&lt;h2&gt;
  
  
  Tech Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Node.js / Python (depending on your implementation)&lt;/li&gt;
&lt;li&gt;Linux VPS (Ubuntu)&lt;/li&gt;
&lt;li&gt;Nginx (reverse proxy)&lt;/li&gt;
&lt;li&gt;PM2 / Systemd (process management)&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The application runs on a local server port and is not exposed directly to the internet.&lt;/p&gt;

&lt;p&gt;Instead, Nginx handles incoming traffic and forwards it to the application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Simple flow:
&lt;/h3&gt;

&lt;p&gt;Client → Nginx → API Server (localhost)&lt;/p&gt;

&lt;p&gt;This setup improves security, scalability, and control over traffic routing.&lt;/p&gt;




&lt;h2&gt;
  
  
  Deployment Approach
&lt;/h2&gt;

&lt;p&gt;The process involved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Setting up a Linux server environment
&lt;/li&gt;
&lt;li&gt;Running the API on a local port
&lt;/li&gt;
&lt;li&gt;Configuring Nginx as a reverse proxy
&lt;/li&gt;
&lt;li&gt;Using a process manager to keep the application running continuously
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once deployed, the API became publicly accessible through the server’s IP address or domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Key Learnings
&lt;/h2&gt;

&lt;p&gt;This project helped me understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How APIs behave outside of local development
&lt;/li&gt;
&lt;li&gt;The role of reverse proxies in production systems
&lt;/li&gt;
&lt;li&gt;Why process managers are important for uptime
&lt;/li&gt;
&lt;li&gt;How Linux servers are used in real deployments
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It was a simple project, but it gave me a clearer picture of how backend services are structured in real world systems.&lt;/p&gt;




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

&lt;p&gt;Deploying this API helped bridge the gap between writing code and actually running it in a production like environment.&lt;/p&gt;

&lt;p&gt;It reinforced the importance of understanding infrastructure, not just application logic.&lt;/p&gt;

</description>
      <category>api</category>
      <category>devops</category>
      <category>linux</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How I Built SwiftDeploy: A Tool That Writes Its Own Infrastructure</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Wed, 06 May 2026 19:28:10 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/how-i-built-swiftdeploy-a-tool-that-writes-its-own-infrastructure-dma</link>
      <guid>https://dev.to/anitaalicloud/how-i-built-swiftdeploy-a-tool-that-writes-its-own-infrastructure-dma</guid>
      <description>&lt;p&gt;&lt;em&gt;A deep dive into declarative deployments, OPA policy gates, and chaos engineering from Stage 4A to 4B&lt;/em&gt;&lt;/p&gt;

&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%2Fdlgs2pjh8kydui19g0ku.jpeg" 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%2Fdlgs2pjh8kydui19g0ku.jpeg" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Most DevOps tasks ask you to configure infrastructure manually. This one asked me to build the tool that does it for me.&lt;/p&gt;

&lt;p&gt;The result is &lt;strong&gt;SwiftDeploy&lt;/strong&gt; which is a CLI tool that reads a single &lt;code&gt;manifest.yaml&lt;/code&gt; file and generates your entire deployment stack from it. Nginx configs, Docker Compose files, policy checks, live metrics dashboards are all derived from one source of truth.&lt;/p&gt;

&lt;p&gt;This post covers the full journey: the design decisions, the guardrails, the chaos, and the lessons learned.&lt;/p&gt;




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

&lt;p&gt;Here is how all the pieces connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────┐
│                    manifest.yaml                     │
│              (single source of truth)                │
└──────────────────────┬──────────────────────────────┘
                       │
                       ▼
              ./swiftdeploy init
                       │
          ┌────────────┴────────────┐
          ▼                         ▼
     nginx.conf              docker-compose.yml
   (generated)                 (generated)
          │                         │
          ▼                         ▼
┌─────────────────────────────────────────────────────┐
│                   Docker Stack                       │
│                                                      │
│   ┌──────────┐    ┌──────────┐    ┌──────────┐      │
│   │  Nginx   │───▶│   App    │    │   OPA    │      │
│   │  :8080   │    │  :3000   │    │  :8181   │      │
│   └──────────┘    └──────────┘    └──────────┘      │
│   (public)        (internal)      (internal)         │
└─────────────────────────────────────────────────────┘
          │                         ▲
          ▼                         │
     curl :8080              CLI queries OPA
    (your browser)          before deploy/promote
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key insight: &lt;strong&gt;you only ever touch &lt;code&gt;manifest.yaml&lt;/code&gt;&lt;/strong&gt;. The tool handles everything else.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 1 — The Design: A Tool That Writes Its Own Files
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Problem with Handwritten Config
&lt;/h3&gt;

&lt;p&gt;When you write &lt;code&gt;nginx.conf&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt; by hand, you introduce drift. Change a port in one place and forget to update it in another. After a few weeks, nobody knows which file is the source of truth.&lt;/p&gt;

&lt;p&gt;SwiftDeploy solves this with a three-layer system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;manifest.yaml          →    templates/*.tmpl    →    generated files
(VALUES)                    (STRUCTURE)              (VALUES + STRUCTURE)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;manifest.yaml&lt;/code&gt; holds all the values — ports, image names, modes, timeouts. The templates hold the structure — how nginx.conf and docker-compose.yml should look. The CLI combines them at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  How &lt;code&gt;swiftdeploy init&lt;/code&gt; Works
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Read manifest into a Python dict
&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;manifest.yaml&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="c1"&gt;# Build a replacements map
&lt;/span&gt;&lt;span class="n"&gt;replacements&lt;/span&gt; &lt;span class="o"&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;{{NGINX_PORT}}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx&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;port&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;{{SERVICE_PORT}}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;services&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;port&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="c1"&gt;# ... etc
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Read template, replace placeholders, write output
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;templates/nginx.conf.tmpl&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;replacements&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nginx.conf&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;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;f&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="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple string replacement. No Jinja2, no templating engine — just Python's built-in &lt;code&gt;str.replace()&lt;/code&gt;. The grader can delete &lt;code&gt;nginx.conf&lt;/code&gt; and &lt;code&gt;docker-compose.yml&lt;/code&gt;, run &lt;code&gt;./swiftdeploy init&lt;/code&gt;, and they regenerate perfectly every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  The API Service
&lt;/h3&gt;

&lt;p&gt;The API is a Python HTTP server using only the standard library — no Flask, no FastAPI. This keeps the Docker image under 60MB (well under the 300MB limit).&lt;/p&gt;

&lt;p&gt;It runs in two modes controlled by a &lt;code&gt;MODE&lt;/code&gt; environment variable injected by Docker Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;stable mode  →  normal behaviour
canary mode  →  adds X-Mode: canary header + activates /chaos endpoint
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same image runs both modes. The only difference is the environment variable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Nginx Reverse Proxy
&lt;/h3&gt;

&lt;p&gt;Nginx sits in front of the app and adds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;X-Deployed-By: swiftdeploy&lt;/code&gt; header on every response&lt;/li&gt;
&lt;li&gt;JSON error bodies on 502/503/504 (instead of ugly HTML)&lt;/li&gt;
&lt;li&gt;Structured access logs in the required format&lt;/li&gt;
&lt;li&gt;Forwards &lt;code&gt;X-Mode&lt;/code&gt; header from the upstream app&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Critically, &lt;strong&gt;the app port is never exposed directly&lt;/strong&gt;. Only Nginx's port is mapped to the host. All traffic must flow through it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2 — The Guardrails: OPA Policy Enforcement
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why OPA?
&lt;/h3&gt;

&lt;p&gt;The task required that the CLI never make allow/deny decisions itself. All logic must live in OPA (Open Policy Agent).&lt;/p&gt;

&lt;p&gt;This matters because it separates concerns cleanly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI  →  collects data, calls OPA, surfaces the result
OPA  →  owns all decision logic, never called by the app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to change a policy, you edit a &lt;code&gt;.rego&lt;/code&gt; file. You never touch the CLI. If you want to change a threshold, you edit &lt;code&gt;data.json&lt;/code&gt;. You never touch the Rego files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Policy Structure
&lt;/h3&gt;

&lt;p&gt;Each policy domain owns exactly one question:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Infrastructure policy&lt;/strong&gt; — &lt;em&gt;Is the host healthy enough to deploy?&lt;/em&gt;&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;import&lt;/span&gt; &lt;span class="n"&gt;rego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&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;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;msg&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;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;infrastructure&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;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"Disk free (%.1fGB) is below minimum threshold (%.1fGB)"&lt;/span&gt;&lt;span class="p"&gt;,&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="p"&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;infrastructure&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="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;msg&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;cpu_load&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;infrastructure&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="n"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"CPU load (%.2f) exceeds maximum threshold (%.2f)"&lt;/span&gt;&lt;span class="p"&gt;,&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;cpu_load&lt;/span&gt;&lt;span class="p"&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;infrastructure&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;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;Canary safety policy&lt;/strong&gt; — &lt;em&gt;Is the canary healthy enough to promote?&lt;/em&gt;&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="ow"&gt;import&lt;/span&gt; &lt;span class="n"&gt;rego&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v1&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;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;violations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;violations&lt;/span&gt; &lt;span class="n"&gt;contains&lt;/span&gt; &lt;span class="n"&gt;msg&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;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;canary&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;msg&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s2"&gt;"Error rate (%.2f%%) exceeds maximum threshold (%.2f%%)"&lt;/span&gt;&lt;span class="p"&gt;,&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;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&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;canary&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="o"&gt;*&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Threshold values live in &lt;code&gt;data.json&lt;/code&gt; — never hardcoded in Rego:&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;"infrastructure"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"min_disk_free_gb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_cpu_load"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;16.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"min_mem_free_percent"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;10.0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canary"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_error_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"max_p99_latency_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Hard Gate in Action
&lt;/h3&gt;

&lt;p&gt;When the CPU load exceeded the threshold, &lt;code&gt;swiftdeploy deploy&lt;/code&gt; was blocked:&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="go"&gt;[deploy] Running OPA pre-deploy policy checks...
  Host stats: disk=80.08GB free, cpu_load=12.88, mem_free=50.0%
[policy] Checking Infrastructure...
  [BLOCK] Infrastructure policy FAILED:
    x CPU load (12.88) exceeds maximum threshold (2.00)
[deploy] BLOCKED by policy. Fix violations above before deploying.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy never started. The CLI surfaced the exact violation reason from OPA — no guessing required.&lt;/p&gt;

&lt;h3&gt;
  
  
  OPA Isolation
&lt;/h3&gt;

&lt;p&gt;OPA is intentionally isolated from public Nginx ingress. It runs on port 8181 inside the Docker network. It is NOT behind Nginx, and its port is only accessible to the CLI running on the host. A user hitting &lt;code&gt;localhost:8080&lt;/code&gt; cannot reach OPA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Failure Handling
&lt;/h3&gt;

&lt;p&gt;The CLI handles every distinct OPA failure mode differently:&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;except&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;URLError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# OPA unreachable — warn but don't crash
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;violations&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;OPA unreachable: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# OPA returned garbage — different message
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;violations&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&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;OPA returned invalid JSON&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Catch-all — still doesn't crash
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;violations&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unexpected OPA error: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI never crashes or hangs when OPA is unavailable. It warns the operator and continues.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3 — The Chaos: Breaking Things on Purpose
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The /metrics Endpoint
&lt;/h3&gt;

&lt;p&gt;The API exposes a &lt;code&gt;/metrics&lt;/code&gt; endpoint in Prometheus text 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="c"&gt;# HELP http_requests_total Total HTTP requests&lt;/span&gt;
&lt;span class="c"&gt;# TYPE http_requests_total counter&lt;/span&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="c"&gt;# HELP http_request_duration_seconds Request latency&lt;/span&gt;
&lt;span class="c"&gt;# TYPE http_request_duration_seconds histogram&lt;/span&gt;
&lt;span class="n"&gt;http_request_duration_seconds_bucket&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;le&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"0.005"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="mi"&gt;38&lt;/span&gt;

&lt;span class="c"&gt;# HELP app_mode Current deployment mode (0=stable, 1=canary)&lt;/span&gt;
&lt;span class="c"&gt;# TYPE app_mode gauge&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="c"&gt;# HELP chaos_active Current chaos state (0=none, 1=slow, 2=error)&lt;/span&gt;
&lt;span class="c"&gt;# TYPE chaos_active gauge&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;No third-party libraries — pure Python calculating histogram buckets manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injecting Chaos
&lt;/h3&gt;

&lt;p&gt;After promoting to canary mode, chaos was injected:&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;# Slow mode — every request sleeps 3 seconds&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/chaos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode": "slow", "duration": 3}'&lt;/span&gt;

&lt;span class="c"&gt;# Error mode — 50% of requests return HTTP 500&lt;/span&gt;
curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/chaos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode": "error", "rate": 0.5}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Status Dashboard Capturing the Failure
&lt;/h3&gt;

&lt;p&gt;With error mode active at 50%, the status dashboard showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=======================================================
  SwiftDeploy Status Dashboard
  2026-05-06T14:18:43Z
=======================================================

  Mode:        CANARY
  Uptime:      892s
  Req/s:       2.40
  P99 Latency: 250ms
  Error Rate:  48.20%

  Policy Compliance:
    + Infrastructure: PASSING
    x Canary Safety: FAILING
      -&amp;gt; Error rate (48.20%) exceeds maximum threshold (1.00%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The canary safety policy immediately flagged the failure. Attempting to promote to stable at this point would have been blocked by OPA.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recovery
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/chaos &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"mode": "recover"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within one scrape cycle the dashboard showed error rate back to 0% and canary safety back to PASSING.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 4 — The Audit Trail
&lt;/h2&gt;

&lt;p&gt;Every significant event is written to &lt;code&gt;history.jsonl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"deploy_success"&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;"2026-05-06T13:53:39Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"promote_success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"canary"&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;"2026-05-06T14:01:22Z"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"event"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status_scrape"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"canary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.482&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;"2026-05-06T14:18:43Z"&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;Running &lt;code&gt;./swiftdeploy audit&lt;/code&gt; parses this file and generates a clean Markdown report:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Timeline&lt;/span&gt;
| Timestamp | Event | Details |
|---|---|---|
| 2026-05-06T13:53:39Z | Deploy | Stack deployed successfully |
| 2026-05-06T14:01:22Z | Promote | Mode switched to canary |

&lt;span class="gu"&gt;## Policy Violations&lt;/span&gt;
| Timestamp | Policy | Reason |
|---|---|---|
| 2026-05-06T14:18:43Z | Canary Safety | error_rate=48.20%, p99=250ms |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;&lt;strong&gt;1. Single source of truth is worth the extra complexity&lt;/strong&gt;&lt;br&gt;
It felt like overkill to build a template engine just to generate two config files. But when the grader deletes your generated files and reruns &lt;code&gt;init&lt;/code&gt;, you're grateful every value comes from one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. OPA's syntax changes between versions&lt;/strong&gt;&lt;br&gt;
The latest OPA image requires &lt;code&gt;import rego.v1&lt;/code&gt; and the &lt;code&gt;if&lt;/code&gt;/&lt;code&gt;contains&lt;/code&gt; keywords. Older Rego syntax silently fails to load. Always check your OPA container logs first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start OPA before running policy checks&lt;/strong&gt;&lt;br&gt;
OPA is part of the stack, so it doesn't exist before &lt;code&gt;docker compose up&lt;/code&gt;. The fix was to start OPA first as a separate step, wait 3 seconds for it to load policies, then run the pre-deploy check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Chaos engineering reveals what metrics matter&lt;/strong&gt;&lt;br&gt;
Before injecting chaos, the &lt;code&gt;/metrics&lt;/code&gt; endpoint felt like box-ticking. After watching the error rate spike to 48% in real time on the status dashboard while OPA simultaneously flagged the canary safety policy — the value became obvious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Policy as code beats policy as documentation&lt;/strong&gt;&lt;br&gt;
A README saying "don't deploy if CPU load is above 2.0" gets ignored. A Rego file that blocks the deploy enforces it automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Full Subcommand Reference
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./swiftdeploy init              &lt;span class="c"&gt;# generate nginx.conf + docker-compose.yml&lt;/span&gt;
./swiftdeploy validate          &lt;span class="c"&gt;# 5 pre-flight checks&lt;/span&gt;
./swiftdeploy deploy            &lt;span class="c"&gt;# OPA check + start stack + health wait&lt;/span&gt;
./swiftdeploy promote canary    &lt;span class="c"&gt;# switch to canary mode&lt;/span&gt;
./swiftdeploy promote stable    &lt;span class="c"&gt;# switch back to stable&lt;/span&gt;
./swiftdeploy status            &lt;span class="c"&gt;# live metrics + policy compliance dashboard&lt;/span&gt;
./swiftdeploy audit             &lt;span class="c"&gt;# generate audit_report.md&lt;/span&gt;
./swiftdeploy teardown          &lt;span class="c"&gt;# stop all containers&lt;/span&gt;
./swiftdeploy teardown &lt;span class="nt"&gt;--clean&lt;/span&gt;  &lt;span class="c"&gt;# stop + delete generated files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;The full project is available on GitHub: &lt;a href="https://github.com/AnitaAliCloud/hng4-devops" rel="noopener noreferrer"&gt;https://github.com/AnitaAliCloud/hng4-devops&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built as part of the HNG DevOps Track — Stage 4A and 4B&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>devops</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>How I Built an Anomaly Detection Engine for DDoS Protection</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Tue, 28 Apr 2026 03:56:36 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/how-i-built-an-anomaly-detection-engine-for-ddos-protection-1ibg</link>
      <guid>https://dev.to/anitaalicloud/how-i-built-an-anomaly-detection-engine-for-ddos-protection-1ibg</guid>
      <description>&lt;p&gt;Introduction&lt;br&gt;
Imagine you run a busy website. On a normal day, about 50 people visit per second. Then suddenly, 5,000 requests flood in every second from a single IP address. Your server crashes. Your real users can't access anything. This is a DDoS (Distributed Denial of Service) attack.&lt;br&gt;
In this post, I'll explain how I built a tool that watches incoming traffic in real time, learns what "normal" looks like, and automatically blocks attackers before they can cause damage.&lt;/p&gt;

&lt;p&gt;What Does the Tool Do?&lt;br&gt;
My anomaly detection engine does 6 things automatically:&lt;/p&gt;

&lt;p&gt;Watches every HTTP request coming into the server&lt;br&gt;
Learns what normal traffic looks like over time&lt;br&gt;
Detects when traffic becomes abnormal&lt;br&gt;
Blocks the attacker using the Linux firewall&lt;br&gt;
Alerts me on Slack within 10 seconds&lt;br&gt;
Unbans the IP automatically after a timeout&lt;/p&gt;

&lt;p&gt;The Architecture&lt;br&gt;
Internet Traffic&lt;br&gt;
      ↓&lt;br&gt;
   Nginx (logs every request as JSON)&lt;br&gt;
      ↓&lt;br&gt;
  Nextcloud (the actual app)&lt;/p&gt;

&lt;p&gt;Detector Daemon reads Nginx logs&lt;br&gt;
      ↓&lt;br&gt;
  Sliding Window → tracks request rates&lt;br&gt;
  Rolling Baseline → learns normal traffic&lt;br&gt;
  Z-score Detection → spots anomalies&lt;br&gt;
  iptables → blocks attackers&lt;br&gt;
  Slack → sends alerts&lt;br&gt;
  Dashboard → shows live metrics&lt;/p&gt;

&lt;p&gt;Part 1 — How the Sliding Window Works&lt;br&gt;
Think of the sliding window like a 60 second camera 🎥&lt;br&gt;
Every request that comes in gets a timestamp. We store these timestamps in a Python deque (double-ended queue) — one for each IP address and one for global traffic.&lt;/p&gt;

&lt;p&gt;from collections import deque&lt;br&gt;
import time&lt;/p&gt;

&lt;h1&gt;
  
  
  One deque per IP
&lt;/h1&gt;

&lt;p&gt;ip_windows = {}&lt;/p&gt;

&lt;p&gt;def record_request(ip):&lt;br&gt;
    now = time.time()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ip not in ip_windows:
    ip_windows[ip] = deque()

# Add this request
ip_windows[ip].append(now)

# Remove requests older than 60 seconds from the LEFT
cutoff = now - 60
while ip_windows[ip] and ip_windows[ip][0] &amp;lt; cutoff:
    ip_windows[ip].popleft()

# Current rate = how many requests in last 60 seconds
current_rate = len(ip_windows[ip]) / 60
return current_rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;from collections import deque&lt;br&gt;
import time&lt;/p&gt;

&lt;h1&gt;
  
  
  One deque per IP
&lt;/h1&gt;

&lt;p&gt;ip_windows = {}&lt;/p&gt;

&lt;p&gt;def record_request(ip):&lt;br&gt;
    now = time.time()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ip not in ip_windows:
    ip_windows[ip] = deque()

# Add this request
ip_windows[ip].append(now)

# Remove requests older than 60 seconds from the LEFT
cutoff = now - 60
while ip_windows[ip] and ip_windows[ip][0] &amp;lt; cutoff:
    ip_windows[ip].popleft()

# Current rate = how many requests in last 60 seconds
current_rate = len(ip_windows[ip]) / 60
return current_rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The magic is the eviction — old timestamps get removed from the left side of the deque automatically. So the deque always contains only the last 60 seconds of requests. The current rate is simply the length of the deque divided by 60.&lt;/p&gt;

&lt;p&gt;Part 2 — How the Baseline Learns from Traffic&lt;br&gt;
The baseline answers one question: "What is normal?"&lt;br&gt;
We can't hardcode this because every website is different. A news site might normally get 1000 req/s. A small blog might get 2 req/s. So we let the system learn.&lt;br&gt;
Every second we record how many requests came in. Every 60 seconds we look at the last 30 minutes of data and calculate:&lt;/p&gt;

&lt;p&gt;import math&lt;/p&gt;

&lt;p&gt;def recalculate_baseline(per_second_counts):&lt;br&gt;
    # Calculate average requests per second&lt;br&gt;
    mean = sum(per_second_counts) / len(per_second_counts)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Calculate how much it normally varies
variance = sum((x - mean) ** 2 for x in per_second_counts) / len(per_second_counts)
stddev = math.sqrt(variance)

# Apply floors to prevent false alarms on quiet traffic
effective_mean = max(mean, 1.0)
effective_stddev = max(stddev, 1.0)

return effective_mean, effective_stddev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We also maintain per-hour slots — the system prefers the current hour's data when it has enough samples. This means the baseline adapts to time-of-day patterns. Rush hour traffic looks different from 3 AM traffic!&lt;/p&gt;

&lt;p&gt;Part 3 — How the Detection Logic Makes a Decision&lt;br&gt;
Once we have the baseline we use a Z-score to decide if current traffic is anomalous.&lt;br&gt;
The Z-score answers: "How many standard deviations away from normal is this?"&lt;/p&gt;

&lt;p&gt;def is_anomalous(current_rate, mean, stddev):&lt;br&gt;
    # Z-score calculation&lt;br&gt;
    z_score = (current_rate - mean) / stddev&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Rate multiplier
rate_multiplier = current_rate / mean

# Flag as anomalous if EITHER condition fires
if z_score &amp;gt; 3.0:
    return True, "z-score exceeded 3.0"

if rate_multiplier &amp;gt; 5.0:
    return True, "rate exceeded 5x baseline"

return False, None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

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

&lt;p&gt;Normal traffic: 50 req/s (mean=50, stddev=10)&lt;br&gt;
Attack traffic: 5000 req/s from one IP&lt;br&gt;
Z-score = (5000 - 50) / 10 = 495&lt;br&gt;
495 &amp;gt; 3.0 → ANOMALY DETECTED! 🚨&lt;/p&gt;

&lt;p&gt;We also detect error surges — if an IP is getting lots of 404/500 errors it might be scanning for vulnerabilities. In that case we tighten the thresholds automatically.&lt;/p&gt;

&lt;p&gt;Part 4 — How iptables Blocks an IP&lt;br&gt;
iptables is Linux's built-in firewall. It runs in the kernel and can drop packets before they even reach your application.&lt;br&gt;
When we detect an attack:&lt;/p&gt;

&lt;p&gt;import subprocess&lt;/p&gt;

&lt;p&gt;def ban_ip(ip):&lt;br&gt;
    # Add a DROP rule — silently discard all packets from this IP&lt;br&gt;
    subprocess.run([&lt;br&gt;
        "iptables", "-A", "INPUT", &lt;br&gt;
        "-s", ip, &lt;br&gt;
        "-j", "DROP"&lt;br&gt;
    ])&lt;br&gt;
    print(f"Banned {ip}")&lt;/p&gt;

&lt;p&gt;def unban_ip(ip):&lt;br&gt;
    # Remove the DROP rule&lt;br&gt;
    subprocess.run([&lt;br&gt;
        "iptables", "-D", "INPUT",&lt;br&gt;
        "-s", ip,&lt;br&gt;
        "-j", "DROP"&lt;br&gt;&lt;br&gt;
    ])&lt;br&gt;
    print(f"Unbanned {ip}")&lt;br&gt;
The -j DROP means "jump to DROP action" — the packet is silently discarded. The attacker doesn't even get an error message back. From their perspective the server just stopped responding.&lt;br&gt;
Bans lift automatically on a backoff schedule:&lt;/p&gt;

&lt;p&gt;1st offence → 10 minutes&lt;br&gt;
2nd offence → 30 minutes&lt;br&gt;
3rd offence → 2 hours&lt;br&gt;
4th+ → permanent&lt;/p&gt;

&lt;p&gt;Part 5 — The Live Dashboard&lt;br&gt;
The dashboard is a simple web page that refreshes every 3 seconds showing:&lt;/p&gt;

&lt;p&gt;Global requests per second&lt;br&gt;
Currently banned IPs&lt;br&gt;
Top 10 source IPs&lt;br&gt;
CPU and memory usage&lt;br&gt;
Current baseline mean and stddev&lt;br&gt;
System uptime&lt;/p&gt;

&lt;p&gt;It's built using Python's built-in http.server — no web framework needed!&lt;/p&gt;

&lt;p&gt;Part 6 — Slack Alerts&lt;br&gt;
When an IP gets banned, a Slack message arrives within 10 seconds:&lt;br&gt;
🚨 IP BANNED&lt;br&gt;
IP: 192.168.1.100&lt;br&gt;
Condition: Anomalous request rate&lt;br&gt;
Current Rate: 450.00 req/s&lt;br&gt;
Baseline: 12.00 req/s&lt;br&gt;
Ban Duration: 10 minutes&lt;br&gt;
Timestamp: 2026-04-28 03:22:36 UTC&lt;br&gt;
And when the ban expires:&lt;br&gt;
✅ IP UNBANNED&lt;br&gt;
IP: 192.168.1.100&lt;br&gt;
Reason: ban-expired&lt;br&gt;
Timestamp: 2026-04-28 03:32:36 UTC&lt;/p&gt;

&lt;p&gt;What I Learned&lt;br&gt;
Building this project taught me:&lt;/p&gt;

&lt;p&gt;Z-scores are powerful — a simple maths formula can detect attacks that would be impossible to catch with hardcoded thresholds&lt;br&gt;
Baselines must be dynamic — hardcoding "block if &amp;gt; 100 req/s" is wrong because normal traffic varies by time of day&lt;br&gt;
iptables is incredibly fast — kernel-level packet dropping happens before the request even reaches Python&lt;br&gt;
Threading needs care — shared data structures need locks to prevent race conditions&lt;br&gt;
Deques are perfect for sliding windows — O(1) append and popleft make them ideal for real-time rate tracking&lt;/p&gt;

&lt;p&gt;Try It Yourself&lt;br&gt;
The full source code is available at:&lt;br&gt;
&lt;a href="https://github.com/AnitaAliCloud/hng-stage3-devops" rel="noopener noreferrer"&gt;https://github.com/AnitaAliCloud/hng-stage3-devops&lt;/a&gt;&lt;br&gt;
The live dashboard is running at:&lt;br&gt;
&lt;a href="http://anitacloud.duckdns.org:8080" rel="noopener noreferrer"&gt;http://anitacloud.duckdns.org:8080&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built as part of the HNG14 DevOps internship programme &lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>networking</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
