<?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: Tanay Jain</title>
    <description>The latest articles on DEV Community by Tanay Jain (@tanayjdev).</description>
    <link>https://dev.to/tanayjdev</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%2F3843172%2Fbca77b7a-7dce-4561-a15a-e19f299d5df6.jpeg</url>
      <title>DEV Community: Tanay Jain</title>
      <link>https://dev.to/tanayjdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tanayjdev"/>
    <language>en</language>
    <item>
      <title>From Manual Deployments to Production-Style CI/CD: Building an Automated Flask Pipeline with GitHub Actions, Amazon ECR, and AWS EC2</title>
      <dc:creator>Tanay Jain</dc:creator>
      <pubDate>Wed, 15 Jul 2026 05:56:03 +0000</pubDate>
      <link>https://dev.to/tanayjdev/from-manual-deployments-to-production-style-cicd-building-an-automated-flask-pipeline-with-github-21ch</link>
      <guid>https://dev.to/tanayjdev/from-manual-deployments-to-production-style-cicd-building-an-automated-flask-pipeline-with-github-21ch</guid>
      <description>&lt;p&gt;Most developers eventually reach the point where deploying an application manually starts to feel repetitive. Mine looked like this:&lt;/p&gt;

&lt;p&gt;SSH into EC2 → pull the latest code → build or pull the Docker image → restart the containers → open the browser → hope everything still works.&lt;/p&gt;

&lt;p&gt;It wasn't difficult. It also wasn't reliable. Every deployment depended on me remembering the correct sequence of commands, in the correct order, with no automatic verification, no quality gate, and no guarantee that what worked on my machine would work in production.&lt;/p&gt;

&lt;p&gt;The application wasn't the problem. The deployment process was.&lt;/p&gt;

&lt;p&gt;That realization changed how I approached this project. Instead of learning GitHub Actions, Docker, and AWS as separate topics, I treated them as parts of one system, with one goal:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A single &lt;code&gt;git push&lt;/code&gt; should be enough to take verified code from my laptop to a running application on AWS.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today, every push to &lt;code&gt;main&lt;/code&gt; automatically lints the code with Flake8, runs the test suite with Pytest, builds a Docker image with Buildx (reusing cached layers and dependencies), pushes the image to Amazon ECR, connects to an EC2 instance over SSH, deploys the new container, verifies it with a health check, and posts the result to Slack. No manual steps, no guessing whether it actually started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Build a CI/CD Pipeline at All?
&lt;/h2&gt;

&lt;p&gt;Most beginner AWS tutorials stop once the application is running: launch an EC2 instance, install Docker, run the container, visit the public IP. That's technically a deployment, but it leaves the real questions unanswered — how does new code reach the server, what stops broken code from being deployed, how does the server know which image to run, and how do you actually know a deployment succeeded?&lt;/p&gt;

&lt;p&gt;Those questions are what CI/CD is for. The pipeline is the process that answers all of them, every time, without relying on memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;The application itself is intentionally simple: a Flask app connected to PostgreSQL, packaged with Docker. Keeping it simple let me focus on the deployment pipeline instead of business logic — the interesting part was never the Flask code, it was everything built around it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python (Flask) + PostgreSQL, Docker &amp;amp; Docker Compose&lt;/li&gt;
&lt;li&gt;GitHub Actions, with matrix builds, reusable workflows, and composite actions&lt;/li&gt;
&lt;li&gt;Amazon ECR for image storage, AWS EC2 for compute&lt;/li&gt;
&lt;li&gt;Pytest for testing, Flake8 for code quality&lt;/li&gt;
&lt;li&gt;Docker Buildx with layer and dependency caching&lt;/li&gt;
&lt;li&gt;Post-deploy health checks and Slack notifications&lt;/li&gt;
&lt;/ul&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/tanayjdev" rel="noopener noreferrer"&gt;
        tanayjdev
      &lt;/a&gt; / &lt;a href="https://github.com/tanayjdev/flask-docker-app" rel="noopener noreferrer"&gt;
        flask-docker-app
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Production-style Flask application with an end-to-end CI/CD pipeline using GitHub Actions, Docker, Amazon ECR and AWS infrastructure (EC2, ALB, RDS, VPC, CloudWatch).
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Flask + PostgreSQL - Production-style CI/CD Pipeline on AWS&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/tanayjdev/flask-docker-app/actions/workflows/main-pipeline.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/tanayjdev/flask-docker-app/actions/workflows/main-pipeline.yml/badge.svg" alt="Main CI/CD Pipeline"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/4fea845cc80e8cd4501d4a4ced884b4414fe15c85e5b6e00475f9496ee71bf92/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52656769737472792d416d617a6f6e2532304543522d4646393930303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/4fea845cc80e8cd4501d4a4ced884b4414fe15c85e5b6e00475f9496ee71bf92/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f52656769737472792d416d617a6f6e2532304543522d4646393930303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465" alt="Amazon ECR"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/685206ad25428b2182e1044c8b75329ebd81c5c089713602e1492261684727f8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f76657273696f6e2d76332e302d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/685206ad25428b2182e1044c8b75329ebd81c5c089713602e1492261684727f8/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f76657273696f6e2d76332e302d626c7565" alt="Version"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/7f01b5d029c060f5a3153c00653932dd28311556da65555be5af045897902009/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4157532d4465706c6f7965642d4646393930303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/7f01b5d029c060f5a3153c00653932dd28311556da65555be5af045897902009/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4157532d4465706c6f7965642d4646393930303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465" alt="AWS"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/dbdd64fcb04f8411badd299a63960c4b2e0d380473359b544b1d4a228f1b0b3a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f636b65722d436f6e7461696e6572697a65642d3234393645443f6c6f676f3d646f636b6572266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/dbdd64fcb04f8411badd299a63960c4b2e0d380473359b544b1d4a228f1b0b3a/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f446f636b65722d436f6e7461696e6572697a65642d3234393645443f6c6f676f3d646f636b6572266c6f676f436f6c6f723d7768697465" alt="Docker"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/4e5805dae81762da48b9c2609a56292944b135cc3e7d5af025b7ee2ab2013678/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466c61736b2d507974686f6e2d3030303030303f6c6f676f3d666c61736b266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/4e5805dae81762da48b9c2609a56292944b135cc3e7d5af025b7ee2ab2013678/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f466c61736b2d507974686f6e2d3030303030303f6c6f676f3d666c61736b266c6f676f436f6c6f723d7768697465" alt="Flask"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/7a95a541d853422747f461fc717e48ecd26d3b11abf35b10e3cf016cf8516045/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d6f6e69746f72696e672d436c6f756457617463682d4646344630303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465"&gt;&lt;img src="https://camo.githubusercontent.com/7a95a541d853422747f461fc717e48ecd26d3b11abf35b10e3cf016cf8516045/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4d6f6e69746f72696e672d436c6f756457617463682d4646344630303f6c6f676f3d616d617a6f6e617773266c6f676f436f6c6f723d7768697465" alt="CloudWatch"&gt;&lt;/a&gt;
&lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/a8fa9e7b20796558c7a5379692083476e79d6327ffdaf8d59054681edcc83571/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d456475636174696f6e616c2d626c7565"&gt;&lt;img src="https://camo.githubusercontent.com/a8fa9e7b20796558c7a5379692083476e79d6327ffdaf8d59054681edcc83571/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f4c6963656e73652d456475636174696f6e616c2d626c7565" alt="License"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Key Highlights&lt;/h2&gt;
&lt;/div&gt;
&lt;ul&gt;
&lt;li&gt;Production-style Flask application&lt;/li&gt;
&lt;li&gt;Dockerized deployment&lt;/li&gt;
&lt;li&gt;GitHub Actions CI/CD Pipeline&lt;/li&gt;
&lt;li&gt;Amazon Elastic Container Registry (ECR)&lt;/li&gt;
&lt;li&gt;Automated deployment to Amazon EC2&lt;/li&gt;
&lt;li&gt;AWS Infrastructure (EC2, ALB, RDS, VPC)&lt;/li&gt;
&lt;li&gt;Matrix builds for multiple Python versions&lt;/li&gt;
&lt;li&gt;Reusable GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;Composite GitHub Actions&lt;/li&gt;
&lt;li&gt;Docker layer &amp;amp; pip dependency caching&lt;/li&gt;
&lt;li&gt;Slack deployment notifications&lt;/li&gt;
&lt;li&gt;Project evolution from local Docker → AWS Infrastructure → Full CI/CD&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Project Goal&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Build, containerize, and deploy a production-style Flask web application on AWS while implementing an end-to-end CI/CD pipeline using GitHub Actions and modern DevOps practices.&lt;/p&gt;
&lt;p&gt;The project demonstrates modern DevOps practices including automated code quality checks, testing, container image builds, continuous deployment, AWS infrastructure, monitoring, deployment automation, and infrastructure best practices.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Current Architecture (v3.0)&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/tanayjdev/flask-docker-app/docs/architecture-v3.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Ftanayjdev%2Fflask-docker-app%2FHEAD%2Fdocs%2Farchitecture-v3.png" alt="Architecture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;What This Project Is&lt;/h2&gt;

