Containers don't just run in production—they power the entire delivery pipeline. From reproducible build environments to daemonless image builds in Kubernetes, containers are the backbone of modern CI/CD.

1. Why Containers in CI/CD?

Before containers, CI environments were fragile snowflakes—shared Jenkins agents with conflicting tool versions, "works on CI but not locally" bugs, and hours spent debugging environment drift. Containers eliminate this entire category of problems:

  • Reproducible build environments — Every job runs in a defined image with pinned tool versions
  • No "works on CI but not locally" — Same image locally and in CI; identical results
  • Fast spin-up — Containers start in seconds vs. minutes for VMs
  • Isolation between jobs — Parallel jobs can't pollute each other's filesystem or processes
  • Ephemeral and clean-slate — Every run starts fresh; no accumulated state or leftover artifacts
Architecture Insight

As an architect, think of CI containers as disposable compute units. They enforce the constraint that your build process must be fully declarative—if it can't run in a fresh container, it has hidden dependencies.

2. Containers AS the CI Environment

Each CI job runs inside a container. The container image defines the complete toolchain available to that job:

GitHub Actions

jobs:
  build:
    runs-on: ubuntu-latest
    container:
      image: node:20-alpine
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test

The container: image: node:20-alpine line means "run every step of this job inside a node:20-alpine container." The runner pulls the image, starts the container, and executes your steps inside it.

GitLab CI

test:
  image: node:20
  stage: test
  script:
    - npm ci
    - npm test

Jenkins (Declarative Pipeline)

pipeline {
  agent {
    docker { image 'node:20' }
  }
  stages {
    stage('Test') {
      steps {
        sh 'npm ci && npm test'
      }
    }
  }
}

In all three systems, the pattern is the same: declare an image, get a pristine environment with exactly the tools you need.

3. Building Images in CI

Beyond using containers as the CI environment, CI pipelines also build container images as artifacts. The common pattern:

  1. Builddocker build creates the image
  2. Test — Run the test suite inside the freshly built image
  3. Scan — Security vulnerability scan (Trivy, Snyk, Grype)
  4. Push — Push to registry on success
  5. Deploy — Update the running service with the new image

GitHub Actions Workflow: Build, Test, Push

name: Build and Push

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=sha
            type=ref,event=branch

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Run tests against built image
        run: |
          docker run --rm ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${GITHUB_SHA::7} npm test

4. Docker-in-Docker (DinD) vs Socket Mounting

When your CI job itself runs in a container, how do you run docker build inside it? Two approaches:

Docker-in-Docker (DinD)

A privileged container runs its own Docker daemon (dockerd) inside:

# GitLab CI example with DinD
build:
  image: docker:24
  services:
    - docker:24-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  script:
    - docker build -t myapp .
  • ✅ Full isolation — inner Docker has its own storage
  • ✅ Clean layer cache per job (or shared via volumes)
  • ❌ Requires --privileged mode (security concern)
  • ❌ No layer cache sharing between jobs (unless configured)

Socket Mounting

Bind-mount the host's Docker socket into the CI container:

# Mount the host Docker socket
docker run -v /var/run/docker.sock:/var/run/docker.sock \
  docker:24 docker build -t myapp .
  • ✅ Shares layer cache with host (fast builds)
  • ✅ No privileged mode needed on the inner container
  • ❌ Jobs can see/kill each other's containers (no isolation)
  • ❌ Container breakout risk — socket access = root on host
ConcernDinDSocket Mount
IsolationStrongNone
Cache sharingHardAutomatic
SecurityNeeds --privilegedHost root access
Best forMulti-tenant CITrusted single-tenant

5. Kaniko & Daemonless Builds

In Kubernetes-based CI (Tekton, GitHub Actions on ARC, GitLab K8s runners), you often cannot run a Docker daemon. The solution: daemonless image builders.

Kaniko

Kaniko builds container images from a Dockerfile entirely in userspace—no daemon, no root, no privileged mode:

# Kubernetes pod spec for Kaniko
apiVersion: v1
kind: Pod
spec:
  containers:
    - name: kaniko
      image: gcr.io/kaniko-project/executor:latest
      args:
        - "--dockerfile=Dockerfile"
        - "--context=git://github.com/org/repo.git"
        - "--destination=ghcr.io/org/repo:latest"
      volumeMounts:
        - name: docker-config
          mountPath: /kaniko/.docker
  volumes:
    - name: docker-config
      secret:
        secretName: registry-credentials

How it works: Kaniko reads the Dockerfile, executes each instruction in a snapshot filesystem, computes layer diffs, and pushes the resulting image directly to a registry—no dockerd involved.

Not Just Docker

