CI/CD Pipeline Best Practices: From Code to Production
A well-designed CI/CD pipeline is the backbone of modern software development. It's the difference between deploying with confidence and deploying with fear. After setting up and maintaining CI/CD pipelines for multiple teams, I've learned what works and what doesn't. Let me share the practices that have proven most valuable.
The Foundation: What CI/CD Should Do
A good CI/CD pipeline should:
- Catch bugs early - Before they reach production
- Deploy consistently - Same process every time
- Provide feedback quickly - Developers shouldn't wait hours
- Enable rollbacks - When things go wrong
- Maintain quality - Enforce standards automatically
Pipeline Stages
Stage 1: Linting and Formatting
Catch style issues before code review:
# .github/workflows/ci.yml
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run lint
- run: npm run format:check
Why it matters: Consistent code style reduces cognitive load and makes reviews faster.
Stage 2: Type Checking
For TypeScript projects:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run type-check
Why it matters: Catch type errors before runtime.
Stage 3: Unit Tests
Fast feedback on logic errors:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm test -- --coverage
- uses: codecov/codecov-action@v3
with:
files: ./coverage/lcov.info
Best practices:
- Run tests in parallel when possible
- Set coverage thresholds (but don't obsess over 100%)
- Fail fast on first error
Stage 4: Integration Tests
Test components working together:
integration-test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run test:integration
Stage 5: Build
Ensure code compiles and bundles correctly:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm ci
- run: npm run build
- uses: actions/upload-artifact@v3
with:
name: build-artifacts
path: dist/
Stage 6: Security Scanning
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- run: npm audit --audit-level=high
- uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
Deployment Strategies
Blue-Green Deployment
Maintain two identical production environments:
deploy-blue:
runs-on: ubuntu-latest
steps:
- name: Deploy to blue environment
run: |
# Deploy to blue
kubectl set image deployment/app app=myapp:${{ github.sha }} -n blue
- name: Health check
run: |
# Wait for blue to be healthy
kubectl wait --for=condition=available deployment/app -n blue --timeout=300s
- name: Switch traffic
run: |
# Switch load balancer to blue
kubectl patch service app -p '{"spec":{"selector":{"version":"blue"}}}'
Advantages:
- Zero-downtime deployments
- Easy rollback (switch back to green)
- Test new version before switching traffic
Canary Deployments
Gradually roll out to a percentage of users:
deploy-canary:
runs-on: ubuntu-latest
steps:
- name: Deploy canary (10%)
run: |
kubectl set image deployment/app-canary app=myapp:${{ github.sha }}
# Route 10% traffic to canary
- name: Monitor metrics
run: |
# Wait and check error rates
sleep 300
# If error rate < threshold, continue
- name: Full rollout
if: success()
run: |
kubectl set image deployment/app app=myapp:${{ github.sha }}
# Route 100% traffic
Environment Management
Separate Environments
Development → Staging → Production
Each environment should mirror production as closely as possible.
Environment Variables
Never commit secrets. Use secret management:
deploy:
runs-on: ubuntu-latest
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
API_KEY: ${{ secrets.API_KEY }}
steps:
- name: Deploy
run: |
# Use environment variables
echo "Deploying with secure credentials"
Rollback Strategies
Automated Rollback
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy
id: deploy
run: |
# Deploy and capture version
VERSION=$(kubectl rollout status deployment/app)
echo "version=$VERSION" >> $GITHUB_OUTPUT
- name: Health check
run: |
# Check if deployment is healthy
if ! kubectl rollout status deployment/app --timeout=5m; then
echo "Deployment failed, rolling back"
kubectl rollout undo deployment/app
exit 1
fi
Manual Rollback
Always have a one-command rollback:
# Kubernetes
kubectl rollout undo deployment/app
# Docker
docker service rollback app
# Custom script
./scripts/rollback.sh production v1.2.3
Testing in Production
Feature Flags
Deploy code behind flags:
// Deploy code, but don't enable yet
if (featureFlags.isEnabled('new-checkout-flow', userId)) {
return <NewCheckoutFlow />;
}
return <OldCheckoutFlow />;
Monitoring and Alerts
Set up alerts for:
- Error rates
- Response times
- Resource usage
- Business metrics
monitor:
runs-on: ubuntu-latest
steps:
- name: Check error rate
run: |
ERROR_RATE=$(curl -s metrics/api/error-rate)
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
echo "Error rate too high: $ERROR_RATE"
exit 1
fi
Pipeline Optimization
Parallel Execution
Run independent jobs in parallel:
jobs:
lint:
# ...
test:
# ...
build:
# ...
security:
# ...
# All run in parallel
Caching
Cache dependencies to speed up builds:
- uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
Conditional Execution
Skip unnecessary steps:
test:
if: contains(github.event.head_commit.message, '[skip tests]') == false
runs-on: ubuntu-latest
steps:
- run: npm test
Common Pitfalls
1. Testing in Production
Don't use production as your test environment. Always test in staging first.
2. Ignoring Failed Tests
If tests are flaky, fix them. Don't disable them.
3. Manual Deployments
Automate everything. Manual steps are error-prone.
4. No Rollback Plan
Always have a way to rollback. Test your rollback process.
5. Slow Pipelines
If your pipeline takes hours, developers will skip it. Optimize for speed.
Real-World Example
Challenge: Deployment pipeline taking 45 minutes, frequent production issues.
Solution:
- Parallelized tests - Reduced test time from 30min to 8min
- Added caching - Build time reduced from 10min to 2min
- Implemented canary deployments - Catch issues before full rollout
- Added automated rollback - Rollback time reduced from 15min to 30sec
- Improved monitoring - Detect issues within 1 minute
Result:
- Pipeline time: 45min → 12min
- Production incidents: 5/month → 1/month
- Deployment confidence: High
Best Practices Summary
- Fail fast - Catch issues early in the pipeline
- Run in parallel - Don't wait unnecessarily
- Cache dependencies - Speed up builds
- Test everything - Unit, integration, e2e
- Monitor deployments - Know when something's wrong
- Automate rollbacks - Recover quickly
- Document everything - Future you will thank you
Conclusion
A good CI/CD pipeline is invisible when it works and invaluable when it doesn't. The investment in setting up a robust pipeline pays dividends in:
- Reduced bugs in production
- Faster development cycles
- Higher team confidence
- Better code quality
Remember: Your pipeline is only as good as your tests. Invest in good tests, and your pipeline will catch issues before they reach production.
What CI/CD challenges have you faced? What practices have worked best for your team?
Related Posts
Database Migration Strategies: Zero-Downtime Deployments
Learn how to perform database migrations without downtime. From schema changes to data migrations, master the techniques that keep your application running.
Container Orchestration with Kubernetes: A Practical Guide
Learn Kubernetes fundamentals and practical patterns for deploying and managing containerized applications at scale. Real-world examples and best practices.
Docker and Containerization: Best Practices for Production
Master Docker containerization with production-ready best practices. Learn how to build efficient, secure, and maintainable containerized applications.
Observability in Modern Applications: Logging, Metrics, and Tracing
Master the three pillars of observability: logging, metrics, and distributed tracing. Learn how to build observable systems that are easy to debug and monitor.
Cost Optimization in Cloud Infrastructure: Real-World Strategies
Learn practical strategies to reduce cloud infrastructure costs without sacrificing performance or reliability. Real techniques that have saved thousands of dollars.
Building Developer Tools: Creating Internal Tools That Teams Love
Learn how to build internal developer tools that improve productivity and make your team more efficient. From CLI tools to admin dashboards.