&lt;/div&gt;
&lt;p&gt;A production-style Flask web application deployed on AWS using Docker and automated through a complete CI/CD pipeline.&lt;/p&gt;
&lt;p&gt;The application tracks page visits…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/tanayjdev/flask-docker-app" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  The Pipeline, End to End
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Push
  │
  ▼
Lint (Flake8)
  │
  ▼
Unit Tests (Pytest)
  │
  ▼
Docker Build (Buildx + cache)
  │
  ▼
Push Image to Amazon ECR
  │
  ▼
Deploy to EC2 over SSH
  │
  ▼
Health Check (/health)
  │
  ▼
Slack Notification
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each stage has exactly one responsibility, and each stage protects the next: if a stage fails, the pipeline stops immediately and broken code never reaches production. That single rule shaped every other decision in this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 1 — Code Quality First
&lt;/h2&gt;

&lt;p&gt;Linting runs before anything else, on purpose:&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;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
  &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-python@v5&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;python-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.10"&lt;/span&gt;
        &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pip install flake8&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;flake8 . --count --statistics&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not because style matters more than correctness — because it's the &lt;em&gt;fastest&lt;/em&gt; gate. Spinning up a full Docker build only to fail on a stray import is wasted time and wasted compute. Fail fast, fix early, then move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stage 2 — Tests Gate the Pipeline
&lt;/h2&gt;

&lt;p&gt;Passing lint doesn't mean the app works, so the next job runs the test suite — and only starts if lint succeeded:&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;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That one &lt;code&gt;needs&lt;/code&gt; keyword turns a set of independent jobs into an explicit dependency graph. Inside the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
pytest test_app.py &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a test fails, nothing downstream runs. No image is built, nothing is deployed, and production never sees the broken code. Separating &lt;em&gt;verification&lt;/em&gt; from &lt;em&gt;deployment&lt;/em&gt; this way is one of the most useful ideas in the whole pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making It Fast: Caching
&lt;/h2&gt;

&lt;p&gt;Early on, every run re-downloaded every dependency from scratch, even when nothing had changed. &lt;code&gt;setup-python&lt;/code&gt;'s built-in &lt;code&gt;cache: pip&lt;/code&gt; handles most of it, and a dedicated &lt;code&gt;actions/cache&lt;/code&gt; step — keyed off a hash of &lt;code&gt;requirements.txt&lt;/code&gt; — restores previously downloaded packages instantly when the file hasn't changed. Less waiting, faster feedback after every commit.&lt;/p&gt;

&lt;p&gt;Docker layers get the same treatment. Docker only rebuilds what changed, so the Dockerfile's instruction order matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app.py .&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dependencies rarely change; application code changes constantly. Installing dependencies &lt;em&gt;before&lt;/em&gt; copying the source means Docker can reuse that dependency layer across nearly every build, and only rebuild the thin application layer on top. It's a small reordering that noticeably speeds up almost every run — a reminder that performance work isn't always about adding new tools, sometimes it's about arranging the ones you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building and Storing the Image
&lt;/h2&gt;

&lt;p&gt;The image itself is built with Docker Buildx rather than the legacy builder, since it has better support for modern caching:&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="pi"&gt;-&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;docker/setup-buildx-action@v3&lt;/span&gt;
&lt;span class="pi"&gt;-&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;docker/build-push-action@v5&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;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
    &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;cache-from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha&lt;/span&gt;
    &lt;span class="na"&gt;cache-to&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;type=gha,mode=max&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once built, the image needs somewhere to live before deployment. I used Amazon ECR: GitHub Actions authenticates with AWS credentials stored as repository secrets, then pushes the image automatically, tagged both &lt;code&gt;latest&lt;/code&gt; and with the commit SHA so every deployment is traceable back to an exact commit.&lt;/p&gt;

&lt;p&gt;Splitting image &lt;em&gt;creation&lt;/em&gt; from image &lt;em&gt;storage&lt;/em&gt; from image &lt;em&gt;deployment&lt;/em&gt; keeps each piece single-purpose — GitHub Actions builds the artifact, ECR stores it, EC2 just pulls whatever's latest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying to EC2
&lt;/h2&gt;

&lt;p&gt;The deploy job runs only after the image has been built and pushed successfully, and connects to EC2 over SSH using a key stored in GitHub Secrets. Because ECR is a &lt;em&gt;private&lt;/em&gt; registry, the server has to authenticate before it can pull anything — this part is easy to gloss over, but skip it and every pull fails silently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ecr get-login-password &lt;span class="nt"&gt;--region&lt;/span&gt; ap-south-1 &lt;span class="se"&gt;\&lt;/span&gt;
  | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; &amp;lt;ecr-registry-url&amp;gt;

docker compose pull
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--force-recreate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No manual login, no copying files, no rebuilding images on the server. EC2 authenticates, pulls whatever ECR says is latest, and recreates the containers — which means every environment always runs the exact same image, byte for byte.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Deployment Isn't Successful Until It's Healthy
&lt;/h2&gt;

&lt;p&gt;Containers starting doesn't mean the application is working. After the restart, the workflow waits briefly and then checks the app's own health endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="s2"&gt;"%{http_code}"&lt;/span&gt; http://localhost:5000/health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;HTTP 200 means the deployment succeeded; anything else fails the workflow immediately, logs are dumped, and the run is marked as failed. Without this, "the container started" and "the application works" get treated as the same thing — and they aren't. With it, success actually means what it says: the app is responding, not just running.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beyond a Single Workflow: Matrix Builds, Reusable Workflows, and Composite Actions
&lt;/h2&gt;

