<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Emmanuel Chukwudi</title>
    <description>The latest articles on DEV Community by Emmanuel Chukwudi (@emmsddev).</description>
    <link>https://dev.to/emmsddev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3618182%2F188990d5-c2db-4fb7-b510-34288de5cf77.jpg</url>
      <title>DEV Community: Emmanuel Chukwudi</title>
      <link>https://dev.to/emmsddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/emmsddev"/>
    <language>en</language>
    <item>
      <title>Agile, Scrum, and Azure Boards: The Theory Behind the Tool</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Thu, 18 Jun 2026 14:15:50 +0000</pubDate>
      <link>https://dev.to/emmsddev/agile-scrum-and-azure-boards-the-theory-behind-the-tool-34g8</link>
      <guid>https://dev.to/emmsddev/agile-scrum-and-azure-boards-the-theory-behind-the-tool-34g8</guid>
      <description>&lt;p&gt;Most of us land in Azure Boards, Jira, or Trello before we ever read the Agile Manifesto. We learn to drag cards across columns and fill in story points because that's what the team does not because we understand why the framework is shaped that way. This post works backward: starting from the theory (Agile, Scrum, Sprints, Backlogs) and ending at how Azure Boards actually implements it, so the tool stops feeling like a checkbox exercise and starts making sense as a system.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Agile Actually Is
&lt;/h3&gt;

&lt;p&gt;Agile isn't a process it's a set of values for how software gets built. The Agile Manifesto (2001) boils down to four preferences:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Individuals and interactions over rigid processes and tools&lt;/li&gt;
&lt;li&gt;Working software over comprehensive documentation&lt;/li&gt;
&lt;li&gt;Customer collaboration over fixed contracts&lt;/li&gt;
&lt;li&gt;Responding to change over following a fixed plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that says "use two-week sprints" or "hold a daily standup." Those are implementation details that came later, baked into specific frameworks. Agile itself is just the philosophy: build in small increments, get feedback constantly, and stay willing to change direction. Scrum, Kanban, and XP are different ways of operationalizing that philosophy Scrum is just the one that became the default in most companies, including the one Azure Boards is modeled around.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrum: Roles, Events, Artifacts
&lt;/h3&gt;

&lt;p&gt;Scrum structures Agile into a repeatable rhythm built from three categories.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Roles&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product Owner&lt;/strong&gt; owns the backlog, decides what gets built and in what order, represents the business/customer side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scrum Master&lt;/strong&gt; owns the process, not the product. Removes blockers, protects the team from scope creep mid-sprint, facilitates the events below.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development Team&lt;/strong&gt; the engineers actually building the thing. Cross-functional and self-organizing; nobody outside the team assigns individual tasks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Events&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Sprint Planning&lt;/strong&gt;: the team pulls items from the product backlog into the sprint backlog and commits to a sprint goal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Daily Scrum (Standup)&lt;/strong&gt;: a short daily sync, traditionally answering: what did I do, what will I do, what's blocking me.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sprint Review&lt;/strong&gt;: a demo of what was actually completed, shown to stakeholders, at the end of the sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sprint Retrospective&lt;/strong&gt;: the team reflects on how the sprint went and agrees on one or two process improvements for next time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Artifacts&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Product Backlog&lt;/strong&gt;: the full, ever-evolving list of everything that might get built, ranked by priority.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sprint Backlog&lt;/strong&gt;: the slice of that list the team has committed to for the current sprint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Increment&lt;/strong&gt;: the actual working, shippable output of the sprint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  What a Sprint Actually Is
&lt;/h3&gt;

&lt;p&gt;A Sprint is a fixed-length time box usually one, two, or four weeks, and it doesn't change once a team picks a length. Inside that window the team plans, builds, and reviews one slice of work toward a sprint goal. The fixed length is the point: it forces scope to flex around time instead of time flexing around scope, which is what keeps a project from quietly sliding into "we'll ship it when it's done."&lt;/p&gt;

&lt;h3&gt;
  
  
  Backlogs: Product vs. Sprint
&lt;/h3&gt;

