How to Set Up CI/CD for a Web Application
Build a CI/CD pipeline that makes deploys boring and safe — the full flow from git push to production, a working GitHub Actions YAML file, deployment strategies, and the best practices that keep pipelines fast and reliable.
CI/CD turns deployment from a nerve-wracking manual ritual into a boring automated one. You push code, and a pipeline installs dependencies, lints, tests, builds, and deploys — the same way, every time. The payoff is speed and safety: bad code fails the build instead of production, and every release is identical. Here's how I set one up, with a complete GitHub Actions file you can drop in.
The pipeline: what runs, in what order
CI (Continuous Integration) and CD (Continuous Delivery/Deployment) are one flow. Continuous Integration is the test-and-build half; Continuous Delivery is the deploy half. Together they look like this:
git push / open PR
│
▼
┌─────────────── CI ───────────────┐
│ 1. Checkout code │
│ 2. Install dependencies (npm ci) │
│ 3. Lint │
│ 4. Run unit tests │
│ 5. Build │
│ 6. Security scan │
│ 7. Package the artifact │
└──────────────┬───────────────────┘
▼
┌─────────────── CD ───────────────┐
│ 8. Deploy to staging │
│ 9. Integration / e2e / smoke │
│ 10. (optional) manual approval │
│ 11. Deploy to production │
│ 12. Monitor + rollback if needed │
└──────────────────────────────────┘
The key idea: the same artifact you built and tested in CI is the one you promote to production. You never rebuild for prod — that reintroduces the risk you just eliminated.
Choosing a platform
| Platform | Best for | Notes |
|---|---|---|
| GitHub Actions | Repos already on GitHub | Easiest start, huge marketplace, generous free tier |
| GitLab CI | Teams on GitLab | Tightly integrated, .gitlab-ci.yml |
| Jenkins | Self-hosted, full control | Powerful but you maintain it |
| CircleCI | Fast builds, good caching | Strong Docker support |
For most teams I default to GitHub Actions — the pipeline lives next to the code and there's nothing extra to host. The choice matters less than people think; the concepts (stages, artifacts, secrets, environments) transfer between all of them. Learn the ideas once and you can read any pipeline.
A working GitHub Actions pipeline
This is a real, runnable CI workflow for a Node app. It runs on every push and PR, and only deploys from main.
1name: CI/CD
2
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8
9jobs:
10 ci:
11 runs-on: ubuntu-latest
12 steps:
13 - name: Checkout
14 uses: actions/checkout@v4
15
16 - name: Setup Node
17 uses: actions/setup-node@v4
18 with:
19 node-version: 20
20 cache: npm
21
22 - name: Install dependencies
23 run: npm ci
24
25 - name: Lint
26 run: npm run lint
27
28 - name: Run tests
29 run: npm test -- --ci
30
31 - name: Build
32 run: npm run build
33
34 - name: Security audit
35 run: npm audit --audit-level=high
36
37 deploy:
38 needs: ci
39 if: github.ref == 'refs/heads/main'
40 runs-on: ubuntu-latest
41 environment: production
42 steps:
43 - name: Checkout
44 uses: actions/checkout@v4
45
46 - name: Deploy
47 run: ./scripts/deploy.sh
48 env:
49 DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}A few things worth pointing out:
npm ci, notnpm install— it installs exactly what's in the lockfile, so CI is reproducible.cache: npm— caches dependencies between runs to keep the pipeline fast.needs: ci— deploy only runs if CI passed.secrets.DEPLOY_TOKEN— credentials come from GitHub's secret manager, never the repo.environment: production— lets you require a manual approval before this job runs.
Adding staging and e2e tests
The workflow above deploys straight to production once CI passes, which is fine for a small project. For anything with real users, insert a staging step: deploy the tested artifact to a staging environment, run end-to-end and smoke tests against it, and only then promote to production. In GitHub Actions you model this with separate jobs and environment protection rules, which can also gate the production job behind a required reviewer's approval.
1 deploy-staging:
2 needs: ci
3 runs-on: ubuntu-latest
4 environment: staging
5 steps:
6 - uses: actions/checkout@v4
7 - name: Deploy to staging
8 run: ./scripts/deploy.sh staging
9 - name: Run e2e smoke tests
10 run: npm run test:e2e -- --base-url=$STAGING_URL
11
12 deploy-production:
13 needs: deploy-staging # only runs if staging + its tests passed
14 if: github.ref == 'refs/heads/main'
15 runs-on: ubuntu-latest
16 environment: production # can require a manual approval here
17 steps:
18 - uses: actions/checkout@v4
19 - name: Promote to production
20 run: ./scripts/deploy.sh productionThe chain ci → deploy-staging → deploy-production means a failure anywhere short-circuits the rest. Nothing reaches production that didn't pass every gate before it.
Deployment strategies
How you release matters as much as that you release. The three common strategies:
| Strategy | How it works | Trade-off |
|---|---|---|
| Rolling | Replace instances gradually, a few at a time | Simple; brief mixed-version window |
| Blue/green | Two identical environments; switch traffic all at once | Instant rollback; needs double the infra |
| Canary | Route a small % of traffic to the new version first | Safest for risky changes; more complex routing |
For a payment-critical system like an e-commerce checkout, I lean toward blue/green or canary — being able to roll back instantly is worth the extra infrastructure. For a low-risk internal tool, rolling is fine.
Best practices that actually matter
- Keep pipelines fast. A pipeline over ~10 minutes stops being run frequently. Cache dependencies, parallelize independent jobs, and only run e2e tests where they add value.
- Secrets live in the CI secret manager, never in YAML or the repo.
- Build once, promote the artifact. Don't rebuild per environment.
- Use Infrastructure as Code. Define infra (Terraform, etc.) in version control so environments are reproducible and reviewable.
- Fail fast. Put the cheapest, most likely-to-fail checks first (lint, unit tests) so a broken build dies in seconds, not minutes.
- Always have a rollback path. Monitoring plus a one-command (or one-click) rollback is what makes fast deploys safe.
- Gate on quality, not just green. A pipeline that passes because you deleted the failing test isn't safety. Require coverage thresholds, keep flaky tests out (quarantine and fix them), and make the security audit a real gate.
What good looks like
A healthy pipeline has a few observable properties. It's fast — most runs finish in a handful of minutes, so people actually wait for it instead of merging around it. It's trustworthy — a green build genuinely means the code is deployable, because the tests are meaningful and not routinely skipped. It's reproducible — the same commit always produces the same artifact and the same result. And it's reversible — when something does slip through (it will), you can roll back in seconds, not scramble through a manual redeploy.
When I set one of these up on a payment-critical system, the goal wasn't zero incidents — that's not realistic. The goal was making the cost of any single deploy small: small changes, shipped often, each easy to reason about and trivial to undo. A pipeline that lets you deploy ten small safe changes a day beats one that makes you batch a month of risky changes into a single terrifying release.
Where this fits
CI/CD automates the deployment you first do by hand — so if you haven't done a manual deploy yet, start with How to Deploy a Full-Stack Application to understand what the pipeline is automating. For the bigger picture of production readiness, see the hub: Deploying, Scaling & Architecting Full-Stack Apps.