Full Stack Webapp using K8S with CI/CD
Architecture & Substrate Services
This guide shows how to deploy a full stack blog website with Postgress and User login.
- System Overview
System Overview
- Frontend: Next.js (TypeScript), SSR, frontend-blog/
- Backend: Node.js/Express, REST API, backend-blog/
- Database: PostgreSQL, stateful data, k8s/postgres.yaml
- Platform: Docker images, Kubernetes workloads, NGINX Ingress, k8s/
- CI/CD: GitHub Actions for build, test, image publish, and deploy
Frontend: Next.js (TypeScript), SSR, frontend-blog/
Backend: Node.js/Express, REST API, backend-blog/
Database: PostgreSQL, stateful data, k8s/postgres.yaml
Platform: Docker images, Kubernetes workloads, NGINX Ingress, k8s/
CI/CD: GitHub Actions for build, test, image publish, and deploy
See detailed diagrams in docs/ARCHITECTURE.md and docs/SYSTEM_DESIGN.md.
- Environments
Environments
- Local: Developers run services via Node and Docker (or docker-compose)
- CI: GitHub Actions executes unit/type checks, builds images
- Staging (optional): Same as prod with reduced scale
- Production: Kubernetes namespace blog-app; ingress exposes / and /api
Local: Developers run services via Node and Docker (or docker-compose)
CI: GitHub Actions executes unit/type checks, builds images
Staging (optional): Same as prod with reduced scale
Production: Kubernetes namespace blog-app; ingress exposes / and /api
Environment configuration is provided via env vars and Kubernetes Secrets.
- Repository Structure
Repository Structure
- frontend-blog/ Next.js app, TypeScript, pages/components
- backend-blog/ Express app, server.js, routes, auth
- k8s/ Kubernetes manifests: namespace.yaml, postgres.yaml, backend.yaml, frontend.yaml, ingress.yaml
- Makefile-blog Build/push/deploy/status/logs convenience targets
- docs/ Architecture, System Design, Tutorial, and this manual
frontend-blog/ Next.js app, TypeScript, pages/components
backend-blog/ Express app, server.js, routes, auth
k8s/ Kubernetes manifests: namespace.yaml, postgres.yaml, backend.yaml, frontend.yaml, ingress.yaml
Makefile-blog Build/push/deploy/status/logs convenience targets
docs/ Architecture, System Design, Tutorial, and this manual
- Development Workflow
- Create a feature branch from main
- Implement with tests and type checks
- Run services locally (see Tutorial for commands)
- Submit PR → CI runs checks and builds
- Merge to main when green
Development Workflow
Create a feature branch from main
Implement with tests and type checks
Run services locally (see Tutorial for commands)
Submit PR → CI runs checks and builds
Merge to main when green
Branch naming: feature/, fix/, chore/
- Build, Test, and Release
Build, Test, and Release
- Frontend
- Install: npm ci
- Type check: tsc --noEmit
- Build: npm run build
- Backend
- Install: npm ci
- Test: npm test (if tests are provided)
Frontend
Install: npm ci
Type check: tsc --noEmit
Build: npm run build
Backend
Install: npm ci
Test: npm test (if tests are provided)
Container images
- Built by CI and locally via make -f Makefile-blog build-all
- Tagged with latest, branch/sha variants per CI config
- Pushed via make -f Makefile-blog push-all or CI
Built by CI and locally via make -f Makefile-blog build-all
Tagged with latest, branch/sha variants per CI config
Pushed via make -f Makefile-blog push-all or CI
Releases
- GitHub release tags (optional)
- Image tags form the deployment artifact identity
GitHub release tags (optional) Image tags form the deployment artifact identity
- Configuration and Secrets
Configuration and Secrets Backend
- DATABASE_URL (required)
- JWT_SECRET (required)
- PORT (default 8080)
- SEED (optional)
DATABASE_URL (required)
JWT_SECRET (required)
PORT (default 8080)
SEED (optional)
Frontend
- NEXT_PUBLIC_API_BASE (default /api behind ingress)
NEXT_PUBLIC_API_BASE (default /api behind ingress)
Kubernetes
- Use Secret objects for sensitive values (backend-secret, postgres-secret)
- Non-sensitive config via Deployment env or ConfigMaps
Use Secret objects for sensitive values (backend-secret, postgres-secret)
Non-sensitive config via Deployment env or ConfigMaps
- Deployment (Kubernetes)
Deployment (Kubernetes)
Targets live under namespace blog-app.
Order of operations
- namespace.yaml
- postgres.yaml
- backend.yaml
- frontend.yaml
- ingress.yaml
namespace.yaml
postgres.yaml
backend.yaml
frontend.yaml
ingress.yaml
Make targets
make -f Makefile-blog deploy-all ``make -f Makefile-blog status
Rollout updates
- CI updates image tags, then applies manifests
- Kubernetes performs rolling deployments with readiness/liveness probes
CI updates image tags, then applies manifests Kubernetes performs rolling deployments with readiness/liveness probes Ingress
- / → frontend service port 80 (target 3000)
- /api → backend service port 80 (target 8080)
/ → frontend service port 80 (target 3000)
/api → backend service port 80 (target 8080)
- Security
Security
- Secrets via Kubernetes Secret
- Rotate JWT_SECRET and DB credentials regularly
- Restrict ServiceAccount permissions (RBAC) where applicable
- TLS termination at ingress (recommended for production)
- Image scanning in CI (see .github/workflows/security-scan.yml)
Secrets via Kubernetes Secret
Rotate JWT_SECRET and DB credentials regularly
Restrict ServiceAccount permissions (RBAC) where applicable
TLS termination at ingress (recommended for production)
Image scanning in CI (see .github/workflows/security-scan.yml)
- Observability
Observability
- Logs: stdout/stderr; fetch with kubectl logs or Make targets
- Probes: readiness/liveness on frontend/backend
- Metrics: use kubectl top for resource usage; integrate metrics stack as needed
- Events: kubectl describe to inspect resource changes
Logs: stdout/stderr; fetch with kubectl logs or Make targets
Probes: readiness/liveness on frontend/backend
Metrics: use kubectl top for resource usage; integrate metrics stack as needed
Events: kubectl describe to inspect resource changes
10. Operations and Runbooks
Health
kubectl --kubeconfig k8sConfig.yml -n blog-app get pods,svc,ingress
kubectl --kubeconfig k8sConfig.yml -n blog-app rollout status deploy/backend
kubectl --kubeconfig k8sConfig.yml -n blog-app rollout status deploy/frontend
Logs
make -f Makefile-blog logs-backend
make -f Makefile-blog logs-frontend
make -f Makefile-blog logs-postgres
Scaling
kubectl --kubeconfig k8sConfig.yml -n blog-app scale deployment backend --replicas=4
kubectl --kubeconfig k8sConfig.yml -n blog-app scale deployment frontend --replicas=4
Common Incidents
- Backend cannot reach DB → verify DATABASE_URL, DB pod status, and service name postgres
- Ingress not routing → check ingress controller, rules in k8s/ingress.yaml, DNS/LB
- High latency → scale replicas, review resource limits, enable caching layer (future)
Backend cannot reach DB → verify DATABASE_URL, DB pod status, and service name postgres
Ingress not routing → check ingress controller, rules in k8s/ingress.yaml, DNS/LB
High latency → scale replicas, review resource limits, enable caching layer (future)
11. API and Service Contracts (Concrete)
Base URL
- Local: http://localhost:8080
- In-cluster (frontend): ${INGRESS}`/api
- Frontend uses NEXT_PUBLIC_API_BASE (defaults to /api)
Local: http://localhost:8080
In-cluster (frontend): ${INGRESS}/api Frontend uses NEXT_PUBLIC_API_BASE(defaults to/api`)
Authentication
- Scheme: Bearer JWT in Authorization header
- Token TTL: 7d
Scheme: Bearer JWT in Authorization header
Token TTL: 7d
Endpoints
- GET /healthz
- 200
\{ "status": "ok" \} - POST /api/signup
- Request:
\{ "email": string, "name": string, "password": string \} - Responses:
- 200
\{ "token": string, "user": \{ id, email, name \}}` - 400
\{ "error": "missing fields" \} - 409
\{ "error": "email exists" \} - POST /api/login
- Request:
\{ "email": string, "password": string \} - Responses:
- 200
\{ "token": string, "user": \{ id, email, name \}}` - 400
\{ "error": "missing fields" \} - 401
\{ "error": "invalid creds" \} - GET /api/posts
- 200 [{ id, title, content, created_at, author }`] (ordered by created_at desc)
- POST /api/posts (auth required)
- Request: { "title": string, "content": string }`
- Headers: Authorization: Bearer
- Responses:
- 200 { id, title, content, created_at }`
- 400
\{ "error": "missing fields" \} - 401
\{ "error": "missing token" | "invalid token" \}
GET /healthz
200 \{ "status": "ok" \}
POST /api/signup
Request: \{ "email": string, "name": string, "password": string \}
Responses:
200 \{ "token": string, "user": \{ id, email, name \} }400{ "error": "missing fields" }409{ "error": "email exists" }POST /api/login Request:{ "email": string, "password": string }Responses: 200{ "token": string, "user": { id, email, name } \}
400 \{ "error": "missing fields" \}
401 \{ "error": "invalid creds" \}
GET /api/posts
200 [\{ id, title, content, created_at, author \}](ordered bycreated_at desc) POST /api/posts (auth required) Request: \{ "title": string, "content": string \}
Headers: Authorization: Bearer
Responses:
200 { id, title, content, created_at }400{ "error": "missing fields" }401{ "error": "missing token" | "invalid token" }`
Error Model
- JSON payload
\{ "error": string \}with appropriate HTTP status code.
JSON payload \{ "error": string \} with appropriate HTTP status code.
12. Data Model (PostgreSQL)
Tables (created on startup if not exists)
- users
- id serial primary key
- email text unique not null
- name text not null
- password_hash text not null
- created_at timestamp default now()
- posts
- id serial primary key
- user_id integer references users(id) on delete cascade
- title text not null
- content text not null
- created_at timestamp default now()
users
id serial primary key
email text unique not null
name text not null
password_hash text not null
created_at timestamp default now()
posts
id serial primary key
user_id integer references users(id) on delete cascade
title text not null
content text not null
created_at timestamp default now()
NotesAuthor name surfaced by joining posts.user_id → users.id (alias author)
- Deleting a user cascades to posts
Deleting a user cascades to posts 13. Frontend Application (Key Behaviors)
- Pages: pages/login.tsx, pages/index.tsx, pages/post/[id].tsx
- Auth flow: token/user persisted in localStorage after login/signup; redirect to /login if missing
- Post creation: POST /api/posts with Bearer token; refresh list on success
- Post detail: loads via GET /api/posts then client-side filters by id
- Configuration: NEXT_PUBLIC_API_BASE (default /api), override in local dev to http://localhost:8080/api
Pages: pages/login.tsx, pages/index.tsx, pages/post/[id].tsx
Auth flow: token/user persisted in localStorage after login/signup; redirect to /login if missing
Post creation: POST /api/posts with Bearer token; refresh list on success
Post detail: loads via GET /api/posts then client-side filters by id
Configuration: NEXT_PUBLIC_API_BASE (default /api), override in local dev to http://localhost:8080/api
16. Ports, Networking, and Compose
- Backend: 8080 (mapped to host via compose)
- Frontend: 3000 (mapped to host via compose)
- PostgreSQL: 5432 (mapped to host via compose)
Backend: 8080 (mapped to host via compose)
Frontend: 3000 (mapped to host via compose)
PostgreSQL: 5432 (mapped to host via compose)
docker-compose.yml
- Seeds backend when SEED=true
- Mounts ./blog1.txt and ./blog2.txt into backend container for seeding
- Sets backend DATABASE_URL=postgres://postgres:postgres@db:5432/blogdb
- Frontend NEXT_PUBLIC_API_BASE=http://localhost:8080/api
Seeds backend when SEED=true
Mounts ./blog1.txt and ./blog2.txt into backend container for seeding
Sets backend DATABASE_URL=postgres://postgres:postgres@db:5432/blogdb
Frontend NEXT_PUBLIC_API_BASE=http://localhost:8080/api
Seed Behavior
- On startup (when SEED=true), backend:
- Creates demo user demo@local (pwd demo1234) if not present
- Reads first lines of blog1.txt and blog2.txt as titles; remaining content as body
- Inserts posts under demo user
On startup (when SEED=true), backend:
Creates demo user demo@local (pwd demo1234) if not present
Reads first lines of blog1.txt and blog2.txt as titles; remaining content as body
Inserts posts under demo user
17. Health and Probes
- Implemented endpoint: GET /healthz → { status: "ok" }`
- Recommended Kubernetes probes:
- Readiness/Liveness: align with implemented endpoints (e.g., /healthz)
- If manifests reference /api/health or /api/ready, update them to /healthz or extend backend with those endpoints for parity
Implemented endpoint: GET /healthz → { status: "ok" }Recommended Kubernetes probes: Readiness/Liveness: align with implemented endpoints (e.g.,/healthz) If manifests reference /api/healthor/api/ready, update them to /healthz` or extend backend with those endpoints for parity
18. Security Considerations (Implementation-Specific)
- Passwords hashed with bcryptjs (cost 10)
- JWT signed with HS256 (default); keep JWT_SECRET strong and rotated
- CORS enabled globally via cors() middleware; restrict origins in production as needed
- Validate inputs server-side for all endpoints (basic presence checks included; extend as necessary)
Passwords hashed with bcryptjs (cost 10)
JWT signed with HS256 (default); keep JWT_SECRET strong and rotated
CORS enabled globally via cors() middleware; restrict origins in production as needed
Validate inputs server-side for all endpoints (basic presence checks included; extend as necessary)
19. Versioning and Compatibility
- Backward-compatible API changes should not break existing frontend
- For breaking changes, introduce versioned routes (e.g., /v2/api/...) and dual-run during migration
Backward-compatible API changes should not break existing frontend
For breaking changes, introduce versioned routes (e.g., /v2/api/...) and dual-run during migration
20. Appendix
Make targets
make -f Makefile-blog build-all
make -f Makefile-blog push-all
make -f Makefile-blog deploy-all
make -f Makefile-blog status
Future Implementation
- Watch Dogs for the Cluster
Watch Dogs for the Cluster Using Grafana for observing the cluster Behaviors
- Multiple pods for handling higher traffic
Multiple pods for handling higher traffic Deploying multiple instances of backend and front end for handling higher concurrency on the website.
- Setting up multiple staging environments
Setting up multiple staging environments Setting up multiple envs for production, development & testing.