&lt;p&gt;These two get conflated constantly, so it's worth being precise.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Product Backlog&lt;/strong&gt; is the master list every feature, bug fix, and technical debt item the team might ever do, ranked roughly by priority. It's never "finished"; it's a living document that the Product Owner continuously reorders as priorities shift.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;Sprint Backlog&lt;/strong&gt; is a temporary, much smaller subset: the items pulled out of the product backlog during sprint planning that the team has actually committed to delivering in the current sprint. Once a sprint starts, the sprint backlog is meant to stay stable; new work doesn't get added mid-sprint just because it seems urgent (that's actually one of the more common ways teams sabotage their own velocity).&lt;/p&gt;

&lt;h3&gt;
  
  
  Where Azure Boards Fits In
&lt;/h3&gt;

&lt;p&gt;Azure Boards is Microsoft's implementation of all of the above, and once you know the theory, the tool stops feeling arbitrary.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The work item hierarchy&lt;/strong&gt; mirrors how Agile thinking breaks down scope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Epic        → large business objective, spans months/quarters
  Feature   → a shippable slice of that objective
    Story   → a single piece of user-facing value (or a Bug, same level)
      Task  → the technical steps an engineer actually executes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A concrete example from a typical DevOps backlog: an Epic like "Migrate to GitOps delivery," a Feature underneath it like "Integrate ArgoCD with AKS," a Story like "As a DevOps engineer, I want ArgoCD to auto-sync manifest changes," and Tasks like "Install ArgoCD via Helm" or "Configure the Application CRD."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sprint backlog in Azure Boards&lt;/strong&gt; is the practical manifestation of the Scrum artifact: stories get pulled into the current sprint, estimated in story points, and broken into tasks with hour estimates. The Capacity tab compares planned hours against each person's actual availability, and the Taskboard lets the team drag tasks across To Do → In Progress → Done daily — which is what generates the Burndown chart, a declining line tracking remaining work against the sprint timeline.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Velocity&lt;/strong&gt; is the historical record of completed story points per sprint, shown as a bar chart over recent sprints. It exists purely for forecasting if a team averages 30 points a sprint, that becomes a sane ceiling for planning the next one. It's a trailing indicator of real throughput, not a target to optimize; teams that start inflating point estimates to chase a "better" velocity number just make the number meaningless.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kanban boards with WIP limits&lt;/strong&gt; are Azure Boards' answer to continuous flow rather than time-boxed sprints. Columns represent actual process states (Backlog → Dev → Code Review → Testing → Done), and a WIP limit caps how many items can sit in a column at once. Hit the limit, and the team has to clear existing work before pulling anything new which is the entire mechanism that turns a Kanban board into a flow-management tool instead of a glorified to-do list. The Cumulative Flow Diagram stacks item counts per state over time, making bottlenecks visible as a widening band before anyone has to notice manually.&lt;/p&gt;

&lt;h3&gt;
  
  
  Scrum vs. Kanban, in One Line
&lt;/h3&gt;

&lt;p&gt;Scrum optimizes for predictable, time-boxed delivery with a fixed commitment per cycle. Kanban optimizes for continuous flow with no fixed cycle, using WIP limits instead of sprints to control pace. Azure Boards doesn't force a choice most real teams run sprints for planning cadence and a Kanban board for the daily execution view of that same backlog.&lt;/p&gt;

&lt;h3&gt;
  
  
  Takeaway
&lt;/h3&gt;

&lt;p&gt;The tool only makes sense once you see it as a direct implementation of the theory: Agile sets the values, Scrum operationalizes them into roles/events/artifacts, the Sprint is the time box that everything else hangs off of, and the Backlog is the prioritized list that feeds it. Azure Boards just gives all of that a UI Epics and Features for scope, Sprint Backlogs and Capacity views for commitment, Velocity for forecasting, and Kanban WIP limits for flow control.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>agile</category>
      <category>scrum</category>
      <category>devops</category>
    </item>
    <item>
      <title>Azure Virtual Machine Scale Sets (VMSS): A Complete Guide</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Sun, 14 Jun 2026 10:16:23 +0000</pubDate>
      <link>https://dev.to/emmsddev/azure-virtual-machine-scale-sets-vmss-a-complete-guide-5gln</link>
      <guid>https://dev.to/emmsddev/azure-virtual-machine-scale-sets-vmss-a-complete-guide-5gln</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Learn how to deploy, configure, and auto-scale fleets of identical VMs on Azure from zero to production-ready.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Imagine your application suddenly gets a traffic spike maybe a product launch, a viral post, or a scheduled batch job. Without automation, you're either over-provisioned (paying for idle VMs) or scrambling to manually spin up instances while users hit errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Azure Virtual Machine Scale Sets (VMSS)&lt;/strong&gt; solve this. They let you deploy and manage a group of identical, load-balanced VMs that automatically scale in or out based on demand all from a single configuration.&lt;/p&gt;

&lt;p&gt;In this guide, we'll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What VMSS is and how it works under the hood&lt;/li&gt;
&lt;li&gt;Orchestration modes: Uniform vs Flexible&lt;/li&gt;
&lt;li&gt;How to create a VMSS via the Portal and Azure CLI&lt;/li&gt;
&lt;li&gt;Configuring autoscaling rules&lt;/li&gt;
&lt;li&gt;Integrating with a Load Balancer&lt;/li&gt;
&lt;li&gt;Updating your Scale Set (rolling upgrades)&lt;/li&gt;
&lt;li&gt;Real-world best practices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's build.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a Virtual Machine Scale Set?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;Virtual Machine Scale Set&lt;/strong&gt; is an Azure compute resource that lets you create and manage a group of load-balanced VMs. Key characteristics:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;All VMs in a scale set are created from the &lt;strong&gt;same base image and configuration&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;The set can &lt;strong&gt;automatically increase or decrease&lt;/strong&gt; the number of VM instances based on demand or a schedule&lt;/li&gt;
&lt;li&gt;VMs are distributed across &lt;strong&gt;Availability Zones or Fault/Update Domains&lt;/strong&gt; for high availability&lt;/li&gt;
&lt;li&gt;Integrates natively with &lt;strong&gt;Azure Load Balancer&lt;/strong&gt;, &lt;strong&gt;Application Gateway&lt;/strong&gt;, and &lt;strong&gt;Azure Monitor&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        ┌─────────────────────────────────────┐
                        │         Azure Load Balancer          │
                        └────────────────┬────────────────────┘
                                         │
                   ┌─────────────────────┼─────────────────────┐
                   │                     │                       │
            ┌──────▼──────┐      ┌───────▼─────┐      ┌────────▼────┐
            │   VM #1      │      │    VM #2     │      │   VM #3     │
            │ (instance 0) │      │ (instance 1) │      │ (instance 2)│
            └─────────────┘      └─────────────┘      └────────────-┘
                   │                     │                       │
                   └─────────────────────┼─────────────────────┘
                                         │
                              ┌──────────▼──────────┐
                              │   Autoscale Engine   │
                              │  (Azure Monitor)     │
                              └─────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When CPU crosses a threshold (or any metric you define), Azure's autoscale engine fires and adds or removes VM instances automatically. The Load Balancer redistributes traffic across the new fleet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Orchestration Modes: Uniform vs Flexible
&lt;/h2&gt;

&lt;p&gt;Before creating a VMSS, you need to choose an orchestration mode. This is one of the most important decisions and a common source of confusion.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniform Orchestration (Classic)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;All VMs are &lt;strong&gt;identical&lt;/strong&gt; same size, same image, same config&lt;/li&gt;
&lt;li&gt;Azure manages the VMs as a fleet; you interact with the scale set, not individual VMs&lt;/li&gt;
&lt;li&gt;Best for &lt;strong&gt;stateless workloads&lt;/strong&gt;: web servers, API backends, batch processing&lt;/li&gt;
&lt;li&gt;Supports up to &lt;strong&gt;1,000 VM instances&lt;/strong&gt; (with platform images)&lt;/li&gt;
&lt;li&gt;Built-in integration with autoscale&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flexible Orchestration (Modern — Recommended)
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;VMs can have &lt;strong&gt;different sizes and configurations&lt;/strong&gt; within the same scale set&lt;/li&gt;
&lt;li&gt;You get &lt;strong&gt;full VM-level control&lt;/strong&gt; SSH, unique managed identities, individual updates&lt;/li&gt;
&lt;li&gt;Supports mixing &lt;strong&gt;spot and on-demand&lt;/strong&gt; instances in the same set&lt;/li&gt;
&lt;li&gt;Works across &lt;strong&gt;Availability Zones&lt;/strong&gt; with zone balancing&lt;/li&gt;
&lt;li&gt;Supports up to &lt;strong&gt;1,000 instances&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Microsoft's recommended mode for new workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Feature&lt;/th&gt;
&lt;th&gt;Uniform&lt;/th&gt;
&lt;th&gt;Flexible&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VM customization&lt;/td&gt;
&lt;td&gt;All identical&lt;/td&gt;
&lt;td&gt;Individual VM control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max instances&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;td&gt;1,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscale&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Spot + On-demand mix&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability Zones&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Use case&lt;/td&gt;
&lt;td&gt;Stateless fleets&lt;/td&gt;
&lt;td&gt;General purpose&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;For new projects, default to &lt;strong&gt;Flexible orchestration&lt;/strong&gt; unless you have a specific reason for Uniform.&lt;/p&gt;
&lt;/blockquote&gt;




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

&lt;p&gt;Before creating a VMSS, make sure you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An Azure subscription&lt;/li&gt;
&lt;li&gt;Azure CLI installed: &lt;code&gt;az --version&lt;/code&gt; (install from &lt;a href="https://aka.ms/installazurecliwindows" rel="noopener noreferrer"&gt;aka.ms/installazurecliwindows&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A Resource Group and Virtual Network ready (we'll create these below)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Login to Azure&lt;/span&gt;
az login

&lt;span class="c"&gt;# Set your subscription&lt;/span&gt;
az account &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;--subscription&lt;/span&gt; &lt;span class="s2"&gt;"your-subscription-id"&lt;/span&gt;

&lt;span class="c"&gt;# Create a resource group&lt;/span&gt;
az group create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--location&lt;/span&gt; eastus

&lt;span class="c"&gt;# Create a VNet and subnet&lt;/span&gt;
az network vnet create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; vmss-vnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--address-prefix&lt;/span&gt; 10.0.0.0/16 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-name&lt;/span&gt; vmss-subnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet-prefix&lt;/span&gt; 10.0.1.0/24
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 1: Create a VMSS via the Azure Portal
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Navigate to Virtual Machine Scale Sets
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;In the Azure Portal, search for &lt;strong&gt;"Virtual machine scale sets"&lt;/strong&gt; in the top search bar&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;+ Create&lt;/strong&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%2Fp0zgkf0h2x8556oispe9.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%2Fp0zgkf0h2x8556oispe9.png" alt=" " width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 2: Basics Tab
&lt;/h3&gt;

&lt;p&gt;Fill in the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Subscription&lt;/td&gt;
&lt;td&gt;Your subscription&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Resource group&lt;/td&gt;
&lt;td&gt;&lt;code&gt;vmss-demo-rg&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual machine scale set name&lt;/td&gt;
&lt;td&gt;&lt;code&gt;my-app-vmss&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Region&lt;/td&gt;
&lt;td&gt;East US&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Availability zone&lt;/td&gt;
&lt;td&gt;Zones 1, 2, 3 (select all for HA)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orchestration mode&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;Flexible&lt;/strong&gt; (recommended)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security type&lt;/td&gt;
&lt;td&gt;Standard&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Image&lt;/td&gt;
&lt;td&gt;Ubuntu Server 22.04 LTS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VM architecture&lt;/td&gt;
&lt;td&gt;x64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Size&lt;/td&gt;
&lt;td&gt;Standard_B2s (2 vCPUs, 4 GB RAM)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authentication type&lt;/td&gt;
&lt;td&gt;SSH public key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;&lt;code&gt;azureuser&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH public key&lt;/td&gt;
&lt;td&gt;Paste your public key&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip on Availability Zones:&lt;/strong&gt; Selecting all three zones means Azure will spread your VMs across three physically separate datacenters. If one zone goes down, the others keep serving traffic.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Step 3: Disks Tab
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OS disk type&lt;/strong&gt;: Premium SSD (for production) or Standard SSD (for dev/test)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encryption&lt;/strong&gt;: Platform-managed key (default) or Customer-managed key&lt;/li&gt;
&lt;/ul&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%2Fhozshcx1icatlojr8hf0.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%2Fhozshcx1icatlojr8hf0.png" alt=" " width="800" height="364"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 4: Networking Tab
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Virtual network&lt;/strong&gt;: Select &lt;code&gt;vmss-vnet&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Subnet&lt;/strong&gt;: &lt;code&gt;vmss-subnet&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Load balancing&lt;/strong&gt;: Select &lt;strong&gt;Azure load balancer&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Click &lt;strong&gt;Create a load balancer&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Name: &lt;code&gt;my-lb&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Type: &lt;strong&gt;Public&lt;/strong&gt; (for internet-facing) or &lt;strong&gt;Internal&lt;/strong&gt; (for private)&lt;/li&gt;
&lt;li&gt;Protocol: TCP&lt;/li&gt;
&lt;li&gt;Frontend port: 80&lt;/li&gt;
&lt;li&gt;Backend port: 80&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Public IP address&lt;/strong&gt;: Create new → &lt;code&gt;my-app-lb-pip&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;NIC network security group&lt;/strong&gt;: Advanced → Create NSG&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Add inbound rule: Allow TCP 80 from Internet&lt;/li&gt;
&lt;li&gt;Add inbound rule: Allow TCP 22 from your IP (for SSH)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fldfi6njymvjw9p6dwpor.png" alt=" " width="799" height="360"&gt;
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 5: Scaling Tab
&lt;/h3&gt;

&lt;p&gt;This is where VMSS gets powerful.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initial instance count&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling policy&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Autoscale&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Configure autoscale:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Configure&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;minimum instances&lt;/strong&gt;: 2&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;maximum instances&lt;/strong&gt;: 10&lt;/li&gt;
&lt;li&gt;Set &lt;strong&gt;default instance count&lt;/strong&gt;: 2&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Add a scale-out rule:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metric: &lt;strong&gt;Percentage CPU&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Operator: Greater than&lt;/li&gt;
&lt;li&gt;Threshold: &lt;strong&gt;75%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Duration: 5 minutes&lt;/li&gt;
&lt;li&gt;Action: Increase count by &lt;strong&gt;2&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cool down: 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Add a scale-in rule:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Metric: &lt;strong&gt;Percentage CPU&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Operator: Less than&lt;/li&gt;
&lt;li&gt;Threshold: &lt;strong&gt;25%&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Duration: 5 minutes&lt;/li&gt;
&lt;li&gt;Action: Decrease count by &lt;strong&gt;1&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Cool down: 5 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Cool down periods&lt;/strong&gt; prevent flapping where the scale set rapidly adds and removes instances in response to brief spikes. Always set a cool down of at least 5 minutes.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Step 6: Health Tab
&lt;/h3&gt;

&lt;p&gt;Enable &lt;strong&gt;Application health monitoring&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Extension: &lt;strong&gt;Application Health Extension&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Protocol: HTTP&lt;/li&gt;
&lt;li&gt;Port: 80&lt;/li&gt;
&lt;li&gt;Path: &lt;code&gt;/health&lt;/code&gt; (or &lt;code&gt;/&lt;/code&gt; if you don't have a health endpoint)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This lets Azure know whether individual VM instances are actually serving traffic successfully — not just whether the VM is running.&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 7: Advanced Tab
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Custom data (cloud-init):&lt;/strong&gt; You can pass a startup script that runs on every new instance:&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;#cloud-config&lt;/span&gt;
&lt;span class="na"&gt;package_update&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;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
&lt;span class="na"&gt;runcmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl enable nginx&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;systemctl start nginx&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Hello from $(hostname)" &amp;gt; /var/www/html/index.html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Paste this in the &lt;strong&gt;Custom data&lt;/strong&gt; field (base64 encoding is handled automatically by the portal).&lt;/p&gt;




&lt;h3&gt;
  
  
  Step 8: Review + Create
&lt;/h3&gt;

&lt;p&gt;Review all settings, then click &lt;strong&gt;Create&lt;/strong&gt;. Azure will:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Provision the Load Balancer and Public IP&lt;/li&gt;
&lt;li&gt;Create the initial VM instances (2 in our case)&lt;/li&gt;
&lt;li&gt;Register them with the load balancer backend pool&lt;/li&gt;
&lt;li&gt;Apply the autoscale policy&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The deployment typically takes &lt;strong&gt;3–5 minutes&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%2F1q3vrb2rmqik2h09bvba.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%2F1q3vrb2rmqik2h09bvba.png" alt=" " width="799" height="370"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 2: Create a VMSS via Azure CLI
&lt;/h2&gt;

&lt;p&gt;The CLI approach is faster, scriptable, and version-controllable ideal for CI/CD pipelines and IaC workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Create the VMSS
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vmss create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; Ubuntu2204 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vm-sku&lt;/span&gt; Standard_B2s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-count&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-username&lt;/span&gt; azureuser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generate-ssh-keys&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; vmss-vnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subnet&lt;/span&gt; vmss-subnet &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--public-ip-address&lt;/span&gt; my-app-lb-pip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb&lt;/span&gt; my-app-lb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backend-pool-name&lt;/span&gt; my-app-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb-sku&lt;/span&gt; Standard &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zones&lt;/span&gt; 1 2 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--orchestration-mode&lt;/span&gt; Flexible &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--upgrade-policy-mode&lt;/span&gt; Rolling &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--custom-data&lt;/span&gt; cloud-init.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;--lb&lt;/code&gt; flag automatically creates an Azure Load Balancer and wires the VMSS backend pool to it.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h3&gt;
  
  
  Open Port 80 on the Load Balancer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a load balancer rule for HTTP traffic&lt;/span&gt;
az network lb rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb-name&lt;/span&gt; my-app-lb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; http-rule &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--frontend-port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backend-port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--frontend-ip-name&lt;/span&gt; loadBalancerFrontEnd &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--backend-pool-name&lt;/span&gt; my-app-backend &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--probe-name&lt;/span&gt; healthProbe

&lt;span class="c"&gt;# Create a health probe&lt;/span&gt;
az network lb probe create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb-name&lt;/span&gt; my-app-lb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; healthProbe &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protocol&lt;/span&gt; http &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--port&lt;/span&gt; 80 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--path&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Configure Autoscale Rules
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get the VMSS resource ID&lt;/span&gt;
&lt;span class="nv"&gt;VMSS_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az vmss show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create the autoscale profile&lt;/span&gt;
az monitor autoscale create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource&lt;/span&gt; &lt;span class="nv"&gt;$VMSS_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-type&lt;/span&gt; Microsoft.Compute/virtualMachineScaleSets &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-autoscale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-count&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-count&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--count&lt;/span&gt; 2

&lt;span class="c"&gt;# Add scale-out rule (CPU &amp;gt; 75% → add 2 instances)&lt;/span&gt;
az monitor autoscale rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--autoscale-name&lt;/span&gt; my-app-autoscale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"Percentage CPU &amp;gt; 75 avg 5m"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scale&lt;/span&gt; out 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cooldown&lt;/span&gt; 5

&lt;span class="c"&gt;# Add scale-in rule (CPU &amp;lt; 25% → remove 1 instance)&lt;/span&gt;
az monitor autoscale rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--autoscale-name&lt;/span&gt; my-app-autoscale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"Percentage CPU &amp;lt; 25 avg 5m"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scale&lt;/span&gt; &lt;span class="k"&gt;in &lt;/span&gt;1 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cooldown&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Verify the Deployment
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# List all instances in the scale set&lt;/span&gt;
az vmss list-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Check instance health&lt;/span&gt;
az vmss get-instance-view &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-id&lt;/span&gt; 0

&lt;span class="c"&gt;# Get the public IP of the load balancer&lt;/span&gt;
az network public-ip show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-lb-pip &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; ipAddress &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open a browser and navigate to the public IP — you should see your Nginx page.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 3: Upgrade Policies... how to Update Your Fleet
&lt;/h2&gt;

&lt;p&gt;One of the trickiest parts of managing a scale set is rolling out updates (new OS image, new app version) without downtime. VMSS supports three upgrade modes:&lt;/p&gt;

&lt;h3&gt;
  
  
  Automatic Upgrades
&lt;/h3&gt;

&lt;p&gt;Azure automatically upgrades VM instances as soon as the scale set model is updated. No manual intervention needed, but instances may restart without warning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vmss update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.mode&lt;span class="o"&gt;=&lt;/span&gt;Automatic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Best for: Dev/test environments.&lt;/p&gt;




&lt;h3&gt;
  
  
  Rolling Upgrades (Recommended for Production)
&lt;/h3&gt;

&lt;p&gt;Azure upgrades VMs in batches, validating health before moving to the next batch. Requires health probes to be configured.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vmss update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.mode&lt;span class="o"&gt;=&lt;/span&gt;Rolling &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.rollingUpgradePolicy.maxBatchInstancePercent&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.rollingUpgradePolicy.maxUnhealthyInstancePercent&lt;span class="o"&gt;=&lt;/span&gt;20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.rollingUpgradePolicy.maxUnhealthyUpgradedInstancePercent&lt;span class="o"&gt;=&lt;/span&gt;5 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.rollingUpgradePolicy.pauseTimeBetweenBatches&lt;span class="o"&gt;=&lt;/span&gt;PT30S
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this config, Azure upgrades 20% of VMs at a time, waits 30 seconds between batches, and stops if more than 5% of upgraded instances become unhealthy.&lt;/p&gt;




&lt;h3&gt;
  
  
  Manual Upgrades
&lt;/h3&gt;

&lt;p&gt;The scale set model updates, but instances are only upgraded when you explicitly tell Azure to do so. Maximum control, but requires operator action.&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;# Update the model (e.g., new image version)&lt;/span&gt;
az vmss update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; upgradePolicy.mode&lt;span class="o"&gt;=&lt;/span&gt;Manual

&lt;span class="c"&gt;# Manually upgrade specific instances&lt;/span&gt;
az vmss update-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; 0 1 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 4: Scaling Operations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Manual Scaling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Scale to 5 instances manually&lt;/span&gt;
az vmss scale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--new-capacity&lt;/span&gt; 5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Schedule-Based Scaling
&lt;/h3&gt;

&lt;p&gt;Useful for predictable traffic patterns (e.g., scale up at 8am, scale down at 8pm).&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;# Scale up at 8am UTC on weekdays&lt;/span&gt;
az monitor autoscale profile create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--autoscale-name&lt;/span&gt; my-app-autoscale &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; weekday-peak &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--min-count&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-count&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--count&lt;/span&gt; 4 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--recurrence&lt;/span&gt; week mon tue wed thu fri &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--timezone&lt;/span&gt; &lt;span class="s2"&gt;"UTC"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--start&lt;/span&gt; 08:00 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--end&lt;/span&gt; 20:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  SSH Into a Specific Instance
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get the NAT rules to find which port maps to which instance&lt;/span&gt;
az network lb inbound-nat-rule list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb-name&lt;/span&gt; my-app-lb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# SSH using the NAT port (e.g., port 50000 maps to instance 0)&lt;/span&gt;
ssh &lt;span class="nt"&gt;-p&lt;/span&gt; 50000 azureuser@&amp;lt;load-balancer-public-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 5: Monitoring Your Scale Set
&lt;/h2&gt;

&lt;h3&gt;
  
  
  View Autoscale Activity
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az monitor activity-log list &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-events&lt;/span&gt; 20 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?contains(operationName.value, 'autoscale')]"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; table
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Key Metrics to Monitor in Azure Monitor
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Alert threshold&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Percentage CPU&lt;/td&gt;
&lt;td&gt;Average CPU across all instances&lt;/td&gt;
&lt;td&gt;&amp;gt; 80% for 10 min&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Network In/Out&lt;/td&gt;
&lt;td&gt;Traffic volume&lt;/td&gt;
&lt;td&gt;Spike detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Disk Read/Write&lt;/td&gt;
&lt;td&gt;Storage I/O&lt;/td&gt;
&lt;td&gt;&amp;gt; 90% of provisioned IOPS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VmAvailabilityMetric&lt;/td&gt;
&lt;td&gt;Instance health status&lt;/td&gt;
&lt;td&gt;Any unhealthy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscale Scale Actions&lt;/td&gt;
&lt;td&gt;Scale in/out events&lt;/td&gt;
&lt;td&gt;Alert on unexpected scale-in&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a CPU alert&lt;/span&gt;
az monitor metrics alert create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; high-cpu-alert &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--scopes&lt;/span&gt; &lt;span class="nv"&gt;$VMSS_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"avg Percentage CPU &amp;gt; 85"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--window-size&lt;/span&gt; 5m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--evaluation-frequency&lt;/span&gt; 1m &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; my-action-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--description&lt;/span&gt; &lt;span class="s2"&gt;"VMSS CPU exceeded 85% for 5 minutes"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 6: Using a Custom Image with VMSS
&lt;/h2&gt;

&lt;p&gt;Remember the Azure Custom Image we built in the previous article? Here's how to plug it into a VMSS.&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;# Get your gallery image version ID&lt;/span&gt;
&lt;span class="nv"&gt;IMAGE_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az sig image-version show &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; my-resource-group &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gallery-name&lt;/span&gt; MyAppGallery &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gallery-image-definition&lt;/span&gt; MyAppImage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--gallery-image-version&lt;/span&gt; 1.0.0 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Create VMSS using your custom image&lt;/span&gt;
az vmss create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; &lt;span class="nv"&gt;$IMAGE_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vm-sku&lt;/span&gt; Standard_B2s &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-count&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--admin-username&lt;/span&gt; azureuser &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--generate-ssh-keys&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--lb&lt;/span&gt; my-app-lb &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--zones&lt;/span&gt; 1 2 3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--orchestration-mode&lt;/span&gt; Flexible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the golden image pattern in action — every instance spins up from your pre-configured, pre-hardened image.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Production-Ready VMSS Setup
&lt;/h2&gt;

&lt;p&gt;Here's what a production VMSS deployment typically looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                         Internet
                            │
                   ┌────────▼────────┐
                   │  Azure Front    │
                   │  Door / WAF     │
                   └────────┬────────┘
                            │
                   ┌────────▼────────┐
                   │  App Gateway /  │
                   │  Load Balancer  │
                   └────────┬────────┘
                            │
              ┌─────────────┼─────────────┐
              │ Zone 1      │ Zone 2       │ Zone 3
         ┌────▼────┐   ┌────▼────┐   ┌────▼────┐
         │  VM #1  │   │  VM #2  │   │  VM #3  │
         │ (VMSS)  │   │ (VMSS)  │   │ (VMSS)  │
         └────┬────┘   └────┬────┘   └────┬────┘
              │              │              │
              └──────────────┼──────────────┘
                             │
                    ┌────────▼────────┐
                    │  Azure Monitor  │
                    │  + Autoscale    │
                    └────────┬────────┘
                             │
              ┌──────────────┼───────────────┐
              │              │               │
      ┌───────▼──┐   ┌───────▼──┐   ┌───────▼──┐
      │ Azure DB │   │Key Vault │   │  Storage  │
      └──────────┘   └──────────┘   └───────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key components:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Azure Front Door or WAF&lt;/strong&gt;: DDoS protection and global routing at the edge&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Application Gateway or Load Balancer&lt;/strong&gt;: Layer 7 or Layer 4 traffic distribution&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VMSS across 3 Availability Zones&lt;/strong&gt;: High availability against datacenter failures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Monitor + Autoscale&lt;/strong&gt;: Reactive and scheduled scaling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure Key Vault&lt;/strong&gt;: Secrets injected at runtime, never baked into images&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed Identity&lt;/strong&gt;: VM instances authenticate to Azure services without credentials&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Best Practices
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Design for Statelessness
&lt;/h3&gt;

&lt;p&gt;VMs in a scale set can be added or removed at any time. Your application should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Store session data in &lt;strong&gt;Azure Cache for Redis&lt;/strong&gt;, not in-memory&lt;/li&gt;
&lt;li&gt;Write files to &lt;strong&gt;Azure Blob Storage&lt;/strong&gt; or a shared file system, not local disk&lt;/li&gt;
&lt;li&gt;Use &lt;strong&gt;Azure Service Bus or Event Hub&lt;/strong&gt; for message queuing&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Use Spot Instances for Cost Savings
&lt;/h3&gt;

&lt;p&gt;For fault-tolerant, interruptible workloads (batch jobs, rendering, CI runners), mix spot instances with on-demand:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vmss create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-batch-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--priority&lt;/span&gt; Spot &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--eviction-policy&lt;/span&gt; Deallocate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--max-price&lt;/span&gt; 0.05 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--image&lt;/span&gt; Ubuntu2204 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--vm-sku&lt;/span&gt; Standard_D4s_v3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-count&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Spot instances can save up to 90% compared to on-demand pricing — with the tradeoff that Azure can evict them when capacity is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Always Configure Health Probes
&lt;/h3&gt;

&lt;p&gt;Without health probes, Azure doesn't know if your application is actually working. A VM could be running but serving 500 errors, and autoscale would keep it in the pool.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az vmss update &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--set&lt;/span&gt; virtualMachineProfile.extensionProfile.extensions&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{
    "name": "HealthExtension",
    "properties": {
      "publisher": "Microsoft.ManagedServices",
      "type": "ApplicationHealthLinux",
      "typeHandlerVersion": "1.0",
      "settings": {
        "protocol": "http",
        "port": 80,
        "requestPath": "/health"
      }
    }
  }]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Protect Against Accidental Scale-In
&lt;/h3&gt;

&lt;p&gt;In production, you may want to prevent certain instances from being terminated during a scale-in event (e.g., an instance running a long job).&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;# Protect a specific instance from scale-in&lt;/span&gt;
az vmss update-instances &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--resource-group&lt;/span&gt; vmss-demo-rg &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; my-app-vmss &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; 2 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--protect-from-scale-in&lt;/span&gt; &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Quick Reference — Common CLI Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create VMSS&lt;/span&gt;
az vmss create &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--image&lt;/span&gt; &amp;lt;image&amp;gt; &lt;span class="nt"&gt;--instance-count&lt;/span&gt; &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# List instances&lt;/span&gt;
az vmss list-instances &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--output&lt;/span&gt; table

&lt;span class="c"&gt;# Scale manually&lt;/span&gt;
az vmss scale &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--new-capacity&lt;/span&gt; &amp;lt;n&amp;gt;

&lt;span class="c"&gt;# Update instances to latest model&lt;/span&gt;
az vmss update-instances &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;

&lt;span class="c"&gt;# Reimage an instance (fresh OS disk)&lt;/span&gt;
az vmss reimage &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--instance-id&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Delete a specific instance&lt;/span&gt;
az vmss delete-instances &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt; &lt;span class="nt"&gt;--instance-ids&lt;/span&gt; &amp;lt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;# Show autoscale settings&lt;/span&gt;
az monitor autoscale show &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;autoscale-name&amp;gt;

&lt;span class="c"&gt;# Delete the entire VMSS&lt;/span&gt;
az vmss delete &lt;span class="nt"&gt;--resource-group&lt;/span&gt; &amp;lt;rg&amp;gt; &lt;span class="nt"&gt;--name&lt;/span&gt; &amp;lt;name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Azure Virtual Machine Scale Sets are one of the most powerful tools in a cloud engineer's toolkit. Once you understand the orchestration modes, upgrade policies, and autoscale configuration, you can build infrastructure that handles anything from a quiet weekend to a viral traffic spike without manual intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recap of what we covered:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Uniform vs Flexible orchestration modes&lt;/li&gt;
&lt;li&gt;Creating a VMSS via Portal and Azure CLI&lt;/li&gt;
&lt;li&gt;Wiring up a Load Balancer with health probes&lt;/li&gt;
&lt;li&gt;Configuring CPU-based and schedule-based autoscaling&lt;/li&gt;
&lt;li&gt;Rolling upgrade strategies for zero-downtime deployments&lt;/li&gt;
&lt;li&gt;Using custom images from Azure Compute Gallery&lt;/li&gt;
&lt;li&gt;Production architecture patterns and best practices&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Found this helpful? Drop a ❤️ and share it with your team. Questions or corrections? Leave a comment below.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>cloud</category>
      <category>vmss</category>
    </item>
    <item>
      <title>Deploying Your First App on Kubernetes: A Beginner's Guide (Minikube &amp; Kind)</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Mon, 25 May 2026 11:44:23 +0000</pubDate>
      <link>https://dev.to/emmsddev/deploying-your-first-app-on-kubernetes-a-beginners-guide-minikube-kind-3654</link>
      <guid>https://dev.to/emmsddev/deploying-your-first-app-on-kubernetes-a-beginners-guide-minikube-kind-3654</guid>
      <description>&lt;p&gt;If you've just learned the basics of Kubernetes Pods, Deployments, ReplicaSets, and Services the best next step is to actually use them. Reading about self-healing and rolling updates is one thing; watching Kubernetes recreate a deleted Pod in real time is another.&lt;/p&gt;

&lt;p&gt;In this guide, you'll deploy a simple Node.js app on a local Kubernetes cluster. We'll cover both &lt;strong&gt;Minikube&lt;/strong&gt; and &lt;strong&gt;Kind&lt;/strong&gt; (Kubernetes in Docker), so you can follow along whichever tool you prefer.&lt;/p&gt;

&lt;p&gt;By the end, you'll have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A containerised Node.js app running in Kubernetes&lt;/li&gt;
&lt;li&gt;3 replicas managed by a Deployment and ReplicaSet&lt;/li&gt;
&lt;li&gt;A Service exposing the app to your browser&lt;/li&gt;
&lt;li&gt;Hands-on experience with self-healing and scaling&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Before we start, make sure you have these installed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/get-docker/" rel="noopener noreferrer"&gt;Docker&lt;/a&gt; required by both Minikube and Kind&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kubernetes.io/docs/tasks/tools/" rel="noopener noreferrer"&gt;kubectl&lt;/a&gt; the Kubernetes CLI&lt;/li&gt;
&lt;li&gt;Either &lt;strong&gt;Minikube&lt;/strong&gt; or &lt;strong&gt;Kind&lt;/strong&gt; (installation covered below)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 1: Setting Up Your Local Cluster
&lt;/h2&gt;

&lt;p&gt;You only need one of these. If you're not sure which to pick:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Minikube&lt;/strong&gt;: slightly friendlier for beginners, has a built-in way to open services in the browser&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kind&lt;/strong&gt;: lighter, faster, great if you already have Docker set up&lt;/li&gt;
&lt;/ul&gt;




&lt;h3&gt;
  
  
  Option A: Minikube
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Install Minikube&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Homebrew):&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;brew &lt;span class="nb"&gt;install &lt;/span&gt;minikube
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux:&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;curl &lt;span class="nt"&gt;-LO&lt;/span&gt; https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
&lt;span class="nb"&gt;sudo install &lt;/span&gt;minikube-linux-amd64 /usr/local/bin/minikube
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (via winget):&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;winget &lt;span class="nb"&gt;install &lt;/span&gt;Kubernetes.minikube
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Start your cluster:&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;minikube start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify it's running:&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;kubectl get nodes
&lt;span class="c"&gt;# NAME       STATUS   ROLES           AGE   VERSION&lt;/span&gt;
&lt;span class="c"&gt;# minikube   Ready    control-plane   10s   v1.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  Option B: Kind (Kubernetes in Docker)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Install Kind&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;macOS (Homebrew):&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;brew &lt;span class="nb"&gt;install &lt;/span&gt;kind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Linux:&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;curl &lt;span class="nt"&gt;-Lo&lt;/span&gt; ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x ./kind
&lt;span class="nb"&gt;sudo mv&lt;/span&gt; ./kind /usr/local/bin/kind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows (via Chocolatey):&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;choco &lt;span class="nb"&gt;install &lt;/span&gt;kind
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Create your cluster:&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;kind create cluster &lt;span class="nt"&gt;--name&lt;/span&gt; hello-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Verify it's running:&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;kubectl get nodes
&lt;span class="c"&gt;# NAME                         STATUS   ROLES           AGE   VERSION&lt;/span&gt;
&lt;span class="c"&gt;# hello-cluster-control-plane  Ready    control-plane   10s   v1.x.x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 2: Build the Node.js App
&lt;/h2&gt;

