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:

PracticeWhat It MeansWho 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 DeploymentEvery 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.

Rendering diagram...

Pipeline Stages in Detail#

StageWhat It DoesTools
Build & LintCompile 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 testsUnit 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 scanningSAST: 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 artifactBuild 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/stagingAutomatically apply the new image to lower environments. Run smoke tests and health checks after deploy.kubectl, Helm, ArgoCD, Flux
Deploy to productionAfter 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 installnpm 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#

PlatformHosting ModelBest ForKey StrengthWatch Out For
GitHub ActionsCloud SaaS (free tier)Teams on GitHub, open-source projects, modern workflowsMarketplace of 2,000+ reusable actions; deep GitHub integration; no server to manageVendor lock-in to GitHub; 2,000 min/month free for private repos, then pay-per-minute
GitLab CICloud + self-hostedTeams wanting a unified DevOps platform (code + CI + registry + security scanning in one tool)Built-in SAST, DAST, SCA scanning; integrated environments and review appsSteeper learning curve; interface can feel overwhelming for simple pipelines
JenkinsSelf-hosted open-sourceEnterprises with complex custom pipelines, air-gapped networks, or large existing investmentMaximum flexibility; 1,800+ plugins; runs anywhere on your own infrastructureHigh operational overhead — you maintain the server, upgrades, and plugin compatibility. Not cloud-native.
CircleCICloud + self-hostedSpeed-optimized teams, Docker-heavy workflows, intelligent test splittingFast runner spin-up; Docker layer caching built-in; test-splitting to parallelize suitesCost 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.

Rendering diagram...

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 RuleWhat It DoesWhen to Use
Required reviewersOne 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 timerPipeline 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 restrictionsOnly 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 secretsCredentials 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.

Rendering diagram...

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.

Rendering diagram...

CI/CD Anti-Patterns#

Common mistakes that undermine pipeline reliability and trust:

Anti-PatternWhat Goes WrongThe Fix
Flaky testsTests 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 environmentThe 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 YAMLCredentials 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 everywhereRequiring 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 branchesBranches 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.

Rendering diagram...

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.

Rendering diagram...

DockerHow do I package my application and its dependencies into a portable, reproducible unit?

CI/CDHow do I take that unit from a commit through automated testing, security scanning, and into a registry — and then safely into production?

KubernetesHow 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#

ConceptKey Takeaway
CI vs. Continuous Delivery vs. Continuous DeploymentCI: 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 everywhereBuild 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 feedbackCI (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 anatomyWorkflows 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 promotionDev 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.
GitOpsPush-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 strategiesRolling (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 gapsAI 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-patternsFlaky 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: