Capstone: Deploy a Complete Application and Wrap Up the Series
You've reached the end
Over fourteen articles, we learned each piece in isolation. This final article ties them into a complete picture: deploy a real multi-component application — a frontend running several replicas behind an Ingress, and a database with durable storage — using nearly every concept covered. Then we clean up the cluster and look back over the whole journey.
Project architecture
A classic two-tier web app, with every component a piece of the series:
Internet
│
▼
Ingress (capstone.local) ← Article 9
│
▼
Service "frontend" (ClusterIP) ← Article 5
│ load balancing
├──► frontend Pod (nginx) ┐ Deployment, 2 replicas ← Article 4
└──► frontend Pod (nginx) ┘ + probes + resources ← Articles 10, 11
└─ content from ConfigMap ← Article 7
Service "db" (ClusterIP) ──► postgres Pod ← Articles 4, 5
├─ password from Secret ← Article 7
├─ data on a PVC ← Article 8
└─ readiness probe ← Article 10
Everything lives in its own namespace capstone (Article 6), to make cleanup easy and keep it separate from whatever else is running.
Database tier: Secret + PVC + probe
The database needs three things we've learned: a password that's not hard-coded (Secret), data that doesn't vanish (PVC), and a way for Kubernetes to know it's ready (readiness probe). Excerpt of the manifest:
apiVersion: apps/v1
kind: Deployment
metadata: { name: db, namespace: capstone }
spec:
replicas: 1
selector: { matchLabels: { app: db } }
template:
metadata: { labels: { app: db } }
spec:
containers:
- name: postgres
image: postgres:16-alpine
env:
- name: POSTGRES_PASSWORD
valueFrom: { secretKeyRef: { name: db-secret, key: POSTGRES_PASSWORD } } # Article 7
resources: # Article 11
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
readinessProbe: # Article 10
exec: { command: ["sh","-c","pg_isready -U postgres"] }
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
volumes:
- name: data
persistentVolumeClaim: { claimName: db-data } # Article 8
Frontend tier: multiple replicas + ConfigMap + Ingress
The frontend is nginx running 2 replicas (ready for load balancing and fault tolerance), serving a page from a ConfigMap, with probes and resources, and exposed externally through a Service + Ingress:
apiVersion: apps/v1
kind: Deployment
metadata: { name: frontend, namespace: capstone }
spec:
replicas: 2
# ... selector, template ...
containers:
- name: nginx
image: nginx:1.27-alpine
livenessProbe: { httpGet: { path: /, port: 80 } }
readinessProbe: { httpGet: { path: /, port: 80 } }
volumeMounts:
- { name: content, mountPath: /usr/share/nginx/html }
volumes:
- name: content
configMap: { name: web-content } # index.html page from a ConfigMap
Deploying the whole thing is just a few apply commands:
kubectl create namespace capstone
kubectl apply -f app.yaml # configmap, secret, pvc, db deployment+service
kubectl apply -f frontend.yaml # frontend deployment+service, ingress
The full picture: every piece running together
kubectl get all,ingress,pvc -n capstone
NAME READY STATUS RESTARTS AGE
pod/db-574d8bd786-wwbxb 1/1 Running 0 50s
pod/frontend-86658d7ff6-2dzmt 1/1 Running 0 15s
pod/frontend-86658d7ff6-gtftl 1/1 Running 0 15s
NAME TYPE CLUSTER-IP PORT(S) AGE
service/db ClusterIP 10.96.135.57 5432/TCP 50s
service/frontend ClusterIP 10.104.95.30 80/TCP 15s
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/db 1/1 1 1 50s
deployment.apps/frontend 2/2 2 2 15s
NAME CLASS HOSTS PORTS AGE
ingress.../capstone nginx capstone.local 80 15s
NAME STATUS CAPACITY ACCESS MODES STORAGECLASS
persistentvolumeclaim/db-data Bound 200Mi RWO standard
A complete application: 2 frontend pods, 1 db pod, two Services, one Ingress, one Bound PVC. Every concept in the series is present in a few dozen lines of YAML.
Verify: it actually works
The web page through Ingress — a request with host capstone.local goes through Ingress → Service → one of the two frontend pods → the page from the ConfigMap:
minikube ssh "curl -s -H 'Host: capstone.local' http://localhost/ | grep h1"
<title>KKloud Capstone</title>
<h1>KKloud — Capstone on minikube</h1>
The database uses Secret + PVC — writing and reading real data, password taken from the Secret, data on a durable PVC:
kubectl exec -n capstone deploy/db -- psql -U postgres \
-c "INSERT INTO notes(msg) VALUES('capstone works'); SELECT * FROM notes;"
INSERT 0 1
id | msg
----+----------------
1 | capstone works
(1 row)
Both tiers work and fit together. From a fresh cluster to a web app with a database — declared entirely in YAML, reproducible, committable to Git. This is the whole series, distilled. The full manifests are in the repo nghiadaulau/kubernetes-minikube-series, directory 14-capstone.
🧹 Cleanup: minikube delete
Once you're done learning, return the machine to a clean state. Since everything sits in the capstone namespace, deleting the namespace cleans up the whole application:
kubectl delete namespace capstone # delete the entire app + PVC inside it
And when you're done with the whole series, one command deletes the cluster too:
minikube delete # wipe the cluster — the machine is as if nothing existed
This is also the beauty of minikube: learn freely, then delete without a trace, with no cloud bill.
Series wrap-up
Fifteen articles, from "what is Kubernetes" to a project running for real:
- Fundamentals (Articles 0–4): from containers to orchestration and desired state; the control plane / node architecture; installing minikube; the Pod (smallest unit); Deployment/ReplicaSet (self-healing, scaling, rolling updates).
- Connectivity & configuration (Articles 5–9): Service (a stable address + load balancing), Namespace/Label (organization & the selector glue), ConfigMap/Secret (separating configuration), PV/PVC (durable storage), Ingress (HTTP routing).
- Operations (Articles 10–14): probes (health checks), resources/HPA (resources & autoscaling), the other workloads (StatefulSet/DaemonSet/Job), debugging, and the capstone.
A few core ideas worth taking with you:
- Desired state (declarative) — you describe "the destination," and the control loops pull the system there and keep it there. This is the soul of Kubernetes (and also the declarative spirit from the Ansible series).
- Everything through the API server, everything is an object — Pod, Service, ConfigMap... are all objects declared in YAML;
applyand let the controller handle it. - Labels are the glue — Deployment, Service, selectors all find each other dynamically via labels; that's why scaling/self-healing/load balancing fit together.
- Separation of concerns — configuration (ConfigMap/Secret) separate from the image, storage (PVC) separate from the pod, routing (Service/Ingress) separate from the backend. Each piece is independently replaceable.
Where to go next
This series deliberately stops at the fundamentals. From here you can go deep:
- Kubernetes from scratch — build a cluster by hand (kubeadm), understand the internals of etcd/scheduler/CNI/CRI. (A separate series coming soon.)
- Helm — package and manage Kubernetes applications as "packages," for parameterized, reproducible deploys.
- Security & governance: RBAC, NetworkPolicy, Pod Security, managing secrets properly (recall the base64 warning in Article 7).
- GitOps (ArgoCD/Flux): make Git the source of truth and have the cluster sync to it automatically — a natural extension of declarative thinking.
- Operators & CRDs: extending Kubernetes itself, the way to run databases/complex systems in production.
- Connecting the other series: run Kubernetes on EC2/EKS (the AWS series), package apps with Docker (the Docker series), configure nodes with Ansible (the Ansible series), and build on the networking/SSH you already understand (the Networking and Linux series).
Thanks for following the whole series. You can now not only run kubectl but understand why each object exists and how they fit together — a solid foundation to step into production Kubernetes and go deeper into the cloud-native ecosystem.
You've reached the end