<?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.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>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>
