Capstone: full CI/CD pipeline
Build a tiny API, put it in an image, run it on a local Kubernetes cluster, and see how GitHub Actions would ship the same image. Observability is optional and last.
| Levels | Local path = Intermediate · GitHub Actions + ServiceMonitor = Production |
| Time | ~2 hours local · another hour if you add CI and metrics |
| Prerequisites | Git, Docker, Kubernetes, GitHub Actions beginner bands |
| You will be able to | (1) run this app in Docker (2) deploy the same image on kind (3) explain what the workflow does — and what it does not do |
Last verified: 2026-08-16
Start when you can
docker builda Dockerfile andcurlthe containerkubectl apply -fa Deployment on kind andkubectl port-forward- Read a workflow and name the trigger, the runner, and one step
What you are building
flowchart TB App[app/ Express API] --> Image[docker/ Dockerfile] Image --> Kind[kind cluster] Image -.->|push, not deploy| GHCR[GHCR] Kind --> Curl[curl / and /health] Kind -.->|optional| SM[ServiceMonitor]
This is a one-service learning app, not a three-tier product. The old name “3-tier” oversold it.
Files and what each one is for
full-cicd-pipeline/
├── app/ # The software. One Express process.
│ ├── index.js # /, /health, /metrics
│ └── package.json
├── docker/Dockerfile # How that software becomes an image
├── k8s/
│ ├── deployment.yaml # Keep 2 copies running
│ ├── service.yaml # Stable name + ports http and metrics
│ └── servicemonitor.yaml # Optional. Needs Prometheus Operator.
└── .github/workflows/deploy.yml # Build + push. Deploy step is a stub.
Step 0 — run the app with Node
Goal: See the three HTTP paths before any container exists.
cd projects/full-cicd-pipeline/app
npm install
npm start
In another terminal:
curl -s http://127.0.0.1:3000/
curl -s http://127.0.0.1:3000/health
curl -s http://127.0.0.1:3000/metrics
Expected output: JSON with a message; {"status":"healthy"}; Prometheus text that includes http_requests_total.
Stop the process with Ctrl+C.
Step 1 — same app, as an image
The Dockerfile must be built with this directory as the context (so COPY app/ works):
cd projects/full-cicd-pipeline
docker build -f docker/Dockerfile -t devops-demo-app:local .
docker run --rm -d --name demo -p 3000:3000 devops-demo-app:local
curl -s http://127.0.0.1:3000/health
docker stop demo
Expected output: {"status":"healthy"}.
If it failed: COPY app/ not found → you ran docker build from docker/ with the old COPY ../app/ paths. Use the command above. Port 3000 busy → -p 3001:3000.
Step 2 — same image, on kind
kind create cluster --name learn # skip if you already have it from module 04
kind load docker-image devops-demo-app:local --name learn
kubectl apply -f k8s/deployment.yaml -f k8s/service.yaml
kubectl wait --for=condition=available deploy/devops-demo-app --timeout=90s
kubectl port-forward svc/devops-demo-app 8080:80
Another terminal:
curl -s http://127.0.0.1:8080/health
curl -s http://127.0.0.1:8080/metrics
Expected output: the same JSON and metrics as Step 0. kubectl get pods shows two Ready pods.
If it failed: ImagePullBackOff → the node does not have devops-demo-app:local. Re-run kind load against the same cluster name. connection refused on 8080 → wait for Ready, or kubectl describe pod.
Leave servicemonitor.yaml for Step 4. Applying it on a cluster without the Prometheus Operator only creates a CRD error.
Step 3 — read the GitHub Actions workflow (deploy is a stub)
Open .github/workflows/deploy.yml.
| Piece | What it does |
|---|---|
on.push / on.pull_request to main |
When the recipe runs |
docker/build-push-action |
Builds this Dockerfile and pushes to GHCR |
| Deploy step | Prints the image name. It does not call kubectl. |
That is honest: a learning repo cannot ship credentials to your cluster. The working deploy is Step 2.
To see the build job go green, copy this folder into a GitHub repo (or push this repo) and enable Actions. Packages: write is already set so GHCR push can work on main.
Step 4 — optional metrics
/metrics is already on the app and on the Service port named metrics.
Only if you installed kube-prometheus-stack (Production band of 06-prometheus):
kubectl apply -f k8s/servicemonitor.yaml
Then check Prometheus targets for devops-demo-app. If you have not installed the Operator, skip this step.
How this connects
- Docker — the image
- Kubernetes — Deployment + Service
- GitHub Actions — build on push
- Prometheus —
/metrics+ ServiceMonitor
When not to grow this app: adding a database and a frontend here will hide the pipeline. Learn the path first.
Practice
- Basic. Change the JSON message in
app/index.js, rebuild,kind load, andkubectl rollout restart deploy/devops-demo-app. Success: port-forward shows the new message. - Intermediate. Add a
/versionroute that readsprocess.env.APP_VERSIONand set that env var on the Deployment. Success: curl returns the value you set. - Production. List three things the stub deploy step would need before it could roll out to a real cluster (credentials, image pull, rollout check). Write them in your notes; do not invent a kubeconfig in this repo.