Case study · Jan 8, 2026
CI/CD Pipeline for a Full-stack App
A practical GitHub Actions workflow for shipping a Next.js + Node app — typecheck, lint, test, Docker image build, and a deploy job that runs only on green main.
Status — Completed lab. Built and validated locally on a real Next.js + Node project. The workflow file and Dockerfile are reusable across small full-stack apps; not currently running publicly.
One-line summary
A practical CI/CD workflow for shipping full-stack applications with GitHub Actions, automated checks, Docker-based packaging, and deployment-focused thinking.
Problem
A lot of "tutorial" pipelines fail in real life: they install dependencies the wrong way, forget to cache the Next.js build, run tests after deploy, or wire deploy steps that fire even when verification fails. I wanted a workflow that's boring and correct — one that fails fast, caches the expensive bits, and lets me deploy with confidence.
Goal
Produce a CI/CD pipeline I'd actually want on a small production app:
- Typecheck, lint, and tests run on every PR and every push to
main. - Cache restored from the lockfile hash so installs stay snappy.
- Build job that produces a tagged Docker image — same artifact that ships to production.
- Deploy job that runs only when verification passes and only on
main, with a clear rollback story.
Architecture
PR / push ──▶ verify (typecheck + lint + test)
│
▼
build (Docker image, multi-stage)
│
▼ (only on main)
deploy (push image + restart service)
What I built
- A multi-job GitHub Actions workflow with
concurrencyto cancel stale runs on rapid pushes. actions/setup-node@v4withcache: npm(or pnpm) keyed on the lockfile — install times dropped from ~45s to ~6s warm.- A multi-stage Dockerfile that produces a ~150 MB runtime image from a
Next.js standalone output, runs as a non-root user, and includes a
HEALTHCHECKso Compose / orchestrators know when it's actually ready. - Deploy job gated on
github.ref == 'refs/heads/main'andneeds: [verify, build]so it never runs without a green check. - Secrets injected via GitHub Environments (one per stage), not stuffed into repo secrets.
Tools used
- CI/CD — GitHub Actions
- Packaging — Docker (multi-stage, standalone Next output)
- Runtime — Node 20, Linux
- Deployment — SSH-based deploy script for self-hosted; equivalent Vercel / Fly.io paths documented in the workflow
Key DevOps concepts demonstrated
- Fail fast — verify is a separate, cheaper job that gates everything downstream.
- Cache discipline — install is the slow step; key it on the lockfile.
- Reproducible artifact — the same Docker image runs in CI smoke tests and in production.
- Environments + protected deploys — production deploys require a green build and an explicit branch.
- Rollback thinking — the deploy script keeps the last 3 image tags
on the host; rollback is
restart with previous tag.
Lessons learned
- Use
actions/setup-node@v4's built-in cache instead ofactions/cachefornode_modules. Less config, less drift. - Don't deploy from the same job that runs tests. Splitting saves re-running expensive verification when only the deploy step needs a retry.
- A healthcheck in the Dockerfile costs nothing and saves real debugging time when Compose decides "started" doesn't mean "ready".
Next improvements
- Add a build-time SBOM and image vulnerability scan (Trivy).
- Wire a smoke-test job that hits
/api/healthagainst the freshly-built image before deploy. - Cache the Next.js
.next/cachedirectory between jobs to shave the build phase too.
Links
The companion writing note has the working YAML and Dockerfile snippets: A CI/CD pipeline that actually saves time.