&lt;p&gt;Create a new folder for the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;k8s-hello &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;k8s-hello
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;app.js&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nano/vim app.js&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http&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;os&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;os&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;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Hello from Pod: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Running on port 3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;os.hostname()&lt;/code&gt;?&lt;/strong&gt; In Kubernetes, each Pod gets a unique hostname. When the Service load-balances traffic across multiple Pods, you'll see different hostnames on each refresh proving which Pod served you.&lt;/p&gt;
&lt;/blockquote&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%2Fe31nft01uuv55zvmyzt4.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%2Fe31nft01uuv55zvmyzt4.png" alt=" " width="799" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;Dockerfile&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:18-alpine&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; app.js .&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "app.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 3: Build and Load the Docker Image
&lt;/h2&gt;

&lt;p&gt;This step differs between Minikube and Kind pay attention here.&lt;/p&gt;




&lt;h3&gt;
  
  
  Minikube
&lt;/h3&gt;

&lt;p&gt;Minikube runs its own Docker daemon inside a VM. Point your local Docker CLI at it so your build lands inside Minikube directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;minikube docker-env&lt;span class="si"&gt;)&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; hello-app:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From this point, Minikube can see the image locally without needing Docker Hub.&lt;/p&gt;




&lt;h3&gt;
  
  
  Kind
