Tools: Latest: Deploying Your First App on Kubernetes: A Beginner's Guide (Minikube & Kind)

Tools: Latest: Deploying Your First App on Kubernetes: A Beginner's Guide (Minikube & Kind)

Prerequisites

Part 1: Setting Up Your Local Cluster

Option A: Minikube

Option B: Kind (Kubernetes in Docker)

Part 2: Build the Node.js App

Part 3: Build and Load the Docker Image

Minikube

Part 4: Write the Kubernetes YAML

Part 5: Deploy It

Part 6: Open the App in Your Browser

Minikube

Part 7: Experiments (The Real Learning)

1. Self-healing...delete a Pod manually

2. Scaling up

3. Scaling down

4. Rolling update with zero downtime

5. Inspect a Pod

6. Roll back

Part 8: Clean Up

What You Just Built

What's Next? 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. In this guide, you'll deploy a simple Node.js app on a local Kubernetes cluster. We'll cover both Minikube and Kind (Kubernetes in Docker), so you can follow along whichever tool you prefer. By the end, you'll have: Before we start, make sure you have these installed: You only need one of these. If you're not sure which to pick: Windows (via winget): Windows (via Chocolatey): Create a new folder for the project: Why os.hostname()? 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. This step differs between Minikube and Kind pay attention here. Minikube runs its own Docker daemon inside a VM. Point your local Docker CLI at it so your build lands inside Minikube directly: From this point, Minikube can see the image locally without needing Docker Hub. Kind doesn't share a Docker daemon. You build the image normally, then explicitly load it into the cluster: Skipping kind load is the most common beginner mistake with Kind. Without it, your Pods will get stuck in ImagePullBackOff because Kind can't find the image. Create deployment.yaml in your project folder: What's happening here: Check your Pods are coming up: Wait until all three show Running: Check your ReplicaSet and Service too: Minikube opens the URL in your browser automatically. Kind doesn't expose NodePort services directly, so use port-forwarding: Then open http://localhost:8080. Hit refresh a few times. You'll see the Pod hostname change the Service is load-balancing across your 3 Pods. Now that everything is running, try these one by one. Each one demonstrates a core Kubernetes behaviour. Kubernetes detects the replica count dropped to 2 and immediately creates a new Pod. This is the ReplicaSet controller doing its job. Two new Pods appear almost instantly. Four Pods terminate gracefully, one remains. Edit app.js to change the response message: Update the Deployment: Watch the rolling update: Kubernetes replaces Pods one at a time, keeping the app available throughout. This shows you the Pod's IP, which Node it's on, its labels, and a full event log — useful for debugging. If something goes wrong with an update: Kubernetes switches back to the previous ReplicaSet. Note for Kind users: Kind clusters don't survive a machine restart. If you reboot and come back to this project, run kind create cluster --name hello-cluster and kind load docker-image hello-app:v1 --name hello-cluster before applying your YAML again. Here's what was happening under the hood the whole time: Every concept from the Kubernetes basics maps to something you just did: Now that you have the fundamentals working, here are good next topics to explore: If you ran into issues or have questions, drop them in the comments. The most common problems are forgetting kind load docker-image (Kind) or not running eval $(minikube docker-env) before building (Minikube). Templates let you quickly answer FAQs or store snippets for re-use. Hide child comments as well For further actions, you may consider blocking this person and/or reporting abuse

Command

Copy

