Module 5: Docker in CI/CD

1. Introduction: Docker in CI/CD

Overview

You know Docker. You build images, write efficient Dockerfiles, handle multi-stage builds, and bring up containers with docker-compose. You learned all of that in guide #15. But there's a problem: every time you make a change to your code, you open the terminal, run docker build, wait 3-5 minutes, tag manually, and push to the registry. If you work on a team, each developer has their own build process — and sometimes the images differ because the local environments are different.

This module eliminates that manual process. After this, every push to the repository triggers an automatic build: the image gets built, tagged with the commit's SHA (traceability), scanned for vulnerabilities (security), and pushed to the registry (availability) — all without human intervention.

The mindset shift: You go from "I build Docker when I remember to" to "Docker builds automatically on every push, with caching that makes it fast, tags that make it traceable, and security scanning that makes it safe."

Context: You come from Module 4, where you learned to handle secrets safely in CI. Now those secrets (like registry tokens) get used to authenticate the image push. And what you build here — images in a registry — is exactly what Module 6 needs in order to deploy automatically.


Where Are We in the Guide?

Context

This guide has 8 modules organized into 3 phases:

Phase 1: CI Fundamentals (Modules 1-3)
├── Module 1: Introduction to CI/CD and GitHub Actions    ✅ Completed
├── Module 2: Automated Testing in CI                     ✅ Completed
└── Module 3: AI-Specific CI Checks                       ✅ Completed

Phase 2: CD & Deployment Pipelines (Modules 4-6)
├── Module 4: Secrets and Environment Management          ✅ Completed
├── Module 5: Docker in CI/CD                             ← YOU ARE HERE
└── Module 6: Deployment Pipelines

Phase 3: Production Pipelines (Modules 7-8)
├── Module 7: Monitoring, Notifications and Advanced Patterns
└── Module 8: Capstone Project — Production AI Pipeline

Estimated duration of the module: 60-90 minutes.

Where are we headed?

This module is step 5 of 8. You automate the building and distribution of Docker images. The progression is deliberate:

  1. First you understood CI (module 1) — workflows, jobs, steps, triggers
  2. Then you automated testing (module 2) — pytest, matrix, caching, reports
  3. You added AI-specific checks (module 3) — prompt regression, cost estimation
  4. You handled secrets safely (module 4) — API keys, OIDC, environments
  5. Now you automate Docker (this module) — build, tag, scan, push
  6. Then you deploy (module 6) — staging → approval → production
  7. You add monitoring (module 7) — notifications, scheduled workflows
  8. You integrate everything (module 8) — a complete commit-to-production pipeline

The problem: manual Docker builds

What your Docker flow looks like today

Developer: "I pushed a fix to the embeddings endpoint"

Terminal:
  $ docker build -t my-ai-app .         # 3-5 minutes of waiting
  $ docker tag my-ai-app:latest ghcr.io/user/my-ai-app:v1.2.3   # manual tag
  $ docker push ghcr.io/user/my-ai-app:v1.2.3                    # manual push

Another developer:
  $ docker build -t my-ai-app .         # Their build uses different cached layers
  $ docker tag my-ai-app:latest ghcr.io/user/my-ai-app:v1.2.3   # Same tag, different image!
  $ docker push ghcr.io/user/my-ai-app:v1.2.3                    # It overwrites the previous one

Visible problems:

  • 📋 A repetitive manual process — Build, tag, push, every time you change something
  • 📋 Inconsistent tags — Who decided it's v1.2.3? Which commit is that?
  • 📋 Non-reproducible builds — Your laptop has a cache; the CI runner doesn't
  • 📋 No security verification — The image goes to the registry without a scan
  • 📋 No quality guarantee — The image gets pushed even when the tests failed

What it looks like after this module

Developer: "I pushed a fix to the embeddings endpoint"

