Type something to search...

CI/CD That Actually Works: From 4-Hour Deploys to 20 Minutes

When deployments take 4 hours, nobody deploys on Friday. Or Thursday. Or any day they don’t absolutely have to.

Slow deployments create fear. Fear creates batched releases. Batched releases create big, risky changes. It’s a death spiral for engineering velocity.

Here’s how we broke that cycle.

The Starting Point

The inherited pipeline:

  • Manual build process with tribal knowledge
  • No automated testing
  • Deployment required SSH access and a runbook
  • Average deployment: 4 hours
  • Frequency: Every 2-3 weeks (when absolutely necessary)

The Target State

  • Push to main triggers deployment
  • Automated tests gate every merge
  • Deployments complete in under 20 minutes
  • Rollback takes 2 minutes
  • Developers deploy multiple times per day

The Implementation

Step 1: Containerize Everything

Before automating deployment, we needed reproducible builds:

FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:18-alpine AS runner
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

Same container runs in development, staging, and production. No more “works on my machine.”

Step 2: Automated Testing

Every pull request runs:

  • Unit tests (Jest)
  • Integration tests
  • E2E tests (Playwright)
  • Linting and type checking

Failed tests block the merge. No exceptions.

Step 3: GitHub Actions Pipeline

name: Deploy
on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
      - run: npm run e2e

  deploy:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and push
        run: |
          docker build -t app:${{ github.sha }} .
          docker push registry/app:${{ github.sha }}
      - name: Deploy
        run: kubectl set image deployment/app app=registry/app:${{ github.sha }}

Step 4: Feature Flags

Decoupled deployment from release:

  • Deploy code anytime
  • Enable features when ready
  • Instant rollback via flag toggle

The Results

MetricBeforeAfter
Deployment time4 hours18 minutes
Deployment frequencyBi-weeklyMultiple daily
Failed deployments~20%<2%
Time to rollback1 hour2 minutes

Key Principles

  1. Automate the scary parts first — Manual steps are where errors happen
  2. Make deployments boring — If deployment is an event, you’re doing it wrong
  3. Test in production — With feature flags and monitoring, not YOLO
  4. Document everything — The pipeline is code, but context matters

Ready to fix your deployment process? Let’s talk.

Related Posts

Zero-Downtime Cloud Migration: A Practical Playbook

Migrating a production system serving hundreds of thousands of users from DigitalOcean to AWS sounds straightforward on paper. In practice, it's a high-stakes operation where the margin for error is z

read more

Enterprise SSO Integration: OAuth 2.0, SAML, and the Reality of Federated Auth

Enterprise authentication is where good intentions meet corporate IT reality. Every vendor has a slightly different SAML implementation. Every IT department has unique security requirements. And every

read more