<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Shreyas Yadav</title>
    <description>The latest articles on Forem by Shreyas Yadav (@shreyas_yadav_e6fbf9ad3f6).</description>
    <link>https://forem.com/shreyas_yadav_e6fbf9ad3f6</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2755410%2F436c6739-5a53-4526-a270-b8006824466e.jpg</url>
      <title>Forem: Shreyas Yadav</title>
      <link>https://forem.com/shreyas_yadav_e6fbf9ad3f6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/shreyas_yadav_e6fbf9ad3f6"/>
    <language>en</language>
    <item>
      <title>I Built a Nightly Pipeline That Deploys My App While I Sleep</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Mon, 09 Mar 2026 04:07:07 +0000</pubDate>
      <link>https://forem.com/shreyas_yadav_e6fbf9ad3f6/i-built-a-nightly-pipeline-that-deploys-my-app-while-i-sleep-3lh4</link>
      <guid>https://forem.com/shreyas_yadav_e6fbf9ad3f6/i-built-a-nightly-pipeline-that-deploys-my-app-while-i-sleep-3lh4</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Next.js 14 · Express.js · MySQL 8 · Docker · GitHub Actions · AWS EC2 · AWS ECR · AWS RDS · Route 53 · Nginx · Let's Encrypt&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I built a &lt;strong&gt;Job Application Tracker&lt;/strong&gt;, a full-stack SPA with GitHub OAuth login, and set up an automated nightly pipeline that builds Docker images, runs smoke tests on a temporary EC2, pushes verified images to ECR, and deploys to a persistent QA server, all without manual intervention. Here's exactly how I did it.&lt;/p&gt;




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

&lt;ol&gt;
&lt;li&gt;Architecture Overview&lt;/li&gt;
&lt;li&gt;The Application: Job Application Tracker&lt;/li&gt;
&lt;li&gt;Dockerizing the App&lt;/li&gt;
&lt;li&gt;Local Development with Docker Compose&lt;/li&gt;
&lt;li&gt;AWS Infrastructure Setup&lt;/li&gt;
&lt;li&gt;GitHub Actions CI/CD Pipeline&lt;/li&gt;
&lt;li&gt;Domain Name with Route 53 and SSL with Let's Encrypt&lt;/li&gt;
&lt;li&gt;Nginx as a Reverse Proxy&lt;/li&gt;
&lt;li&gt;Security Best Practices&lt;/li&gt;
&lt;li&gt;Lessons Learned&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  1. Architecture Overview
&lt;/h2&gt;

&lt;p&gt;The project is split into two repositories, a separation of concerns that keeps application code and infrastructure code independent:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Repo&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job-application-tracker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application source code (frontend, backend, Dockerfiles, local docker-compose)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;job-application-tracker-infra&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Infrastructure: GitHub Actions workflows, Nginx config, smoke tests, prod docker-compose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  High-Level Architecture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Developer pushes to source repo
         │
         ▼
  GitHub Actions (infra repo)
  Nightly at 2 AM UTC
         │
  ┌──────▼──────┐
  │  1. BUILD   │  Build Docker images with timestamp tag
  │             │  Push to AWS ECR
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  2. SMOKE   │  Launch temporary EC2 (t3.micro)
  │    TEST     │  Run containers, execute curl tests
  │             │  Terminate EC2 (pass or fail)
  └──────┬──────┘
         │ (only if tests pass)
  ┌──────▼──────┐
  │  3. PROMOTE │  Retag timestamp → :latest in ECR
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  4. DEPLOY  │  SSH-less deploy via AWS SSM
  │    TO QA    │  Pull :latest from ECR
  │             │  docker compose up on persistent EC2
  └─────────────┘
         │
  ┌──────▼──────┐
  │  QA EC2     │  Nginx (SSL/HTTPS)
  │  shri.      │  Frontend :3000 → /
  │  software   │  Backend  :5000 → /api/
  └──────┬──────┘
         │
  ┌──────▼──────┐
  │  AWS RDS    │  MySQL 8 (persistent, managed)
  └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. The Application: Job Application Tracker
&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%2Fal5g8xak6wldwlk76085.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%2Fal5g8xak6wldwlk76085.png" alt="Job Application Tracker, live at shri.software" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The finished app running at &lt;a href="https://shri.software" rel="noopener noreferrer"&gt;shri.software&lt;/a&gt; with HTTPS enforced&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The app lets users track their job applications (Applied → Interview → Offer / Rejected). Authentication is handled via &lt;strong&gt;GitHub OAuth&lt;/strong&gt; using NextAuth.js, no username/password to manage.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tech Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js 14.2 with React 18, Tailwind CSS, NextAuth.js&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Express.js 4.18, JWT middleware&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Database:&lt;/strong&gt; MySQL 8 (local via Docker, production via AWS RDS)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Database Schema
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Users created on first GitHub login&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;                  &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;email&lt;/span&gt;               &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;                &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;avatar_url&lt;/span&gt;          &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;provider&lt;/span&gt;            &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;provider_account_id&lt;/span&gt; &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="k"&gt;UNIQUE&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;provider_account_id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;          &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;          &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;-- Each row belongs to one user (CASCADE delete keeps DB clean)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;job_applications&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt;           &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="n"&gt;AUTO_INCREMENT&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;company&lt;/span&gt;      &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;role&lt;/span&gt;         &lt;span class="nb"&gt;VARCHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;status&lt;/span&gt;       &lt;span class="nb"&gt;ENUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'applied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'interview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'offer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'rejected'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="s1"&gt;'applied'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;date_applied&lt;/span&gt; &lt;span class="nb"&gt;DATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt;      &lt;span class="nb"&gt;INT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="k"&gt;FOREIGN&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DELETE&lt;/span&gt; &lt;span class="k"&gt;CASCADE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;updated_at&lt;/span&gt;   &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="k"&gt;CURRENT_TIMESTAMP&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&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.amazonaws.com%2Fuploads%2Farticles%2Flemw1j5j9jtwyqrdbp97.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%2Flemw1j5j9jtwyqrdbp97.png" alt="App dashboard showing an application entry" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard view, filter by status, edit or delete entries inline&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Backend API
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Auth&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/health&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Health check for monitoring&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/users/upsert&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None&lt;/td&gt;
&lt;td&gt;Called by NextAuth on login&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;List user's applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Create application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PUT&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Update application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/applications/:id&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;JWT&lt;/td&gt;
&lt;td&gt;Delete application&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h3&gt;
  
  
  Authentication Flow
&lt;/h3&gt;