&lt;p&gt;Once the core pipeline was solid, three GitHub Actions features made it easier to maintain and extend:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Matrix builds&lt;/strong&gt; run the test job across multiple Python versions in parallel (&lt;code&gt;3.9&lt;/code&gt;, &lt;code&gt;3.10&lt;/code&gt;, &lt;code&gt;3.11&lt;/code&gt;), so a change that only breaks one version gets caught before it reaches &lt;code&gt;main&lt;/code&gt;, instead of after.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable workflows&lt;/strong&gt; let the testing logic live in one &lt;code&gt;workflow_call&lt;/code&gt;-triggered file that other workflows invoke by reference, instead of copy-pasting the same lint-and-test steps into every pipeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Composite actions&lt;/strong&gt; package a repeated sequence — checkout, Python setup, dependency install — into a single custom action (&lt;code&gt;setup-python-env&lt;/code&gt;), so each job calls it in one line instead of repeating four steps every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these change &lt;em&gt;what&lt;/em&gt; the pipeline does. They change how much duplication exists in &lt;em&gt;how&lt;/em&gt; it's defined — which matters the moment there's more than one workflow file to maintain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mistakes I Made Along the Way
&lt;/h2&gt;

&lt;p&gt;The project wasn't smooth. A few of the failures taught me more than any of the successful runs did.&lt;/p&gt;

&lt;p&gt;The biggest one: I initially configured the Docker health check to hit an HTTPS endpoint, even though the Flask app only served plain HTTP. The container reported unhealthy indefinitely — not because the app was broken, but because the health check itself was wrong. It's an easy trap: a red status doesn't always mean the thing you think it means, and a deployment is only as trustworthy as the check verifying it.&lt;/p&gt;

&lt;p&gt;I also spent real time debugging GitHub Actions IAM permissions, ECR authentication, and SSH connectivity — each one forced me to actually understand how the pieces fit together instead of treating the pipeline as a black box I copy-pasted into existence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd Improve Next
&lt;/h2&gt;

&lt;p&gt;The pipeline is fully automated, but there's a clear list of what production systems usually add next:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rolling or blue-green deployments to eliminate the brief downtime during &lt;code&gt;--force-recreate&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Automatic rollback if a deployment's health check fails&lt;/li&gt;
&lt;li&gt;Infrastructure as Code with Terraform, instead of hand-provisioned AWS resources&lt;/li&gt;
&lt;li&gt;Kubernetes for orchestration once a single EC2 instance stops being enough&lt;/li&gt;
&lt;li&gt;Observability with Prometheus and Grafana, beyond a single &lt;code&gt;/health&lt;/code&gt; check&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Cheap checks should run first — lint before test, test before build, build before deploy.&lt;/li&gt;
&lt;li&gt;A &lt;code&gt;needs&lt;/code&gt; dependency graph is what turns a pile of jobs into an actual pipeline.&lt;/li&gt;
&lt;li&gt;A private registry means the deploy target has to authenticate — don't skip that step in your own writeup or your own script.&lt;/li&gt;
&lt;li&gt;A health check only means something if it's checking the right thing.&lt;/li&gt;
&lt;li&gt;Reusable workflows and composite actions aren't about &lt;em&gt;new&lt;/em&gt; capability — they're about not repeating yourself once you have more than one pipeline.&lt;/li&gt;
&lt;li&gt;CI/CD isn't one tool. It's a sequence of quality gates that all have to agree before production changes.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;This project started as a way to learn GitHub Actions. It ended up teaching me far more about how a deployment pipeline actually fits together — from code quality and testing, to container builds and image registries, to cloud deployment and the health check that proves it worked.&lt;/p&gt;

&lt;p&gt;There's still plenty left to learn — Kubernetes, Infrastructure as Code, rollback strategies — but building this gave me a real, working mental model of how modern software delivery happens, instead of a list of tool names I could recognize but not explain.&lt;/p&gt;

&lt;p&gt;If you're learning DevOps or cloud engineering, build a pipeline yourself instead of only reading about one. Debugging the failures taught me more than watching the successful runs ever did.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tanayjdev/flask-docker-app" class="crayons-btn crayons-btn--primary" rel="noopener noreferrer"&gt;Check out the full source and workflow files on GitHub&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;I'm curious — what was the moment that convinced you to stop deploying manually? Or if you're still doing it by hand, what's the biggest thing stopping you from automating it?&lt;/p&gt;

</description>
      <category>devops</category>
      <category>githubactions</category>
      <category>aws</category>
      <category>docker</category>
    </item>
    <item>
      <title>How I Deployed a Flask App on AWS with RDS, ALB, and VPC as a Student</title>
      <dc:creator>Tanay Jain</dc:creator>
      <pubDate>Mon, 15 Jun 2026 08:37:17 +0000</pubDate>
      <link>https://dev.to/tanayjdev/how-i-deployed-a-flask-app-on-aws-with-rds-alb-and-vpc-as-a-student-9ld</link>
      <guid>https://dev.to/tanayjdev/how-i-deployed-a-flask-app-on-aws-with-rds-alb-and-vpc-as-a-student-9ld</guid>
      <description>&lt;p&gt;Most Flask deployment tutorials end after launching an EC2 instance.&lt;/p&gt;

&lt;p&gt;Mine did too.&lt;/p&gt;

&lt;p&gt;The application worked, but the architecture had serious problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database exposed on the same machine as the application&lt;/li&gt;
&lt;li&gt;No load balancing&lt;/li&gt;
&lt;li&gt;No monitoring&lt;/li&gt;
&lt;li&gt;No network isolation&lt;/li&gt;
&lt;li&gt;Application accessible directly through EC2 public IP&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I rebuilt the entire stack using AWS VPC, RDS, ALB, Security Groups, IAM, and CloudWatch — to understand how production cloud systems are actually designed.&lt;/p&gt;

&lt;p&gt;This article documents that journey.&lt;/p&gt;




&lt;h2&gt;
  
  
  Table of Contents
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Why I Built This&lt;/li&gt;
&lt;li&gt;Initial Architecture (v1.0)&lt;/li&gt;
&lt;li&gt;Architecture Evolution&lt;/li&gt;
&lt;li&gt;Target Architecture (v2.0)&lt;/li&gt;
&lt;li&gt;Tech Stack&lt;/li&gt;
&lt;li&gt;AWS Services Used&lt;/li&gt;
&lt;li&gt;VPC Design&lt;/li&gt;
&lt;li&gt;Security Model&lt;/li&gt;
&lt;li&gt;RDS Migration&lt;/li&gt;
&lt;li&gt;Load Balancer&lt;/li&gt;
&lt;li&gt;Monitoring&lt;/li&gt;
&lt;li&gt;Challenges&lt;/li&gt;
&lt;li&gt;Security Improvements&lt;/li&gt;
&lt;li&gt;Limitations&lt;/li&gt;
&lt;li&gt;What This Project Taught Me&lt;/li&gt;
&lt;li&gt;Project Outcomes&lt;/li&gt;
&lt;li&gt;Roadmap&lt;/li&gt;
&lt;li&gt;Source Code&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Why I Built This
&lt;/h2&gt;

&lt;p&gt;I wanted to move beyond tutorial deployments and understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How databases are isolated in production&lt;/li&gt;
&lt;li&gt;Why load balancers exist and what they actually do&lt;/li&gt;
&lt;li&gt;How AWS networking works at a real level&lt;/li&gt;
&lt;li&gt;How monitoring is implemented across multiple services&lt;/li&gt;
&lt;li&gt;How security is enforced between application layers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of deploying another demo app, I focused on building a production-style architecture around a simple application — a Flask visit counter backed by PostgreSQL.&lt;/p&gt;




