CI/CD That Actually Ships: Our Practical Pipeline for Web Apps
Five stages, eight to twelve minutes, and a rollback that takes thirty seconds. The pipeline we actually run, minus the YAML mysticism.
A five-stage GitHub Actions pipeline that lints, tests, builds, and deploys web apps to production in 8-12 minutes with health checks and instant rollback, free under 2,000 minutes a month.
CI/CD has a reputation for being a YAML swamp you wade into once and never touch again. It doesn't have to be. The pipeline we run on most client web apps is five stages, takes 8-12 minutes from commit to production, and costs nothing until you blow past 2,000 GitHub Actions minutes a month. Setup is an afternoon of work, not a quarter.
The shape is simple: lint and typecheck first, then tests, then build, then an automated staging deploy, then production behind a manual approval. Everything before that approval button is hands-off. The version we describe here has shipped thousands of times at a 99.8% success rate. Here's how each piece earns its place.
The five stages, and why this order
Order matters because each stage is more expensive than the last. Lint and typecheck run in about a minute. Tests run in three to five. The Docker build is another two or three. Staging and production are two minutes each. You want the cheap checks to fail first so nobody pays for the expensive ones on code that was never going to make it.
Four principles hold the whole thing together. Feedback inside five minutes, so a broken commit is obvious before the author has switched tasks. Zero manual steps until production. Staging that matches production down to the environment variables and data structure. And a rollback that takes thirty seconds, not a war room.
Stage 1: lint and typecheck
Don't run a five-minute test suite against code that doesn't compile. ESLint catches the quality rules, Prettier keeps formatting boring and consistent, TypeScript catches the type errors, and import sorting stops the diffs from getting noisy. Sixty seconds, all in.
Run ESLint with max-warnings set to zero. Warnings are technical debt that compounds quietly. The first one you tolerate becomes forty. When someone pushes a lint error, the pipeline stops in under a minute and they fix it before any test runner spins up. On our projects that one rule alone saves 20 to 30 hours of CI time a month.
Stage 2: tests, weighted toward what catches bugs
We run tests in parallel and weight the suite by how many real bugs each type catches, not by what the testing pyramid tells you to do. Our split is roughly 70% integration, 25% unit, 5% E2E. That's upside down from the classic pyramid, and it finds more genuine problems. Integration tests exercise the API endpoints, the database interactions, and the user flows where things actually break. Unit tests stay fast and cover business logic, utilities, and the gnarly calculations. E2E is expensive and flaky, so we point it only at signup, login, and payment, the journeys we cannot afford to ship broken.
Skip the coverage theater. 100% is a vanity number that rewards testing getters. We hold statements, functions, and lines at 70% and branches at 60%, and we spend the effort on business logic, edge cases, error handling, and integration points. We don't write tests for trivial accessors, third-party libraries, or UI components, which belong in visual tests instead. And we never try to E2E every page; it's too slow and too brittle to be worth it.
Stage 3: build artifacts you can deploy anywhere
The build produces one thing: an artifact that runs identically in any environment. That means a minified, tree-shaken production bundle, a Docker image carrying the app and its dependencies, source maps for debugging production, and build metadata stamped with the commit hash and timestamp so you always know exactly what's live.
Use a multi-stage Docker build. It drops the image from a gigabyte-plus down to around 150MB, which makes every pull and every deploy faster. We store images in GitHub Container Registry, which is free for public repos, cleans up old images on its own, and pulls from anywhere. Docker Hub, AWS ECR, and Google Artifact Registry all work fine if you're already in that ecosystem.
Stage 4: staging as a dress rehearsal
Staging only tells you the truth if it mirrors production. Same infrastructure, sanitized production-like data, test versions of external services, and the exact same Docker image that's headed to prod. Anything less and you're testing a different system than the one you ship.
Run database migrations before the new code deploys, and roll them back automatically if they fail. After the deploy, fire a handful of smoke tests that confirm the basics respond. If a smoke test fails, the pipeline stops cold and never touches production. That gate is the whole point of staging.
Stage 5: production, with a human in the loop
Production is the one place we keep a manual approval, and we deploy blue-green. Two identical environments: blue is live and taking traffic, green is the new version. We deploy to green, run health checks against it, then switch traffic over. Blue stays up so rollback is instant, and we shut it down after 24 hours once we trust the new version.
From clicking approve to traffic switched is two to three minutes. If green fails its health checks, traffic never moves. If something slips through anyway, rollback is one command, because when production is down every second is real money and real trust. One-click rollback isn't a nice-to-have; it's the difference between a thirty-second blip and an incident.
After the deploy: monitoring and alerts
Shipping is the start of the deploy, not the end. We watch error rate for spikes, response time for slowdowns, traffic for sudden drops that signal an outage, plus database connections, memory, and disk for the slow leaks that crash you at 3am. We drop a deploy marker into Sentry so we can tell at a glance whether an error spike lines up with a release.
Alerts route by urgency: Slack for team notifications, PagerDuty for the on-call, email for things that can wait until morning. Success notifications go to Slack too, so everyone knows what changed in production without having to ask.
What you actually get
The whole pipeline lives in a single GitHub Actions file. Lint in a minute, tests in three to five, Docker image in two to three, automatic staging deploy, a pause for approval, then production with health checks and a Slack ping when it lands. Eight to twelve minutes commit to production, free under 2,000 Actions minutes a month, and about four to six hours to set up. The payoff is the part that's hard to put a number on: you ship multiple times a day and stop being afraid of the deploy button.
We help teams design and ship production-grade software in eLearning, fintech, and AI. Let's talk about your project.
Book a call