Buildah — OCI image builder from Red Hat, daemonless, integrates with Podman. Podman — Drop-in Docker replacement, rootless by default. Tekton — Kubernetes-native CI/CD framework where each pipeline step is a container in a pod. These tools are increasingly common in enterprise CI where Docker daemon access is restricted.

6. Caching in CI

Without caching, every CI run rebuilds every layer from scratch. With proper caching, only changed layers rebuild—cutting build times from 10+ minutes to under 60 seconds.

Caching Strategies

StrategyHow It WorksBest For
--cache-from Pull a previous image from registry, use its layers as cache Any CI system
BuildKit inline cache Embed cache metadata in pushed image (--build-arg BUILDKIT_INLINE_CACHE=1) Simple setups
GitHub Actions cache cache-from: type=gha in build-push-action GitHub-hosted runners
Local cache mount --mount=type=cache,target=/root/.cache in Dockerfile RUN Package manager caches (npm, pip, go)

Example: Registry Cache

# Pull previous image for cache, build with cache-from
docker pull ghcr.io/org/myapp:latest || true
docker build \
  --cache-from ghcr.io/org/myapp:latest \
  --tag ghcr.io/org/myapp:$SHA \
  --tag ghcr.io/org/myapp:latest \
  .
docker push ghcr.io/org/myapp:$SHA
docker push ghcr.io/org/myapp:latest

Example: Dockerfile with Cache Mounts

# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci --prefer-offline
COPY . .
RUN npm run build

7. The Full Pipeline

Here's the complete flow from code push to production deployment:

CI/CD Pipeline with Containers 📝 Push Code CI Trigger Webhook 🐳 Build Image 🧪 Test In Container 🔒 Scan Security 📦 Push Registry 🚀 Deploy Staging Promote Production 🐳 Every stage runs in a container: • Build: docker:24 or kaniko image • Test: application image with test deps • Scan: trivy/grype scanner image • Deploy: kubectl/helm image 🚦 Quality Gates (auto-fail pipeline): • Tests fail → no push to registry • Critical CVE found → block deploy • Image too large → warning/fail

Industry Callout

🏢 How Netflix & Spotify Ship Thousands of Containers/Day

Netflix uses Titus (their container platform) to run CI/CD at massive scale. Every microservice team pushes independently; the platform handles image builds, security scanning, canary deployments, and automatic rollbacks. They deploy thousands of container changes per day across hundreds of microservices.

Spotify built their CI/CD on top of Kubernetes with custom controllers. Each squad owns their pipeline definition. Builds run in ephemeral pods, images are scanned with inline policies, and promotion to production is gated by automated integration tests running in containers.

The key pattern: standardized container-based pipelines let thousands of engineers ship independently without stepping on each other.

Interactive Quizzes

Quiz 1: DinD vs Socket Mount

Your CI runs on a shared Kubernetes cluster with multiple teams. You need to build Docker images but are concerned about one team's CI job interfering with another's containers. Which approach is more appropriate?

Quiz 2: Why Containers in CI?

A team reports that tests pass on a developer's machine and in CI, but fail in staging. Which CI improvement would BEST prevent environment-related discrepancies?

Quiz 3: Caching Strategy

Your Docker build in CI takes 8 minutes, mostly spent on npm install (which rarely changes). Your CI uses GitHub Actions. What's the most effective caching approach?

Hands-On Task

🛠️ Build It: GitHub Actions Workflow for GHCR

Create a GitHub Actions workflow that builds a Docker image and pushes it to GitHub Container Registry (GHCR).

Requirements:

  1. Trigger on push to main branch
  2. Log in to GHCR using GITHUB_TOKEN
  3. Build the image with a tag based on the Git SHA
  4. Run tests inside the built image before pushing
  5. Push to ghcr.io/<your-username>/<repo>:<sha>
  6. Enable BuildKit layer caching

Starter template:

# .github/workflows/build.yml
name: Build and Push to GHCR

on:
  push:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      # TODO: Checkout code
      # TODO: Login to GHCR
      # TODO: Build image with SHA tag
      # TODO: Run tests in the built image
      # TODO: Push image (only if tests pass)
      

Validation: Push to a real repo with a Dockerfile and confirm the image appears in the repo's Packages tab on GitHub.

Key Takeaways

  • Containers as CI environments give you reproducible, isolated, ephemeral build agents with zero drift
  • The image IS the artifact — build once, test that image, deploy that same image everywhere
  • DinD vs socket mounting is a security/isolation trade-off; prefer DinD for multi-tenant, socket mount for single-tenant trusted environments
  • Kaniko and Buildah enable image builds without a Docker daemon—essential for Kubernetes-native CI
  • Layer caching (registry cache, GHA cache, cache mounts) can reduce build times by 80%+
  • Quality gates (test, scan, size check) in the pipeline prevent bad images from reaching production
  • The full pipeline: push → build → test → scan → push → deploy staging → promote production