Skip to content
All projects

Case study · Jan 8, 2026

Completed lab

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.

GitHub Actions
Docker
Node.js
Next.js
CI/CD
Deployment

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 concurrency to cancel stale runs on rapid pushes.
  • actions/setup-node@v4 with cache: 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 HEALTHCHECK so Compose / orchestrators know when it's actually ready.
  • Deploy job gated on github.ref == 'refs/heads/main' and needs: [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 of actions/cache for node_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/health against the freshly-built image before deploy.
  • Cache the Next.js .next/cache directory between jobs to shave the build phase too.

The companion writing note has the working YAML and Dockerfile snippets: A CI/CD pipeline that actually saves time.