Final Project: Deploy a Production-Ready Web App
This capstone project pulls the full Kubernetes training path together. You will deploy a small production-style web application, validate it, break and fix it, and review it against a practical production readiness checklist.
Scenario
You have been asked to deploy a small internal web application to Kubernetes. The application must be reachable through Ingress, configured through Kubernetes objects instead of hardcoded image changes, deployed with basic operational guardrails, and validated before it is considered ready.
This is not meant to be a complex software development project. The application will use nginx and a custom index.html file from a ConfigMap so the focus stays on Kubernetes operations.
What you will build
- A dedicated Namespace
- A ConfigMap containing custom web content
- A Secret containing a fake application token
- A dedicated ServiceAccount
- A Deployment with two replicas
- Resource requests and limits
- Startup, readiness, and liveness probes
- A ClusterIP Service
- An Ingress route
- Basic NetworkPolicy examples
- A validation and troubleshooting checklist
Architecture
Browser or curl
sends request to finalboss.localhost
Ingress controller receives traffic
Ingress routes to Service
Service routes to ready Pods
Pods run nginx
nginx serves index.html from ConfigMap
Project requirements
- All application resources must run in a Namespace named final-boss-lab.
- The Deployment must be named production-webapp.
- The Deployment must run two replicas.
- The web content must come from a ConfigMap.
- The workload must reference a Secret.
- The workload must use a dedicated ServiceAccount.
- The container must define CPU and memory requests and limits.
- The container must define startup, readiness, and liveness probes.
- The app must be exposed internally with a ClusterIP Service.
- The app must be reachable through Ingress using finalboss.localhost.
- You must validate rollout status, endpoints, logs, events, and browser or curl access.
- You must intentionally break and fix at least one part of the deployment.
Before you start
This project assumes Docker Desktop is running, your kind cluster exists, kubectl is installed, your current context is kind-stealth-k8s-lab, and the NGINX Ingress Controller is installed from the Ingress lesson.
kubectl config current-context kubectl get nodes kubectl get pods -n ingress-nginx kubectl get service -n ingress-nginx
If the ingress controller is not installed, install it:
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.12.1/deploy/static/provider/kind/deploy.yaml kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=180s
Step 1: Create the Namespace
Create final-boss-namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: final-boss-lab
labels:
app.kubernetes.io/part-of: kubernetes-final-project
environment: training
kubectl apply -f final-boss-namespace.yaml kubectl get namespace final-boss-lab --show-labels
Step 2: Create the ConfigMap web content
Create final-boss-configmap.yaml:
apiVersion: v1
kind: ConfigMap
metadata:
name: production-webapp-content
namespace: final-boss-lab
labels:
app: production-webapp
data:
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>Kubernetes Final Boss</title>
<style>
body {
font-family: Arial, sans-serif;
background: #08111f;
color: #f8fafc;
margin: 0;
padding: 4rem;
}
.card {
max-width: 760px;
border: 1px solid #334155;
border-radius: 18px;
padding: 2rem;
background: #111827;
}
h1 { color: #93c5fd; }
code { color: #86efac; }
</style>
</head>
<body>
<div class="card">
<h1>Kubernetes Final Boss Complete</h1>
<p>This page is being served by nginx inside Kubernetes.</p>
<p>The web content came from a <code>ConfigMap</code>.</p>
<p>The workload is running behind a <code>Deployment</code>, <code>Service</code>, and <code>Ingress</code>.</p>
</div>
</body>
</html>
kubectl apply -f final-boss-configmap.yaml kubectl describe configmap production-webapp-content -n final-boss-lab
Step 3: Create the Secret
Create final-boss-secret.yaml:
apiVersion: v1
kind: Secret
metadata:
name: production-webapp-secret
namespace: final-boss-lab
labels:
app: production-webapp
type: Opaque
stringData:
APP_TOKEN: fake-final-boss-token
APP_MODE: production-style-training
kubectl apply -f final-boss-secret.yaml kubectl describe secret production-webapp-secret -n final-boss-lab
Step 4: Create the ServiceAccount
Create final-boss-serviceaccount.yaml:
apiVersion: v1
kind: ServiceAccount
metadata:
name: production-webapp-sa
namespace: final-boss-lab
labels:
app: production-webapp
kubectl apply -f final-boss-serviceaccount.yaml kubectl get serviceaccounts -n final-boss-lab
Step 5: Create a read-only Role and RoleBinding
This simple app does not need Kubernetes API access, but this step shows how you would grant limited permissions to a workload identity when needed.
Create final-boss-rbac.yaml:
apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: production-webapp-readonly namespace: final-boss-lab rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: production-webapp-readonly-binding namespace: final-boss-lab subjects: - kind: ServiceAccount name: production-webapp-sa namespace: final-boss-lab roleRef: kind: Role name: production-webapp-readonly apiGroup: rbac.authorization.k8s.io
kubectl apply -f final-boss-rbac.yaml kubectl auth can-i list configmaps --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab kubectl auth can-i delete pods --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab kubectl auth can-i list secrets --as=system:serviceaccount:final-boss-lab:production-webapp-sa -n final-boss-lab
Step 6: Create the Deployment
Create final-boss-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: production-webapp
namespace: final-boss-lab
labels:
app: production-webapp
spec:
replicas: 2
selector:
matchLabels:
app: production-webapp
template:
metadata:
labels:
app: production-webapp
spec:
serviceAccountName: production-webapp-sa
containers:
- name: nginx
image: nginx:1.27
ports:
- containerPort: 80
env:
- name: APP_MODE
valueFrom:
secretKeyRef:
name: production-webapp-secret
key: APP_MODE
- name: APP_TOKEN
valueFrom:
secretKeyRef:
name: production-webapp-secret
key: APP_TOKEN
volumeMounts:
- name: web-content
mountPath: /usr/share/nginx/html
readOnly: true
startupProbe:
httpGet:
path: /
port: 80
failureThreshold: 30
periodSeconds: 2
readinessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 3
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /
port: 80
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 3
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
volumes:
- name: web-content
configMap:
name: production-webapp-content
kubectl apply -f final-boss-deployment.yaml kubectl rollout status deployment/production-webapp -n final-boss-lab kubectl get deployments -n final-boss-lab kubectl get pods -n final-boss-lab -o wide
Step 7: Inspect the Deployment and Pods
kubectl describe deployment production-webapp -n final-boss-lab kubectl get pods -n final-boss-lab kubectl describe pod <pod-name> -n final-boss-lab
Confirm the Pod uses the dedicated ServiceAccount, mounts the ConfigMap, references the Secret, defines requests and limits, and has startup, readiness, and liveness probes.
Step 8: Create the Service
Create final-boss-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: production-webapp
namespace: final-boss-lab
labels:
app: production-webapp
spec:
type: ClusterIP
selector:
app: production-webapp
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
kubectl apply -f final-boss-service.yaml kubectl get service production-webapp -n final-boss-lab kubectl describe service production-webapp -n final-boss-lab kubectl get endpoints production-webapp -n final-boss-lab
Step 9: Test the Service with port-forward
kubectl port-forward -n final-boss-lab service/production-webapp 8080:80
Open:
http://localhost:8080
You should see the custom Kubernetes Final Boss page. Press Ctrl+C to stop port-forwarding.
Step 10: Create the Ingress
Create final-boss-ingress.yaml:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: production-webapp
namespace: final-boss-lab
labels:
app: production-webapp
spec:
ingressClassName: nginx
rules:
- host: finalboss.localhost
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: production-webapp
port:
number: 80
kubectl apply -f final-boss-ingress.yaml kubectl get ingress -n final-boss-lab kubectl describe ingress production-webapp -n final-boss-lab
Step 11: Test the Ingress with curl
In one PowerShell window, port-forward to the ingress controller:
kubectl port-forward -n ingress-nginx service/ingress-nginx-controller 8080:80
In a second PowerShell window, test with the Host header:
curl.exe -H "Host: finalboss.localhost" http://localhost:8080
Step 12: Test the Ingress in a browser
Open Notepad as Administrator and edit:
C:\Windows\System32\drivers\etc\hosts
Add:
127.0.0.1 finalboss.localhost
With the ingress controller port-forward still running, open:
http://finalboss.localhost:8080
When testing is complete, remove the temporary hosts file entry.
Step 13: Add NetworkPolicy examples
This project includes NetworkPolicy manifests so you practice the production pattern. Enforcement depends on whether your cluster networking plugin supports NetworkPolicy.
Create final-boss-networkpolicy.yaml:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: final-boss-lab
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-webapp-http
namespace: final-boss-lab
spec:
podSelector:
matchLabels:
app: production-webapp
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- protocol: TCP
port: 80
kubectl apply -f final-boss-networkpolicy.yaml kubectl get networkpolicies -n final-boss-lab kubectl describe networkpolicy allow-webapp-http -n final-boss-lab
Step 14: Validate the full deployment
kubectl get all -n final-boss-lab kubectl get configmaps -n final-boss-lab kubectl get secrets -n final-boss-lab kubectl get serviceaccounts -n final-boss-lab kubectl get role,rolebinding -n final-boss-lab kubectl get ingress -n final-boss-lab kubectl get networkpolicies -n final-boss-lab kubectl get endpoints production-webapp -n final-boss-lab kubectl rollout status deployment/production-webapp -n final-boss-lab kubectl get events -n final-boss-lab --sort-by=.metadata.creationTimestamp
Step 15: Break the Service selector on purpose
Edit final-boss-service.yaml and change:
selector: app: production-webapp
to:
selector: app: does-not-match
Apply it and inspect endpoints:
kubectl apply -f final-boss-service.yaml kubectl get endpoints production-webapp -n final-boss-lab kubectl describe service production-webapp -n final-boss-lab
The endpoint list should be empty because the Service selector no longer matches the Pod labels.
Step 16: Fix the Service selector
Change the selector back to app: production-webapp, then run:
kubectl apply -f final-boss-service.yaml kubectl get endpoints production-webapp -n final-boss-lab
Step 17: Break the readiness probe on purpose
Edit final-boss-deployment.yaml and change the readiness probe path to /not-ready, then apply it:
kubectl apply -f final-boss-deployment.yaml kubectl rollout status deployment/production-webapp -n final-boss-lab
Troubleshoot readiness failures:
kubectl get pods -n final-boss-lab kubectl describe pod <pod-name> -n final-boss-lab kubectl get endpoints production-webapp -n final-boss-lab kubectl get events -n final-boss-lab --sort-by=.metadata.creationTimestamp
Step 18: Fix the readiness probe
Change the readiness probe path back to /, then run:
kubectl apply -f final-boss-deployment.yaml kubectl rollout status deployment/production-webapp -n final-boss-lab kubectl get pods -n final-boss-lab kubectl get endpoints production-webapp -n final-boss-lab
Step 19: Simulate a bad image rollout
kubectl set image deployment/production-webapp nginx=nginx:not-a-real-final-boss-tag -n final-boss-lab kubectl rollout status deployment/production-webapp -n final-boss-lab kubectl get pods -n final-boss-lab kubectl describe pod <bad-pod-name> -n final-boss-lab
Step 20: Recover with rollout undo
kubectl rollout undo deployment/production-webapp -n final-boss-lab kubectl rollout status deployment/production-webapp -n final-boss-lab kubectl get pods -n final-boss-lab kubectl get endpoints production-webapp -n final-boss-lab
Step 21: Run the production readiness review
Workload design
- Is the app managed by a Deployment?
- Are there at least two replicas?
- Are labels and selectors consistent?
- Is the image pinned to a specific version?
Configuration
- Does web content come from a ConfigMap?
- Is sensitive-style configuration stored in a Secret?
- Are real secrets excluded from Git?
Identity and access
- Does the workload use a dedicated ServiceAccount?
- Are RBAC permissions limited?
- Can you prove permissions with kubectl auth can-i?
Resources and health
- Are CPU and memory requests set?
- Are CPU and memory limits set?
- Are startup, readiness, and liveness probes configured?
- Do the Pods become Ready?
Networking
- Does the Service have endpoints?
- Does port-forward to the Service work?
- Does Ingress route to the Service?
- Does curl or browser access work with the expected hostname?
Security and policy
- Are NetworkPolicy manifests present?
- Do you understand whether your local CNI enforces them?
- Are Secrets protected from casual display in logs?
Operations
- Can you check rollout status?
- Can you read logs?
- Can you read events?
- Can you troubleshoot missing endpoints?
- Can you recover from a bad image rollout?
Final scoring rubric
- Pass: The app deploys, Service endpoints exist, and Service port-forward works.
- Strong pass: Ingress works, probes are healthy, resources are set, and rollback works.
- Final boss complete: You can explain every object, intentionally break and fix Service selectors and readiness, recover from a bad rollout, and walk through the production readiness checklist without guessing.
Step 22: Clean up
Stop any active port-forward sessions with Ctrl+C. Then delete the Namespace and everything inside it:
kubectl delete namespace final-boss-lab
Confirm cleanup:
kubectl get namespaces kubectl get pods -A
Remove the temporary hosts file entry if you added one:
127.0.0.1 finalboss.localhost
What you just proved
- You can create organized resources in a Namespace.
- You can deploy a replicated workload.
- You can use ConfigMaps and Secrets.
- You can use a dedicated ServiceAccount and limited RBAC.
- You can set resource requests and limits.
- You can configure health probes.
- You can expose the app with a Service and route traffic through Ingress.
- You can apply NetworkPolicy patterns.
- You can validate rollout, logs, events, and endpoints.
- You can troubleshoot broken selectors, probes, and rollouts.
- You can review production readiness before calling the workload done.
Final reflection questions
- Which object actually ran the application containers?
- Which object kept two replicas running?
- Which object gave the Pods stable internal access?
- Which object routed HTTP traffic into the cluster?
- Which object provided the custom web page?
- Which object stored fake sensitive configuration?
- Which identity did the workload run as?
- What happened when the Service selector was wrong?
- What happened when the readiness probe was wrong?
- How did you recover from a bad image rollout?
- What would you change before deploying a real internet-facing app?
- Which part of the production readiness checklist would be hardest in your own environment?
Answer key
- The Pods ran the application containers.
- The Deployment, through a ReplicaSet, kept two replicas running.
- The Service gave the Pods stable internal access.
- The Ingress routed HTTP traffic into the cluster through the ingress controller.
- The ConfigMap provided the custom web page.
- The Secret stored fake sensitive configuration.
- The workload used the production-webapp-sa ServiceAccount.
- The Service had no endpoints because it did not match the Pod labels.
- The Pods could be Running but not Ready, and Service endpoints could disappear or rollout could stall.
- By using kubectl rollout undo deployment/production-webapp -n final-boss-lab.
- Examples: real TLS, real DNS, external secret manager, image scanning, monitoring, alerting, backup/restore if state exists, stronger policies, and production ingress/load balancer configuration.
- Answers will vary. The important part is identifying the operational gap honestly.