Capstone: Deploy a Complete Application and Wrap Up the Series

K
Kai··6 min read

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; apply and 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.