This repo is a small multi-service application used to learn Docker and Kubernetes. The user-facing piece is a React app where you submit a numeric index; an Express API accepts it, persists submitted indices in PostgreSQL, stores a “pending” placeholder in Redis, and publishes the index on a Redis channel. A separate worker process subscribes to that channel, computes a Fibonacci number (with a deliberately expensive recursive implementation so work is noticeable), and writes the result back into Redis. The UI reads both the historical list of indices from Postgres and the latest calculated values from Redis.
It was built as a course-style exercise: the same problem is solved with several moving parts (web, API, background worker, cache, database) so you can practice container images, Deployments, Services, Ingress, Secrets, PersistentVolumeClaims, and CI/CD to a real cluster.
flowchart LR
subgraph ingress [Ingress]
IN[nginx ingress]
end
subgraph cluster [Cluster]
CL[client pods\nnginx + static React]
SV[server pods\nExpress :5000]
WK[worker pod\nRedis subscriber]
RD[(redis)]
PG[(postgres + PVC)]
end
U[Browser] --> IN
IN -->|"/" SPA| CL
IN -->|"/api/*"| SV
CL -->|same-origin /api| IN
SV --> PG
SV --> RD
SV -->|publish insert| RD
WK -->|subscribe insert| RD
WK -->|hset results| RD
client/— Create React App, production image is multi-stage: build with Node, serve static files with nginx on port 3000 (client/Dockerfile,client/nginx/default.conf).server/— Express API on port 5000; usespgfor Postgres andredisfor cache + pub/sub (server/index.js).worker/— Long-running Node process: subscribes to Redisinsert, runsfib(), updates the Redis hashvalues(worker/index.js).k8s/— Manifests for Deployments, ClusterIP Services, Ingress, PVC, and Postgres password via Secretpgpassword.- Ingress (
k8s/ingress-service.yml) sends/api/...to the server service and everything else to the client service, with path rewriting compatible with the ingress-nginx controller.
Replica counts in manifests: 3 each for client and server, 1 for worker, Redis, and Postgres (Postgres uses a 2Gi PVC).
There is no docker-compose file in this repo; you run dependencies and apps yourself.
- PostgreSQL and Redis running and reachable (local install or your own containers).
- Environment variables for the API match
server/keys.js:PGUSER,PGHOST,PGDATABASE,PGPASSWORD,PGPORT,REDIS_HOST,REDIS_PORT. - From each directory, install and start:
- Server:
cd server && npm install && npm run dev(ornpm startfor production mode without nodemon). - Worker:
cd worker && npm install && npm start. - Client:
cd client && npm install && npm start(dev server, typically http://localhost:3000).
- Server:
The React app calls /api/... relative to the page origin. In the cluster, Ingress makes that the same host as the UI. On your laptop, the CRA dev server does not proxy to the API unless you add a proxy field in client/package.json (e.g. to http://localhost:5000) or change the client to use an explicit API base URL—otherwise API requests from the browser will not reach Express.
- Tests: the CI workflow builds
client/Dockerfile.devand runsnpm testwithCI=true.
The manifests target a cluster that already has:
- An ingress controller (annotations assume ingress-nginx).
- A Secret named
pgpasswordwith keyPGPASSWORD(seenotes.txtfor an example imperative command).
Typical flow:
kubectl apply -f k8s- Build and push your own images (or use the pipeline), then roll out:
kubectl set image deployment/server-deployment server=<your-registry>/multi-server:<tag>- Same pattern for
client-deployment/worker-deployment.
GitHub Actions (.github/workflows/deploy.yaml): on push to branch main, it runs client tests in Docker, authenticates to Google Cloud, configures docker for GCR/GAR, fetches credentials for GKE cluster multi-cluster in us-central1-c, builds three images tagged rallycoding/multi-*-k8s-gh:latest and :${{ env.SHA }}, pushes them, then kubectl apply -f k8s and kubectl set image for each deployment.
Legacy Travis CI (.travis.yml): decrypts a GCP service account, gets cluster credentials, runs the same style of test, then bash ./deploy.sh on main. deploy.sh builds/pushes rallycoding/multi-*-k8s tags and updates deployments.
You will need your own GCP project, cluster, Docker registry, and GitHub secrets (DOCKER_USERNAME, DOCKER_PASSWORD, GKE_SA_KEY, etc.)—the values in the workflow are examples from the course and will not work on your account without replacement.
| Decision | Why |
|---|---|
| Split web / API / worker | Mirrors real systems: UI, synchronous API, and async CPU-ish work scaled and deployed independently. |
| Redis for pub/sub + hot state | Fast channel between API and worker; hash holds “current” computed values for the UI without hammering Postgres on every poll. |
| Postgres for submitted indices | Durable list of “indexes we have seen”; survives Redis restarts for that part of the data model. |
| ClusterIP + Ingress | Internal services are not exposed directly; one entry point (Ingress) routes by path, which is a common production pattern. |
| nginx for production static assets | Smaller runtime than react-scripts in production; standard static hosting. |
| PVC for Postgres | Pod restarts do not wipe the database when using a volume claim. |
Secrets for PGPASSWORD |
Avoids putting the database password in plain text in Deployment manifests. |
| CI: Dockerized test + deploy to GKE | Validates the client in a container similar to CI, then delivers the same images to the cluster. |
- Local developer experience: Add
docker-compose.yml(or a small Makefile) so Redis, Postgres, server, worker, and client start with one command; add a CRAproxyorREACT_APP_API_URLso local/apiworks without manual tweaks. - Security and ops: Rotate example registry/project names out of workflows; pin image digests e.g,.
image: nginx@sha256:3c3c9f...; add resource requests/limits and liveness/readiness probes. - Ingress / API: Align branch names (
mainvsmaster) with the default branch; fix any image name drift between raw manifests (rallycoding/multi-server) and CI tags (…-k8s-gh) so a freshapplybeforeset imagealways pulls an existing image. - Application code: Replace recursive Fibonacci with an iterative (or memoized) version for realistic performance; use async/await consistently on the server for Redis; add migrations (e.g.
node-pg-migrate,Dbmate) instead ofCREATE TABLEin the connect handler. - Observability: Structured logging, metrics, and tracing from API and worker; optional HorizontalPodAutoscaler for server/client based on CPU or latency.
| Path | Role |
|---|---|
client/ |
React SPA + Dockerfiles (dev test + prod nginx) |
server/ |
Express API + Dockerfile |
worker/ |
Redis consumer + Dockerfile |
k8s/ |
Kubernetes manifests |
.github/workflows/deploy.yaml |
GKE deploy pipeline |
.travis.yml |
Legacy Travis + deploy.sh |
deploy.sh |
Build, push, apply, rolling image update |
notes.txt |
kubectl cheat sheet |