&lt;/h3&gt;

&lt;p&gt;Kind doesn't share a Docker daemon. You build the image normally, then explicitly load it into the cluster:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;-t&lt;/span&gt; hello-app:v1 &lt;span class="nb"&gt;.&lt;/span&gt;
kind load docker-image hello-app:v1 &lt;span class="nt"&gt;--name&lt;/span&gt; hello-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Skipping &lt;code&gt;kind load&lt;/code&gt; is the most common beginner mistake with Kind.&lt;/strong&gt; Without it, your Pods will get stuck in &lt;code&gt;ImagePullBackOff&lt;/code&gt; because Kind can't find the image.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Part 4: Write the Kubernetes YAML
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;deployment.yaml&lt;/code&gt; in your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello-deployment&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello&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;hello-app:v1&lt;/span&gt;
          &lt;span class="na"&gt;imagePullPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Never&lt;/span&gt;   &lt;span class="c1"&gt;# use local image, don't pull from Docker Hub&lt;/span&gt;
          &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Service&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello-service&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NodePort&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;hello&lt;/span&gt;          &lt;span class="c1"&gt;# matches the Pod label above this is how Services find Pods&lt;/span&gt;
  &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;targetPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3000&lt;/span&gt;
      &lt;span class="na"&gt;nodePort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What's happening here:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;Deployment&lt;/strong&gt; tells Kubernetes to keep 3 replicas of our Pod running at all times&lt;/li&gt;
