Skip to main content

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.

  1. 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.

  1. 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.

  1. 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

  1. Development Workflow
  2. Create a feature branch from main
  3. Implement with tests and type checks
  4. Run services locally (see Tutorial for commands)
  5. Submit PR → CI runs checks and builds
  6. 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/

  1. 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

  1. 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

  1. Deployment (Kubernetes)

Deployment (Kubernetes) Targets live under namespace blog-app. Order of operations

  1. namespace.yaml
  2. postgres.yaml
  3. backend.yaml
  4. frontend.yaml
  5. 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)

  1. 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)

  1. 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

  1. Watch Dogs for the Cluster

Watch Dogs for the Cluster Using Grafana for observing the cluster Behaviors

  1. 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.

  1. Setting up multiple staging environments

Setting up multiple staging environments Setting up multiple envs for production, development & testing.