&lt;h2&gt;
  
  
  Initial Architecture (v1.0)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Flask App
    │
PostgreSQL Container
    │
Docker Compose
    │
Single EC2 Instance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything ran together on one machine. It worked for learning but had real limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL inside a container — no managed backups, no isolation&lt;/li&gt;
&lt;li&gt;No health checks — broken app still received traffic&lt;/li&gt;
&lt;li&gt;No monitoring — no visibility into what was happening&lt;/li&gt;
&lt;li&gt;Direct EC2 access — no traffic management layer&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Architecture Evolution
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before (v1.0)&lt;/th&gt;
&lt;th&gt;After (v2.0)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flask + PostgreSQL on one machine&lt;/td&gt;
&lt;td&gt;Flask on EC2 + RDS PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Direct EC2 public IP access&lt;/td&gt;
&lt;td&gt;ALB as single entry point&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No monitoring&lt;/td&gt;
&lt;td&gt;CloudWatch Dashboard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No network isolation&lt;/td&gt;
&lt;td&gt;Public + Private Subnets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PostgreSQL in a container&lt;/td&gt;
&lt;td&gt;Managed Amazon RDS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Single point of failure&lt;/td&gt;
&lt;td&gt;Production-style 3-tier architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Target Architecture (v2.0)
&lt;/h2&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%2Fohqtqxhjs1w6qvatksaw.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%2Fohqtqxhjs1w6qvatksaw.png" alt="Architecture Diagram" width="799" height="571"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 1: Final 3-tier AWS architecture used in this project.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
    │
    ▼
Application Load Balancer
    │
    ▼
EC2 Instance (Docker + Flask)
    │
    ▼
Amazon RDS PostgreSQL (Private Subnet)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows a standard &lt;strong&gt;3-tier architecture&lt;/strong&gt; pattern:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Components&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Presentation&lt;/td&gt;
&lt;td&gt;Application Load Balancer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application&lt;/td&gt;
&lt;td&gt;EC2 + Docker + Flask&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Data&lt;/td&gt;
&lt;td&gt;Amazon RDS PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;&lt;strong&gt;Application&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python, Flask&lt;/li&gt;
&lt;li&gt;PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Containerization&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;li&gt;Docker Compose&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;AWS Services&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VPC, EC2, RDS PostgreSQL&lt;/li&gt;
&lt;li&gt;Application Load Balancer&lt;/li&gt;
&lt;li&gt;IAM, CloudWatch&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Networking and Security&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Security Groups&lt;/li&gt;
&lt;li&gt;Public and Private Subnets&lt;/li&gt;
&lt;li&gt;Internet Gateway, Route Tables&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  AWS Services Used
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Amazon VPC&lt;/td&gt;
&lt;td&gt;Network isolation and segmentation&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon EC2&lt;/td&gt;
&lt;td&gt;Application hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker&lt;/td&gt;
&lt;td&gt;Application containerization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon RDS PostgreSQL&lt;/td&gt;
&lt;td&gt;Managed database&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Load Balancer&lt;/td&gt;
&lt;td&gt;Traffic distribution and health checks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IAM&lt;/td&gt;
&lt;td&gt;Access control and least-privilege&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon CloudWatch&lt;/td&gt;
&lt;td&gt;Monitoring and metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security Groups&lt;/td&gt;
&lt;td&gt;Traffic filtering between layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Internet Gateway&lt;/td&gt;
&lt;td&gt;Internet access for public subnets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Route Tables&lt;/td&gt;
&lt;td&gt;Subnet traffic routing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Building a Custom VPC
&lt;/h2&gt;

&lt;p&gt;The first step was replacing the default VPC with a custom one designed around isolation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;VPC: 10.0.0.0/16
│
├── Public Subnets
│   ├── 10.0.1.0/24   (AZ: ap-south-1b)
│   └── 10.0.4.0/24   (AZ: ap-south-1a)
│
└── Private Subnets
    ├── 10.0.2.0/24   (AZ: ap-south-1b)
    └── 10.0.3.0/24   (AZ: ap-south-1a)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Public subnets host the EC2 instance and ALB — internet-facing resources.&lt;/p&gt;

&lt;p&gt;Private subnets host RDS — no route to the internet gateway, no direct access from anywhere outside the VPC.&lt;/p&gt;

&lt;p&gt;The database is unreachable from the internet not just because of a firewall rule, but because it has no path there at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Group Layering
&lt;/h2&gt;

&lt;p&gt;Each layer only accepts traffic from the layer directly above it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet
   │
   ▼  HTTP 80, HTTPS 443
ALB Security Group
   │
   ▼  HTTP 80 — from ALB Security Group only
EC2 Security Group
   │
   ▼  PostgreSQL 5432 — from EC2 Security Group only
RDS Security Group
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nobody can reach EC2 directly from the internet.&lt;br&gt;
Nobody can reach RDS — not from the internet, not even from a developer's machine.&lt;br&gt;
Every request must pass through the ALB first.&lt;/p&gt;


&lt;h2&gt;
  
  
  IAM and Access Control
&lt;/h2&gt;

&lt;p&gt;Before deploying anything, I set up proper access control:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IAM Admin User with MFA — root account never used for daily work&lt;/li&gt;
&lt;li&gt;IAM Roles for AWS service-to-service access&lt;/li&gt;
&lt;li&gt;Least-privilege policy — no account has more permissions than it needs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Root account credentials are the most dangerous thing in an AWS account. Building this habit early matters.&lt;/p&gt;


&lt;h2&gt;
  
  
  Migrating Database to Amazon RDS
&lt;/h2&gt;

&lt;p&gt;Moving PostgreSQL from a Docker container to Amazon RDS was one of the biggest architectural improvements.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Containerized PostgreSQL&lt;/th&gt;
&lt;th&gt;Amazon RDS PostgreSQL&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Manual backups&lt;/td&gt;
&lt;td&gt;Automated backups&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manual patching&lt;/td&gt;
&lt;td&gt;Automatic patching&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container restart = potential data loss&lt;/td&gt;
&lt;td&gt;Managed persistent storage&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No monitoring built in&lt;/td&gt;
&lt;td&gt;CloudWatch integration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The Flask application connects to RDS using SSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;psycopg2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;DB_HOST&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sslmode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;require&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;connect_timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;RDS is accessible only from the EC2 Security Group. Nothing else can reach it.&lt;/p&gt;




&lt;h2&gt;
  
  
  Application Load Balancer
&lt;/h2&gt;

&lt;p&gt;The ALB became the single entry point for all traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Health Check Configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;               &lt;span class="s"&gt;/health&lt;/span&gt;
&lt;span class="py"&gt;Interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;           &lt;span class="s"&gt;30 seconds&lt;/span&gt;
&lt;span class="err"&gt;Healthy&lt;/span&gt; &lt;span class="py"&gt;Threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="err"&gt;Success&lt;/span&gt; &lt;span class="py"&gt;Code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="s"&gt;200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;/health&lt;/code&gt; endpoint verifies actual database connectivity before reporting healthy:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"connected"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If RDS goes down, the health check fails, and the ALB stops routing traffic to that instance.&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%2F81damdnpw3xfmcbsll1l.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%2F81damdnpw3xfmcbsll1l.png" alt="ALB Target Health" width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 2: ALB Target Group showing healthy EC2 target.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  CloudWatch Monitoring
&lt;/h2&gt;

