Deploy Your First Application on Kubernetes
Deploy a web app to a local minikube cluster with a Deployment, Service, ConfigMap and autoscaler, then practise rolling updates and rollbacks.
Blueprint Walkthrough
Architecture Overview & Problem Statement
This project deploys a web application to a local Kubernetes cluster with minikube, using a Deployment, a Service, a ConfigMap and a HorizontalPodAutoscaler. You then practise a rolling update, a bad release with a rollback, and pod self-healing. You finish with a small set of manifests in Git that rebuild the whole setup with one kubectl apply.
The problem this solves
Running one Docker container is fine on a laptop. Picture a team at a company like Swiggy or Spotify shipping a service to real users. They need several copies for reliability, automatic restarts when a container crashes, updates with no downtime, and more copies when traffic spikes. Docker alone does none of that.
Kubernetes is the platform that does. You declare the desired state, for example "run three copies of this app", and Kubernetes keeps working to make reality match. A crashed pod is replaced, an update rolls out gradually, and the autoscaler adds pods under load. These are the same resources you will use every day as a DevOps engineer.
What you will build
| Resource | What it does |
|---|---|
| Pod | The smallest unit: one or more containers. Pods are temporary and replaced when they die |
| Deployment | Keeps the desired number of identical pods running and handles rolling updates |
| Service | A stable address that load balances across the pods, even as their IPs change |
| ConfigMap | Configuration kept outside the image, so you change settings without rebuilding |
| HorizontalPodAutoscaler | Adds or removes pods based on CPU usage |
What you will have at the end
- Four manifests (
configmap.yaml,deployment.yaml,service.yaml,hpa.yaml) in a Git repository - A running app reachable from your browser, with three replicas behind a Service
- Proof that a rolling update causes no downtime, that a bad release can be rolled back, and that deleted pods come back
- A short README with your terminal output, which makes a good first portfolio piece
Prerequisites and cost
You need Docker installed and running, about 4 GB of free RAM and 2 CPU cores. Everything runs on your laptop, so there is no cloud cost.
Milestone 1: Install the Tools and Start a Local Cluster
This milestone installs kubectl and minikube and starts a single-node Kubernetes cluster on your machine.
minikube runs a real Kubernetes cluster inside a container on your laptop, which makes it ideal for learning without cloud bills. kubectl is the command-line client you use to talk to any Kubernetes cluster. Both work the same way against a managed cluster later.
# macOS (Homebrew)brew install minikube kubectl # Linuxcurl -LO https://github.com/kubernetes/minikube/releases/latest/download/minikube-linux-amd64sudo install minikube-linux-amd64 /usr/local/bin/minikubecurl -LO "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl minikube versionkubectl version --clientStart the cluster and enable the metrics add-on, which the autoscaler needs to read CPU usage:
minikube start --driver=docker --cpus=2 --memory=4096# Takes 2 to 3 minutes the first time minikube addons enable metrics-server kubectl cluster-infokubectl get nodes# Expected: one node named minikube with STATUS ReadyTipminikube is for learning. In production you use a managed service such as Amazon EKS, Google GKE or Azure AKS. The resources you write here work the same way on all of them.
Common MistakeRunning
minikube startwhile Docker is not running. It fails with a driver error. Start Docker Desktop (or the Docker service) first, then retry.
Milestone 2: Write the ConfigMap and Deployment
This milestone writes the two manifests that define what runs: the configuration and the Deployment that keeps three pods alive.
A Deployment does not manage pods directly by name. It uses labels and a selector: the Deployment says "I own every pod labelled app: webapp", and the pod template stamps that label on each pod it creates. If the two do not match, the Deployment is rejected, so keep them in sync. Resource requests and limits tell the scheduler how much CPU and memory a pod needs and caps what it may use. The autoscaler in the next milestone depends on the CPU request.
mkdir k8s-first-deploy && cd k8s-first-deploy && git initCreate configmap.yaml:
apiVersion: v1kind: ConfigMapmetadata: name: webapp-config labels: app: webappdata: # Each key becomes an environment variable in the pods APP_NAME: "DevOps Network App" APP_VERSION: "1.0.0" ENVIRONMENT: "learning" LOG_LEVEL: "info"Create deployment.yaml:
apiVersion: apps/v1kind: Deploymentmetadata: name: webapp labels: app: webappspec: replicas: 3 # Which pods this Deployment owns. Must match template.metadata.labels selector: matchLabels: app: webapp # Replace pods gradually so the app stays up during updates strategy: type: RollingUpdate rollingUpdate: maxSurge: 1 # start at most 1 extra pod during an update maxUnavailable: 0 # never drop below the desired count template: metadata: labels: app: webapp spec: containers: - name: webapp image: nginx:1.28-alpine ports: - containerPort: 80 name: http # Inject every ConfigMap key as an environment variable envFrom: - configMapRef: name: webapp-config # requests = guaranteed minimum, limits = hard maximum resources: requests: cpu: 50m memory: 64Mi limits: cpu: 200m memory: 128Mi # Readiness: is the pod ready to receive traffic? readinessProbe: httpGet: path: / port: 80 initialDelaySeconds: 5 periodSeconds: 10 # Liveness: should Kubernetes restart this container? livenessProbe: httpGet: path: / port: 80 initialDelaySeconds: 15 periodSeconds: 20 failureThreshold: 3TipEnvironment variables from a ConfigMap are read when a pod starts. If you edit the ConfigMap later, run
kubectl rollout restart deployment/webappso new pods pick up the change.
Common MistakeA
selector.matchLabelsthat does not match the pod template labels. The apply fails with a selector error. Copy the sameapp: webapplabel into both places.
Milestone 3: Add the Service and the Autoscaler
This milestone writes the Service that exposes the pods and the autoscaler that adds pods under load.
Pods get new IP addresses every time they are replaced, so nothing should talk to a pod IP directly. A Service gives the group one stable address and load balances across every healthy pod that matches its selector. NodePort publishes that address on a port of the node, which is how minikube reaches it from your laptop. The HorizontalPodAutoscaler (HPA) watches average CPU across the pods and changes the replica count to hold a target, here 70 percent of the CPU each pod requested.
Create service.yaml:
apiVersion: v1kind: Servicemetadata: name: webapp labels: app: webappspec: type: NodePort selector: app: webapp ports: - name: http port: 80 # port the Service listens on targetPort: 80 # port on the pod nodePort: 30080 # port on the node (30000-32767)Create hpa.yaml:
apiVersion: autoscaling/v2kind: HorizontalPodAutoscalermetadata: name: webapp-hpaspec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: webapp minReplicas: 2 maxReplicas: 6 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 70Common MistakeCreating an HPA for a Deployment whose containers have no CPU
requests. The HPA shows<unknown>as its target and never scales, because utilisation is measured against the request. Keep therequestsblock from Milestone 2.
Milestone 4: Deploy and Explore the App
This milestone applies all four manifests and learns the commands you will use for debugging on any cluster.
kubectl apply is declarative: you hand Kubernetes a file describing the end state and it works out what to create or change. Running it again with the same files changes nothing, which is why manifests belong in Git.
kubectl apply -f . kubectl get pods --watch# Expected: 3 pods go from ContainerCreating to Running 1/1. Press Ctrl+C kubectl get deployments,services,hpa# Expected: webapp 3/3 ready, a NodePort service on 80:30080, an HPA with 2 to 6 replicas minikube service webapp --url# Open the printed URL in your browser to see the nginx welcome pageTipOn macOS and Windows with the Docker driver,
minikube service webapp --urlkeeps running to hold a tunnel open. Leave that terminal alone and use another one for the next commands.
Now explore the app the way you would during an incident:
POD=$(kubectl get pods -l app=webapp -o jsonpath='{.items[0].metadata.name}') kubectl describe pod $POD # read the Events section at the bottomkubectl logs $PODkubectl logs -l app=webapp --tail=5kubectl exec -it $POD -- sh # a shell inside the container; type exit to leavekubectl top pods # CPU and memory (metrics-server needs a minute to warm up)| Command | Use it to |
|---|---|
kubectl get pods -o wide |
See which node and IP each pod has |
kubectl describe pod NAME |
Find out why a pod is Pending or crashing |
kubectl logs -f NAME |
Follow application logs |
kubectl exec -it NAME -- sh |
Look around inside a running container |
kubectl top pods |
Check live CPU and memory use |
kubectl get events --sort-by=.lastTimestamp |
See recent cluster events in order |
Common MistakeReading only
kubectl get podswhen something is wrong. The status column is a summary. The real reason is almost always inkubectl describe pod(Events) orkubectl logs.
Milestone 5: Roll Out an Update, Break It, Roll It Back
This milestone updates the image with no downtime, ships a deliberately broken version, and rolls back.
During a rolling update the Deployment starts a new pod, waits for its readiness probe to pass, then removes an old one. With maxUnavailable: 0 there is always full capacity serving traffic. Every change creates a numbered revision, which is what makes rollback possible.
kubectl set image deployment/webapp webapp=nginx:1.29-alpinekubectl rollout status deployment/webapp# Expected: successfully rolled out kubectl rollout history deployment/webapp# Expected: two revisionsNow ship a bad release by pointing at an image tag that does not exist:
kubectl set image deployment/webapp webapp=nginx:does-not-existkubectl get pods# Expected: new pods show ErrImagePull or ImagePullBackOff# Old pods keep running, so the site still answers kubectl rollout status deployment/webapp --timeout=30s# Expected: waiting, then a timeout. The rollout is stuck kubectl rollout undo deployment/webappkubectl rollout status deployment/webappkubectl get pods# Expected: back to 3 healthy podsThe site stayed up the whole time, because the readiness probe and maxUnavailable: 0 kept the old pods serving until new ones were healthy.
Tip
kubectl rollout undois the first thing to try when a release goes wrong. It returns to the previous revision in seconds, without a rebuild.
Common MistakeFixing a bad release by editing pods one by one. Pods are disposable and the Deployment recreates them from its template. Change the Deployment, or roll it back.
Milestone 6: Test Self-Healing and Autoscaling
This milestone proves Kubernetes repairs itself and reacts to load.
The Deployment controller compares the number of running pods with replicas all the time. Delete one and it creates a replacement. The HPA does the same for load: it checks CPU every 15 seconds or so and adjusts the replica count.
POD=$(kubectl get pods -l app=webapp -o jsonpath='{.items[0].metadata.name}')kubectl delete pod $PODkubectl get pods --watch# Expected: one pod Terminating, a new one appears and reaches Running# Press Ctrl+C. READY returns to 3/3 within secondsGenerate load and watch the autoscaler. Open two terminals:
# Terminal 1: watch the autoscalerkubectl get hpa webapp-hpa --watch # Terminal 2: start a load generator inside the clusterkubectl run load --image=busybox:1.36 --restart=Never -- \ /bin/sh -c "while true; do wget -q -O- http://webapp > /dev/null; done"CPU on a static nginx page is low, so scaling can take a few minutes and may stay small on a slow laptop. Watching TARGETS rise above 70 percent and REPLICAS increase is the goal. Stop the load when you are done:
kubectl delete pod load# Replicas scale back down after about 5 minutes of low CPUCommon MistakeExpecting the HPA to react instantly. It samples on an interval and scales down slowly on purpose, to avoid flapping. Give it a few minutes before deciding it is broken.
Validation & Testing
This final step verifies every resource and proves the update caused no downtime, then cleans up.
# 1. Three pods runningkubectl get pods -l app=webapp# Expected: 3 pods, STATUS Running, READY 1/1 # 2. ConfigMap values reach the containerkubectl exec deploy/webapp -- env | grep APP_NAME# Expected: APP_NAME=DevOps Network App # 3. The app answers through the ServiceAPP_URL=$(minikube service webapp --url)curl -s -o /dev/null -w "%{http_code}\n" "$APP_URL"# Expected: 200 # 4. The autoscaler is healthykubectl get hpa webapp-hpa# Expected: TARGETS shows a CPU percentage (not <unknown>), MINPODS 2, MAXPODS 6 # 5. A rolling update causes no downtime( for i in $(seq 1 120); do curl -s -o /dev/null -w "%{http_code}\n" "$APP_URL"; sleep 0.5; done ) > codes.txt &kubectl set image deployment/webapp webapp=nginx:1.28-alpinekubectl rollout status deployment/webappwaitsort codes.txt | uniq -c# Expected: only 200 responsesClean up when you are finished:
kubectl delete -f .minikube stopCommit the four manifests to Git with a README that includes your outputs from steps 1 to 5. A reviewer can see that you can deploy, update, roll back and debug an application on Kubernetes.