$ -weight: 500;">brew -weight: 500;">install minikube -weight: 500;">brew -weight: 500;">install minikube -weight: 500;">brew -weight: 500;">install minikube -weight: 500;">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -weight: 600;">sudo -weight: 500;">install minikube-linux-amd64 /usr/local/bin/minikube -weight: 500;">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -weight: 600;">sudo -weight: 500;">install minikube-linux-amd64 /usr/local/bin/minikube -weight: 500;">curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 -weight: 600;">sudo -weight: 500;">install minikube-linux-amd64 /usr/local/bin/minikube winget -weight: 500;">install Kubernetes.minikube winget -weight: 500;">install Kubernetes.minikube winget -weight: 500;">install Kubernetes.minikube minikube -weight: 500;">start minikube -weight: 500;">start minikube -weight: 500;">start -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # minikube Ready control-plane 10s v1.x.x -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # minikube Ready control-plane 10s v1.x.x -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # minikube Ready control-plane 10s v1.x.x -weight: 500;">brew -weight: 500;">install kind -weight: 500;">brew -weight: 500;">install kind -weight: 500;">brew -weight: 500;">install kind -weight: 500;">curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 chmod +x ./kind -weight: 600;">sudo mv ./kind /usr/local/bin/kind -weight: 500;">curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 chmod +x ./kind -weight: 600;">sudo mv ./kind /usr/local/bin/kind -weight: 500;">curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.22.0/kind-linux-amd64 chmod +x ./kind -weight: 600;">sudo mv ./kind /usr/local/bin/kind choco -weight: 500;">install kind choco -weight: 500;">install kind choco -weight: 500;">install kind kind create cluster --name hello-cluster kind create cluster --name hello-cluster kind create cluster --name hello-cluster -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # hello-cluster-control-plane Ready control-plane 10s v1.x.x -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # hello-cluster-control-plane Ready control-plane 10s v1.x.x -weight: 500;">kubectl get nodes # NAME STATUS ROLES AGE VERSION # hello-cluster-control-plane Ready control-plane 10s v1.x.x mkdir k8s-hello && cd k8s-hello mkdir k8s-hello && cd k8s-hello mkdir k8s-hello && cd k8s-hello const http = require('http'); const os = require('os'); const server = http.createServer((req, res) => { res.end(`Hello from Pod: ${os.hostname()}\n`); }); server.listen(3000, () => console.log('Running on port 3000')); const http = require('http'); const os = require('os'); const server = http.createServer((req, res) => { res.end(`Hello from Pod: ${os.hostname()}\n`); }); server.listen(3000, () => console.log('Running on port 3000')); const http = require('http'); const os = require('os'); const server = http.createServer((req, res) => { res.end(`Hello from Pod: ${os.hostname()}\n`); }); server.listen(3000, () => console.log('Running on port 3000')); FROM node:18-alpine WORKDIR /app COPY app.js . CMD ["node", "app.js"] FROM node:18-alpine WORKDIR /app COPY app.js . CMD ["node", "app.js"] FROM node:18-alpine WORKDIR /app COPY app.js . CMD ["node", "app.js"] eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v1 . eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v1 . eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v1 . -weight: 500;">docker build -t hello-app:v1 . kind load -weight: 500;">docker-image hello-app:v1 --name hello-cluster -weight: 500;">docker build -t hello-app:v1 . kind load -weight: 500;">docker-image hello-app:v1 --name hello-cluster -weight: 500;">docker build -t hello-app:v1 . kind load -weight: 500;">docker-image hello-app:v1 --name hello-cluster apiVersion: apps/v1 kind: Deployment metadata: name: hello-deployment spec: replicas: 3 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: hello image: hello-app:v1 imagePullPolicy: Never # use local image, don't pull from Docker Hub ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: hello--weight: 500;">service spec: type: NodePort selector: app: hello # matches the Pod label above this is how Services find Pods ports: - port: 80 targetPort: 3000 nodePort: 30080 apiVersion: apps/v1 kind: Deployment metadata: name: hello-deployment spec: replicas: 3 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: hello image: hello-app:v1 imagePullPolicy: Never # use local image, don't pull from Docker Hub ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: hello--weight: 500;">service spec: type: NodePort selector: app: hello # matches the Pod label above this is how Services find Pods ports: - port: 80 targetPort: 3000 nodePort: 30080 apiVersion: apps/v1 kind: Deployment metadata: name: hello-deployment spec: replicas: 3 selector: matchLabels: app: hello template: metadata: labels: app: hello spec: containers: - name: hello image: hello-app:v1 imagePullPolicy: Never # use local image, don't pull from Docker Hub ports: - containerPort: 3000 --- apiVersion: v1 kind: Service metadata: name: hello--weight: 500;">service spec: type: NodePort selector: app: hello # matches the Pod label above this is how Services find Pods ports: - port: 80 targetPort: 3000 nodePort: 30080 -weight: 500;">kubectl apply -f deployment.yaml -weight: 500;">kubectl apply -f deployment.yaml -weight: 500;">kubectl apply -f deployment.yaml deployment.apps/hello-deployment created -weight: 500;">service/hello--weight: 500;">service created deployment.apps/hello-deployment created -weight: 500;">service/hello--weight: 500;">service created deployment.apps/hello-deployment created -weight: 500;">service/hello--weight: 500;">service created -weight: 500;">kubectl get pods -weight: 500;">kubectl get pods -weight: 500;">kubectl get pods 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 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 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 -weight: 500;">kubectl get replicaset -weight: 500;">kubectl get -weight: 500;">service hello--weight: 500;">service -weight: 500;">kubectl get replicaset -weight: 500;">kubectl get -weight: 500;">service hello--weight: 500;">service -weight: 500;">kubectl get replicaset -weight: 500;">kubectl get -weight: 500;">service hello--weight: 500;">service minikube -weight: 500;">service hello--weight: 500;">service minikube -weight: 500;">service hello--weight: 500;">service minikube -weight: 500;">service hello--weight: 500;">service -weight: 500;">kubectl port-forward -weight: 500;">service/hello--weight: 500;">service 8080:80 -weight: 500;">kubectl port-forward -weight: 500;">service/hello--weight: 500;">service 8080:80 -weight: 500;">kubectl port-forward -weight: 500;">service/hello--weight: 500;">service 8080:80 Hello from Pod: hello-deployment-57c4d87bf-abc12 Hello from Pod: hello-deployment-57c4d87bf-ghi56 Hello from Pod: hello-deployment-57c4d87bf-def34 Hello from Pod: hello-deployment-57c4d87bf-abc12 Hello from Pod: hello-deployment-57c4d87bf-ghi56 Hello from Pod: hello-deployment-57c4d87bf-def34 Hello from Pod: hello-deployment-57c4d87bf-abc12 Hello from Pod: hello-deployment-57c4d87bf-ghi56 Hello from Pod: hello-deployment-57c4d87bf-def34 # grab any pod name -weight: 500;">kubectl get pods # delete it -weight: 500;">kubectl delete pod hello-deployment-57c4d87bf-abc12 # watch what happens -weight: 500;">kubectl get pods -w # grab any pod name -weight: 500;">kubectl get pods # delete it -weight: 500;">kubectl delete pod hello-deployment-57c4d87bf-abc12 # watch what happens -weight: 500;">kubectl get pods -w # grab any pod name -weight: 500;">kubectl get pods # delete it -weight: 500;">kubectl delete pod hello-deployment-57c4d87bf-abc12 # watch what happens -weight: 500;">kubectl get pods -w -weight: 500;">kubectl scale deployment hello-deployment --replicas=5 -weight: 500;">kubectl get pods -weight: 500;">kubectl scale deployment hello-deployment --replicas=5 -weight: 500;">kubectl get pods -weight: 500;">kubectl scale deployment hello-deployment --replicas=5 -weight: 500;">kubectl get pods -weight: 500;">kubectl scale deployment hello-deployment --replicas=1 -weight: 500;">kubectl get pods -weight: 500;">kubectl scale deployment hello-deployment --replicas=1 -weight: 500;">kubectl get pods -weight: 500;">kubectl scale deployment hello-deployment --replicas=1 -weight: 500;">kubectl get pods res.end(`Hello from Pod v2: ${os.hostname()}\n`); res.end(`Hello from Pod v2: ${os.hostname()}\n`); res.end(`Hello from Pod v2: ${os.hostname()}\n`); # Minikube eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v2 . # Kind -weight: 500;">docker build -t hello-app:v2 . kind load -weight: 500;">docker-image hello-app:v2 --name hello-cluster # Minikube eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v2 . # Kind -weight: 500;">docker build -t hello-app:v2 . kind load -weight: 500;">docker-image hello-app:v2 --name hello-cluster # Minikube eval $(minikube -weight: 500;">docker-env) -weight: 500;">docker build -t hello-app:v2 . # Kind -weight: 500;">docker build -t hello-app:v2 . kind load -weight: 500;">docker-image hello-app:v2 --name hello-cluster -weight: 500;">kubectl set image deployment/hello-deployment hello=hello-app:v2 -weight: 500;">kubectl set image deployment/hello-deployment hello=hello-app:v2 -weight: 500;">kubectl set image deployment/hello-deployment hello=hello-app:v2 -weight: 500;">kubectl rollout -weight: 500;">status deployment/hello-deployment -weight: 500;">kubectl rollout -weight: 500;">status deployment/hello-deployment -weight: 500;">kubectl rollout -weight: 500;">status deployment/hello-deployment -weight: 500;">kubectl describe pod <pod-name> -weight: 500;">kubectl describe pod <pod-name> -weight: 500;">kubectl describe pod <pod-name> -weight: 500;">kubectl rollout undo deployment/hello-deployment -weight: 500;">kubectl rollout undo deployment/hello-deployment -weight: 500;">kubectl rollout undo deployment/hello-deployment -weight: 500;">kubectl delete -f deployment.yaml -weight: 500;">kubectl delete -f deployment.yaml -weight: 500;">kubectl delete -f deployment.yaml minikube -weight: 500;">stop minikube -weight: 500;">stop minikube -weight: 500;">stop kind delete cluster --name hello-cluster kind delete cluster --name hello-cluster kind delete cluster --name hello-cluster 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 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 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 - A containerised Node.js app running in Kubernetes - 3 replicas managed by a Deployment and ReplicaSet - A Service exposing the app to your browser - Hands-on experience with self-healing and scaling - Docker required by both Minikube and Kind - -weight: 500;">kubectl the Kubernetes CLI - Either Minikube or Kind (installation covered below) - Minikube: slightly friendlier for beginners, has a built-in way to open services in the browser - Kind: lighter, faster, great if you already have Docker set up - The Deployment tells Kubernetes to keep 3 replicas of our Pod running at all times - It automatically creates a ReplicaSet to enforce that replica count - The Service uses the app: hello label selector to find all matching Pods and route traffic to them - imagePullPolicy: Never tells Kubernetes to use the locally available image instead of going to Docker Hub - Namespaces isolate workloads for different teams or environments - ConfigMaps & Secrets externalise config and credentials from your container - Ingress a cleaner alternative to NodePort for routing external traffic - Persistent Volumes attach storage that survives Pod restarts - Liveness & Readiness Probes teach Kubernetes when your Pod is actually healthy