&lt;p&gt;I configured a unified CloudWatch dashboard covering all three tiers.&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%2Fpgzgvj69v42joi7vaa2u.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%2Fpgzgvj69v42joi7vaa2u.png" alt="CloudWatch Dashboard" width="800" height="336"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Figure 3: CloudWatch dashboard monitoring EC2, RDS, and ALB metrics.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Load Balancer&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request Count&lt;/li&gt;
&lt;li&gt;Target Response Time&lt;/li&gt;
&lt;li&gt;Healthy Host Count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;EC2&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU Utilization&lt;/li&gt;
&lt;li&gt;Network In / Out&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;RDS&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU Utilization&lt;/li&gt;
&lt;li&gt;Database Connections&lt;/li&gt;
&lt;li&gt;Free Storage Space&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Having visibility across all layers meant that when something broke during testing, I could immediately see which tier was affected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Challenges I Encountered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  RDS Connectivity Issues
&lt;/h3&gt;

&lt;p&gt;When I first deployed, the Flask application could not connect to PostgreSQL at all.&lt;/p&gt;

&lt;p&gt;The issue was Security Group configuration. The RDS Security Group was only allowing inbound traffic from a specific IP — which changed every time the EC2 instance restarted.&lt;/p&gt;

&lt;p&gt;Fixing this by referencing the EC2 Security Group ID instead of an IP address made me understand AWS network security in a way no tutorial had managed before.&lt;/p&gt;

&lt;h3&gt;
  
  
  Health Check Design
&lt;/h3&gt;

&lt;p&gt;My first health check endpoint simply returned &lt;code&gt;200 OK&lt;/code&gt; with no logic.&lt;/p&gt;

&lt;p&gt;The problem was that the application could lose database connectivity and still appear healthy to the ALB. Traffic kept routing to a broken instance.&lt;/p&gt;

&lt;p&gt;I redesigned the endpoint to verify actual database access before returning healthy status. If the database query fails, the endpoint returns &lt;code&gt;503&lt;/code&gt;. The ALB catches this and stops routing traffic until connectivity is restored.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security Improvements
&lt;/h2&gt;

&lt;p&gt;The migration introduced multiple security improvements over the original setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Database moved into private subnets — no public IP, no internet route&lt;/li&gt;
&lt;li&gt;Security Group based access control between every layer&lt;/li&gt;
&lt;li&gt;IAM Admin User with MFA — root account locked away&lt;/li&gt;
&lt;li&gt;Least-privilege access model throughout&lt;/li&gt;
&lt;li&gt;ALB as the only public entry point — EC2 not directly reachable&lt;/li&gt;
&lt;li&gt;SSL connection between Flask and RDS PostgreSQL&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These controls significantly reduced the attack surface compared to the original single-machine deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  Approximate Cost
&lt;/h2&gt;