GitHub Actions (automatic):
  1. Build with docker/build-push-action
  2. Cache layers with the GitHub Cache backend
  3. Tag: sha-abc1234 + v1.2.3 (if it's a release)
  4. Scan with trivy → 0 CRITICAL vulnerabilities
  5. Push to GHCR → ghcr.io/user/my-ai-app:sha-abc1234
  
  ✅ The image is available in 2 minutes. Traceable. Safe. Reproducible.

The difference: zero manual intervention, traceability by commit, security through scanning, and consistency through a centralized build.


What makes Docker in CI different from Docker locally

The key differences

AspectDocker locallyDocker in CI
CacheLayers on the local disk (persistent)A clean runner on every run (no cache by default)
TagsWhatever you decideAutomatic: SHA, semver, branch
PushManual (docker push)Automatic after the build
SecurityYou decide whether to scanScanning is mandatory in the pipeline
ReproducibilityIt depends on your local environmentIdentical on every run (a standardized runner)
TriggerWhen you rememberEvery push/PR, automatically
Multi-platformOnly your architectureamd64 + arm64 if you need it

The most important difference: the cache

On your laptop, Docker caches layers on disk. If you only change the code (not the dependencies), the build takes seconds because the pip install layers are cached. In CI, every workflow run starts with a clean runner — there's no cache. Without explicit configuration, every build downloads the base image and reinstalls every dependency from scratch.

For an AI project with heavy dependencies (torch, transformers, langchain), this means 5-10 minute builds without a cache. With caching configured, 1-2 minutes. Configuring caching is the first priority after the basic build, not a "nice to have."


Goal of the module

By completing this module you will be able to:

  • ✅ Configure docker/build-push-action to build images automatically in CI
  • ✅ Implement Docker layer caching in Actions: the GitHub Cache backend and the Registry cache backend
  • ✅ Push images to GHCR with GITHUB_TOKEN and to Docker Hub with secrets
  • ✅ Design tagging strategies: SHA, semver, latest, branch-based
  • ✅ Integrate image scanning with trivy to detect vulnerabilities in CI
  • ✅ Configure multi-platform builds (amd64 + arm64) when necessary
  • ✅ Build a complete Docker CI Pipeline as the module's project

The professional goal

When someone on your team pushes a change, the Docker image gets built, scanned, tagged with the commit's SHA, and pushed to the registry — without anyone opening a terminal or running a single command. If the image has critical vulnerabilities, the pipeline blocks it. That's professional Docker in CI/CD.


Module contents

The capsule map

#CapsuleWhat you'll learnType
01Introduction (this one)Why automate Docker, the manual problem, the visionIntro
02Building Docker Images in CIdocker/build-push-action, buildx, the workflow YAMLTechnical
03Docker Layer Caching in ActionsGitHub Cache vs Registry cache, configuration, impactTechnical
04Container RegistriesGHCR with GITHUB_TOKEN, Docker Hub with secrets, a comparisonTechnical
05Image Tagging StrategiesSHA, semver, latest, branch-based, rollbacksTechnical
06Image Scanning in CItrivy, severities, quality gates, interpretationTechnical
07Multi-Platform Buildsamd64 + arm64, QEMU, when you need itTechnical
08Project: Docker CI PipelineThe complete pipeline: build → tag → scan → pushProject

The learning flow

First you build a basic build in CI (capsule 02). Then you make it fast with caching (capsule 03). Then you configure where to push the image (capsule 04). You learn to tag for traceability and rollbacks (capsule 05). You integrate security scanning to detect vulnerabilities (capsule 06). Optionally, you add multi-platform for different architectures (capsule 07). Finally, you integrate everything into a complete pipeline (capsule 08).

The progression is: build → optimization → distribution → traceability → security → project.

Estimated duration of the module: 60-90 minutes.


What carries over from Module 4

Your current pipeline (after Module 4) handles secrets safely:

name: CI Pipeline
on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  quality-gate:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
          cache: "pip"
      - run: pip install -r requirements-dev.txt
      - name: Lint
        run: ruff check src/ tests/
      - name: Type check
        run: mypy src/ --ignore-missing-imports
      - name: Tests
        run: pytest tests/ -v --tb=short
      - name: Prompt regression
        run: python scripts/evaluate_prompts.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      - name: Cost estimation
        run: python scripts/estimate_costs.py

Functional. Automated tests, AI-specific checks, safe secrets. But one crucial step is missing: the Docker image. Your code passes every check, but does it build correctly in Docker? Is the resulting image safe? Is it available in a registry for deployment?

What this module adds

# What you add AFTER the quality gate
  docker:
    runs-on: ubuntu-latest
    needs: quality-gate
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:sha-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

One new job: build, cache, tag, push. Automatic. Traceable. Fast.


Connection with the guide's project

This module's project: Docker CI Pipeline

The mini-project builds a complete pipeline:

  1. Build — It builds the image with docker/build-push-action
  2. Cache — The GitHub Cache backend for fast builds
  3. Tag — The commit's SHA + semver for releases
  4. Scan — trivy to detect vulnerabilities
  5. Push — To GHCR with authentication via GITHUB_TOKEN
Push / PR
    ↓
┌──────────────────────────────────────┐
│  Job: Docker CI Pipeline             │
│                                      │
│  1. Checkout + Setup Buildx          │
│  2. Login to GHCR                    │
│  3. Build image (with cache)         │
│  4. Tag: sha-abc1234 + v1.2.3        │
│  5. Scan with trivy                  │
│  6. Push to GHCR                     │
│                                      │
│  If scan CRITICAL → ❌ Block push    │
│  If scan clean → ✅ Push to registry │
└──────────────────────────────────────┘

Connection with later modules

Module 4: Secrets management (tokens for registries)
    ↓
Module 5: Docker CI Pipeline (build + push images)  ← YOU ARE HERE
    ↓
Module 6: Deployment Pipelines (deploys the image to the server)
    ↓
Module 8: Capstone pipeline (lint → test → build → push → deploy)

The image this pipeline produces is the one Module 6 deploys to staging and production. Without an image in a registry, there's nothing to deploy.


Prerequisites

  • Modules 1-4 completed: Workflows, testing, AI checks, secrets management
  • Docker fundamentals (guide #15): Dockerfile, docker build, multi-stage builds, docker-compose
  • A GitHub account: For GHCR (included for free)
  • Intermediate Python: Functions, classes, REST APIs

If you don't have these prerequisites

What you're missingRecommended resource
Docker basicsDocker Essentials Guide (#15, NIEVA)
GitHub ActionsModules 1-2 of this guide
Secrets in CIModule 4 of this guide

Technical setup

The module's file structure

your-ai-project/
├── .github/
│   └── workflows/
│       ├── ci.yml              # Quality gate (Modules 1-4)
│       └── docker.yml          # Docker CI Pipeline (this module)
├── Dockerfile                  # Your existing Dockerfile
├── .dockerignore               # Excluding unnecessary files
├── src/
│   └── ai_app/
│       ├── __init__.py
│       ├── main.py             # FastAPI app
│       └── chain.py            # LangChain pipeline
├── tests/
│   └── test_chain.py
├── requirements.txt
└── requirements-dev.txt

Quick verification

# Verify that Docker works
docker --version
# Expected output: Docker version 24.x or higher

# Verify that your Dockerfile builds
docker build -t test:local .
# Expected output: a successful build

# Verify that you have a repo on GitHub
git remote -v
# Expected output: origin  https://github.com/your-user/your-repo.git

If all three commands work, you're ready for the module.


What this module does NOT cover

  • Docker basics: How to write a Dockerfile, multi-stage builds, docker-compose — that's guide #15
  • Deployment: How to deploy the image to a server — that's Module 6
  • Kubernetes: Container orchestration — that's guide #17
  • Docker in local development: docker-compose for development — guide #15
  • Custom registries: AWS ECR, Google Artifact Registry — mentioned but not configured

Analogy: The automated production line

Imagine a car factory. In an artisan workshop, each mechanic assembles the whole car by hand: installs the engine, paints the body, verifies everything works, and parks it in the lot. It's slow, inconsistent, and depends on who does it.

In a modern factory, the production line automates everything: the chassis enters, it gets welded automatically, painted by robots, passes through quality control, and comes out to the lot — every car the same as the previous one, traceable by serial number, inspected by sensors.

Your manual Docker build is the artisan workshop. Docker in CI/CD is the production line:

  • The automatic build = the robotic welding (always consistent)
  • The layer cache = the pre-fabricated parts (don't redo what didn't change)
  • The SHA tags = the serial number (you know exactly what each unit is)
  • Trivy scanning = quality control (it detects defects before delivery)
  • The push to the registry = parking in the lot (available for the buyer)

The mindset shift

Before this module

Developer: "I finished the embeddings feature"
Developer: *opens the terminal, runs docker build*
Developer: *waits 4 minutes*
Developer: *tags it manually as v1.3.0*
Developer: *runs docker push*
Developer: *tells the team on Slack*
Ops: *downloads the image, verifies that it works*
Ops: *deploys manually*

Total time: 15-30 minutes
Confidence: "it should work"
Traceability: "I think it's today's version"

After this module

Developer: "I finished the embeddings feature"
Developer: *git push*

GitHub Actions (automatic, 2 minutes):
  ✅ Tests pass
  ✅ Image built (cache → 45 seconds)
  ✅ Tag: sha-abc1234 + main + latest
  ✅ Scan: 0 CRITICAL vulnerabilities
  ✅ Push to GHCR
  ✅ Summary in the workflow

Ops: *sees the image in Packages, knows exactly which commit it is*

Total time: 2 minutes (automatic)
Confidence: "the tests passed, the scan is clean"
Traceability: "commit abc1234, March 8th, 2:34 PM"

That's the difference between manual Docker and Docker in CI/CD.


Quick self-assessment

Before starting the capsules, verify that you have the necessary context:

  1. What does docker build -t myapp . do? (guide #15)
  2. What is a multi-stage build in Docker? (guide #15)
  3. How do secrets work in GitHub Actions? (Module 4)
  4. What is the GITHUB_TOKEN? (Module 4)
  5. What is an artifact in GitHub Actions? (Module 2)

If any question sounds completely new, review the corresponding module or guide before continuing.


Evidence of success

By the end of this module, you should be able to:

  • Configure a workflow that builds Docker images automatically on every push
  • Implement caching that reduces build time from 5+ minutes to < 2 minutes
  • Push images to GHCR using GITHUB_TOKEN (without configuring extra secrets)
  • Tag images with the commit's SHA for full traceability
  • Integrate trivy to scan for vulnerabilities before the push
  • Explain when multi-platform builds are necessary and when they aren't
  • Build the project's complete Docker CI Pipeline

If you tick every check → you're ready for Module 6.


Summary

  • Manual Docker builds are the problem: Slow, inconsistent, no traceability, no security
  • Docker in CI automates it: Build, tag, scan, push — on every push, with no intervention
  • The cache is the first priority: Without a cache, 5-10 min builds; with a cache, 1-2 min
  • Tags = traceability: The commit's SHA tells you exactly what code is running in production
  • Security scanning = prevention: Detecting vulnerabilities before the push, not after the deploy
  • GHCR is the natural default: Authentication with GITHUB_TOKEN, the same ecosystem as Actions
  • The image you build here is the one Module 6 deploys to staging and production

Additional resources

  1. docker/build-push-action — The official action for building and pushing Docker images
  2. GitHub Container Registry Docs — GHCR's documentation
  3. Docker Layer Caching in CI — How Docker's cache works
  4. trivy — Container Scanner — A vulnerability scanner for images
  5. GitHub Actions — Publishing Docker Images — GitHub's official guide
  6. Docker Essentials Guide (#15, NIEVA) — Prerequisite: Docker fundamentals