# 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/Dockerfile` using 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 `tododb` database 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`** — `NodePort` type. Maps external port `30080` → pod port `5000`. Accessible at `http://:30080`. - **`postgres-service`** — `ClusterIP` type. Only reachable inside the cluster at `postgres-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`** — `hostPath` pointing to `/mnt/data/postgres` inside the minikube VM. Survives pod restarts and redeployments. - **`postgres-pvc`** — Claims 2Gi from `postgres-pv`. Mounted at `/var/lib/postgresql/data` inside 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 the `postgres-secret` Kubernetes Secret - **Liveness probe**: `GET /health` every 10s — restarts the pod if it hangs - **Readiness probe**: `GET /health` every 5s — holds traffic until the app is truly ready - **Init container**: `busybox` runs `nc -z postgres-service 5432` in a loop until PostgreSQL accepts connections before the main container starts ### PostgreSQL (`postgres`) - `POSTGRES_USER`, `POSTGRES_PASSWORD`, `POSTGRES_DB` — injected from `postgres-secret` - `PGDATA=/var/lib/postgresql/data/pgdata` — prevents permission errors on mounted volumes - SQL init script from ConfigMap is placed at `/docker-entrypoint-initdb.d/init.sql` and 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: ```bash # 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) ```bash # 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 bash start-app.sh ``` This script creates all Kubernetes objects in order: 1. Namespace 2. ConfigMap 3. PersistentVolume, PVC, Secret, StatefulSet (PostgreSQL) 4. Services 5. Deployment (Flask app) ### View the Application in a Browser ```bash # 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 ```bash # 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 ```bash # 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 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'` |