A Flask-based blog application deployed on Kubernetes (minikube) with PostgreSQL database, CI/CD pipeline via GitHub Actions, and Docker containerization.
This is a full-stack blog application that allows users to create, read, update, and delete blog posts. The application follows a modular architecture with separate routes for API and web interfaces, uses SQLAlchemy ORM for database operations, and is containerized for easy deployment on Kubernetes.
- CRUD Operations: Create, read, update, and delete blog posts
- RESTful API: JSON-based API endpoints for blog operations
- PostgreSQL Database: Persistent storage with SQLAlchemy ORM
- Docker Containerized: Multi-stage Docker build with gunicorn WSGI server
- Kubernetes Deployment: Production-ready manifests for minikube
- CI/CD Pipeline: GitHub Actions for automated build and deployment
- Health Checks: Liveness and readiness probes for container health monitoring
- Secrets Management: Kubernetes secrets for sensitive configuration
flask-blog/
├── app/ # Flask application package
│ ├── __init__.py # App factory and extensions
│ ├── config.py # Configuration classes
│ ├── models/
│ │ ├── __init__.py
│ │ └── blog_post.py # BlogPost model
│ └── routes/
│ ├── api/ # API routes
│ │ ├── __init__.py
│ │ ├── create.py
│ │ └── read.py
│ └── web/ # Web routes
│ ├── __init__.py
│ ├── create.py
│ ├── read.py
│ ├── update.py
│ └── delete.py
├── k8s/ # Kubernetes manifests
│ ├── postgres.yaml # PostgreSQL deployment + service
│ ├── flask-blog-deployment.yaml
│ └── flask-blog-service.yaml
├── tests/ # Unit tests
│ ├── __init__.py
│ ├── conftest.py
│ └── test_models.py
├── .github/workflows/
│ └── ci.yml # CI/CD pipeline
├── Dockerfile # Docker build configuration
├── docker-entrypoint.sh # Container startup script
├── wsgi.py # WSGI entry point
├── requirements.txt # Python dependencies
└── README.md # This file
| Component | Technology |
|---|---|
| Framework | Flask 3.0.0 |
| Database | PostgreSQL 15 |
| ORM | SQLAlchemy 2.0 |
| Migrations | Flask-Migrate 4.0.5 |
| WSGI Server | gunicorn 21.2.0 |
| Container | Docker (Python 3.11-slim) |
| Orchestration | Kubernetes (minikube) |
| CI/CD | GitHub Actions |
- Python 3.11
- Docker Desktop (with Kubernetes enabled or minikube)
- minikube
- kubectl
- Git
git clone <repository-url>
cd flask-blogpip install -r requirements.txtCreate a .env file in the project root:
FLASK_ENV=development
SECRET_KEY=dev-secret-key
DATABASE_URL=postgresql://[Database name]:[Database password]@localhost:5432/flask_blog_devpython app.pyThe app will be available at http://localhost:5000
pytest tests/ -vminikube startCreate a secret to store sensitive data:
PowerShell:
kubectl create secret generic flask-blog-secrets `
--from-literal=DATABASE_URL="postgresql://[Database name]:[Database password]@postgres:5432/flask_blog_dev" `
--from-literal=SECRET_KEY="dev-secret-key-change-in-prod"Linux/Mac:
kubectl create secret generic flask-blog-secrets \
--from-literal=DATABASE_URL="postgresql://[Database name]:[Database password]@postgres:5432/flask_blog_dev" \
--from-literal=SECRET_KEY="dev-secret-key-change-in-prod"Note: The %40 is the URL-encoded form of @ in the password (password).
kubectl apply -f k8s/postgres.yaml
kubectl apply -f k8s/flask-blog-deployment.yaml
kubectl apply -f k8s/flask-blog-service.yamlkubectl get podsYou should see:
NAME READY STATUS RESTARTS AGE
flask-blog-xxxxxxxxx-xxxxx 1/1 Running 0 1m
postgres-xxxxxxxxx-xxxxx 1/1 Running 0 1m
minikube service flask-blog --urlThis will output a URL like http://192.168.49.2:30XXX - open it in your browser.
| Deployment | Replicas | Image | Port |
|---|---|---|---|
| flask-blog | 2 | manches300/flask-blog:latest | 5000 |
| postgres | 1 | postgres:15 | 5432 |
| Service | Type | Port | Target Port |
|---|---|---|---|
| flask-blog | NodePort | 80 | 5000 |
| postgres | ClusterIP | 5432 | 5432 |
The Flask app uses these environment variables:
| Variable | Source | Description |
|---|---|---|
FLASK_ENV |
Direct env | Application environment (production) |
DATABASE_URL |
Secret | PostgreSQL connection string |
SECRET_KEY |
Secret | Flask secret key for sessions |
- Liveness Probe: HTTP GET
/on port 5000 (initial delay: 30s, period: 10s) - Readiness Probe: HTTP GET
/on port 5000 (initial delay: 5s, period: 5s)
The GitHub Actions workflow (.github/workflows/ci.yml) performs:
-
Test Job (Ubuntu runner):
- Installs Python dependencies
- Runs pytest test suite
-
Build and Deploy Job (Self-hosted runner):
- Logs into Docker Hub
- Builds and pushes Docker image
- Applies Kubernetes manifests
- Restarts deployment
- Go to GitHub Repository → Settings → Actions → Runners
- Click "New self-hosted runner"
- Select Windows and x64
- Download and configure the runner on your machine
- Run the runner (
.\run.cmd) - Ensure
dockerandkubectlare in PATH
Add these to GitHub Repository → Settings → Secrets and variables → Actions:
| Name | Description |
|---|---|
DOCKERHUB_USERNAME |
Your Docker Hub username (e.g., manches300) |
DOCKERHUB_PASSWORD |
Your Docker Hub password or access token |
# Flask app logs
kubectl logs -l app=flask-blog
# PostgreSQL logs
kubectl logs -l app=postgres
# Follow logs in real-time
kubectl logs -l app=flask-blog -f
# Previous instance logs (after crash)
kubectl logs -l app=flask-blog --previous# Get postgres pod name
kubectl get pods -l app=postgres
# Connect to database
kubectl exec -it <postgres-pod-name> -- psql -U postgres -d flask_blog_dev
# List tables
kubectl exec -it <postgres-pod-name> -- psql -U postgres -d flask_blog_dev -c "\dt"
# Query data
kubectl exec -it <postgres-pod-name> -- psql -U postgres -d flask_blog_dev -c "SELECT * FROM blog_posts;"# Scale to 5 replicas
kubectl scale deployment flask-blog --replicas=5
# Check pods
kubectl get pods -l app=flask-blogkubectl rollout restart deployment flask-blogminikube service flask-blog --url# Delete specific pod
kubectl delete pod <pod-name>
# Delete all flask-blog pods (deployment will recreate)
kubectl delete pods -l app=flask-blog
# Force delete immediately
kubectl delete pod <pod-name> --force --grace-period=0| Endpoint | Method | Description |
|---|---|---|
/posts |
GET | Get all blog posts |
/posts/<id> |
GET | Get single blog post |
/posts/create |
POST/GET | Create new blog post |
/posts/<id>/update |
POST/GET | Update blog post |
/posts/<id>/delete |
POST | Delete blog post |
# Check logs
kubectl logs -l app=flask-blog
# Check previous instance logs
kubectl logs -l app=flask-blog --previous
# Describe pod for events
kubectl describe pod -l app=flask-blog-
Verify secret exists and has data:
kubectl get secret flask-blog-secrets
Should show
DATA 2 -
If secret is empty (DATA 0), recreate it:
kubectl delete secret flask-blog-secrets kubectl create secret generic flask-blog-secrets \ --from-literal=DATABASE_URL="postgresql://[Database name]:[Database password]@postgres:5432/flask_blog_dev" \ --from-literal=SECRET_KEY="dev-secret-key-change-in-prod"
-
Restart pods:
kubectl delete pods -l app=flask-blog
- Verify image exists on Docker Hub: https://hub.docker.com/r/manches300/flask-blog
- Check image name in deployment YAML
- Ensure Docker Hub credentials are correct
The Dockerfile handles this automatically:
RUN printf '%s\n' '#!/bin/bash' ... > docker-entrypoint.shIf you still get this error, rebuild with --no-cache:
docker build --no-cache -t manches300/flask-blog:latest .
docker push manches300/flask-blog:latest
kubectl rollout restart deployment flask-blogCommon issues:
- Indentation must be consistent (2 spaces)
- No trailing spaces after
: periodSeconds: 10needs a space after colon
- Secrets: Sensitive data stored in Kubernetes secrets, not in YAML files
- Password Encoding: Special characters in passwords are URL-encoded (
@→%40) - No Hardcoded Credentials: Database credentials loaded from environment/secrets
- Production Config: No fallback to localhost in production
- Add HTTPS/TLS termination with Ingress
- Implement user authentication (Flask-Login)
- Add horizontal pod autoscaling (HPA)
- Set up monitoring with Prometheus/Grafana
- Add persistent volume for PostgreSQL data
- Implement database backups
- Add rate limiting
- Container security scanning with Trivy
MIT
Developed and deployed by Kevin
For issues or questions, please open an issue on the repository.