Everything you need to know to build reliable, maintainable CI/CD pipelines.
Put the cheapest, fastest checks first. A linting error that takes 2 seconds to detect shouldn't wait behind a 10-minute integration test suite.
Lint (10s) → Unit Tests (1m) → Build (2m) → Integration Tests (5m) → Deploy
Running the same pipeline twice on the same commit should produce the same result. No side effects, no partial states.
If your deployment process gives you anxiety, it's too complex. Automate until deploying feels like pushing a button (because it should be pushing a button).
┌── Lint ──┐
push → ├── Test ──├── Build → Deploy
└── Scan ──┘
Independent stages should run in parallel, not sequentially.
Build once, deploy the same artifact to dev → staging → production. Never rebuild for each environment. Use environment variables for configuration differences.
main ─────●──────●──────●──────●─────
\ / \ /
└──┘ └──┘
feature/x feature/y
main is always deployablemain, merge back via PRmain (or tags for versioned releases)main ─────●────────────────●─────
\ /
develop ────●──●──●──●──●──────
\ /
feature/x
develop for integrationmain for releasesdevelopmain ─●─●─●─●─●─●─●─●─●─
main (or very short-lived branches)Build → Dev (auto) → Staging (auto) → Production (manual approval)
| Environment | Deploy Trigger | Purpose |
|---|---|---|
| Dev | Push to develop | Developer testing, integration |
| Staging | Push to main | Pre-production validation, QA |
| Production | Git tag v* + manual approval | Live users |
Never hardcode environment-specific values. Use environment variables:
# docker-compose.yml
services:
app:
environment:
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- LOG_LEVEL=${LOG_LEVEL:-info}Each environment sets these via:
# Stage 1: Build (has compilers, dev tools — ~1GB)
FROM python:3.12 AS builder
RUN pip install -r requirements.txt
# Stage 2: Runtime (minimal image — ~120MB)
FROM python:3.12-slim
COPY --from=builder /opt/venv /opt/venv| Technique | Impact |
|---|---|
| Multi-stage builds | -80% image size |
| Alpine/slim base images | -60% base size |
.dockerignore | -50% build context |
--no-cache-dir (pip) | -10% layer size |
| Combine RUN commands | -5% (fewer layers) |
latest# NEVER commit secrets to your repository
env:
DATABASE_URL: "postgresql://admin:password123@prod-db:5432/app"GitHub Actions:
env:
DB_URL: ${{ secrets.DATABASE_URL }}GitLab CI:
variables:
DB_URL: $CI_REGISTRY_PASSWORD # Masked, protected variableJenkins:
environment {
DB_URL = credentials('database-url')
}Azure DevOps:
variables:
- group: production-secrets # Variable groupGitHub Actions:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}GitLab CI:
cache:
key: "$CI_COMMIT_REF_SLUG"
paths:
- .cache/pip/
- node_modules/# GitHub Actions — GHA cache backend
- uses: docker/build-push-action@v5
with:
cache-from: type=gha
cache-to: type=gha,mode=max| Cache Target | Key | Expected Speedup |
|---|---|---|
| pip/npm packages | Hash of lockfile | 30-60s |
| Docker layers | Branch + Dockerfile hash | 2-5min |
| Build artifacts | Commit SHA | 1-3min |
| Test results | Not recommended (always re-run) | — |
/ E2E Tests \ ← Slow, flaky (run nightly or on main only)
/ Integration \ ← Medium (run on every PR)
/ Unit Tests \ ← Fast, reliable (run on every push)
/____________________\
| Test Type | When to Run | Timeout | Retry |
|---|---|---|---|
| Lint | Every push | 2min | No |
| Unit tests | Every push | 5min | No |
| Integration tests | Every PR | 10min | 1x |
| E2E tests | Main branch, nightly | 20min | 2x |
| Security scan | Every PR | 5min | No |
1. Quarantine: Move flaky tests to a separate job that's allowed to fail
2. Retry once: Retry failed tests once before marking as failed
3. Fix or delete: Flaky tests that aren't fixed within 2 weeks should be deleted
# Docker: just pull and run the previous tag
docker pull myapp:v1.2.3 # previous known-good version
docker compose up -d --no-deps appgit revert HEAD # Creates a new commit that undoes the last change
git push # Triggers normal CI/CD pipelineif feature_flags.is_enabled("new-checkout-flow", user):
return new_checkout(request)
else:
return old_checkout(request)Blue (current): 100% traffic → v1.2.3
Green (new): 0% traffic → v1.3.0
# Gradually shift traffic
Blue: 90% → Green: 10% (canary)
Blue: 50% → Green: 50% (50/50)
Blue: 0% → Green: 100% (full cutover)
uses: actions/checkout@abc123)mainmain| Metric | Good | Needs Work | Critical |
|---|---|---|---|
| Pipeline duration | < 10 min | 10-20 min | > 20 min |
| Success rate | > 95% | 90-95% | < 90% |
| Time to deploy | < 30 min | 30-60 min | > 60 min |
| Mean time to recovery | < 1 hour | 1-4 hours | > 4 hours |
The Four Keys of DevOps performance:
1. Deployment Frequency: How often you deploy to production
2. Lead Time for Changes: Time from commit to production
3. Change Failure Rate: % of deployments that cause failures
4. Mean Time to Recovery: How quickly you recover from failures
Datanest Digital | datanest.dev | MIT License
Questions? Email support@datanest.dev
Production-ready pipeline templates for GitHub Actions, GitLab CI, Jenkins, and Azure DevOps.
Copy, configure, deploy. Stop writing CI/CD pipelines from scratch.



Get the full Cicd Pipeline Blueprints and unlock everything.
Get the complete guide with every chapter unlocked, including code samples, diagrams, and best practices.
Access all interactive tools with complete data, all workload profiles, and the full scenario library.
Downloadable source code, configuration files, and working examples from every chapter.
Free updates for life. Every new chapter, tool, and improvement included.