A full-stack blog application built with Node.js + Express + React, deployed through a complete DevOps pipeline β Docker, Kubernetes (Minikube), Horizontal Pod Autoscaling, CI/CD via GitHub Actions, and load-tested with k6.
This project reimplements a Python/Flask CRUD service from scratch in the Node.js ecosystem and takes it through the entire DevOps lifecycle.
Run locally via Minikube β see Getting Started below.
Docker Hub Image: manches300/node-blog-app:latest
| Layer | Technology |
|---|---|
| Backend | Node.js 20, Express 4.18 |
| Frontend | React 19, Vite |
| Database | PostgreSQL 15, Sequelize 6 ORM |
| Containerization | Docker (multi-stage build), Docker Compose |
| Orchestration | Kubernetes v1.28 (Minikube) |
| Autoscaling | Horizontal Pod Autoscaler (HPA) |
| Load Testing | k6 |
| CI/CD | GitHub Actions (self-hosted runner) |
- β Full CRUD operations β Create, Read, Update, Delete blog posts
- β
React SPA served directly from Express (single port
5000) - β Multi-stage Docker build β lean Alpine-based runtime image
- β Kubernetes deployment with liveness & readiness probes
- β HPA β auto-scales from 2 to 10 replicas based on CPU/memory
- β Secrets management β sensitive config stored in Kubernetes Secrets, never in Git
- β Self-healing β pods replaced automatically within ~11 seconds of failure
- β
CI/CD pipeline β automated build, push to Docker Hub, and deploy on every push to
main
Node_blog_application/
βββ app/ # Express backend source
βββ frontend/ # React + Vite frontend
βββ k8s/
β βββ deployment.yaml # Kubernetes Deployment
β βββ service.yaml # NodePort Service
β βββ hpa.yaml # Horizontal Pod Autoscaler
β βββ postgres.yaml # PostgreSQL Deployment + ClusterIP Service
βββ .github/
β βββ workflows/
β βββ ci.yml # GitHub Actions CI/CD pipeline
βββ loadtest.js # k6 load test script
βββ Dockerfile # Multi-stage Docker build
βββ docker-compose.yaml # Local development stack
βββ app.js # Express entry point
βββ .env.example # Environment variable template
# Clone the repo
git clone https://github.com/manches3003/Node_blog_application.git
cd Node_blog_application
# Copy and configure environment variables
cp .env.example .env
# Start the full stack (app + postgres)
docker compose up --buildApp will be available at http://localhost:5000
β οΈ If your database password contains@, it must be percent-encoded as%40in theDATABASE_URL. See Notes below.
# Start Minikube
minikube start --driver=docker
# Enable metrics server (required for HPA)
minikube addons enable metrics-server
# Pull the image into Minikube
minikube image pull manches300/node-blog-app:latest
# Create Kubernetes Secret with your credentials
kubectl create secret generic node-blog-secrets \
--from-literal=DATABASE_URL="postgresql://postgres:password@postgres:5432/node_blog_db" \
--from-literal=SECRET_KEY="your-secret-key"
# Deploy PostgreSQL
kubectl apply -f k8s/postgres.yaml
# Deploy the application
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/hpa.yaml --server-side=true
# Wait for rollout
kubectl rollout status deployment node-blog-app --timeout=120s
# Get the app URL
minikube service node-blog-app --urlOn Windows with Docker driver, keep the terminal open β the tunnel must stay active for the URL to remain accessible.
| Method | Endpoint | Description |
|---|---|---|
GET |
/api/posts |
Get all posts |
POST |
/api/posts |
Create a new post |
GET |
/api/posts/:id |
Get a single post |
PUT |
/api/posts/:id |
Update a post |
DELETE |
/api/posts/:id |
Delete a post |
GET |
/health |
Health check (used by probes) |
Internet
β
βΌ
NodePort Service (port 30080)
β
βΌ
Deployment: node-blog-app
βββ Pod 1 (node-blog-app) ββββββ
βββ Pod 2 (node-blog-app) βββ ClusterIP: postgres:5432
β² β
β βΌ
HPA (min 2 / max 10) PostgreSQL Pod
CPU: 50% threshold
Memory: 70% threshold
Resource limits per pod:
| Request | Limit | |
|---|---|---|
| CPU | 100m | 500m |
| Memory | 128Mi | 256Mi |
Tested with 500 concurrent virtual users over 100 seconds (5-stage ramp).
| Metric | Value | Threshold | Status |
|---|---|---|---|
| Total Requests | 77,834 | β | β |
| Throughput | 771 req/s | β | β |
| Avg Response Time | 3.83 ms | < 2000 ms | β |
| Median (p50) | 1.66 ms | β | β |
| p90 | 5.78 ms | β | β |
| p95 | 12.05 ms | < 2000 ms | β |
| Max Response Time | 134.39 ms | β | β |
| Error Rate | 0.00% | < 50% | β |
| Max Virtual Users | 500 | >= 500 | β |
# Install k6: https://k6.io/docs/get-started/installation/
k6 run loadtest.jsThe GitHub Actions workflow (.github/workflows/ci.yml) triggers on every push to main:
- Test β Install dependencies, run
npm test - Build β Install frontend deps, run Vite build
- Docker β Login to Docker Hub, build & push
manches300/node-blog-app:latest - Deploy β Setup Minikube, apply all manifests via
kubectl
A self-hosted GitHub Actions runner is required because standard hosted runners cannot run a Minikube cluster.
Copy .env.example to .env and fill in your values:
NODE_ENV=production
PORT=5000
DATABASE_URL=postgresql://postgres:YOUR_PASSWORD@postgres:5432/node_blog_db
SECRET_KEY=your-secret-key-change-in-productionIn Kubernetes, these are injected from the node-blog-secrets Secret β never committed to the repository.
If your PostgreSQL password contains special characters like @, you must percent-encode them in the DATABASE_URL:
@ β %40
On Windows with the Docker driver, the terminal running minikube service node-blog-app --url must stay open. Closing it will make the URL inaccessible.
The HPA requires the Minikube metrics-server addon. Enable it with:
minikube addons enable metrics-server| Challenge | Fix |
|---|---|
ENOENT error on startup |
Dockerfile was copying frontend build to /app/public but app.js referenced /app/frontend/dist β fixed by aligning the static path |
| Docker token error in CI | Docker Desktop access issue during GitHub Actions β regenerated the Docker Hub token |
| Minikube in GitHub Actions | Standard hosted runners can't run Minikube β switched to a self-hosted runner |
@ in DB password |
Percent-encoded as %40 in all DATABASE_URL values |
- Keshav Virajbhai Kansara β 100007269@stud.srh-university.de
This project is for academic purposes. Feel free to reference or fork it for learning.
Made with β and way too many kubectl get pods commands