| .. | ||
| app | ||
| k8s | ||
| sql | ||
| k8s-todo-app-documentation.docx | ||
| prepare-app.sh | ||
| README.md | ||
| start-app.sh | ||
| stop-app.sh | ||
K8s Todo App
A full-stack Todo List web application deployed on Kubernetes using Minikube.
The app stores tasks in a PostgreSQL database and serves a modern web UI via a Python Flask backend.
What the Application Does
Users can:
- Add new tasks via a text input form
- Mark tasks as done by clicking the circle toggle button
- Delete tasks with the ✕ button
- View a live progress counter (e.g.
2/5 done)
All data is persisted to a PostgreSQL database backed by a Kubernetes PersistentVolume.
Architecture Overview
Browser
│
▼ NodePort :30080
┌──────────────────────────────────────┐
│ Namespace: todo-app │
│ │
│ ┌────────────────────┐ │
│ │ Deployment │ │
│ │ todo-app (x2 pods)│ ──────────► │─── postgres-service:5432
│ │ Flask + Gunicorn │ │ │
│ └────────────────────┘ │ ┌──────────────────┐
│ │ │ StatefulSet │
│ │ │ postgres (x1) │
│ │ │ PostgreSQL 16 │
│ │ │ │ │
│ │ │ PVC ──► PV │
│ │ │ (hostPath) │
│ │ └──────────────────┘
└──────────────────────────────────────┘
Container Images Used
| Image | Version | Purpose |
|---|---|---|
todo-app |
latest (built locally) |
Flask web server + Gunicorn WSGI |
postgres |
16-alpine |
Relational database |
busybox |
1.36 |
Init container — waits for PostgreSQL before app starts |
todo-app (custom image)
- Built from
app/Dockerfileusing Python 3.12-slim - Runs a Flask application with Gunicorn (2 workers)
- Connects to PostgreSQL via environment variables
- Exposes port
5000 - Runs as a non-root user (
appuser) for security
postgres:16-alpine
- Official PostgreSQL image (Alpine variant for smaller size)
- Initialises the
tododbdatabase using a mounted SQL ConfigMap - Data is persisted via a PersistentVolumeClaim
Kubernetes Objects
| Object | Name | Description |
|---|---|---|
Namespace |
todo-app |
Isolates all app resources from the rest of the cluster |
Deployment |
todo-app |
Runs 2 replicas of the Flask web app with rolling updates |
StatefulSet |
postgres |
Manages the PostgreSQL pod with stable network identity |
PersistentVolume |
postgres-pv |
2Gi host-path volume on the minikube node |
PersistentVolumeClaim |
postgres-pvc |
Binds to postgres-pv, used by the StatefulSet |
Service |
todo-app-service |
NodePort (30080) — exposes the Flask app to the browser |
Service |
postgres-service |
ClusterIP — internal access to PostgreSQL on port 5432 |
Service |
postgres-headless |
Headless — required by StatefulSet for stable DNS |
Secret |
postgres-secret |
Stores DB credentials (username, password, DB name) |
ConfigMap |
postgres-init-sql |
Holds the SQL init script run by PostgreSQL on first boot |
Networks and Volumes
Virtual Networks (Services)
todo-app-service—NodePorttype. Maps external port30080→ pod port5000. Accessible athttp://<minikube-ip>:30080.postgres-service—ClusterIPtype. Only reachable inside the cluster atpostgres-service.todo-app.svc.cluster.local:5432.postgres-headless—ClusterIP: None. A DNS-only headless service required by the StatefulSet so each pod gets a stable DNS entry (postgres-0.postgres-headless.todo-app.svc.cluster.local).
Persistent Volumes
postgres-pv—hostPathpointing to/mnt/data/postgresinside the minikube VM. Survives pod restarts and redeployments.postgres-pvc— Claims 2Gi frompostgres-pv. Mounted at/var/lib/postgresql/datainside the PostgreSQL container.
Container Configuration
Flask App (todo-app)
DB_HOST— hostname of the PostgreSQL service (postgres-service.todo-app.svc.cluster.local)DB_NAME— database name (tododb)DB_USER/DB_PASS— injected from thepostgres-secretKubernetes Secret- Liveness probe:
GET /healthevery 10s — restarts the pod if it hangs - Readiness probe:
GET /healthevery 5s — holds traffic until the app is truly ready - Init container:
busyboxrunsnc -z postgres-service 5432in a loop until PostgreSQL accepts connections before the main container starts
PostgreSQL (postgres)
POSTGRES_USER,POSTGRES_PASSWORD,POSTGRES_DB— injected frompostgres-secretPGDATA=/var/lib/postgresql/data/pgdata— prevents permission errors on mounted volumes- SQL init script from ConfigMap is placed at
/docker-entrypoint-initdb.d/init.sqland runs automatically on first start - Liveness probe:
pg_isready -U postgres - Readiness probe:
pg_isready -U postgres
Step-by-Step Instructions
Prerequisites — Install Everything on Ubuntu
Run the following commands in sequence in your terminal:
# 1. Update system
sudo apt-get update && sudo apt-get upgrade -y
# 2. Install Docker
sudo apt-get install -y ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
newgrp docker
# 3. Install kubectl
curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
sudo install -o root -g root -m 0755 kubectl /usr/local/bin/kubectl
kubectl version --client
# 4. Install minikube
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube
minikube version
Prepare the Application (run once)
# Navigate to the project directory
cd k8s-todo-app
# Make all scripts executable
chmod +x prepare-app.sh start-app.sh stop-app.sh
# Run the preparation script (starts minikube, builds Docker image, creates host dir)
bash prepare-app.sh
Start the Application
bash start-app.sh
This script creates all Kubernetes objects in order:
- Namespace
- ConfigMap
- PersistentVolume, PVC, Secret, StatefulSet (PostgreSQL)
- Services
- Deployment (Flask app)
View the Application in a Browser
# Option A — get the URL automatically
minikube service todo-app-service -n todo-app
# Option B — open directly
minikube service todo-app-service -n todo-app --url
# Copy the URL and open it in your browser
# e.g. http://192.168.49.2:30080
Useful Commands While Running
# See all running pods
kubectl get pods -n todo-app
# See all objects
kubectl get all -n todo-app
# Watch pods in real time
kubectl get pods -n todo-app -w
# View Flask app logs
kubectl logs -l app=todo-app -n todo-app --tail=50
# View PostgreSQL logs
kubectl logs postgres-0 -n todo-app --tail=50
# Connect to PostgreSQL shell
kubectl exec -it postgres-0 -n todo-app -- psql -U postgres -d tododb
# Scale up/down the web app
kubectl scale deployment todo-app -n todo-app --replicas=3
Pause / Delete the Application
# Delete all Kubernetes objects (data on PV is preserved)
bash stop-app.sh
# To also remove all stored database data
minikube ssh -- "sudo rm -rf /mnt/data/postgres"
# To stop minikube entirely
minikube stop
# To delete the minikube cluster completely
minikube delete
Re-deploy After Stop
bash start-app.sh # no need to run prepare-app.sh again unless minikube was deleted
Troubleshooting
| Problem | Fix |
|---|---|
ImagePullBackOff on todo-app |
Re-run bash prepare-app.sh to rebuild image inside minikube |
Postgres pod in Pending |
Run kubectl describe pv postgres-pv — check hostPath exists |
| App shows "Cannot connect to database" | Wait 30s, postgres is still initialising; check kubectl logs postgres-0 -n todo-app |
| Can't reach browser URL | Run minikube service todo-app-service -n todo-app to get the correct URL |
| Permission error on PV | Run minikube ssh -- 'sudo chmod 777 /mnt/data/postgres' |