| .claude | ||
| backend | ||
| docs/superpowers | ||
| frontend | ||
| .gitignore | ||
| deployment.yaml | ||
| namespace.yaml | ||
| nginx.conf | ||
| prepare-app.sh | ||
| README.md | ||
| service.yaml | ||
| start-app.sh | ||
| statefulset.yaml | ||
| stop-app.sh | ||
Task Manager on Kubernetes
A web-based task manager (Nginx + Flask + PostgreSQL) deployed to a Kubernetes cluster (minikube). Migrated from the Docker Compose version in the first assignment.
What the application does
A simple CRUD task manager. Users can:
- Add a new task by typing a title and pressing Enter.
- Mark a task as completed by toggling its checkbox.
- Delete a task by clicking the X button.
- See all tasks sorted by creation date.
The frontend is a static page served by Nginx. The backend is a Flask REST API. Tasks are stored in PostgreSQL and survive pod restarts thanks to a hostPath PersistentVolume.
Prerequisites
- minikube (any recent version, tested with 1.32+)
- kubectl (any version compatible with the minikube cluster)
- Docker CLI (used by
prepare-app.shto build images directly into minikube's docker daemon) - bash (Linux/macOS native; Git Bash on Windows)
Containers used
| Container | Image | Description |
|---|---|---|
web |
taskapp-web:v1 (built locally from nginx:alpine) |
Serves static HTML/CSS/JS and reverse-proxies /api/* to the api Service. nginx.conf is mounted from a ConfigMap. |
api |
taskapp-api:v1 (built locally from python:3.12-slim) |
Flask REST API on Gunicorn (2 workers). Exposes GET/POST /api/tasks, PUT /api/tasks/:id, DELETE /api/tasks/:id, GET /api/health. |
db |
postgres:15 (Docker Hub) |
Stores tasks table. Data lives at /var/lib/postgresql/data on a PersistentVolume backed by hostPath. |
Kubernetes objects
| Object | Name | Description |
|---|---|---|
| Namespace | taskapp |
Isolates every other resource. |
| Secret | db-credentials |
Holds POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD. Consumed by both Postgres and Flask via envFrom. |
| ConfigMap | nginx-config |
Holds nginx.conf. Mounted into the web container at /etc/nginx/nginx.conf via subPath. |
| PersistentVolume | db-pv |
1 Gi, hostPath: /mnt/data/taskapp-db on the minikube node. ReclaimPolicy Retain. StorageClass manual. |
| PersistentVolumeClaim | db-pvc |
1 Gi, ReadWriteOnce, binds to db-pv via label selector. Mounted by the StatefulSet. |
| StatefulSet | db |
1 replica, postgres:15. Uses the PVC. Has liveness and readiness probes (pg_isready). |
| Deployment | api |
2 replicas, custom Flask image. Probes hit GET /api/health (which checks DB reachability). |
| Deployment | web |
2 replicas, custom Nginx image. Probes hit GET /. |
| Service | web |
NodePort 30080 → 80. The only externally reachable Service. |
| Service | api |
ClusterIP, port 5000. Internal only. |
| Service | db |
Headless (clusterIP: None), port 5432. Internal only. Pairs with the StatefulSet for stable DNS. |
Virtual networks
The cluster's CNI plugin handles all pod-to-pod traffic. Service discovery is via Kubernetes DNS (kube-dns / coredns):
- Inside the namespace, every Service is reachable by short name:
web,api,db. - The
webService is the only one exposed outside the cluster (NodePort 30080). - The
dbService is headless — it returns the pod IPs directly instead of load-balancing. This is the canonical pattern for StatefulSets and givesdb-0a stable DNS identity (db-0.db.taskapp.svc.cluster.local).
Named volumes
| Volume | Mount | Backed by | Reclaim policy |
|---|---|---|---|
db-pv (PVC db-pvc) |
/var/lib/postgresql/data (in db-0) |
hostPath /mnt/data/taskapp-db on the minikube node |
Retain |
Retain means deleting the PVC does not automatically wipe the underlying directory — the data survives even a full stop-app.sh and is reattached on the next prepare-app.sh + start-app.sh. To wipe it manually:
minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
Container configuration performed
web— built fromnginx:alpineplus the static frontend files. Thenginx.confis supplied at runtime via thenginx-configConfigMap, mounted withsubPath: nginx.confso only that single file is overlaid (not the whole config directory). This means the proxy rule (proxy_pass http://api_backend;) andlarge_client_header_bufferssetting can be edited by changing the ConfigMap and reloading, without rebuilding the image.api— built frompython:3.12-slim, installslibpq-devandgccfor thepsycopg2build, thenpip installofrequirements.txt(Flask + Gunicorn + psycopg2). Runs Gunicorn with 2 workers on port 5000. Reads DB credentials from env vars sourced from thedb-credentialsSecret.db— officialpostgres:15image, unmodified. Env (DB name, user, password) sourced from the same Secret.PGDATAis set to/var/lib/postgresql/data/pgdata(a subdir) because Postgres refuses to initialize a non-empty data directory and hostPath dirs occasionally have stray entries.
Resource requests/limits are set on every container so the cluster scheduler can place pods predictably; values are conservative and tested against a 4 GiB minikube VM.
Usage
Prepare the application
Builds images directly into minikube's docker daemon, creates the hostPath directory on the node, and applies Namespace + Secret + ConfigMap + PV + PVC + StatefulSet.
./prepare-app.sh
Start the application
Applies the Deployments and Services. (Re-applies prepare-stage resources idempotently, so it's safe to run on its own too.)
./start-app.sh
The script then opens your default browser at http://<minikube-ip>:30080.
Pause / stop the application
stop-app.sh removes every Kubernetes object (Deployments, Services, StatefulSet, PVC, PV, Namespace, Secret, ConfigMap):
./stop-app.sh
The hostPath data on the minikube node is retained (PV ReclaimPolicy = Retain). To resume from where you left off, run ./prepare-app.sh && ./start-app.sh again.
Delete everything (including data)
./stop-app.sh
minikube ssh -- sudo rm -rf /mnt/data/taskapp-db
Viewing the application in a web browser
After ./start-app.sh finishes, run:
minikube service web -n taskapp
This opens the browser at the right URL automatically. Alternatively, navigate manually:
echo "http://$(minikube ip):30080"
You'll see the Task Manager interface where you can add, complete, and delete tasks.
API endpoints
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check (200 if DB reachable, 503 otherwise). Used by readiness probe. |
| GET | /api/tasks |
List all tasks. |
| POST | /api/tasks |
Create a task. Body: {"title": "..."}. |
| PUT | /api/tasks/:id |
Toggle completion. |
| DELETE | /api/tasks/:id |
Delete a task. |
Project structure
qubernetees/
├── README.md # this file
├── prepare-app.sh # build images, create PV, apply prepare-stage manifests
├── start-app.sh # apply Deployments + Services, open browser
├── stop-app.sh # full teardown
├── namespace.yaml # Namespace + Secret + ConfigMap
├── statefulset.yaml # PV + PVC + StatefulSet
├── deployment.yaml # api + web Deployments
├── service.yaml # api + web + db Services
├── nginx.conf # source of the ConfigMap (kept for readability)
├── backend/ # Flask REST API
│ ├── Dockerfile
│ ├── requirements.txt
│ └── app.py # + /api/health endpoint
└── frontend/ # static HTML/CSS/JS
├── Dockerfile
├── index.html
├── style.css
└── app.js
Sources
- First assignment (Docker version of the same app) — code copied from
cloud assiagment/ - Kubernetes documentation
- Kubernetes StatefulSet docs
- Postgres official Docker image
- minikube documentation
Use of Artificial Intelligence
This Kubernetes deployment was designed and implemented with the assistance of Claude (Anthropic), an AI assistant. Claude was used for:
- Designing the K8s object topology (Namespace, Deployments, StatefulSet, PV, PVC, Services).
- Authoring the manifest files, lifecycle scripts, and this documentation.
- Reviewing the design against the assignment requirements.
AI agent used: Claude Opus 4.7 (Anthropic) via Claude Code CLI.
The application logic itself (Flask backend, frontend) was carried over from the first assignment.