&lt;p&gt;NextAuth.js handles the GitHub OAuth dance. The key insight: after OAuth completes, we generate a JWT signed with &lt;code&gt;NEXTAUTH_SECRET&lt;/code&gt; that the frontend sends to the backend on every API call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// frontend/src/app/api/auth/[...nextauth]/route.js (simplified)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;authOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;GitHubProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;clientId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;clientSecret&lt;/span&gt; &lt;span class="p"&gt;})],&lt;/span&gt;
  &lt;span class="na"&gt;callbacks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;signIn&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Store GitHub identity in our DB on first login&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/users/upsert`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&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="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;provider&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;provider_account_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;providerAccountId&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="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;session&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Mint a JWT for API calls; embed it in the session&lt;/span&gt;
      &lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;backendToken&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&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="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;expiresIn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1h&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;session&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="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The backend verifies this token on every protected route:&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="c1"&gt;// backend/src/middleware/auth.js&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;auth&lt;/span&gt; &lt;span class="o"&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="nx"&gt;next&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&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;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;401&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;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;No token&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;decoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jwt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;verify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NEXTAUTH_SECRET&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;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;decoded&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// { userId, email }&lt;/span&gt;
  &lt;span class="nf"&gt;next&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;h2&gt;
  
  
  3. Dockerizing the App
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Backend Dockerfile
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;

&lt;span class="c"&gt;# 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; shri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="nt"&gt;-G&lt;/span&gt; shri

&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 &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--production&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/ ./src/&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; shri:shri /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; shri&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Key decisions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Alpine base&lt;/strong&gt;, minimal attack surface, smaller image (~50 MB vs ~900 MB for &lt;code&gt;node:20&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Non-root user&lt;/strong&gt;, if the container is compromised, the attacker can't escalate to root on the host&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--production&lt;/code&gt; install&lt;/strong&gt;, excludes dev dependencies from the final image&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Frontend Dockerfile (Multi-Stage Build)
&lt;/h3&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 package-lock.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build

&lt;span class="c"&gt;# --- Stage 2: Runtime ---&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; adduser &lt;span class="nt"&gt;-S&lt;/span&gt; shri &lt;span class="nt"&gt;-G&lt;/span&gt; shri
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy only the artifacts needed to run&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/.next ./.next&lt;/span&gt;
&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/package.json ./&lt;/span&gt;

&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chown&lt;/span&gt; &lt;span class="nt"&gt;-R&lt;/span&gt; shri:shri /app
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; shri&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_modules/.bin/next", "start"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multi-stage builds are critical for Next.js, the build stage pulls in all dev dependencies and compiles TypeScript/JSX. The final stage only contains the compiled output, shrinking the image dramatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Local Development with Docker Compose
&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%2Fdbenkoep5uq2jumpwn5l.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%2Fdbenkoep5uq2jumpwn5l.png" alt="App running locally at localhost:9000" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The app running locally via Docker Compose at localhost:9000&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The source repo's &lt;code&gt;docker-compose.yml&lt;/code&gt; wires everything together for local development with a single command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_ROOT_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_DATABASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db_data:/var/lib/mysql&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mysqladmin"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-h"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;localhost"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10000:5000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DB_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="na"&gt;DB_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_DATABASE}&lt;/span&gt;
      &lt;span class="na"&gt;DB_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_USER}&lt;/span&gt;
      &lt;span class="na"&gt;DB_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${MYSQL_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTAUTH_SECRET}&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;  &lt;span class="c1"&gt;# Wait for MySQL, not just the container&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;9000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NEXTAUTH_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${NEXTAUTH_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;NEXTAUTH_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:9000&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_ID}&lt;/span&gt;
      &lt;span class="na"&gt;GITHUB_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${GITHUB_CLIENT_SECRET}&lt;/span&gt;
      &lt;span class="na"&gt;NEXT_PUBLIC_API_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:10000&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To get started locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Clone the source repo&lt;/span&gt;
git clone https://github.com/Shreyas-Yadav/job-application-tracker
&lt;span class="nb"&gt;cd &lt;/span&gt;job-application-tracker

&lt;span class="c"&gt;# 2. Copy and fill in the env file&lt;/span&gt;
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env with your GitHub OAuth credentials and secrets&lt;/span&gt;

&lt;span class="c"&gt;# 3. Launch everything&lt;/span&gt;
docker compose up

&lt;span class="c"&gt;# App available at:&lt;/span&gt;
&lt;span class="c"&gt;# Frontend: http://localhost:9000&lt;/span&gt;
&lt;span class="c"&gt;# Backend:  http://localhost:10000&lt;/span&gt;
&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.amazonaws.com%2Fuploads%2Farticles%2F66g8ukuhpdr2n13wq028.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%2F66g8ukuhpdr2n13wq028.png" alt="docker compose up --build output in terminal" width="800" height="411"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All three containers (db, backend, frontend) starting up successfully&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; with &lt;code&gt;condition: service_healthy&lt;/code&gt; is important, the backend waits for MySQL to be truly ready (not just the container running) before starting.&lt;/p&gt;


&lt;h2&gt;
  
  
  5. AWS Infrastructure Setup
&lt;/h2&gt;
&lt;h3&gt;
  
  
  5.1 GitHub OAuth App
&lt;/h3&gt;

&lt;p&gt;Go to GitHub → Settings → Developer Settings → OAuth Apps → New OAuth App:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Homepage URL:&lt;/strong&gt; &lt;code&gt;https://shri.software&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization callback URL:&lt;/strong&gt; &lt;code&gt;https://shri.software/api/auth/callback/github&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save the &lt;code&gt;Client ID&lt;/code&gt; and &lt;code&gt;Client Secret&lt;/code&gt;, these go into GitHub Actions secrets.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.2 AWS ECR (Elastic Container Registry)
&lt;/h3&gt;

&lt;p&gt;Create two private repositories to store Docker images:&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%2F28990urbh32rkkkae6nm.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%2F28990urbh32rkkkae6nm.png" alt="AWS ECR, both repositories" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Both ECR repositories created and ready to receive images from the CI pipeline&lt;/em&gt;&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 create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; job-tracker-backend &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
aws ecr create-repository &lt;span class="nt"&gt;--repository-name&lt;/span&gt; job-tracker-frontend &lt;span class="nt"&gt;--region&lt;/span&gt; us-east-1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5.3 AWS RDS (MySQL 8)
&lt;/h3&gt;

&lt;p&gt;Create a MySQL 8 RDS instance in the AWS Console:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Engine: MySQL 8.0&lt;/li&gt;
&lt;li&gt;Instance class: &lt;code&gt;db.t3.micro&lt;/code&gt; (Free Tier eligible)&lt;/li&gt;
&lt;li&gt;Storage: 20 GB gp2&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Important:&lt;/strong&gt; Place in the same VPC as your EC2 instances&lt;/li&gt;
&lt;li&gt;Set a master username and password&lt;/li&gt;
&lt;li&gt;Create a database: &lt;code&gt;jobtracker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Note the endpoint, it looks like &lt;code&gt;job-tracker-db.xxx.us-east-1.rds.amazonaws.com&lt;/code&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%2F84vz82fy48tik7xtv3ob.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%2F84vz82fy48tik7xtv3ob.png" alt="AWS RDS, job-tracker-db instance" width="800" height="485"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;RDS MySQL 8 instance showing "Available" status with the endpoint used by the backend&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Why RDS instead of a containerized DB? Managed backups, automated patching, and persistence across EC2 restarts without dealing with EBS volumes.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.4 QA EC2 Instance
&lt;/h3&gt;

&lt;p&gt;Launch a persistent EC2 instance (Ubuntu 22.04, &lt;code&gt;t3.micro&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# After SSH-ing in, install Docker and the SSM agent&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&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; docker.io docker-compose-plugin

&lt;span class="c"&gt;# Enable SSM agent (usually pre-installed on Ubuntu 22.04 AMIs)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;amazon-ssm-agent
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start amazon-ssm-agent

&lt;span class="c"&gt;# Allow ubuntu user to run docker without sudo&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; docker ubuntu
&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.amazonaws.com%2Fuploads%2Farticles%2F5ej7bvqjekbiv12qqsrv.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%2F5ej7bvqjekbiv12qqsrv.png" alt="AWS EC2, QA instance details" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The persistent QA EC2 instance with LabRole attached, running Docker and Nginx&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Attach the &lt;strong&gt;LabRole&lt;/strong&gt; to this EC2 instance. In AWS Academy, &lt;code&gt;LabRole&lt;/code&gt; is a pre-provisioned IAM role that already has the permissions needed for SSM, ECR, and EC2 operations, you don't create it yourself, it's provided by the lab environment.&lt;/p&gt;
&lt;h3&gt;
  
  
  5.5 IAM Credentials for GitHub Actions (AWS Academy)
&lt;/h3&gt;

&lt;p&gt;AWS Academy accounts don't support OIDC federation or long-lived IAM users. Instead, you get temporary session credentials from the &lt;strong&gt;AWS Details&lt;/strong&gt; panel in the Vocareum lab console. These rotate every session.&lt;/p&gt;

&lt;p&gt;Store them as GitHub Secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AWS_ACCESS_KEY_ID      → from AWS Details panel
AWS_SECRET_ACCESS_KEY  → from AWS Details panel
AWS_SESSION_TOKEN      → from AWS Details panel (required for temporary credentials)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then configure credentials in your workflow using the static credential method:&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;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-session-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SESSION_TOKEN }}&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;us-east-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; AWS Academy session credentials expire when the lab session ends (~4 hours). You'll need to update these three secrets each time you start a new lab session. This is a limitation of the Academy environment, in a real AWS account you would use OIDC to avoid static credentials entirely.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  6. GitHub Actions CI/CD Pipeline
&lt;/h2&gt;

&lt;p&gt;The infra repo contains five workflow files that chain together via &lt;code&gt;workflow_call&lt;/code&gt;. Each does one thing well.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.1 Nightly Orchestrator (&lt;code&gt;nightly.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the entry point, triggered on a schedule or manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/nightly.yml&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;Nightly Build and Deploy&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;schedule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;cron&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;2&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;*'&lt;/span&gt;  &lt;span class="c1"&gt;# 2 AM UTC every day&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;        &lt;span class="c1"&gt;# Allow manual triggers&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;setup&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;outputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ steps.tag.outputs.image_tag }}&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;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tag&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;echo "image_tag=$(date +%Y%m%d%H%M%S)" &amp;gt;&amp;gt; $GITHUB_OUTPUT&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;needs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;setup&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;./.github/workflows/build.yml&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_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;smoke-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="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;build&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;./.github/workflows/smoke-test.yml&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_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;promote&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="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;setup&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;smoke-test&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# Only runs if smoke test passes&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;./.github/workflows/promote.yml&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_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ needs.setup.outputs.image_tag }}&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;

  &lt;span class="na"&gt;deploy-qa&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;promote&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;./.github/workflows/deploy-qa.yml&lt;/span&gt;
    &lt;span class="na"&gt;secrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inherit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The timestamp tag (e.g., &lt;code&gt;20250307021530&lt;/code&gt;) makes every build uniquely identifiable. If a smoke test fails, the image stays tagged only with the timestamp, it's never promoted to &lt;code&gt;:latest&lt;/code&gt; and never deployed.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.2 Build and Push to ECR (&lt;code&gt;build.yml&lt;/code&gt;)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/build.yml&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 and 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;workflow_call&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;image_tag&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;required&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;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&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 source 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;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Shreyas-Yadav/job-application-tracker&lt;/span&gt;
          &lt;span class="na"&gt;token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.SOURCE_REPO_TOKEN }}&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-session-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AWS_SESSION_TOKEN }}&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;us-east-1&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 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 and push backend&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 -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-backend:${{ inputs.image_tag }} ./backend&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-backend:${{ inputs.image_tag }}&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 and push frontend&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 -t ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-frontend:${{ inputs.image_tag }} ./frontend&lt;/span&gt;
          &lt;span class="s"&gt;docker push ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com/job-tracker-frontend:${{ inputs.image_tag }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.3 Smoke Test on Temporary EC2 (&lt;code&gt;smoke-test.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;This is the most interesting part. Instead of testing on the QA instance (risking breaking it), we launch a fresh, temporary EC2 for every test run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/smoke-test.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;smoke-test&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;Launch temporary EC2&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;launch&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;INSTANCE_ID=$(aws ec2 run-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--image-id ${{ secrets.TEMP_EC2_AMI }} \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-type t3.micro \&lt;/span&gt;
            &lt;span class="s"&gt;--subnet-id ${{ secrets.TEMP_EC2_SUBNET_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--security-group-ids ${{ secrets.TEMP_EC2_SG_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--iam-instance-profile Name=LabInstanceProfile \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Instances[0].InstanceId' \&lt;/span&gt;
            &lt;span class="s"&gt;--output text)&lt;/span&gt;
          &lt;span class="s"&gt;echo "instance_id=$INSTANCE_ID" &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;Wait for SSM agent&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;# Wait up to 4 minutes for the instance to boot and SSM to connect&lt;/span&gt;
          &lt;span class="s"&gt;for i in {1..24}; do&lt;/span&gt;
            &lt;span class="s"&gt;STATUS=$(aws ssm describe-instance-information \&lt;/span&gt;
              &lt;span class="s"&gt;--filters "Key=InstanceIds,Values=${{ steps.launch.outputs.instance_id }}" \&lt;/span&gt;
              &lt;span class="s"&gt;--query 'InstanceInformationList[0].PingStatus' \&lt;/span&gt;
              &lt;span class="s"&gt;--output text 2&amp;gt;/dev/null || echo "None")&lt;/span&gt;
            &lt;span class="s"&gt;[ "$STATUS" = "Online" ] &amp;amp;&amp;amp; break&lt;/span&gt;
            &lt;span class="s"&gt;sleep 10&lt;/span&gt;
          &lt;span class="s"&gt;done&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 smoke tests via SSM&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;COMMAND_ID=$(aws ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ steps.launch.outputs.instance_id }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands='[&lt;/span&gt;
              &lt;span class="s"&gt;"aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com",&lt;/span&gt;
              &lt;span class="s"&gt;"IMAGE_TAG=${{ inputs.image_tag }} docker compose -f /tmp/docker-compose.smoke.yml up -d",&lt;/span&gt;
              &lt;span class="s"&gt;"bash /tmp/smoke-test.sh"&lt;/span&gt;
            &lt;span class="s"&gt;]' \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Command.CommandId' --output text)&lt;/span&gt;

          &lt;span class="s"&gt;# Poll until complete&lt;/span&gt;
          &lt;span class="s"&gt;aws ssm wait command-executed \&lt;/span&gt;
            &lt;span class="s"&gt;--command-id $COMMAND_ID \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-id ${{ steps.launch.outputs.instance_id }}&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;Terminate temporary EC2&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always()&lt;/span&gt;  &lt;span class="c1"&gt;# Clean up even if tests fail&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;aws ec2 terminate-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ steps.launch.outputs.instance_id }}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;smoke-test.sh&lt;/code&gt; script runs three checks:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="nv"&gt;BACKEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;1&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:5000"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;span class="nv"&gt;FRONTEND&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3000"&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Wait up to 2 minutes for backend to be ready&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..12&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/health"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break
  echo&lt;/span&gt; &lt;span class="s2"&gt;"Waiting for backend... (&lt;/span&gt;&lt;span class="nv"&gt;$i&lt;/span&gt;&lt;span class="s2"&gt;/12)"&lt;/span&gt;
  &lt;span class="nb"&gt;sleep &lt;/span&gt;10
&lt;span class="k"&gt;done&lt;/span&gt;

&lt;span class="c"&gt;# Test 1: Backend health check&lt;/span&gt;
curl &lt;span class="nt"&gt;-sf&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/health"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'"status":"ok"'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: /health"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: Backend /health returns ok"&lt;/span&gt;

&lt;span class="c"&gt;# Test 2: Auth middleware is working (unauthenticated request → 401)&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&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; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$BACKEND&lt;/span&gt;&lt;span class="s2"&gt;/api/applications"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"401"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: /api/applications should return 401, got &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: /api/applications correctly requires authentication"&lt;/span&gt;

&lt;span class="c"&gt;# Test 3: Frontend is serving pages&lt;/span&gt;
&lt;span class="nv"&gt;STATUS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&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; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FRONTEND&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"200"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FAIL: Frontend returned &lt;/span&gt;&lt;span class="nv"&gt;$STATUS&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;exit &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"PASS: Frontend is serving pages"&lt;/span&gt;

&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"All smoke tests passed!"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  6.4 Promote Image (&lt;code&gt;promote.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Once smoke tests pass, we retag the timestamp image as &lt;code&gt;:latest&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="c1"&gt;# .github/workflows/promote.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;promote&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;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;job-tracker-backend&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;job-tracker-frontend&lt;/span&gt;&lt;span class="pi"&gt;]&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;Retag image as latest&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;REGISTRY="${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com"&lt;/span&gt;
          &lt;span class="s"&gt;# Fetch the manifest of the tested image&lt;/span&gt;
          &lt;span class="s"&gt;MANIFEST=$(aws ecr batch-get-image \&lt;/span&gt;
            &lt;span class="s"&gt;--repository-name ${{ matrix.repo }} \&lt;/span&gt;
            &lt;span class="s"&gt;--image-ids imageTag=${{ inputs.image_tag }} \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'images[0].imageManifest' --output text)&lt;/span&gt;

          &lt;span class="s"&gt;# Push the same manifest with the :latest tag&lt;/span&gt;
          &lt;span class="s"&gt;aws ecr put-image \&lt;/span&gt;
            &lt;span class="s"&gt;--repository-name ${{ matrix.repo }} \&lt;/span&gt;
            &lt;span class="s"&gt;--image-tag latest \&lt;/span&gt;
            &lt;span class="s"&gt;--image-manifest "$MANIFEST"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach (retagging the manifest) is instant, no re-pulling or re-pushing layers. The &lt;code&gt;:latest&lt;/code&gt; image is byte-for-byte identical to the tested timestamp image.&lt;/p&gt;

&lt;h3&gt;
  
  
  6.5 Deploy to QA EC2 (&lt;code&gt;deploy-qa.yml&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The final step deploys to the persistent QA server using AWS SSM, no SSH keys needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/deploy-qa.yml&lt;/span&gt;
&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&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;Check QA EC2 is running&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;check&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;STATE=$(aws ec2 describe-instances \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--query 'Reservations[0].Instances[0].State.Name' \&lt;/span&gt;
            &lt;span class="s"&gt;--output text)&lt;/span&gt;
          &lt;span class="s"&gt;echo "state=$STATE" &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;Sync config files&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.check.outputs.state == 'running'&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;# Base64-encode configs and decode them on the EC2 to avoid quoting issues&lt;/span&gt;
          &lt;span class="s"&gt;COMPOSE_B64=$(base64 -w0 docker-compose.prod.yml)&lt;/span&gt;
          &lt;span class="s"&gt;NGINX_B64=$(base64 -w0 nginx/nginx.conf)&lt;/span&gt;

          &lt;span class="s"&gt;aws ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands="[&lt;/span&gt;
              &lt;span class="s"&gt;\"echo $COMPOSE_B64 | base64 -d &amp;gt; /home/ubuntu/app/docker-compose.prod.yml\",&lt;/span&gt;
              &lt;span class="s"&gt;\"echo $NGINX_B64 | base64 -d &amp;gt; /etc/nginx/sites-enabled/default\",&lt;/span&gt;
              &lt;span class="s"&gt;\"nginx -t &amp;amp;&amp;amp; systemctl reload nginx\"&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;Deploy containers&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;steps.check.outputs.state == 'running'&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;aws ssm send-command \&lt;/span&gt;
            &lt;span class="s"&gt;--instance-ids ${{ secrets.QA_EC2_INSTANCE_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--document-name "AWS-RunShellScript" \&lt;/span&gt;
            &lt;span class="s"&gt;--parameters commands="[&lt;/span&gt;
              &lt;span class="s"&gt;\"aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com\",&lt;/span&gt;
              &lt;span class="s"&gt;\"cd /home/ubuntu/app &amp;amp;&amp;amp; docker compose -f docker-compose.prod.yml pull\",&lt;/span&gt;
              &lt;span class="s"&gt;\"DB_HOST=${{ secrets.DB_HOST }} DB_NAME=${{ secrets.DB_NAME }} DB_USER=${{ secrets.DB_USER }} DB_PASSWORD=${{ secrets.DB_PASSWORD }} NEXTAUTH_SECRET=${{ secrets.NEXTAUTH_SECRET }} docker compose -f docker-compose.prod.yml up -d\",&lt;/span&gt;
              &lt;span class="s"&gt;\"sleep 10 &amp;amp;&amp;amp; curl -sf http://localhost:5000/health\"&lt;/span&gt;
            &lt;span class="s"&gt;]"&lt;/span&gt;
&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.amazonaws.com%2Fuploads%2Farticles%2Fn1b2dve7g5zqtn2j5yah.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%2Fn1b2dve7g5zqtn2j5yah.png" alt="GitHub Actions, Nightly Build pipeline all green" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All 5 jobs in the nightly pipeline completing successfully in 5m 26s&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%2F6kt0c0otongxxwxpnufv.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%2F6kt0c0otongxxwxpnufv.png" alt="GitHub Actions, deploy-qa job steps" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The deploy-qa job expanded showing each SSM step: sync config files, deploy via docker compose, health check&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  7. Domain Name with Route 53 and SSL with Let's Encrypt
&lt;/h2&gt;
&lt;h3&gt;
  
  
  7.1 Get a Domain and Migrate to Route 53
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Purchase a domain on &lt;a href="https://www.name.com" rel="noopener noreferrer"&gt;Name.com&lt;/a&gt; (or any registrar)&lt;/li&gt;
&lt;li&gt;Create a &lt;strong&gt;Hosted Zone&lt;/strong&gt; in AWS Route 53 for your domain&lt;/li&gt;
&lt;li&gt;Note the 4 NS (nameserver) records Route 53 provides&lt;/li&gt;
&lt;li&gt;In Name.com's DNS settings, replace the default nameservers with Route 53's NS records&lt;/li&gt;
&lt;li&gt;Wait 24-48 hours for propagation&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  7.2 Create DNS Records
&lt;/h3&gt;

&lt;p&gt;In Route 53, create an &lt;strong&gt;A record&lt;/strong&gt; pointing to your QA EC2's public IP:&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%2Fa126gjqoetu10iknoqnp.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%2Fa126gjqoetu10iknoqnp.png" alt="Route 53, shri.software hosted zone" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Route 53 hosted zone for shri.software showing the A record, NS, and SOA records&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;Type: A
Name: shri.software
Value: &amp;lt;EC2 Public IP&amp;gt;
TTL: 300
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you stop/start EC2 instances, the public IP changes. Consider using an Elastic IP for the QA instance to keep the IP stable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  7.3 Install Certbot and Get an SSL Certificate
&lt;/h3&gt;

&lt;p&gt;SSH into your QA EC2 and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Install Certbot with the Nginx plugin&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update
&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; certbot python3-certbot-nginx

&lt;span class="c"&gt;# Obtain a certificate (Certbot automatically configures Nginx)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot &lt;span class="nt"&gt;--nginx&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; shri.software

&lt;span class="c"&gt;# Verify auto-renewal is configured&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status certbot.timer
&lt;span class="nb"&gt;sudo &lt;/span&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Certbot will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify domain ownership by placing a file at &lt;code&gt;/.well-known/acme-challenge/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Download the certificate to &lt;code&gt;/etc/letsencrypt/live/shri.software/&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Modify your Nginx config to use the certificate&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  8. Nginx as a Reverse Proxy
&lt;/h2&gt;

&lt;p&gt;Nginx sits in front of both services, routing traffic and terminating SSL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# /etc/nginx/sites-enabled/default&lt;/span&gt;

&lt;span class="c1"&gt;# Redirect HTTP to HTTPS&lt;/span&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;shri.software&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;301&lt;/span&gt; &lt;span class="s"&gt;https://&lt;/span&gt;&lt;span class="nv"&gt;$host$request_uri&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# HTTPS server&lt;/span&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;443&lt;/span&gt; &lt;span class="s"&gt;ssl&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;shri.software&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;ssl_certificate&lt;/span&gt;     &lt;span class="n"&gt;/etc/letsencrypt/live/shri.software/fullchain.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;ssl_certificate_key&lt;/span&gt; &lt;span class="n"&gt;/etc/letsencrypt/live/shri.software/privkey.pem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# NextAuth routes must go to the frontend (Next.js handles them)&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/auth/&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_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_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# All other /api/ routes go to Express backend&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/api/&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:5000&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_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;# Everything else goes to Next.js frontend&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_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_set_header&lt;/span&gt; &lt;span class="s"&gt;X-Real-IP&lt;/span&gt; &lt;span class="nv"&gt;$remote_addr&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;X-Forwarded-Proto&lt;/span&gt; &lt;span class="s"&gt;https&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;The order of &lt;code&gt;location&lt;/code&gt; blocks matters: &lt;code&gt;/api/auth/&lt;/code&gt; must be listed before &lt;code&gt;/api/&lt;/code&gt; because Nginx uses longest prefix matching.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Security Best Practices
&lt;/h2&gt;

&lt;p&gt;Here's what I did deliberately for security:&lt;/p&gt;

&lt;h3&gt;
  
  
  Containers
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Non-root users&lt;/strong&gt; in every container (&lt;code&gt;adduser -S shri&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alpine base images&lt;/strong&gt;, smaller surface area, fewer CVEs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;--production&lt;/code&gt; npm install&lt;/strong&gt;, dev tools never ship to production&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage builds&lt;/strong&gt;, build tools (compilers, test runners) never end up in production images&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Authentication
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub OAuth&lt;/strong&gt;, no password storage, no credential database to protect&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Short-lived JWTs&lt;/strong&gt; (1 hour expiry) between frontend and backend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-scoped queries&lt;/strong&gt;, every DB query filters by &lt;code&gt;user_id&lt;/code&gt; from the verified JWT; users cannot access each other's data&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Infrastructure
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No SSH keys&lt;/strong&gt;, AWS SSM for all remote execution; port 22 is closed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LabRole on EC2&lt;/strong&gt;, AWS Academy's pre-provisioned role grants SSM and ECR access without custom policy authoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RDS in private subnet&lt;/strong&gt;, database is not publicly accessible&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in GitHub Secrets&lt;/strong&gt;, never hardcoded in workflow files or checked into git&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only&lt;/strong&gt;, Nginx enforces HTTP → HTTPS redirect at the server level&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Secrets Management
&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%2F5agkpsh2eohc4ybuqrwz.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%2F5agkpsh2eohc4ybuqrwz.png" alt="GitHub Secrets, all 16 secrets configured" width="800" height="492"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;All secrets stored in GitHub, values are never visible after being saved&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# GitHub Secrets used in this project:&lt;/span&gt;
AWS_ACCOUNT_ID          &lt;span class="c"&gt;# AWS account number&lt;/span&gt;
AWS_ACCESS_KEY_ID       &lt;span class="c"&gt;# From AWS Academy lab details panel&lt;/span&gt;
AWS_SECRET_ACCESS_KEY   &lt;span class="c"&gt;# From AWS Academy lab details panel&lt;/span&gt;
AWS_SESSION_TOKEN       &lt;span class="c"&gt;# From AWS Academy lab details panel (rotates each session)&lt;/span&gt;
TEMP_EC2_AMI            &lt;span class="c"&gt;# AMI ID for smoke test instances&lt;/span&gt;
TEMP_EC2_SUBNET_ID      &lt;span class="c"&gt;# VPC subnet for temporary instances&lt;/span&gt;
TEMP_EC2_SG_ID          &lt;span class="c"&gt;# Security group ID&lt;/span&gt;
QA_EC2_INSTANCE_ID      &lt;span class="c"&gt;# Persistent QA EC2 instance ID&lt;/span&gt;
DB_HOST                 &lt;span class="c"&gt;# RDS endpoint&lt;/span&gt;
DB_NAME / DB_USER / DB_PASSWORD
NEXTAUTH_SECRET         &lt;span class="c"&gt;# Shared secret for JWT signing&lt;/span&gt;
GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET  &lt;span class="c"&gt;# OAuth app credentials&lt;/span&gt;
SOURCE_REPO_TOKEN       &lt;span class="c"&gt;# PAT to check out source repo from infra repo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The full pipeline, from a git push to a verified deployment on HTTPS, runs without any manual steps. The key architectural wins:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two repos&lt;/strong&gt; for clean separation of application vs. infrastructure concerns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timestamp-tagged images&lt;/strong&gt; with promotion to &lt;code&gt;:latest&lt;/code&gt; only after passing tests&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral test infrastructure&lt;/strong&gt; that is created and destroyed per pipeline run&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSM-based deployment&lt;/strong&gt; with no SSH keys to manage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route 53 + Let's Encrypt&lt;/strong&gt; for production-grade DNS and SSL at zero cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live app is running at &lt;code&gt;https://shri.software&lt;/code&gt;. The source code is split across:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source repo: &lt;code&gt;github.com/Shreyas-Yadav/job-application-tracker&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Infra repo: &lt;code&gt;github.com/Shreyas-Yadav/job-application-tracker-infra&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>aws</category>
      <category>cicd</category>
    </item>
    <item>
      <title>Building High-Availability Web Apps on AWS: Auto Scaling, ALB, and Private Subnets</title>
      <dc:creator>Shreyas Yadav</dc:creator>
      <pubDate>Tue, 14 Oct 2025 22:26:38 +0000</pubDate>
      <link>https://forem.com/shreyas_yadav_e6fbf9ad3f6/building-high-availability-web-apps-on-aws-auto-scaling-alb-and-private-subnets-48a9</link>
      <guid>https://forem.com/shreyas_yadav_e6fbf9ad3f6/building-high-availability-web-apps-on-aws-auto-scaling-alb-and-private-subnets-48a9</guid>
      <description>&lt;p&gt;This guide, we’ll see how to deploy a production ready weather application on AWS with enterprise grade architecture. We’l' use &lt;strong&gt;Auto Scaling Groups&lt;/strong&gt; and &lt;strong&gt;Application Load Balancers&lt;/strong&gt; for high availability, &lt;strong&gt;public-private subnets&lt;/strong&gt; for security isolation, and &lt;strong&gt;Amazon ECR&lt;/strong&gt; for containerized deployments. To make it production complete, we'll configure a custom domain with &lt;strong&gt;Route 53&lt;/strong&gt; and enable HTTPS using &lt;strong&gt;AWS Certificate Manager&lt;/strong&gt;. The result? A fully scalable, secure web application accessible via your own domain with SSL encryption exactly how professional applications should be deployed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/Shreyas-Yadav/weather-aws" rel="noopener noreferrer"&gt;https://github.com/Shreyas-Yadav/weather-aws&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Clone the above repo and continue. &lt;/p&gt;

&lt;p&gt;Our weather application uses a multi-tier architecture on AWS that separates the frontend and backend for better security and scalability. &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%2Fu19yqto7lwty49fhm96w.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%2Fu19yqto7lwty49fhm96w.png" alt=" " width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Creating the VPC
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a VPC?
&lt;/h3&gt;

&lt;p&gt;A VPC (Virtual Private Cloud) is your isolated network in AWS. Think of it as your own private data center in the cloud where all your servers and resources will live securely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Setting Up the VPC
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate to VPC Creation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Go to AWS Console → Search "VPC" → Click "Your VPCs" → Click &lt;strong&gt;"Create VPC"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose "VPC and more"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You'll see two options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VPC only&lt;/strong&gt; - Manual setup (tedious)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VPC and more&lt;/strong&gt; - Automatic setup(recommended)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Select &lt;strong&gt;"VPC and more"&lt;/strong&gt; - AWS will automatically create all networking components for you.&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%2F6qflqvjwu1rk8x3crzsb.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%2F6qflqvjwu1rk8x3crzsb.png" alt=" " width="800" height="815"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name tag auto-generation:&lt;/strong&gt; &lt;code&gt;weather-app&lt;/code&gt;&lt;br&gt;
This prefix will be added to all resources, making them easy to identify.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv4 CIDR block:&lt;/strong&gt; &lt;code&gt;10.0.0.0/16&lt;/code&gt;&lt;br&gt;
This is your VPC's IP address range (65,536 available IPs).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IPv6:&lt;/strong&gt; Select "No IPv6 CIDR block"&lt;br&gt;
&lt;strong&gt;Tenancy:&lt;/strong&gt; Keep as "Default"&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Availability Zones and Subnets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of Availability Zones:&lt;/strong&gt; Select &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
This is crucial for high availability - if one data center fails, your app continues running in the other.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of public subnets:&lt;/strong&gt; &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
These will host your frontend servers (internet-accessible)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Number of private subnets:&lt;/strong&gt; &lt;strong&gt;2&lt;/strong&gt;&lt;br&gt;
These will host your backend servers (hidden from internet)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subnet CIDR blocks:&lt;/strong&gt; Keep the defaults:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Public subnet 1a:  10.0.0.0/20
Public subnet 1b:  10.0.16.0/20
Private subnet 1a: 10.0.128.0/20
Private subnet 1b: 10.0.144.0/20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;NAT Gateways&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Select &lt;strong&gt;"1 per AZ"&lt;/strong&gt; (creates 2 total)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What's a NAT Gateway?&lt;/strong&gt;&lt;br&gt;
It allows your private backend servers to access the internet (for API calls, updates) while staying hidden from incoming internet traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why 2 NAT Gateways?&lt;/strong&gt;&lt;br&gt;
High availability each private subnet gets its own NAT Gateway for redundancy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Additional Settings&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VPC endpoints:&lt;/strong&gt; None&lt;br&gt;
&lt;strong&gt;DNS options:&lt;/strong&gt; Enable both&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enable DNS hostnames&lt;/li&gt;
&lt;li&gt;Enable DNS resolution&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Create VPC&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;"Create VPC"&lt;/strong&gt; and wait 2-3 minutes.&lt;/p&gt;

&lt;p&gt;AWS will create:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1 VPC&lt;/li&gt;
&lt;li&gt;4 Subnets (2 public, 2 private)
&lt;/li&gt;
&lt;li&gt;1 Internet Gateway&lt;/li&gt;
&lt;li&gt;2 NAT Gateways&lt;/li&gt;
&lt;li&gt;Route tables&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Step 2: Creating Security Groups
&lt;/h2&gt;
&lt;h3&gt;
  
  
  What are Security Groups?
&lt;/h3&gt;

&lt;p&gt;Security Groups are virtual firewalls that control inbound and outbound traffic to your AWS resources.&lt;/p&gt;

&lt;p&gt;We'll create 4 security groups to control traffic between components.&lt;/p&gt;
&lt;h3&gt;
  
  
  Security Group 1: Frontend ALB Security Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Security Groups → Create security group&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%2Fl6m8einthgud0t5ayoon.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%2Fl6m8einthgud0t5ayoon.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-frontend-alb-sg
Description: Security group for Frontend Application Load Balancer
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from 0.0.0.0/0&lt;/li&gt;
&lt;li&gt;HTTPS (443) from 0.0.0.0/0&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 2: Frontend EC2 Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-frontend-sg
Description: Security group for Frontend EC2 instances
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from Custom → &lt;code&gt;weather-frontend-alb-sg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH (22) from My IP (optional for debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 3: Backend ALB Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-backend-alb-sg
Description: Security group for Backend Application Load Balancer
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;HTTP (80) from Custom → &lt;code&gt;weather-frontend-sg&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;




&lt;h3&gt;
  
  
  Security Group 4: Backend EC2 Security Group
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Name: weather-backend-sg
Description: Security group for Backend EC2 instances
VPC: weather-app-vpc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Inbound Rules:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom TCP (3000) from Custom → &lt;code&gt;weather-backend-alb-sg&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH (22) from My IP (optional for debugging)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Outbound Rules:&lt;/strong&gt; All traffic (default)&lt;/p&gt;

&lt;h3&gt;
  
  
  Traffic Flow
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Frontend ALB (80,443) → Frontend EC2 (80) → Backend ALB (80) → Backend EC2 (3000)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3: IAM Role for EC2 Instances.
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Note for AWS Academy/Lab Users
&lt;/h3&gt;

&lt;p&gt;If you're using &lt;strong&gt;AWS Academy Labs&lt;/strong&gt; or &lt;strong&gt;AWS Learner Lab&lt;/strong&gt;, you cannot create custom IAM roles. Instead, use the pre-configured role provided:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Role name:&lt;/strong&gt; &lt;code&gt;LabRole&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This role already has the necessary permissions for ECR, Parameter Store, and other AWS services.&lt;/p&gt;

&lt;p&gt;When creating EC2 instances or Launch Templates, select &lt;strong&gt;LabRole&lt;/strong&gt; as the IAM instance profile.&lt;/p&gt;


&lt;h3&gt;
  
  
  For Regular AWS Accounts
&lt;/h3&gt;

&lt;p&gt;If you're using a personal/company AWS account (not AWS Labs), follow these steps to create a custom IAM role:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; IAM → Roles → Create role&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Select Trusted Entity&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;Trusted entity type: AWS service
Use case: EC2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Permissions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Search and select these 2 managed policies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;AmazonEC2ContainerRegistryReadOnly&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AmazonSSMManagedInstanceCore&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name and Create&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;Role name: weather-ec2-role
Description: IAM role for Weather App EC2 instances
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create role&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Add Parameter Store Policy&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on &lt;strong&gt;weather-ec2-role&lt;/strong&gt; → &lt;strong&gt;Permissions&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add permissions&lt;/strong&gt; → &lt;strong&gt;Create inline policy&lt;/strong&gt; → &lt;strong&gt;JSON&lt;/strong&gt; tab
&lt;/li&gt;
&lt;/ol&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;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameter"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"ssm:GetParameters"&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;"Resource"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:ssm:*:*:parameter/weather-app/*"&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;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;ol&gt;
&lt;li&gt;Policy name: &lt;code&gt;ParameterStoreReadPolicy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Click **Create &lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Creating ECR Repositories
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Amazon ECR?
&lt;/h3&gt;

&lt;p&gt;Amazon Elastic Container Registry (ECR) is a Docker container registry where we'll store our frontend and backend Docker images. EC2 instances will pull images from here during deployment.&lt;/p&gt;




&lt;h3&gt;
  
  
  Create ECR Repositories
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Amazon ECR → Repositories → Create repository&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%2Fby9e5dymlkrz11uqxudn.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%2Fby9e5dymlkrz11uqxudn.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository 1: Frontend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository name: weather-frontend
Image tag mutability: Mutable
Encryption: AES-256 (default)
Scan on push: Enable (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create repository&lt;/strong&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%2F0cxu70pwk84ernnvkyff.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%2F0cxu70pwk84ernnvkyff.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Repository 2: Backend
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Repository name: weather-backend
Image tag mutability: Mutable
Encryption: AES-256 (default)
Scan on push: Enable (optional)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Create repository&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Note Your Repository URIs
&lt;/h3&gt;

&lt;p&gt;After creation, note down both repository URIs (format: &lt;code&gt;account-id.dkr.ecr.region.amazonaws.com/repository-name&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Frontend URI: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
Backend URI: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll need these URIs to push Docker images and configure EC2 instances.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 5: Build and Push Docker Images to ECR
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;Ensure you have installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS CLI (latest version)&lt;/li&gt;
&lt;li&gt;Docker&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Build Docker Images
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Build images compatible with EC2 architecture (Linux AMD64/ARM64).&lt;/p&gt;

&lt;p&gt;Navigate to your project directory and build both images:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build Backend:&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;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="nt"&gt;-t&lt;/span&gt; weather-backend &lt;span class="nt"&gt;--load&lt;/span&gt; ./backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Build Frontend:&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;docker buildx build &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="nt"&gt;-t&lt;/span&gt; weather-frontend &lt;span class="nt"&gt;--load&lt;/span&gt; ./frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;--platform&lt;/code&gt; flag?&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
EC2 instances run Linux. This ensures your Docker images are compatible with EC2's operating system and CPU architecture.&lt;/p&gt;


&lt;h3&gt;
  
  
  Push Images to ECR
&lt;/h3&gt;

&lt;p&gt;AWS provides ready-to-use push commands for each repository.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;To view push commands:&lt;/strong&gt; ECR → Repositories → Select repository → Click &lt;strong&gt;"View push commands"&lt;/strong&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%2Fbyxaiu3w23s8t7ydvtl3.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%2Fbyxaiu3w23s8t7ydvtl3.png" alt=" " width="800" height="642"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;Authenticate Docker to ECR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Replace &lt;code&gt;us-east-1&lt;/code&gt; with your region and &lt;code&gt;339712997278&lt;/code&gt; with your AWS account ID:&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; us-east-1 | docker login &lt;span class="nt"&gt;--username&lt;/span&gt; AWS &lt;span class="nt"&gt;--password-stdin&lt;/span&gt; 339712997278.dkr.ecr.us-east-1.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Tag Frontend Image&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;docker tag weather-frontend:latest 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Push Frontend Image&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;docker push 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Tag Backend Image&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;docker tag weather-backend:latest 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Push Backend Image&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;docker push 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Verify Images in ECR
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;Amazon ECR → Repositories&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check both repositories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;weather-frontend&lt;/code&gt; - Should show &lt;code&gt;latest&lt;/code&gt; tag&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;weather-backend&lt;/code&gt; - Should show &lt;code&gt;latest&lt;/code&gt; tag&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Creating Target Groups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What are Target Groups?
&lt;/h3&gt;

&lt;p&gt;Target Groups are used by Application Load Balancers to route traffic to registered EC2 instances. They also perform health checks to ensure traffic only goes to healthy instances.&lt;/p&gt;

&lt;p&gt;We'll create 2 target groups - one for frontend and one for backend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Target Group 1: Backend Target Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Target Groups → Create target group&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%2F2uq5pjco3m347mpg5iup.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%2F2uq5pjco3m347mpg5iup.png" alt=" " width="800" height="437"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Basic Configuration&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;Target type: Instances
Target group name: weather-backend-tg
Protocol: HTTP
Port: 3000
VPC: weather-app-vpc
Protocol version: HTTP1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Health Checks&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;Health check protocol: HTTP
Health check path: /health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced health check settings:&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;Healthy threshold: 2
Unhealthy threshold: 3
Timeout: 5 seconds
Interval: 30 seconds
Success codes: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Register Targets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip this step - instances will be added automatically by Auto Scaling Group.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create target group&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Target Group 2: Frontend Target Group
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Basic Configuration&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;Target type: Instances
Target group name: weather-frontend-tg
Protocol: HTTP
Port: 80
VPC: weather-app-vpc
Protocol version: HTTP1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Health Checks&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;Health check protocol: HTTP
Health check path: /health
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced health check settings:&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;Healthy threshold: 2
Unhealthy threshold: 3
Timeout: 5 seconds
Interval: 30 seconds
Success codes: 200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Register Targets&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Skip this step - instances will be added automatically by Auto Scaling Group.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create target group&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 7: Creating Application Load Balancers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an Application Load Balancer?
&lt;/h3&gt;

&lt;p&gt;An Application Load Balancer (ALB) distributes incoming traffic across multiple EC2 instances. We'll create two ALBs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend ALB&lt;/strong&gt; (Internet-facing) - Receives traffic from users&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend ALB&lt;/strong&gt; (Internal) - Receives traffic from frontend instances&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  ALB 1: Backend Application Load Balancer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Load Balancers → Create load balancer → Application Load Balancer&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&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;Load balancer name: weather-backend-alb
Scheme: Internal
IP address type: IPv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Mapping&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;VPC: weather-app-vpc
Availability Zones: Select both
  ☑ us-east-1a → Select private subnet
  ☑ us-east-1b → Select private subnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Groups&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;Remove: default
Add: weather-backend-alb-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Listeners and Routing&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;Protocol: HTTP
Port: 80
Default action: Forward to weather-backend-tg
&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.amazonaws.com%2Fuploads%2Farticles%2Ftpeqxgb0ttw2p32748xj.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%2Ftpeqxgb0ttw2p32748xj.png" alt=" " width="800" height="252"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create load balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 2-3 minutes for the ALB to become active.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Copy the Backend ALB DNS name - you'll need it later!&lt;/p&gt;




&lt;h3&gt;
  
  
  ALB 2: Frontend Application Load Balancer
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Basic Configuration&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;Load balancer name: weather-frontend-alb
Scheme: Internet-facing
IP address type: IPv4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Mapping&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;VPC: weather-app-vpc
Availability Zones: Select both
  ☑ us-east-1a → Select public subnet
  ☑ us-east-1b → Select public subnet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Security Groups&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;Remove: default
Add: weather-frontend-alb-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Listeners and Routing&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;Protocol: HTTP
Port: 80
Default action: Forward to weather-frontend-tg
&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.amazonaws.com%2Fuploads%2Farticles%2F9fegqng71s8lwf2qzoqd.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%2F9fegqng71s8lwf2qzoqd.png" alt=" " width="800" height="314"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create load balancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 2-3 minutes for the ALB to become active.&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Load Balancers
&lt;/h3&gt;

&lt;p&gt;Go to &lt;strong&gt;EC2 → Load Balancers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Check both ALBs show &lt;strong&gt;State: Active&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note down both DNS names:&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;Frontend ALB DNS: weather-frontend-alb-xxxxx.us-east-1.elb.amazonaws.com
Backend ALB DNS: weather-backend-alb-xxxxx.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 8: Configure AWS Systems Manager Parameter Store
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is Parameter Store?
&lt;/h3&gt;

&lt;p&gt;Parameter Store allows you to store configuration data and secrets centrally. EC2 instances will fetch these values at runtime, eliminating hardcoded configuration.&lt;/p&gt;




&lt;h3&gt;
  
  
  Create Parameters
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; AWS Systems Manager → Parameter Store → Create parameter. &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%2Fq1dhqvcn467g712hkzfj.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%2Fq1dhqvcn467g712hkzfj.png" alt=" " width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create the following parameters:&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Parameter 1: AWS Region&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;Name: /weather-app/aws-region
Type: String
Value: us-east-1 (or your region)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 2: Backend ALB Host (used by frontend to call an api)&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;Name: /weather-app/backend-host
Type: String
Value: &amp;lt;Your Backend ALB DNS without http://&amp;gt;
Example: internal-weather-backend-alb-xxxxx.us-east-1.elb.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 3: Backend ALB Port (used by frontend to call an api)&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;Name: /weather-app/backend-port
Type: String
Value: 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 4: Frontend ECR Registry&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;Name: /weather-app/ecr-registry-frontend
Type: String
Value: &amp;lt;Your account ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
Example: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 5: Backend ECR Registry&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;Name: /weather-app/ecr-registry-backend
Type: String
Value: &amp;lt;Your account ID&amp;gt;.dkr.ecr.us-east-1.amazonaws.com/weather-backend
Example: 339712997278.dkr.ecr.us-east-1.amazonaws.com/weather-backend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;strong&gt;Parameter 6: OpenWeather API Key&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;Name: /weather-app/openweather-api-key
Type: String (or SecureString for encryption)
Value: &amp;lt;Your OpenWeatherMap API key&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get your free API key from: &lt;a href="https://openweathermap.org/api" rel="noopener noreferrer"&gt;https://openweathermap.org/api&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Parameters
&lt;/h3&gt;

&lt;p&gt;All 6 parameters should now be visible in Parameter Store with the prefix &lt;code&gt;/weather-app/&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;—&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 9: Create Launch Templates
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is a Launch Template?
&lt;/h3&gt;

&lt;p&gt;A Launch Template defines the configuration for EC2 instances that will be launched by Auto Scaling Groups. We'll create two templates - one for backend and one for frontend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Launch Template 1: Backend Launch Template
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Launch Templates → Create launch template&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Launch Template Name and Description&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;Launch template name: weather-backend-lt
Template version description: Backend instances for weather app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Application and OS Images (AMI)&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;Quick Start: Ubuntu
Ubuntu Server 24.04 LTS (HVM), SSD Volume Type
Architecture: 64-bit (x86)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Instance Type&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;Instance type: t3.micro (or t2.micro for free tier)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key Pair&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;Key pair name: Select your existing key pair
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Network Settings&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;Subnet: Don't include in launch template
Firewall (security groups): weather-backend-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Storage&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;Keep default: 8 GiB, gp3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Advanced Details&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM instance profile:&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;LabRole (for AWS Academy users)
OR weather-ec2-role (for regular AWS accounts)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Metadata accessible:&lt;/strong&gt; Enabled&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the backend user data script from the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Shreyas-Yadav/weather-aws/blob/main/backend-deployment.sh" rel="noopener noreferrer"&gt;Backend User Data Script →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Installs Docker and AWS CLI&lt;/li&gt;
&lt;li&gt;Fetches configuration from Parameter Store&lt;/li&gt;
&lt;li&gt;Pulls Docker image from ECR&lt;/li&gt;
&lt;li&gt;Starts the backend container&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Create launch template&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Launch Template 2: Frontend Launch Template
&lt;/h3&gt;

&lt;p&gt;Follow the same steps with these differences:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Launch template name: weather-frontend-lt
Template version description: Frontend instances for weather app
Security group: weather-frontend-sg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User data:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Copy the frontend user data script from the repository:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/Shreyas-Yadav/weather-aws/blob/main/frontend-deployment.sh" rel="noopener noreferrer"&gt;Frontend User Data Script →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Installs Docker and AWS CLI&lt;/li&gt;
&lt;li&gt;Fetches backend ALB DNS from Parameter Store&lt;/li&gt;
&lt;li&gt;Pulls Docker image from ECR&lt;/li&gt;
&lt;li&gt;Starts the frontend container with backend configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Click &lt;strong&gt;Create launch template&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10: Create Auto Scaling Groups
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is an Auto Scaling Group?
&lt;/h3&gt;

&lt;p&gt;An Auto Scaling Group (ASG) automatically manages EC2 instances - launching new instances when traffic increases and terminating them when traffic decreases. We'll create two ASGs - one for backend and one for frontend. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; Create ASG first for backend and then create for frontend.&lt;/p&gt;




&lt;h3&gt;
  
  
  Auto Scaling Group 1: Backend ASG
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Auto Scaling Groups → Create Auto Scaling group&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Choose Launch Template&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;Auto Scaling group name: weather-backend-asg
Launch template: weather-backend-lt
Version: Default (1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Choose Instance Launch Options&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;VPC: weather-app-vpc

Availability Zones and subnets:
  ☑ weather-app-subnet-private1-us-east-1a
  ☑ weather-app-subnet-private2-us-east-1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Configure Advanced Options&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;Load balancing:
  ☑ Attach to an existing load balancer

Choose from your load balancer target groups:
  ☑ weather-backend-tg

Health checks:
  ☑ Turn on Elastic Load Balancing health checks
  Health check grace period: 300 seconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Configure Group Size and Scaling&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;Group size:
  Desired capacity: 2
  Minimum capacity: 2
  Maximum capacity: 4

Scaling:
  ☑ Target tracking scaling policy

  Scaling policy name: backend-cpu-scaling
  Metric type: Average CPU utilization
  Target value: 70
  Instance warmup: 300 seconds
&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.amazonaws.com%2Fuploads%2Farticles%2F6jwckwpwn6qguxasn90b.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%2F6jwckwpwn6qguxasn90b.png" alt=" " width="800" height="235"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Next&lt;/strong&gt; → &lt;strong&gt;Next&lt;/strong&gt; → &lt;strong&gt;Create Auto Scaling group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 5-10 minutes for instances to launch and become healthy.&lt;/p&gt;




&lt;h3&gt;
  
  
  Auto Scaling Group 2: Frontend ASG
&lt;/h3&gt;

&lt;p&gt;Follow the same steps with these differences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Choose Launch Template&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;Auto Scaling group name: weather-frontend-asg
Launch template: weather-frontend-lt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 2: Network&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;Availability Zones and subnets:
  ☑ weather-app-subnet-public1-us-east-1a
  ☑ weather-app-subnet-public2-us-east-1b
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 3: Load Balancing&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;Target group: weather-frontend-tg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Step 4: Group Size and Scaling&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;Desired capacity: 2
Minimum capacity: 2
Maximum capacity: 4

Scaling policy name: frontend-cpu-scaling
Metric type: Average CPU utilization
Target value: 70
&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.amazonaws.com%2Fuploads%2Farticles%2Frrwrmvrenj946vl2gkn3.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%2Frrwrmvrenj946vl2gkn3.png" alt=" " width="800" height="284"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create Auto Scaling group&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Wait 5-10 minutes for instances to launch and become healthy.&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Auto Scaling Groups
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Check Instances:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;You should see 4 running instances:

&lt;ul&gt;
&lt;li&gt;2 backend instances (in private subnets)&lt;/li&gt;
&lt;li&gt;2 frontend instances (in public subnets)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Check Target Health:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Target Groups&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-backend-tg&lt;/strong&gt; → Targets tab

&lt;ul&gt;
&lt;li&gt;Both targets should show &lt;strong&gt;Healthy&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-frontend-tg&lt;/strong&gt; → Targets tab

&lt;ul&gt;
&lt;li&gt;Both targets should show &lt;strong&gt;Healthy&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;—--&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11: Testing Your Application
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Test Application via Frontend ALB
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Get Frontend ALB DNS:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;strong&gt;EC2 → Load Balancers&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Select &lt;strong&gt;weather-frontend-alb&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the &lt;strong&gt;DNS name&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Example: &lt;code&gt;weather-frontend-alb-841040545.us-east-1.elb.amazonaws.com&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Test in Browser:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open your browser and navigate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://weather-frontend-alb-XXXXXXX.us-east-1.elb.amazonaws.com
&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.amazonaws.com%2Fuploads%2Farticles%2Fylnpqyw7yqvgkco8navu.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%2Fylnpqyw7yqvgkco8navu.png" alt=" " width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weather Application interface&lt;/li&gt;
&lt;li&gt;Search box to enter city name&lt;/li&gt;
&lt;li&gt;Real-time weather data when you search&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Test the Complete Flow:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enter a city name (e.g., "Mumbai", "London", "New York")&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Search&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Weather information should display:

&lt;ul&gt;
&lt;li&gt;Current temperature&lt;/li&gt;
&lt;li&gt;Weather condition (Haze, Clear, etc.)&lt;/li&gt;
&lt;li&gt;Feels like temperature&lt;/li&gt;
&lt;li&gt;Humidity&lt;/li&gt;
&lt;li&gt;Wind speed&lt;/li&gt;
&lt;li&gt;Pressure&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Common Issues:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If the application doesn't work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Target Health:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EC2 → Target Groups → Check both target groups show "Healthy"&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Instance Logs:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Connect to instances via Session Manager&lt;/li&gt;
&lt;li&gt;View logs: &lt;code&gt;sudo docker logs weather-frontend&lt;/code&gt; or &lt;code&gt;sudo docker logs weather-backend&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Check Security Groups:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Verify all security group rules are correct&lt;/li&gt;
&lt;li&gt;Frontend ALB should allow 80 from internet&lt;/li&gt;
&lt;li&gt;Backend ALB should allow 80 from frontend SG&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;—--&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12: Add Custom Domain and Enable HTTPS
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Prerequisites
&lt;/h3&gt;

&lt;p&gt;You need a registered domain name from any registrar (Namecheap, GoDaddy, etc.)&lt;/p&gt;




&lt;h3&gt;
  
  
  Part A: Create Hosted Zone in Route 53
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Route 53 → Hosted zones → Create hosted zone&lt;/p&gt;

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

&lt;p&gt;Domain name: shri.software&lt;br&gt;
Type: Public hosted zone&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create hosted zone&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update Nameservers at Your Domain Registrar:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the 4 NS (nameserver) records from Route 53&lt;/li&gt;
&lt;li&gt;Go to your domain registrar (Namecheap, GoDaddy, etc.)&lt;/li&gt;
&lt;li&gt;Replace existing nameservers with the 4 AWS nameservers&lt;/li&gt;
&lt;li&gt;Save changes&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Wait 15-30 minutes for DNS propagation&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part B: Request SSL/TLS Certificate
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; AWS Certificate Manager (ACM) → Request certificate&lt;br&gt;
&lt;strong&gt;Step 1: Request Certificate&lt;/strong&gt;&lt;br&gt;
Certificate type: Request a public certificate&lt;br&gt;
Click &lt;strong&gt;Next&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Domain Names&lt;/strong&gt;&lt;br&gt;
Fully qualified domain name: shri.software&lt;br&gt;
Add another name: *.shri.software (optional, for subdomains)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Validation Method&lt;/strong&gt;&lt;br&gt;
Validation method: DNS validation - recommended&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Key Algorithm&lt;/strong&gt;&lt;br&gt;
RSA 2048 (default)&lt;br&gt;
Click &lt;strong&gt;Request&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part C: Validate Certificate
&lt;/h3&gt;

&lt;p&gt;After requesting the certificate, ACM will show validation details.&lt;br&gt;
&lt;strong&gt;Click "Create records in Route”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This automatically adds the required CNAME validation records to your hosted zone.&lt;br&gt;
Wait 5-10 minutes for validation to complete.&lt;br&gt;
&lt;strong&gt;Certificate Status:&lt;/strong&gt; Will change from "Pending validation" to "Issued"&lt;/p&gt;




&lt;h3&gt;
  
  
  Part D: Create A Record for Your Domain
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; Route 53 → Hosted zones → shri.software&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Create A Record:&lt;/strong&gt;&lt;br&gt;
Record name: (leave empty for root domain)&lt;br&gt;
Record type: A - Alias&lt;br&gt;
Route traffic to: Alias to Application Load Balancer&lt;br&gt;
Region: us-east-1&lt;br&gt;
Load balancer: weather-frontend-alb&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Create records&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part E: Add HTTPS Listener to Frontend ALB
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Navigate:&lt;/strong&gt; EC2 → Load Balancers → weather-frontend-alb&lt;br&gt;
&lt;strong&gt;Add HTTPS Listener:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Listeners&lt;/strong&gt; tab&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add listener&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protocol: HTTPS&lt;br&gt;
Port: 443&lt;br&gt;
Default action: Forward to weather-frontend-tg&lt;br&gt;
Default SSL/TLS certificate: From ACM → Select your certificate (shri.software)&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Part F: Redirect HTTP to HTTPS
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Edit HTTP:80 Listener:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Select &lt;strong&gt;HTTP:80&lt;/strong&gt; listener&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Actions&lt;/strong&gt; → &lt;strong&gt;Edit listener&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Remove existing forward action&lt;/li&gt;
&lt;li&gt;Add action → &lt;strong&gt;Redirect to...&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Protocol: HTTPS&lt;br&gt;
Port: 443&lt;br&gt;
Status code: 301 - Permanently moved&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Save changes&lt;/strong&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Verify Your Setup
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Test in Browser:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Open: &lt;a href="https://your_domain_name" rel="noopener noreferrer"&gt;https://your_domain_name&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see:&lt;br&gt;
✅ Padlock icon (secure connection)&lt;br&gt;
✅ Weather application loads&lt;br&gt;
✅ HTTP automatically redirects to HTTPS&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%2F6uheosccn406oxmnp1r2.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%2F6uheosccn406oxmnp1r2.png" alt=" " width="800" height="781"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Congratulations!&lt;/strong&gt; Your application is live.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13: Cleanup and Delete Resources
&lt;/h2&gt;

&lt;p&gt;It is essential to delete all resources created in this guide to avoid incurring unnecessary charges on your AWS account.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cleanup Order
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Auto Scaling Groups (ASGs):&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-asg&lt;/code&gt; and &lt;code&gt;weather-backend-asg&lt;/code&gt;. (This action automatically terminates all running EC2 instances.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Application Load Balancers (ALBs):&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-alb&lt;/code&gt; and &lt;code&gt;weather-backend-alb&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Target Groups:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-tg&lt;/code&gt; and &lt;code&gt;weather-backend-tg&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Launch Templates:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend-lt&lt;/code&gt; and &lt;code&gt;weather-backend-lt&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ECR Repositories:&lt;/strong&gt;&lt;br&gt;
Delete &lt;code&gt;weather-frontend&lt;/code&gt; and &lt;code&gt;weather-backend&lt;/code&gt;. (This deletes the stored Docker images.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Route 53 &amp;amp; AWS Certificate Manager (ACM):&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;A Record&lt;/strong&gt; pointing to the ALB.&lt;br&gt;
Delete the &lt;strong&gt;ACM Certificate&lt;/strong&gt;.&lt;br&gt;
Delete the &lt;strong&gt;Hosted Zone&lt;/strong&gt; in Route 53.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IAM Role &amp;amp; Security Groups:&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;IAM Role&lt;/strong&gt; (&lt;code&gt;weather-ec2-role&lt;/code&gt;).&lt;br&gt;
Delete the &lt;strong&gt;4 Security Groups&lt;/strong&gt; (&lt;code&gt;weather-frontend-alb-sg&lt;/code&gt;, &lt;code&gt;weather-frontend-sg&lt;/code&gt;, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Systems Manager Parameters:&lt;/strong&gt;&lt;br&gt;
Delete all &lt;strong&gt;Parameters&lt;/strong&gt; under the &lt;code&gt;/weather-app/&lt;/code&gt; path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;VPC:&lt;/strong&gt;&lt;br&gt;
Delete the &lt;strong&gt;VPC&lt;/strong&gt; (&lt;code&gt;weather-app-vpc&lt;/code&gt;). (This final step cleans up Subnets, Internet Gateway, and NAT Gateways.) &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion and Thank You..!
&lt;/h2&gt;

&lt;p&gt;Congratulations! You have successfully deployed a highly available, secure, and production-ready weather application on AWS using enterprise-grade architecture.&lt;/p&gt;

&lt;p&gt;By completing this guide, you’ve mastered key AWS services including VPC, Auto Scaling Groups, Application Load Balancers, ECR, Route 53, and ACM. This foundational knowledge is crucial for deploying any modern, scalable web application.&lt;/p&gt;

&lt;p&gt;Thank you for following along, and I hope this guide helps you on your cloud journey!&lt;/p&gt;

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