&lt;p&gt;This project was built within AWS Free Tier limits where possible:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;EC2 t2.micro&lt;/td&gt;
&lt;td&gt;Free Tier (750 hrs/month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RDS db.t3.micro&lt;/td&gt;
&lt;td&gt;Free Tier (750 hrs/month)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application Load Balancer&lt;/td&gt;
&lt;td&gt;Minimal cost during testing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CloudWatch&lt;/td&gt;
&lt;td&gt;Free Tier metrics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The goal was to experience production-style architecture without significant cost — and to understand which services carry real costs at scale.&lt;/p&gt;




&lt;h2&gt;
  
  
  Application Endpoints
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Home&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /
Response: Page visited X times!
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Health Check&lt;/strong&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="err"&gt;GET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;/health&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;Response:&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;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"healthy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"database"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"connected"&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;h2&gt;
  
  
  Current Limitations
&lt;/h2&gt;

&lt;p&gt;While this architecture follows production patterns, it still has limitations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single EC2 instance — no redundancy at the application layer&lt;/li&gt;
&lt;li&gt;No Auto Scaling Group — cannot handle traffic spikes&lt;/li&gt;
&lt;li&gt;No CI/CD pipeline — deployments are manual&lt;/li&gt;
&lt;li&gt;No Infrastructure as Code — resources provisioned via console&lt;/li&gt;
&lt;li&gt;No HTTPS — SSL termination via ACM not yet configured&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are planned improvements for the next iteration.&lt;/p&gt;




&lt;h2&gt;
  
  
  What This Project Taught Me
&lt;/h2&gt;

&lt;p&gt;Before this project, I knew how to deploy applications.&lt;/p&gt;

&lt;p&gt;After this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I understood why VPC design matters before writing a single line of application code&lt;/li&gt;
&lt;li&gt;I understood why databases belong in private networks, not just from a tutorial but from seeing what happens when they are not&lt;/li&gt;
&lt;li&gt;I understood how health checks directly affect availability — a bad health check is worse than no health check&lt;/li&gt;
&lt;li&gt;I understood how AWS services interact in a real architecture — not in isolation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The shift from deployment to architecture thinking was the biggest takeaway.&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Outcomes
&lt;/h2&gt;

&lt;p&gt;By the end of this project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designed a custom AWS VPC with public and private subnets across two Availability Zones&lt;/li&gt;
&lt;li&gt;Deployed a Dockerized Flask application on Amazon EC2&lt;/li&gt;
&lt;li&gt;Migrated PostgreSQL from a container to Amazon RDS in private subnets&lt;/li&gt;
&lt;li&gt;Configured an Application Load Balancer with real health check logic&lt;/li&gt;
&lt;li&gt;Implemented Security Group based network isolation across all three tiers&lt;/li&gt;
&lt;li&gt;Built CloudWatch dashboards monitoring EC2, RDS, and ALB metrics&lt;/li&gt;
&lt;li&gt;Documented the full architecture and deployment workflow&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;The current architecture uses manually provisioned AWS resources. Next steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Infrastructure as Code using Terraform — provision the entire stack from code&lt;/li&gt;
&lt;li&gt;Automated CI/CD using GitHub Actions — code push triggers build, test, and deploy&lt;/li&gt;
&lt;li&gt;HTTPS with AWS Certificate Manager&lt;/li&gt;
&lt;li&gt;Container orchestration with Kubernetes on Amazon EKS&lt;/li&gt;
&lt;li&gt;Auto Scaling Groups for high availability&lt;/li&gt;
&lt;li&gt;Centralized logging with CloudWatch Logs&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Full source code, architecture diagrams, Dockerfile, Docker Compose, and documentation:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/tanayjdev/flask-docker-app" rel="noopener noreferrer"&gt;GitHub Repository&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;This project taught me that cloud architecture is not about deploying applications — it is about designing systems that are secure, observable, and resilient.&lt;/p&gt;

&lt;p&gt;Building the VPC, isolating the database, configuring Security Groups, implementing real health checks, and monitoring the full stack with CloudWatch gave me a much deeper understanding of AWS than any tutorial had.&lt;/p&gt;

&lt;p&gt;The application itself is simple.&lt;/p&gt;

&lt;p&gt;The infrastructure behind it is where the real learning happened.&lt;/p&gt;




&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;Feedback, suggestions, and architecture improvements are always welcome.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>devops</category>
      <category>docker</category>
      <category>python</category>
    </item>
    <item>
      <title>How I Built 5 Linux Automation Scripts on AWS EC2</title>
      <dc:creator>Tanay Jain</dc:creator>
      <pubDate>Sat, 23 May 2026 08:21:06 +0000</pubDate>
      <link>https://dev.to/tanayjdev/how-i-built-5-linux-automation-scripts-on-aws-ec2-3pk4</link>
      <guid>https://dev.to/tanayjdev/how-i-built-5-linux-automation-scripts-on-aws-ec2-3pk4</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe3iprwtobuv10ah1fr5q.jpg" 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%2Fe3iprwtobuv10ah1fr5q.jpg" alt=" " width="800" height="380"&gt;&lt;/a&gt;&lt;br&gt;
I wanted to find out what working on a real Linux server actually feels like — not a local VM, not a simulator.&lt;/p&gt;

&lt;p&gt;So in May 2026, I spun up an &lt;strong&gt;Ubuntu 22.04 server on AWS EC2&lt;/strong&gt;, connected via SSH, and spent the entire month doing real work on it.&lt;/p&gt;

&lt;p&gt;Here's what I built.&lt;/p&gt;


&lt;h2&gt;
  
  
  🖥️ Environment
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;AWS EC2 t2.micro&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OS&lt;/td&gt;
&lt;td&gt;Ubuntu 22.04 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editor&lt;/td&gt;
&lt;td&gt;VS Code Codespaces&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auth&lt;/td&gt;
&lt;td&gt;SSH key-based authentication&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Automation&lt;/td&gt;
&lt;td&gt;Bash scripting + cron jobs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  📚 Topics Covered
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Linux Fundamentals
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;User and group management&lt;/li&gt;
&lt;li&gt;File permissions (&lt;code&gt;chmod&lt;/code&gt;, &lt;code&gt;chown&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Process management (&lt;code&gt;ps&lt;/code&gt;, &lt;code&gt;top&lt;/code&gt;, &lt;code&gt;kill&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Networking basics (&lt;code&gt;ss&lt;/code&gt;, &lt;code&gt;curl&lt;/code&gt;, UFW, DNS)&lt;/li&gt;
&lt;li&gt;Package management with &lt;code&gt;apt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Automation &amp;amp; Scripting
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bash scripting — functions and validation&lt;/li&gt;
&lt;li&gt;Log management&lt;/li&gt;
&lt;li&gt;Cron job scheduling&lt;/li&gt;
&lt;li&gt;SSH workflows (&lt;code&gt;scp&lt;/code&gt;, &lt;code&gt;rsync&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Log analysis using &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;awk&lt;/code&gt;, and &lt;code&gt;sed&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  🔧 The 5 Automation Scripts
&lt;/h2&gt;

&lt;p&gt;By the end of the month, I had built and automated &lt;strong&gt;5 production-style Bash scripts&lt;/strong&gt;.&lt;/p&gt;


&lt;h3&gt;
  
  
  1. Server Health Check
&lt;/h3&gt;

&lt;p&gt;A monitoring script that checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPU usage&lt;/li&gt;
&lt;li&gt;RAM usage&lt;/li&gt;
&lt;li&gt;Disk usage&lt;/li&gt;
&lt;li&gt;Service status&lt;/li&gt;
&lt;li&gt;Internet connectivity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scheduled &lt;strong&gt;every 15 minutes&lt;/strong&gt; using cron.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;================================================
        SERVER HEALTH CHECK REPORT
================================================

Date: 2026-05-12 10:00:00
Hostname: ip-172-xx-xx-xx

--- CPU Usage ---
✅ CPU is OK (2.3%)

--- Memory Usage ---
✅ RAM is OK (45%)

--- Services Status ---
✅ ssh: RUNNING
✅ nginx: RUNNING
✅ docker: RUNNING

--- Network ---
✅ Internet: CONNECTED

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

&lt;/div&gt;






&lt;h3&gt;
  
  
  2. Disk Usage Alerter
&lt;/h3&gt;

&lt;p&gt;A script that scans partitions and generates alerts when disk usage exceeds a threshold.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Threshold-based alerts&lt;/li&gt;
&lt;li&gt;Partition monitoring&lt;/li&gt;
&lt;li&gt;Log generation&lt;/li&gt;
&lt;li&gt;Color-coded terminal output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Runs &lt;strong&gt;every hour&lt;/strong&gt; through cron.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Log Cleaner
&lt;/h3&gt;

&lt;p&gt;A maintenance script that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compresses older logs&lt;/li&gt;
&lt;li&gt;Removes outdated logs&lt;/li&gt;
&lt;li&gt;Reduces disk usage automatically&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built using &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;gzip&lt;/code&gt;, and &lt;code&gt;mtime&lt;/code&gt; filters for log retention management.&lt;/p&gt;

&lt;p&gt;Runs &lt;strong&gt;every Sunday&lt;/strong&gt;.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. User Creation Script
&lt;/h3&gt;

&lt;p&gt;A provisioning script for creating users with a consistent setup.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Username validation&lt;/li&gt;
&lt;li&gt;Group assignment&lt;/li&gt;
&lt;li&gt;Home directory creation&lt;/li&gt;
&lt;li&gt;Temporary password generation&lt;/li&gt;
&lt;li&gt;Batch user creation using CSV files
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt; ./user_creation.sh &lt;span class="nt"&gt;--file&lt;/span&gt; users.csv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  5. Backup Script
&lt;/h3&gt;

&lt;p&gt;Creates compressed backups using &lt;code&gt;tar.gz&lt;/code&gt; archives.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Backup verification&lt;/li&gt;
&lt;li&gt;Retention policy&lt;/li&gt;
&lt;li&gt;Automatic cleanup of old backups&lt;/li&gt;
&lt;li&gt;Logging and integrity checks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scheduled &lt;strong&gt;daily at 2 AM&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⏱️ Cron Job Automation
&lt;/h2&gt;

&lt;p&gt;All scripts were automated using cron jobs.&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;# Health check — every 15 minutes&lt;/span&gt;
&lt;span class="k"&gt;*&lt;/span&gt;/15 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/ubuntu/scripts/server_health.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/ubuntu/logs/health_cron.log 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# Disk alerter — every hour&lt;/span&gt;
0 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/ubuntu/scripts/disk_alerter.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/ubuntu/logs/disk_cron.log 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# Backup — daily at 2 AM&lt;/span&gt;
0 2 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; /home/ubuntu/scripts/backup.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/ubuntu/logs/backup_cron.log 2&amp;gt;&amp;amp;1

&lt;span class="c"&gt;# Log cleaner — every Sunday at 11 PM&lt;/span&gt;
0 23 &lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="k"&gt;*&lt;/span&gt; 0 /home/ubuntu/scripts/log_cleaner.sh &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; /home/ubuntu/logs/cleaner_cron.log 2&amp;gt;&amp;amp;1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once configured, the server handled routine maintenance &lt;strong&gt;automatically&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  💡 Biggest Learnings
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Linux becomes comfortable through repetition
&lt;/h3&gt;

&lt;p&gt;At the beginning, basic terminal commands felt unfamiliar.&lt;/p&gt;

&lt;p&gt;After working daily on a remote server, navigating Linux from the command line became much more natural. There's no shortcut — you just have to do it daily.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Automation changes how you think
&lt;/h3&gt;

&lt;p&gt;One of the biggest mindset shifts was noticing repetitive work and immediately thinking:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Can this be automated?"&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That shift alone made scripting feel much more practical — and honestly, more fun.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Real infrastructure teaches different lessons
&lt;/h3&gt;

&lt;p&gt;Working on an actual EC2 instance exposed me to problems that are difficult to fully understand in local environments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSH authentication issues&lt;/li&gt;
&lt;li&gt;File permission problems&lt;/li&gt;
&lt;li&gt;Cron debugging&lt;/li&gt;
&lt;li&gt;Disk usage management&lt;/li&gt;
&lt;li&gt;Log analysis workflows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Solving those problems on a live server taught me far more than just reading commands from documentation.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 What's Next
&lt;/h2&gt;

&lt;p&gt;Next, I'm moving into &lt;strong&gt;AWS Core Infrastructure&lt;/strong&gt; — VPC, IAM, RDS, and Terraform.&lt;/p&gt;

&lt;p&gt;That work starts in June 2026. Follow along if you're on a similar path.&lt;/p&gt;




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

&lt;p&gt;All scripts and documentation are open source:&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://github.com/tanayjdev/linux-bash-scripts" rel="noopener noreferrer"&gt;github.com/tanayjdev/linux-bash-scripts&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;BCA Student • Aspiring Cloud &amp;amp; DevOps Engineer&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>devops</category>
      <category>aws</category>
      <category>bash</category>
    </item>
    <item>
      <title>My CI Pipeline Failed on the First Push. Here's What I Learned.</title>
      <dc:creator>Tanay Jain</dc:creator>
      <pubDate>Sun, 03 May 2026 12:59:58 +0000</pubDate>
      <link>https://dev.to/tanayjdev/my-ci-pipeline-failed-on-the-first-push-heres-what-i-learned-5bjo</link>
      <guid>https://dev.to/tanayjdev/my-ci-pipeline-failed-on-the-first-push-heres-what-i-learned-5bjo</guid>
      <description>&lt;p&gt;My CI pipeline failed on the first push.&lt;/p&gt;

&lt;p&gt;Not because of a big mistake.&lt;br&gt;
Because of a layer conflict in my Dockerfile that &lt;br&gt;
only showed up in a clean environment.&lt;/p&gt;

&lt;p&gt;Locally — everything worked.&lt;br&gt;
CI — red on the first run.&lt;/p&gt;

&lt;p&gt;That one failure taught me more about Docker &lt;br&gt;
than a week of tutorials.&lt;/p&gt;

&lt;p&gt;Here's exactly what I built and what happened.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Project
&lt;/h2&gt;

&lt;p&gt;A Flask + PostgreSQL app running in Docker Compose.&lt;/p&gt;

&lt;p&gt;It tracks page visits and stores the count &lt;br&gt;
in a real PostgreSQL database.&lt;br&gt;
Browser → Flask Container → PostgreSQL → pgdata Volume&lt;/p&gt;

&lt;p&gt;Multi-container setup. Persistent storage. &lt;br&gt;
Environment variables via .env.&lt;br&gt;
Health checks and restart policies.&lt;/p&gt;

&lt;p&gt;Every decision is production-style — &lt;br&gt;
even though it's a learning project.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why I Added CI
&lt;/h2&gt;

&lt;p&gt;Before CI, my workflow looked like this:&lt;br&gt;
Make a change&lt;br&gt;
↓&lt;br&gt;
docker compose down&lt;br&gt;
↓&lt;br&gt;
docker compose build&lt;br&gt;
↓&lt;br&gt;
docker compose up -d&lt;br&gt;
↓&lt;br&gt;
Open browser, check manually&lt;br&gt;
↓&lt;br&gt;
Repeat&lt;/p&gt;

&lt;p&gt;This works for one or two changes.&lt;/p&gt;

&lt;p&gt;But when you're pushing regularly — &lt;br&gt;
manually checking every time is not sustainable.&lt;/p&gt;

&lt;p&gt;I wanted the system to tell me if something broke.&lt;br&gt;
Not discover it later.&lt;/p&gt;


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

&lt;p&gt;&lt;code&gt;.github/workflows/docker-build.yml&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Docker Build CI&lt;/span&gt;

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

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

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout 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@v3&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup Docker Buildx&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;docker/setup-buildx-action@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="s"&gt;docker build -t flask-app:test .&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;Verify image exists&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker images | grep flask-app&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run container test&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 run --rm flask-app:test python -c "&lt;/span&gt;
        &lt;span class="s"&gt;import flask&lt;/span&gt;
        &lt;span class="s"&gt;import psycopg2&lt;/span&gt;
        &lt;span class="s"&gt;print('Flask:', flask.__version__)&lt;/span&gt;
        &lt;span class="s"&gt;print('All imports OK')&lt;/span&gt;
        &lt;span class="s"&gt;"&lt;/span&gt;

    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Check image size&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;SIZE=$(docker image inspect flask-app:test \&lt;/span&gt;
        &lt;span class="s"&gt;--format='{{.Size}}')&lt;/span&gt;
        &lt;span class="s"&gt;echo "Image size: $SIZE bytes"&lt;/span&gt;
        &lt;span class="s"&gt;echo "Build verified"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What Each Step Does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Checkout&lt;/strong&gt; — GitHub's server downloads my code&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Buildx setup&lt;/strong&gt; — Prepares build tools on the runner&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build image&lt;/strong&gt; — Runs my Dockerfile from scratch, &lt;br&gt;
clean environment every time&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify image&lt;/strong&gt; — Confirms image was actually created&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container test&lt;/strong&gt; — Runs the container, &lt;br&gt;
checks Flask and psycopg2 import correctly&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Size check&lt;/strong&gt; — Tracks image size on every build&lt;/p&gt;


&lt;h2&gt;
  
  
  The First Run Failed
&lt;/h2&gt;

&lt;p&gt;Pushed the code. Opened Actions tab. Red.&lt;/p&gt;

&lt;p&gt;The error was in my Dockerfile.&lt;/p&gt;

&lt;p&gt;I had this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layer order was causing a conflict &lt;br&gt;
in the clean CI environment that &lt;br&gt;
didn't show up on my machine.&lt;/p&gt;

&lt;p&gt;Reordered the layers. Pushed again. Green.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is the whole point of CI.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your local machine has cached layers, &lt;br&gt;
existing images, leftover containers.&lt;br&gt;
CI starts completely fresh every time.&lt;/p&gt;

&lt;p&gt;It catches what you miss.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Health Endpoint
&lt;/h2&gt;

&lt;p&gt;I also added a &lt;code&gt;/health&lt;/code&gt; route to Flask:&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="nd"&gt;@app.route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;health&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_connection&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&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;status&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;healthy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;database&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connected&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="mi"&gt;200&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="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;status&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;unhealthy&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;error&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;e&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A running container doesn't mean a working app.&lt;/p&gt;

&lt;p&gt;This endpoint checks actual database connectivity.&lt;br&gt;
200 = Flask is up and DB is reachable.&lt;br&gt;
500 = Something broke inside.&lt;/p&gt;

&lt;p&gt;Real monitoring tools and load balancers &lt;br&gt;
use exactly this kind of endpoint.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture
&lt;/h2&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%2Fz7339zt4ey65a6oz317j.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%2Fz7339zt4ey65a6oz317j.png" alt="Architecture" width="797" height="130"&gt;&lt;/a&gt;&lt;br&gt;
GitHub Actions CI:&lt;br&gt;
Code Push → Build → Test → Validate&lt;br&gt;
↓&lt;br&gt;
Docker Hub (image stored)&lt;br&gt;
↓&lt;br&gt;
AWS EC2 (deployed)&lt;br&gt;
↓&lt;br&gt;
Flask Container ←→ PostgreSQL Container&lt;br&gt;
↓&lt;br&gt;
pgdata Volume&lt;/p&gt;




&lt;h2&gt;
  
  
  Project Versioning
&lt;/h2&gt;

&lt;p&gt;v1.0 — Flask + PostgreSQL + Docker + EC2 deploy&lt;br&gt;
v1.1 — CI pipeline + /health endpoint added&lt;/p&gt;

&lt;p&gt;Tagging versions made me treat this &lt;br&gt;
like a real product — not just practice code.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
Push code → manually test → hope for the best.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
Push code → pipeline runs in ~2 minutes → green or red.&lt;/p&gt;

&lt;p&gt;No guessing. No "I think it should work."&lt;br&gt;
The pipeline knows.&lt;/p&gt;




&lt;h2&gt;
  
  
  Three Things CI Taught Me Practically
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Clean environment matters&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Your machine lies to you.&lt;br&gt;
Cached layers, existing images, leftover state —&lt;br&gt;
none of that exists in CI.&lt;/p&gt;

&lt;p&gt;If it works locally but fails in CI,&lt;br&gt;
the CI is right.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. The pipeline is documentation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Anyone reading the workflow file &lt;br&gt;
understands exactly how the project builds.&lt;br&gt;
No README needed for that part.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Start CI early&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Adding it to an existing project is harder &lt;br&gt;
than starting with it.&lt;/p&gt;

&lt;p&gt;Next project — pipeline goes in on day one.&lt;/p&gt;




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

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/tj2905" rel="noopener noreferrer"&gt;https://github.com/tj2905&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Docker Hub:&lt;/strong&gt; &lt;code&gt;docker pull tanayjain29/flask-devops-app:v1.0&lt;/code&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Learning by building real things.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Turning ideas into working projects.&lt;/em&gt;&lt;br&gt;
&lt;em&gt;Sharing everything on GitHub.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>githubactions</category>
      <category>beginners</category>
    </item>
    <item>
      <title>From Zero to Deployment: Dockerizing a Flask + PostgreSQL App on AWS</title>
      <dc:creator>Tanay Jain</dc:creator>
      <pubDate>Sun, 29 Mar 2026 18:56:16 +0000</pubDate>
      <link>https://dev.to/tanayjdev/from-zero-to-deployment-dockerizing-a-flask-postgresql-app-on-aws-20go</link>
      <guid>https://dev.to/tanayjdev/from-zero-to-deployment-dockerizing-a-flask-postgresql-app-on-aws-20go</guid>
      <description>&lt;p&gt;When I started learning Docker, I had no idea I’d end up deploying a real application on a cloud server within the same month.&lt;/p&gt;

&lt;p&gt;It ended up changing how I think about building software.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idea
&lt;/h2&gt;

&lt;p&gt;I didn’t want to build just another static project.&lt;/p&gt;

&lt;p&gt;I wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A real backend
&lt;/li&gt;
&lt;li&gt;A real database
&lt;/li&gt;
&lt;li&gt;A real deployment
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built a simple web app that tracks page visits and stores them in PostgreSQL.&lt;/p&gt;




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

&lt;ul&gt;
&lt;li&gt;Python Flask (backend)
&lt;/li&gt;
&lt;li&gt;PostgreSQL (database)
&lt;/li&gt;
&lt;li&gt;Docker &amp;amp; Docker Compose
&lt;/li&gt;
&lt;li&gt;AWS EC2 (deployment)
&lt;/li&gt;
&lt;/ul&gt;




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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User (Internet)
   ↓
AWS EC2 Server
   ↓
Docker Engine
   ↓
Flask Container (Port 80)
   ↓ psycopg2
PostgreSQL Container
   ↓
Persistent Volume (pgdata)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Key Concepts I Implemented
&lt;/h2&gt;

&lt;h3&gt;
  
  
  🔹 Multi-container system
&lt;/h3&gt;

&lt;p&gt;Instead of running containers manually, I used Docker Compose.&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 up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts the entire system in one command.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 Environment variables (.env)
&lt;/h3&gt;

&lt;p&gt;No secrets inside the code.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cleaner
&lt;/li&gt;
&lt;li&gt;Safer
&lt;/li&gt;
&lt;li&gt;Production-ready
&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  🔹 Health checks
&lt;/h3&gt;

&lt;p&gt;Docker monitors the application continuously.&lt;br&gt;&lt;br&gt;
If Flask stops responding, the container is marked as unhealthy.&lt;/p&gt;


&lt;h3&gt;
  
  
  🔹 Auto-restart
&lt;/h3&gt;

&lt;p&gt;Using:&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;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If a container crashes, Docker automatically restarts it.&lt;/p&gt;




&lt;h3&gt;
  
  
  🔹 Optimized Dockerfile
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Used &lt;code&gt;python:3.10-slim&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cleared apt cache after install
&lt;/li&gt;
&lt;li&gt;Used &lt;code&gt;--no-cache-dir&lt;/code&gt; for pip
&lt;/li&gt;
&lt;li&gt;Optimized layer ordering
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; Image size reduced significantly (~300MB → ~160MB)&lt;/p&gt;




&lt;h2&gt;
  
  
  Biggest Challenge
&lt;/h2&gt;

&lt;p&gt;Connecting Flask to PostgreSQL inside Docker.&lt;/p&gt;

&lt;p&gt;The key realization:&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Containers communicate using service names, not IP addresses.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_HOST=db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Docker internally resolves &lt;code&gt;db&lt;/code&gt; to the PostgreSQL container.&lt;br&gt;&lt;br&gt;
Once this clicked, the setup became much clearer.&lt;/p&gt;




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

&lt;p&gt;I deployed the app on AWS EC2.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Launch EC2 instance
&lt;/li&gt;
&lt;li&gt;Install Docker
&lt;/li&gt;
&lt;li&gt;Clone project
&lt;/li&gt;
&lt;li&gt;Run Docker Compose
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application was then accessible via a public IP.&lt;/p&gt;




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

&lt;p&gt;Docker is not just about containers.&lt;/p&gt;

&lt;p&gt;It’s about building systems that are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reproducible
&lt;/li&gt;
&lt;li&gt;Isolated
&lt;/li&gt;
&lt;li&gt;Self-healing
&lt;/li&gt;
&lt;li&gt;Easy to deploy
&lt;/li&gt;
&lt;/ul&gt;




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

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/tj2905/flask-docker-app" rel="noopener noreferrer"&gt;https://github.com/tj2905/flask-docker-app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docker Hub: &lt;a href="https://hub.docker.com/r/tanayjain29/flask-devops-app" rel="noopener noreferrer"&gt;https://hub.docker.com/r/tanayjain29/flask-devops-app&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;This project was my first real experience combining backend, database, and deployment into one system.&lt;/p&gt;

&lt;p&gt;If you're learning Docker, building and deploying even a small project like this gives much more clarity than just following tutorials.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>flask</category>
      <category>aws</category>
      <category>postgres</category>
    </item>
  </channel>
</rss>
