CI/CD Pipelines
Docker solves the "works on my machine" problem by packaging your application into a portable container. Kubernetes solves the "how do I run it reliably at scale" problem. But there is still a gap: how does code actually travel from a developer's laptop into that cluster — safely, automatically, and repeatably?
That gap is what CI/CD pipelines fill. CI/CD (Continuous Integration / Continuous Delivery — and in its most automated form, Continuous Deployment) is the automated system that takes every code change and carries it through building, testing, security scanning, and deployment without human intervention. It is the difference between "we'll deploy next Tuesday" and "every merged pull request is live within 15 minutes."
This matters especially when working with AI coding tools. When an AI agent generates dozens of small changes per session, a manual deployment process becomes a bottleneck. CI/CD pipelines are the infrastructure that makes AI-assisted development scalable — and safe.
The Three Practices#
CI/CD is a single acronym but it covers three distinct practices that build on each other:
| Practice | What It Means | Who Approves Deployment |
|---|---|---|
| Continuous Integration (CI) | Developers merge code to the main branch frequently — ideally multiple times per day. Every merge triggers an automated build and test run. The goal is catching defects early, not just running a server. | N/A — CI does not deploy |
| Continuous Delivery (CD) | Code that passes CI is automatically deployed to staging or a production-ready environment. At any moment, the codebase is in a deployable state. Shipping to production is a business decision, not a technical bottleneck. | A human approves the final push to production |
| Continuous Deployment | Every change that passes the full automated pipeline is automatically released to production — zero human gates. Requires very high testing confidence and organizational maturity. | No human — automation decides |
Most teams practice Continuous Delivery, not full Continuous Deployment. Having a human approval gate before production is pragmatic, not a failure — it lets teams coordinate deployment timing, communicate upcoming changes to stakeholders, and align with business schedules without slowing the pipeline itself.
A common misconception about CI: Long-lived feature branches where developers work in isolation for days are not Continuous Integration — they are delayed integration. The longer branches diverge from each other, the larger and more painful the eventual merge conflict. Real CI means integrating to the main branch at least daily.
The Pipeline Architecture#
A production CI/CD pipeline is a chain of automated stages. A failure at any stage stops the pipeline and notifies the team — bad code cannot advance to the next stage.
The CI/CD Pipeline: From Commit to Production
Each stage acts as a quality gate. Cheap and fast checks run first to fail quickly. The same immutable Docker image — built once and tagged with the Git commit SHA — is promoted through every environment. It is never rebuilt per environment.
Pipeline Stages in Detail#
| Stage | What It Does | Tools |
|---|---|---|
| Build & Lint | Compile source code; run a static linter (ESLint, Ruff, Checkstyle) to enforce code style. A lint failure means the code does not meet the project's agreed standards — fast, cheap to fix. | ESLint, Ruff, Checkstyle, TypeScript compiler |
| Automated tests | Unit tests first (fast, no external dependencies, target <5 min total), then integration tests (with a real database or services). Treat flaky tests as the highest-priority bugs — a pipeline that randomly fails is a pipeline nobody trusts. | Jest, Pytest, JUnit, Testcontainers |
| Security scanning | SAST: static analysis for code vulnerabilities (SQL injection, XSS). SCA: scan dependencies for known CVEs. Secrets detection: catch accidentally committed credentials before they reach a registry. | Semgrep, Snyk, Dependabot, Trivy, GitLeaks |
| Publish artifact | Build the Docker image; scan it for CVEs; push to a container registry (GHCR, ECR, GCR) tagged with the Git SHA. The artifact is now immutable and traceable to a specific commit. | docker/build-push-action, Trivy, GHCR, ECR, GCR |
| Deploy to dev/staging | Automatically apply the new image to lower environments. Run smoke tests and health checks after deploy. | kubectl, Helm, ArgoCD, Flux |
| Deploy to production | After human approval (or automated canary analysis), roll out to production using a safe deployment strategy. Monitor error rate and latency in the first minutes post-deploy. | Argo Rollouts, Flagger, kubectl, Helm |
GitHub Actions: Building a Pipeline#
GitHub Actions is the most widely adopted CI/CD platform for teams on GitHub. Pipelines are defined as YAML files inside .github/workflows/ and version-controlled alongside your code — which means pipeline changes go through the same pull request and code review process as your application code. Here is an annotated, production-realistic workflow:
name: CI/CD Pipeline
# ── Triggers ──────────────────────────────────────────────────
on:
push:
branches: [main] # Full pipeline (CI + deploy) on merges to main
pull_request:
branches: [main] # CI only (no deploy) on pull requests
# Prevent duplicate runs: cancel older runs when a newer commit is pushed
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
# ── Job 1: Lint and Test (runs on every push and PR) ──────────
ci:
name: Lint & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Cache node_modules between runs
- name: Install dependencies
run: npm ci # Reproducible install from lockfile (not npm install)
- name: Lint
run: npm run lint
- name: Run tests
run: npm test
# ── Job 2: Build and Push Docker Image ───────────────────────
build:
name: Build & Push Image
needs: ci # Only runs if the ci job passes
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' # Skip this job on pull requests
permissions:
contents: read
packages: write # Required to push to GitHub Container Registry
outputs:
image-tag: ${{ steps.tag.outputs.value }} # Pass the image tag to deploy jobs
steps:
- uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # Auto-provisioned — no manual setup
- name: Compute image tag
id: tag
# Tag with commit SHA for traceability: e.g., ghcr.io/myorg/myapp:sha-a3f82c1
run: echo "value=ghcr.io/${{ github.repository }}:sha-${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.tag.outputs.value }}
cache-from: type=gha # Reuse Docker layer cache from previous runs
cache-to: type=gha,mode=max
# ── Job 3: Deploy to Staging (automatic) ─────────────────────
deploy-staging:
name: Deploy to Staging
needs: build
runs-on: ubuntu-latest
environment: staging # Links to the "staging" GitHub environment
steps:
- uses: actions/checkout@v4
- name: Deploy to staging cluster
run: ./scripts/deploy.sh staging
env:
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
# ── Job 4: Deploy to Production (requires manual approval) ───
deploy-production:
name: Deploy to Production
needs: deploy-staging
runs-on: ubuntu-latest
environment: production # Pauses here until a required reviewer approves
steps:
- uses: actions/checkout@v4
- name: Deploy to production cluster
run: ./scripts/deploy.sh production
env:
IMAGE_TAG: ${{ needs.build.outputs.image-tag }}
KUBE_CONFIG: ${{ secrets.PROD_KUBE_CONFIG }}
Key Concepts Explained#
needs — Creates a sequential dependency. build only runs if ci passes. deploy-staging only runs if build succeeds. Without needs, all jobs run in parallel simultaneously.
environment — Links a job to a named environment configured in your GitHub repository's Settings → Environments page. You configure protection rules there (required reviewers, wait timers, branch restrictions). When the job runs, it pauses until those rules are satisfied before proceeding. Environment-scoped secrets (STAGING_KUBE_CONFIG, PROD_KUBE_CONFIG) are only injected after protection rules pass — a feature-branch pipeline cannot access production credentials.
concurrency with cancel-in-progress: true — Without this, pushing three commits in quick succession launches three pipelines that all try to deploy simultaneously. This setting cancels any older in-progress run on the same branch when a newer one starts. Only the latest commit matters.
${{ secrets.GITHUB_TOKEN }} — A short-lived token automatically provisioned by GitHub for each workflow run. It has the right permissions to push to the repository's container registry without any manual secret configuration. This is why pushing to GHCR from CI requires no separate credential setup.
cache-from: type=gha — CI runners start from a completely clean environment on every run. Without layer caching, every build re-downloads the base image and reinstalls all dependencies from scratch. GitHub Actions cache stores the Docker layer cache between runs, cutting a typical warm build from several minutes to under 30 seconds.
npm ci vs. npm install — npm ci installs from the lockfile exactly, fails if the lockfile is missing or inconsistent with package.json, and never modifies package.json. It is the correct command for CI because it guarantees reproducible installs.
CI/CD Platforms at a Glance#
| Platform | Hosting Model | Best For | Key Strength | Watch Out For |
|---|---|---|---|---|
| GitHub Actions | Cloud SaaS (free tier) | Teams on GitHub, open-source projects, modern workflows | Marketplace of 2,000+ reusable actions; deep GitHub integration; no server to manage | Vendor lock-in to GitHub; 2,000 min/month free for private repos, then pay-per-minute |
| GitLab CI | Cloud + self-hosted | Teams wanting a unified DevOps platform (code + CI + registry + security scanning in one tool) | Built-in SAST, DAST, SCA scanning; integrated environments and review apps | Steeper learning curve; interface can feel overwhelming for simple pipelines |
| Jenkins | Self-hosted open-source | Enterprises with complex custom pipelines, air-gapped networks, or large existing investment | Maximum flexibility; 1,800+ plugins; runs anywhere on your own infrastructure | High operational overhead — you maintain the server, upgrades, and plugin compatibility. Not cloud-native. |
| CircleCI | Cloud + self-hosted | Speed-optimized teams, Docker-heavy workflows, intelligent test splitting | Fast runner spin-up; Docker layer caching built-in; test-splitting to parallelize suites | Cost scales quickly with usage; less GitHub integration depth than Actions |
Practical guidance: For most teams starting today, GitHub Actions is the right choice if your code is on GitHub. It requires zero infrastructure to operate and covers the full CI/CD workflow out of the box. GitLab CI is the right choice if you want security scanning, container registry, and CD all in the same platform without stitching together tools. Jenkins is worth considering only if you have requirements cloud platforms cannot meet — most teams that inherit a Jenkins setup eventually migrate away.
Environment Promotion: Build Once, Promote Everywhere#
One of the most important CI/CD principles: build the Docker image once and promote the same immutable artifact through every environment — never rebuild per environment.
Why "same image" matters: If you rebuild the Docker image separately for staging and production — even from the same Git commit — minor differences can cause the two images to diverge. A base image tag like node:20 may have received a new patch version between the two builds; an unpinned package may have resolved to a different version; the build cache may have been warm for one and cold for the other. You end up testing one artifact and shipping a slightly different one. This is a common source of "it worked in staging" production incidents.
How environment-specific config works: The image tag sha-a3f82c1 is identical in every environment. What differs is the configuration injected at runtime: database URLs, API keys, resource limits, and feature flags. These are injected as environment variables at deploy time — never baked into the image. This aligns directly with the Docker best practice of never embedding secrets in image layers.
Environment Protection Rules#
Modern CI/CD platforms let you attach protection rules to environments that control when a deployment job can proceed:
| Protection Rule | What It Does | When to Use |
|---|---|---|
| Required reviewers | One or more named people or teams must approve before the job runs. The pipeline pauses until someone approves or rejects. | Always on production. Optionally on staging when teams want to coordinate before a staging deploy. |
| Wait timer | Pipeline waits N minutes after the previous stage passes before proceeding — even if all checks pass. Gives time to cancel. | Production, when you want a cooling-off window to observe staging metrics before the rollout continues. |
| Branch restrictions | Only specific branches (e.g., main, releases/**) are allowed to deploy to this environment. Feature branches cannot. | Always on production. Prevents accidental production deploys directly from a feature branch. |
| Environment-scoped secrets | Credentials available only in jobs linked to this environment, and only after protection rules pass. Production credentials are never visible to a job that has not been reviewed. | Always. Production database passwords and API keys should not be accessible from an arbitrary branch pipeline. |
Push-Based vs. Pull-Based CD (GitOps)#
Traditional CD pipelines use a push-based model: when a build finishes, the CI system runs kubectl apply to update the cluster. This works, but it means the CI system must hold production cluster credentials — a security surface that can be exploited if the CI system is compromised.
GitOps is an alternative where the CI pipeline does not talk to the cluster at all. Instead, it updates a Git repository containing the desired cluster state (Kubernetes manifests or Helm values). An operator running inside the cluster continuously polls the Git repository and applies changes it detects. Git becomes the single source of truth.
Push-Based CD vs. Pull-Based GitOps
Push-based CD: CI pipeline holds cluster credentials and applies changes directly. GitOps: CI updates manifests in Git; a cluster-side operator (ArgoCD or Flux) detects the change and applies it. The cluster initiates all outbound connections — no external system holds cluster credentials.
Deployment Strategies#
How you ship a new version matters as much as what you ship. A naive deployment — stop the old version, start the new one — creates downtime and gives you no way to catch problems before all users are affected. The three standard strategies each offer a different balance of complexity, cost, and risk.
Deployment Strategies: Rolling, Blue/Green, Canary
Three strategies for deploying new versions with zero downtime. Rolling update is the Kubernetes default — simple but briefly runs two versions at once. Blue/green enables instant rollback at the cost of double infrastructure. Canary validates on a small slice of real traffic before full rollout.
CI/CD Anti-Patterns#
Common mistakes that undermine pipeline reliability and trust:
| Anti-Pattern | What Goes Wrong | The Fix |
|---|---|---|
| Flaky tests | Tests that pass or fail non-deterministically (due to timing, shared state, or external service calls) erode trust in the entire pipeline. Teams start ignoring failures. | Treat flakiness as a critical bug. Quarantine flaky tests and fix them before merging. Never use retry loops as a permanent fix — they mask the real problem. |
| Slow pipelines (>10 min) | Developers context-switch while waiting. By the time CI completes, the developer is working on something else and has to re-orient to investigate the failure. | Run lint and unit tests first. Parallelize independent jobs with matrix builds. Cache Docker layers and package dependencies. Move slow E2E tests to post-merge or nightly runs. |
| Rebuilding per environment | The image deployed to production differs from the one tested in staging, even if built from the same commit. Minor environment differences cause hard-to-diagnose bugs. | Build once in CI; tag with Git SHA; promote the same tag. Environment configuration varies; the artifact does not. |
| Secrets in YAML | Credentials committed to the repository — even in a 'test' workflow — are permanent in Git history and accessible to anyone with repo access. | Always use encrypted secrets (GitHub Secrets, Vault, AWS Secrets Manager). Never hardcode or echo secrets. Use environment-scoped secrets for production credentials. |
| Manual approval gates everywhere | Requiring human approval for dev and staging deploys negates the value of automation and makes the pipeline feel like bureaucracy rather than a tool. | Fully automate dev and staging deploys. Gate only production — and make that gate async and fast (Slack notification + one-click approve). |
| Long-lived feature branches | Branches that diverge from main for weeks accumulate large merge conflicts. When they finally merge, integration failures are large, painful, and hard to attribute. | Merge to main at least daily. Use feature flags to hide incomplete work behind a toggle, not long-lived branches. |
What AI Agents Get Wrong with CI/CD#
AI-Generated Pipelines: Common Gaps
AI agents produce runnable pipeline YAML quickly, but they consistently omit production requirements: no concurrency controls, hardcoded values instead of secrets, missing environment protection rules, absent Docker layer caching, and no security scanning. The result works on the first run but fails operationally.
Connecting Docker, Kubernetes, and CI/CD#
The three sections of this chapter are designed to work as a complete delivery system. Each layer answers a different question.
Docker — How do I package my application and its dependencies into a portable, reproducible unit?
CI/CD — How do I take that unit from a commit through automated testing, security scanning, and into a registry — and then safely into production?
Kubernetes — How do I run that unit reliably at scale, with self-healing, autoscaling, and zero-downtime updates?
Each layer depends on the previous one. Containers without CI/CD require manual deploys — you have solved the packaging problem but not the delivery problem. CI/CD without containers produces environment-inconsistent artifacts. And Kubernetes without CI/CD means manually building images and running kubectl apply by hand for every change.
Together, they form the foundation of modern production engineering — and the infrastructure that makes AI-assisted development at scale both safe and sustainable.
Summary#
| Concept | Key Takeaway |
|---|---|
| CI vs. Continuous Delivery vs. Continuous Deployment | CI: merge frequently, auto-test on every change. Continuous Delivery: code is always deployable; human approves the production push. Continuous Deployment: fully automated to production — only for mature teams with high test confidence. |
| Build once, promote everywhere | Build the Docker image once in CI; tag with the Git SHA; promote the same tag through dev, staging, and production. Never rebuild per environment — what you test must be what you ship. |
| Fast CI feedback | CI (lint through tests) should complete in under 10 minutes. Run cheap checks first. Parallelize independent jobs. Slow pipelines cause context-switching and get ignored. |
| GitHub Actions anatomy | Workflows in .github/workflows/; jobs run in parallel by default; use needs: for sequencing; environment: links to protection rules (required reviewers, branch restrictions, scoped secrets); concurrency: prevents duplicate deploys. |
| Environment promotion | Dev auto-deploys on merge to main; staging auto-deploys after dev passes; production requires a required-reviewer approval gate. Scoped secrets are injected only after gates pass. |
| GitOps | Push-based: CI runs kubectl apply (needs cluster credentials). Pull-based (ArgoCD/Flux): CI updates manifests in Git; a cluster-side operator applies changes. GitOps adds drift correction, full audit trail, and removes credential exposure. |
| Deployment strategies | Rolling (Kubernetes default, two versions briefly coexist); Blue/Green (instant rollback, double infra cost); Canary (5% real-traffic validation before full rollout, requires traffic-splitting tooling). |
| AI agent gaps | AI generates runnable pipelines but omits concurrency controls, environment gates, Docker layer caching, and security scanning. Always explicitly request these when prompting for pipeline YAML. |
| Anti-patterns | Flaky tests, slow CI (>10 min), rebuilding per environment, hardcoded secrets, approval gates on non-production, long-lived feature branches. Each one erodes pipeline trust. |
Sources:
- Continuous Integration — Martin Fowler
- Blue Green Deployments — Martin Fowler
- What is CI/CD? — Atlassian
- GitHub Actions Workflow Syntax — GitHub Docs
- Using Environments for Deployment — GitHub Docs
- Kubernetes Deployments — kubernetes.io
- Introduction to GitOps — gitops.tech
- ArgoCD Getting Started — argo-cd.readthedocs.io
- Argo Rollouts — argoproj.github.io
- CD4ML: Continuous Delivery for Machine Learning — Martin Fowler