&lt;li&gt;It automatically creates a &lt;strong&gt;ReplicaSet&lt;/strong&gt; to enforce that replica count&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;Service&lt;/strong&gt; uses the &lt;code&gt;app: hello&lt;/code&gt; label selector to find all matching Pods and route traffic to them&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;imagePullPolicy: Never&lt;/code&gt; tells Kubernetes to use the locally available image instead of going to Docker Hub&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Part 5: Deploy It
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;deployment.apps/hello-deployment created
service/hello-service created
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your Pods are coming up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait until all three show &lt;code&gt;Running&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;NAME                                READY   STATUS    RESTARTS   AGE
hello-deployment-57c4d87bf-abc12    1/1     Running   0          15s
hello-deployment-57c4d87bf-def34    1/1     Running   0          15s
hello-deployment-57c4d87bf-ghi56    1/1     Running   0          15s
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your ReplicaSet and Service too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get replicaset
kubectl get service hello-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 6: Open the App in Your Browser
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Minikube
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;minikube service hello-service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Minikube opens the URL in your browser automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Kind
&lt;/h3&gt;

&lt;p&gt;Kind doesn't expose NodePort services directly, so use port-forwarding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl port-forward service/hello-service 8080:80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Hit &lt;strong&gt;refresh a few times&lt;/strong&gt;. You'll see the Pod hostname change the Service is load-balancing across your 3 Pods.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from Pod: hello-deployment-57c4d87bf-abc12
Hello from Pod: hello-deployment-57c4d87bf-ghi56
Hello from Pod: hello-deployment-57c4d87bf-def34
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Part 7: Experiments (The Real Learning)
&lt;/h2&gt;

&lt;p&gt;Now that everything is running, try these one by one. Each one demonstrates a core Kubernetes behaviour.&lt;/p&gt;




&lt;h3&gt;
  
  
  1. Self-healing...delete a Pod manually
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# grab any pod name&lt;/span&gt;
kubectl get pods

&lt;span class="c"&gt;# delete it&lt;/span&gt;
kubectl delete pod hello-deployment-57c4d87bf-abc12

&lt;span class="c"&gt;# watch what happens&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-w&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes detects the replica count dropped to 2 and immediately creates a new Pod. This is the ReplicaSet controller doing its job.&lt;/p&gt;




&lt;h3&gt;
  
  
  2. Scaling up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment hello-deployment &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;5
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two new Pods appear almost instantly.&lt;/p&gt;




&lt;h3&gt;
  
  
  3. Scaling down
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl scale deployment hello-deployment &lt;span class="nt"&gt;--replicas&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1
kubectl get pods
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four Pods terminate gracefully, one remains.&lt;/p&gt;




&lt;h3&gt;
  
  
  4. Rolling update with zero downtime
&lt;/h3&gt;

&lt;p&gt;Edit &lt;code&gt;app.js&lt;/code&gt; to change the response message:&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Hello from Pod v2: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;\n`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Build a new image:&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;# Minikube&lt;/span&gt;
&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;minikube docker-env&lt;span class="si"&gt;)&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; hello-app:v2 &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Kind&lt;/span&gt;
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; hello-app:v2 &lt;span class="nb"&gt;.&lt;/span&gt;
kind load docker-image hello-app:v2 &lt;span class="nt"&gt;--name&lt;/span&gt; hello-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Update the Deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl &lt;span class="nb"&gt;set &lt;/span&gt;image deployment/hello-deployment &lt;span class="nv"&gt;hello&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;hello-app:v2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Watch the rolling update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout status deployment/hello-deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes replaces Pods one at a time, keeping the app available throughout.&lt;/p&gt;




&lt;h3&gt;
  
  
  5. Inspect a Pod
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl describe pod &amp;lt;pod-name&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows you the Pod's IP, which Node it's on, its labels, and a full event log — useful for debugging.&lt;/p&gt;




&lt;h3&gt;
  
  
  6. Roll back
&lt;/h3&gt;

&lt;p&gt;If something goes wrong with an update:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout undo deployment/hello-deployment
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Kubernetes switches back to the previous ReplicaSet.&lt;/p&gt;




&lt;h2&gt;
  
  
  Part 8: Clean Up
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl delete &lt;span class="nt"&gt;-f&lt;/span&gt; deployment.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Minikube:&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;minikube stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Kind:&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;kind delete cluster &lt;span class="nt"&gt;--name&lt;/span&gt; hello-cluster
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note for Kind users:&lt;/strong&gt; Kind clusters don't survive a machine restart. If you reboot and come back to this project, run &lt;code&gt;kind create cluster --name hello-cluster&lt;/code&gt; and &lt;code&gt;kind load docker-image hello-app:v1 --name hello-cluster&lt;/code&gt; before applying your YAML again.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What You Just Built
&lt;/h2&gt;

&lt;p&gt;Here's what was happening under the hood the whole time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your Browser
     ↓
 [Service]             ← watched for Pods with label app: hello
     ↓
 [ReplicaSet]          ← enforced 3 running replicas at all times
  ↓     ↓     ↓
[Pod] [Pod] [Pod]      ← each ran your Node.js container
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every concept from the Kubernetes basics maps to something you just did:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;What you observed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pod&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The unit running your container, with a unique hostname&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ReplicaSet&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Recreated a Pod immediately after you deleted one&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deployment&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Managed the rolling update and rollback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Load-balanced traffic across all 3 Pods using label selectors&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




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

&lt;p&gt;Now that you have the fundamentals working, here are good next topics to explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Namespaces&lt;/strong&gt; isolate workloads for different teams or environments&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ConfigMaps &amp;amp; Secrets&lt;/strong&gt; externalise config and credentials from your container&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ingress&lt;/strong&gt; a cleaner alternative to NodePort for routing external traffic&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent Volumes&lt;/strong&gt; attach storage that survives Pod restarts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liveness &amp;amp; Readiness Probes&lt;/strong&gt; teach Kubernetes when your Pod is actually healthy&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;If you ran into issues or have questions, drop them in the comments. The most common problems are forgetting &lt;code&gt;kind load docker-image&lt;/code&gt; (Kind) or not running &lt;code&gt;eval $(minikube docker-env)&lt;/code&gt; before building (Minikube).&lt;/em&gt;&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>beginners</category>
      <category>devops</category>
      <category>docker</category>
    </item>
    <item>
      <title>How to Read Any GitHub Repo and Write Its Dockerfile From Scratch</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Sun, 17 May 2026 14:13:07 +0000</pubDate>
      <link>https://dev.to/emmsddev/how-to-read-any-github-repo-and-write-its-dockerfile-from-scratch-49i3</link>
      <guid>https://dev.to/emmsddev/how-to-read-any-github-repo-and-write-its-dockerfile-from-scratch-49i3</guid>
      <description>&lt;h2&gt;
  
  
  A DevOps Engineer's Evidence-Based Approach
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;This guide walks through a real open-source project &lt;a href="https://github.com/EmmanuelDevC/ridanexpress" rel="noopener noreferrer"&gt;Ridan Express&lt;/a&gt; and shows you exactly how to analyze a repo's files, understand what the app needs, and write a production-ready Dockerfile without guessing.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Who This Is For
&lt;/h2&gt;

&lt;p&gt;You're learning DevOps. You clone a repo, stare at it, and freeze you don't know where to start. Should you Dockerize it? What base image do you use? What commands do you run inside the container?&lt;/p&gt;

&lt;p&gt;This article teaches you the &lt;strong&gt;mental model&lt;/strong&gt; professionals use: reading a project's files as clues and letting the evidence tell you what to build.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Project: Ridan Express
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/EmmanuelDevC/ridanexpress" rel="noopener noreferrer"&gt;Ridan Express&lt;/a&gt; is a React frontend for a ride/delivery platform (think Uber or DoorDash). It uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18 + Vite as the build tool&lt;/li&gt;
&lt;li&gt;Tailwind CSS + Material UI for styling&lt;/li&gt;
&lt;li&gt;Redux for state management&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;socket.io-client&lt;/code&gt; for real-time features (live tracking)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mapbox-gl&lt;/code&gt; for maps&lt;/li&gt;
&lt;li&gt;Stripe for payments&lt;/li&gt;
&lt;li&gt;Google OAuth for login&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Currently deployed on Vercel. No Dockerfile exists. That's your job.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step 1: Read the Files Before You Write Anything
&lt;/h2&gt;

&lt;p&gt;The golden rule: &lt;strong&gt;never write a Dockerfile cold.&lt;/strong&gt; Always read these files first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;What it tells you&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;package.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Language, runtime version, dependencies, build commands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exact locked dependency versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;vite.config.js&lt;/code&gt; / &lt;code&gt;webpack.config.js&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Build tool and output configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;vercel.json&lt;/code&gt; / &lt;code&gt;netlify.toml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;How it's currently deployed (big clue)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;build/&lt;/code&gt; or &lt;code&gt;dist/&lt;/code&gt; folder&lt;/td&gt;
&lt;td&gt;Where compiled output lands&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.env.example&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Environment variables the app needs&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's walk through each clue in Ridan Express.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clue 1: &lt;code&gt;package.json&lt;/code&gt; → Choose Your Base Image
&lt;/h2&gt;

&lt;p&gt;Opening &lt;code&gt;package.json&lt;/code&gt;, the first thing to notice is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"engines"&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;"node"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"22.x"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The developer told you &lt;strong&gt;exactly&lt;/strong&gt; which Node.js version this app needs. This directly maps to your Dockerfile's first line:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;alpine&lt;/code&gt;? The Alpine variant of Node is a minimal Linux distribution around 50MB instead of 900MB+ for the full image. Always prefer Alpine for production unless you need specific system libraries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule:&lt;/strong&gt; &lt;code&gt;"engines"&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; → your &lt;code&gt;FROM node:X-alpine&lt;/code&gt; version.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clue 2: &lt;code&gt;package-lock.json&lt;/code&gt; → Use &lt;code&gt;npm ci&lt;/code&gt;, Not &lt;code&gt;npm install&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The presence of &lt;code&gt;package-lock.json&lt;/code&gt; alongside &lt;code&gt;package.json&lt;/code&gt; is a deliberate signal. Here's the critical distinction:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm install&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs dependencies, may update versions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;npm ci&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Installs &lt;strong&gt;exact&lt;/strong&gt; versions from &lt;code&gt;package-lock.json&lt;/code&gt;, fails if they don't match&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a Docker build, you always want &lt;code&gt;npm ci&lt;/code&gt;. It's faster, deterministic, and prevents "it worked on my machine" bugs. Your Dockerfile should copy both files before installing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;package*.json&lt;/code&gt; glob copies both &lt;code&gt;package.json&lt;/code&gt; and &lt;code&gt;package-lock.json&lt;/code&gt; in one line.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why copy these before the rest of the code?&lt;/strong&gt; Docker builds in layers and caches each one. If you copy everything first, any code change invalidates the dependency cache and forces a full &lt;code&gt;npm ci&lt;/code&gt; on every build slow. Copying &lt;code&gt;package*.json&lt;/code&gt; first means Docker only re-runs &lt;code&gt;npm ci&lt;/code&gt; when dependencies actually change.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clue 3: &lt;code&gt;vite.config.js&lt;/code&gt; + Scripts → Your Build Command
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;package.json&lt;/code&gt;, the scripts section reads:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"scripts"&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;"ridan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"build"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vite build"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;vite build&lt;/code&gt; compiles your entire React app JSX, TypeScript, CSS modules, imports into plain HTML, CSS, and JavaScript files. No more React, no more JSX, no more Node.js required. Just static files a browser can load directly.&lt;/p&gt;

&lt;p&gt;This translates to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After this runs, a &lt;code&gt;build/&lt;/code&gt; folder appears containing your compiled app, ready to be served.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clue 4: The &lt;code&gt;build/&lt;/code&gt; Folder → You Don't Need Node Anymore
&lt;/h2&gt;

&lt;p&gt;This is the insight that changes everything.&lt;/p&gt;

&lt;p&gt;After &lt;code&gt;npm run build&lt;/code&gt; finishes, the output in &lt;code&gt;build/&lt;/code&gt; is just:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;build/
  index.html
  static/
    js/main.abc123.js
    css/main.def456.css
    media/logo.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These are static files. A browser can load them directly. &lt;strong&gt;You no longer need Node.js, npm, React, or any of your dependencies.&lt;/strong&gt; They were only needed during the build process.&lt;/p&gt;

&lt;p&gt;So why keep a 500MB Node.js environment in your production image just to serve a few HTML files? You don't.&lt;/p&gt;




&lt;h2&gt;
  
  
  Clue 5: Static Output → Serve With Nginx
&lt;/h2&gt;

&lt;p&gt;Since the output is static files, the right tool to serve them is &lt;strong&gt;Nginx&lt;/strong&gt; a battle-tested, lightweight web server used by some of the highest-traffic sites in the world.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/build /usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/usr/share/nginx/html&lt;/code&gt; is Nginx's default document root the folder it serves files from. You're dropping your compiled app right there.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;nginx:alpine&lt;/code&gt; image is around 25MB total. Compare that to keeping Node.js around at 500MB+.&lt;/p&gt;




&lt;h2&gt;
  
  
  Putting It All Together: The Multi-Stage Dockerfile
&lt;/h2&gt;

&lt;p&gt;Here's the complete Dockerfile with every line explained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ── STAGE 1: Build ─────────────────────────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;# Use Node 22 (matches "engines" in package.json), Alpine for small size&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:22-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="c"&gt;# Set working directory inside the container&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="c"&gt;# Copy dependency manifests FIRST (enables Docker layer caching)&lt;/span&gt;
&lt;span class="c"&gt;# Only re-runs npm ci when package files change, not on every code change&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="c"&gt;# Install exact versions from package-lock.json (deterministic, production-safe)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci

&lt;span class="c"&gt;# Copy the rest of the source code&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;

&lt;span class="c"&gt;# Compile React → static HTML/CSS/JS in the /app/build folder&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build


&lt;span class="c"&gt;# ── STAGE 2: Serve ─────────────────────────────────────────────&lt;/span&gt;
&lt;span class="c"&gt;# Start fresh with a minimal Nginx image (~25MB vs 500MB+ for Node)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx:alpine&lt;/span&gt;

&lt;span class="c"&gt;# Copy ONLY the compiled output from Stage 1 nothing else&lt;/span&gt;
&lt;span class="c"&gt;# Node.js, npm, node_modules, and source code are all left behind&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/build /usr/share/nginx/html&lt;/span&gt;

&lt;span class="c"&gt;# Tell Docker this container listens on port 80&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 80&lt;/span&gt;

&lt;span class="c"&gt;# Nginx starts automatically no CMD needed for the default config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Understanding Multi-Stage Builds Visually
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────┐       ┌──────────────────────────┐
│      Stage 1: builder       │       │     Stage 2: final       │
│      node:22-alpine         │       │      nginx:alpine        │
│                             │       │                          │
│  ✓ Node.js runtime          │  ──▶  │  ✓ Compiled HTML/CSS/JS  │
│  ✓ npm + package manager    │  only │  ✓ Nginx web server      │
│  ✓ 300MB node_modules       │  /build  │                       │
│  ✓ React source code        │       │  ✗ No Node.js            │
│  ✓ Vite build toolchain     │       │  ✗ No npm               │
│                             │       │  ✗ No source code        │
│  ← DISCARDED after build →  │       │  ← SHIPPED TO PROD →     │
│       ~600MB                │       │       ~30MB              │
└─────────────────────────────┘       └──────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first stage is a construction site. The second stage is the finished building. You ship the building, not the scaffolding.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Evidence-to-Dockerfile Mental Map
&lt;/h2&gt;

&lt;p&gt;Every line in the Dockerfile traces back to a file in the repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package.json
  ├── "engines": { "node": "22.x" }  ──────▶  FROM node:22-alpine
  └── "build": "vite build"  ────────────────▶  RUN npm run build

package-lock.json exists
  └──────────────────────────────────────────▶  RUN npm ci

vite.config.js exists
  └── output goes to /build folder  ─────────▶  COPY /app/build → nginx

Output is static files (no server-side rendering)
  └──────────────────────────────────────────▶  FROM nginx:alpine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing is guessed. Everything is derived from evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  When to Use Nginx vs Keeping Node Running
&lt;/h2&gt;

&lt;p&gt;You used a static Nginx serve here because Vite pre-compiles everything. But not all React apps work this way:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;App type&lt;/th&gt;
&lt;th&gt;Clue in repo&lt;/th&gt;
&lt;th&gt;Serve with&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static React (Vite/CRA)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;vite build&lt;/code&gt; or &lt;code&gt;react scripts build&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Nginx (static files)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Next.js with SSR&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;next start&lt;/code&gt; in scripts&lt;/td&gt;
&lt;td&gt;Keep Node running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Express API backend&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;server.js&lt;/code&gt; or &lt;code&gt;app.js&lt;/code&gt; at root&lt;/td&gt;
&lt;td&gt;Keep Node running&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nuxt.js&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;nuxt start&lt;/code&gt; in scripts&lt;/td&gt;
&lt;td&gt;Keep Node running&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If &lt;code&gt;npm start&lt;/code&gt; runs a server (not just opens a browser), keep Node. If &lt;code&gt;npm run build&lt;/code&gt; produces a folder of files, use Nginx.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Comes Next for This Project
&lt;/h2&gt;

&lt;p&gt;The Dockerfile handles the frontend. But Ridan Express has more moving parts visible in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;socket.io-client&lt;/code&gt;: there's a Socket.io &lt;strong&gt;server&lt;/strong&gt; somewhere handling real-time ride tracking&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;stripe&lt;/code&gt;: there's a payment processing &lt;strong&gt;backend&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mapbox-gl&lt;/code&gt;: likely server-side route calculations&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;@react-oauth/google&lt;/code&gt; a backend endpoint validates the Google token&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To fully containerize this platform you'd eventually write:&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 (when you find/build the backend)&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;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;3000:80"&lt;/span&gt;&lt;span class="pi"&gt;]&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;5000:5000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;STRIPE_SECRET_KEY=${STRIPE_SECRET_KEY}&lt;/span&gt;

  &lt;span class="na"&gt;socket&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;./socket-server&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;4000:4000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:15-alpine&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="nv"&gt;pgdata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;/var/lib/postgresql/data&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And &lt;em&gt;that's&lt;/em&gt; when Kubernetes becomes relevant when you have multiple services that need to scale independently.&lt;/p&gt;




&lt;h2&gt;
  
  
  Quick Reference Cheat Sheet
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;See this in the repo          →  Do this in Dockerfile
─────────────────────────────────────────────────────────
package.json only             →  npm install
package.json + lock file      →  npm ci
"engines": node X             →  FROM node:X-alpine
"build": "vite build"         →  RUN npm run build → serve with Nginx
"build": "next build"         →  RUN npm run build → keep Node + CMD next start
server.js at root             →  Keep Node, CMD ["node", "server.js"]
requirements.txt              →  FROM python:3.X-slim
go.mod                        →  FROM golang:X-alpine + multi-stage
pom.xml (Java/Maven)          →  FROM maven:X AS builder + FROM eclipse-temurin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;Writing a Dockerfile isn't about memorizing syntax it's about reading the project. The files in every repo are instructions waiting to be translated:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package.json&lt;/code&gt; engines&lt;/strong&gt; → tells you the runtime version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;package-lock.json&lt;/code&gt;&lt;/strong&gt; → tells you to use &lt;code&gt;npm ci&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build scripts&lt;/strong&gt; → tells you the compile command&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Output type&lt;/strong&gt; (static vs server) → tells you whether to use Nginx or keep Node&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-stage builds&lt;/strong&gt; → keep images small by separating build from runtime&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Next time you open a repo, don't stare at it blankly. Start with &lt;code&gt;package.json&lt;/code&gt;, follow the clues, and let the evidence write the Dockerfile for you.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Found this useful? The same detective approach applies to CI/CD pipelines and Kubernetes manifests the repo always tells you what it needs. Follow for more DevOps breakdowns.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Validate JWTs from Multiple Issuers in kgateway</title>
      <dc:creator>Emmanuel Chukwudi</dc:creator>
      <pubDate>Sun, 17 May 2026 07:52:37 +0000</pubDate>
      <link>https://dev.to/emmsddev/validate-jwts-from-multiple-issuers-in-kgateway-561f</link>
      <guid>https://dev.to/emmsddev/validate-jwts-from-multiple-issuers-in-kgateway-561f</guid>
      <description>&lt;p&gt;Production APIs often need to accept tokens from more than one identity provider for example, a tenant's own Auth0 tenant &lt;em&gt;and&lt;/em&gt; Google Workspace for internal tools. kgateway's &lt;code&gt;JWTPolicy&lt;/code&gt; resource lets you declare multiple issuers in one policy and attach it to any &lt;code&gt;HTTPRoute&lt;/code&gt;, so you don't need a separate gateway per IdP.&lt;/p&gt;

&lt;p&gt;This guide walks through a working, reproducible configuration. By the end you will have a policy that validates tokens from two issuers, rejects mismatched audiences, and forwards selected claims as upstream headers.&lt;/p&gt;




&lt;h2&gt;
  
  
  What is a JWT?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;JSON Web Token (JWT)&lt;/strong&gt; is a compact, self-contained credential that an identity provider (IdP) issues to a user or service after they authenticate. Instead of your API checking a username and password on every request, the client attaches a JWT and your API trusts it because it was cryptographically signed by someone it already trusts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Think of it like a signed event wristband.&lt;/strong&gt; The venue (IdP) checks your ID once at the gate and gives you a wristband. Staff inside the venue (your APIs) can verify the wristband is genuine without phoning the front gate again. The wristband also says which areas you can access and it expires at midnight.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Structure of a JWT
&lt;/h3&gt;

&lt;p&gt;A JWT is three Base64URL-encoded JSON objects joined by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9   ← Header
.
eyJzdWIiOiJ1c2VyXzEyMyIsImVtYWlsIjoiYWxpY2VAZXhhbXBsZS5jb20i...  ← Payload
.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c  ← Signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Part&lt;/th&gt;
&lt;th&gt;What it contains&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Header&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Algorithm (&lt;code&gt;RS256&lt;/code&gt;) and token type. Tells the verifier how to check the signature.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Payload&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Claims about the user and the token who issued it, who it's for, when it expires.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Signature&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Cryptographic proof the token hasn't been tampered with. Verified against the IdP's public key.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can paste any JWT into &lt;a href="https://jwt.io" rel="noopener noreferrer"&gt;jwt.io&lt;/a&gt; to decode it instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  What's inside the payload?
&lt;/h3&gt;

&lt;p&gt;The payload is a JSON object of &lt;strong&gt;claims&lt;/strong&gt; statements about the token and its subject. Some are standard; some are custom fields added by your IdP.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"iss"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://my-tenant.auth0.com/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;issuer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;who&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;created&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;the&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;user's&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;unique&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;ID&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;audience&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;which&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;this&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"exp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1717000000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;expiry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Unix&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;timestamp&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;claim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;added&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;by&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Auth&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roles"&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="s2"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"editor"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;custom&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;claim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;RBAC&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How signature verification works (JWKS)
&lt;/h3&gt;

&lt;p&gt;JWTs signed with RS256 use asymmetric cryptography: the IdP signs tokens with a &lt;strong&gt;private key&lt;/strong&gt; that only it holds, and publishes the corresponding &lt;strong&gt;public keys&lt;/strong&gt; at a well known URL called the &lt;strong&gt;JWKS endpoint&lt;/strong&gt; (JSON Web Key Set). Anyone including kgateway can fetch these public keys and verify that a token was genuinely issued by that IdP and hasn't been altered since.&lt;/p&gt;

&lt;p&gt;This means kgateway never needs to call back to your IdP on every request. It fetches the JWKS once, caches the keys, and verifies signatures locally at the data plane making validation fast and offline capable.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why this matters for multi-issuer setups:&lt;/strong&gt; Each IdP has its own JWKS endpoint and its own signing keys. kgateway can hold keys from multiple providers simultaneously, matching each incoming token to the right key by checking its &lt;code&gt;iss&lt;/code&gt; claim first.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  What you'll build
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;HTTPRoute&lt;/code&gt; on &lt;code&gt;/api&lt;/code&gt; that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Accepts RS256-signed JWTs from &lt;strong&gt;Auth0&lt;/strong&gt; and &lt;strong&gt;Google&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enforces &lt;code&gt;aud: my-api&lt;/code&gt; on tokens from both providers&lt;/li&gt;
&lt;li&gt;Forwards the &lt;code&gt;sub&lt;/code&gt; and &lt;code&gt;email&lt;/code&gt; claims as &lt;code&gt;X-User-Id&lt;/code&gt; and &lt;code&gt;X-User-Email&lt;/code&gt; headers to your upstream service&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Before you begin
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;kgateway ≥ 1.2 installed in your cluster&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;kubectl&lt;/code&gt; access with permissions to create custom resources&lt;/li&gt;
&lt;li&gt;An Auth0 tenant with an API audience configured&lt;/li&gt;
&lt;li&gt;A Google OAuth 2.0 client ID&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  How kgateway validates JWTs
&lt;/h2&gt;

&lt;p&gt;Validation happens in the Envoy data plane &lt;strong&gt;before&lt;/strong&gt; a request ever reaches your upstream. On each request, kgateway:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Extracts the bearer token&lt;/strong&gt; from the &lt;code&gt;Authorization: Bearer &amp;lt;token&amp;gt;&lt;/code&gt; header (configurable to cookies or query params).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resolves the matching issuer&lt;/strong&gt; by comparing the token's &lt;code&gt;iss&lt;/code&gt; claim against each issuer declared in &lt;code&gt;JWTPolicy.spec.providers&lt;/code&gt;. The first match wins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fetches and caches JWKS&lt;/strong&gt; from the provider's &lt;code&gt;jwks_uri&lt;/code&gt;. Keys are cached per the &lt;code&gt;cacheDuration&lt;/code&gt; you set and never re-fetched mid-request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validates claims and signature&lt;/strong&gt; verifies &lt;code&gt;exp&lt;/code&gt;, &lt;code&gt;nbf&lt;/code&gt;, &lt;code&gt;aud&lt;/code&gt;, and the cryptographic signature. Any failure returns &lt;code&gt;401 Unauthorized&lt;/code&gt; immediately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forwards claims as headers&lt;/strong&gt; injects declared claims into request headers so your upstream can make authorization decisions without reparsing the JWT.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Step 1: Create the JWTPolicy
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;JWTPolicy&lt;/code&gt; is a namespace scoped custom resource that declares which issuers to trust, where to fetch their public keys, and which claims to forward upstream. Create a file named &lt;code&gt;jwt-policy.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.kgateway.dev/v1alpha1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;JWTPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multi-issuer-policy&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;providers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# Provider 1: Auth0 tenant&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;auth0&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://my-tenant.auth0.com/&lt;/span&gt;     &lt;span class="c1"&gt;# note the trailing slash&lt;/span&gt;
      &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
      &lt;span class="na"&gt;remoteJwks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://my-tenant.auth0.com/.well-known/jwks.json&lt;/span&gt;
        &lt;span class="na"&gt;cacheDuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10m&lt;/span&gt;
      &lt;span class="na"&gt;claimsToHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Email&lt;/span&gt;

    &lt;span class="c1"&gt;# Provider 2: Google&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;google&lt;/span&gt;
      &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://accounts.google.com&lt;/span&gt;      &lt;span class="c1"&gt;# no trailing slash&lt;/span&gt;
      &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;my-api&lt;/span&gt;
      &lt;span class="na"&gt;remoteJwks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;uri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://www.googleapis.com/oauth2/v3/certs&lt;/span&gt;
        &lt;span class="na"&gt;cacheDuration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5m&lt;/span&gt;
      &lt;span class="na"&gt;claimsToHeaders&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sub&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Id&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;claim&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;email&lt;/span&gt;
          &lt;span class="na"&gt;header&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;X-User-Email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Issuer strings must be exact.&lt;/strong&gt; The &lt;code&gt;issuer&lt;/code&gt; field is compared character-for-character against the token's &lt;code&gt;iss&lt;/code&gt; claim. Auth0 includes a trailing slash in its tokens; Google does not. A mismatch here means every token from that provider will be rejected, even if the signature is valid.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Step 2: Attach the policy to your HTTPRoute
&lt;/h2&gt;

&lt;p&gt;Reference the policy via an annotation on your &lt;code&gt;HTTPRoute&lt;/code&gt;. You do not need to modify the route's rules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPRoute&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api-route&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;gateway.kgateway.dev/jwt-policy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;multi-issuer-policy&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parentRefs&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;my-gateway&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&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;PathPrefix&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/api&lt;/span&gt;
      &lt;span class="na"&gt;backendRefs&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;my-service&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8080&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Step 3: Apply and verify
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Apply both resources&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl apply &lt;span class="nt"&gt;-f&lt;/span&gt; jwt-policy.yaml &lt;span class="nt"&gt;-f&lt;/span&gt; httproute.yaml

&lt;span class="c"&gt;# Confirm the policy is accepted by the control plane&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl get jwtpolicy multi-issuer-policy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.status.conditions[?(@.type=="Ready")].status}'&lt;/span&gt;
True

&lt;span class="c"&gt;# Test with a valid Auth0 token&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$AUTH0_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; https://my-gateway/api/health
200 OK

&lt;span class="c"&gt;# Test rejection: no token → 401&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;curl https://my-gateway/api/health
401 Unauthorized

&lt;span class="c"&gt;# Confirm upstream receives the forwarded headers&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;kubectl logs deploy/my-service | &lt;span class="nb"&gt;grep &lt;/span&gt;X-User-Id
X-User-Id: user_123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;JWKS caching on first request:&lt;/strong&gt; kgateway fetches JWKS the first time a token from a given issuer arrives. If the &lt;code&gt;jwks_uri&lt;/code&gt; is unreachable at that moment, the request fails with &lt;code&gt;503&lt;/code&gt;. Use a &lt;code&gt;cacheDuration&lt;/code&gt; of at least &lt;code&gt;5m&lt;/code&gt; in production never &lt;code&gt;0s&lt;/code&gt; outside of development.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Claim validation reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Claim&lt;/th&gt;
&lt;th&gt;Validated automatically&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;iss&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Must exactly match a declared provider's &lt;code&gt;issuer&lt;/code&gt;. First match wins; no fallback.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;aud&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes, if configured&lt;/td&gt;
&lt;td&gt;Token must contain at least one value from the &lt;code&gt;audiences&lt;/code&gt; list. Omit the field to skip audience validation (not recommended in production).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;exp&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Expired tokens are rejected with 401. Clock skew tolerance is 60 s by default.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nbf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Tokens with a future &lt;code&gt;nbf&lt;/code&gt; (not-before) are rejected.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sub&lt;/code&gt;, &lt;code&gt;email&lt;/code&gt;, &lt;code&gt;roles&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;kgateway does not validate custom claims. Use &lt;code&gt;claimsToHeaders&lt;/code&gt; to forward them and enforce access rules in your upstream service.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use a local JWKS secret&lt;/strong&gt;: Mount JWKS as a Kubernetes secret for air gapped or high security environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Claim based routing&lt;/strong&gt;: Route requests to different backends based on forwarded claim headers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full OIDC with Auth0&lt;/strong&gt;: Add the authorization code flow for browser facing applications.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitor validation errors&lt;/strong&gt;: Surface JWT rejection rates in Prometheus and set alerting thresholds.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>kgateway</category>
      <category>jwt</category>
      <category>kubernetes</category>
      <category>security</category>
    </item>
  </channel>
</rss>
