Every code merge should produce a fresh Docker image. Manual builds are slow and error-prone. CI/CD pipelines automate the entire process: build, tag, scan, and push. In this lesson, you'll learn to build optimized Docker images in pipelines, cache layers for speed, scan for vulnerabilities, and push to any container registry.

1. Learning Objectives

By the end of this lesson, you will be able to:

  • Build Docker images in CI/CD pipelines
  • Use Docker layer caching for faster builds
  • Implement multi-architecture builds
  • Tag images with git SHA, version tags, and latest
  • Scan images for vulnerabilities
  • Push to Docker Hub, GHCR, ECR, and ACR

2. Why This Matters

Real-world scenario: Your team builds images manually on developer laptops. Images are inconsistent, tags are chaotic, and no one scans for vulnerabilities. A CI/CD pipeline ensures every commit produces a consistent, tagged, and scanned image ready for deployment.

3. Core Concepts

GitHub Actions Docker Build

# .github/workflows/docker-build.yml
name: Docker Build and Push

on:
  push:
    branches: [main, develop]
    tags: ['v*']
  pull_request:
    branches: [main]

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

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Extract metadata (tags, labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,format=short
            type=raw,value=latest,enable={{is_default_branch}}
          labels: |
            org.opencontainers.image.title=${{ github.event.repository.name }}
            org.opencontainers.image.description=${{ github.event.repository.description }}
            org.opencontainers.image.revision=${{ github.sha }}
            org.opencontainers.image.created=${{ github.event.repository.created_at }}
      
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            BUILD_VERSION=${{ github.sha }}
            BUILD_DATE=${{ github.event.repository.updated_at }}
      
      - name: Image digest
        run: echo "Image digest: ${{ steps.build.outputs.digest }}"
GitHub Actions Docker build workflow

Multi-Platform Builds

# Multi-platform build (amd64, arm64)
name: Multi-Platform Docker Build

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up QEMU (for emulation)
        uses: docker/setup-qemu-action@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Build and push multi-platform
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64,linux/arm/v7
          push: true
          tags: myapp:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

# Dockerfile for multi-platform compatibility
# Use base images that support multiple architectures
FROM --platform=$TARGETPLATFORM python:3.11-slim

ARG TARGETPLATFORM
ARG BUILDPLATFORM
RUN echo "Building for $TARGETPLATFORM on $BUILDPLATFORM"

# For binaries that need architecture-specific downloads
RUN if [ "$TARGETPLATFORM" = "linux/amd64" ]; then \
      curl -o tool https://example.com/tool-amd64; \
    elif [ "$TARGETPLATFORM" = "linux/arm64" ]; then \
      curl -o tool https://example.com/tool-arm64; \
    fi
Multi-platform Docker builds

Layer Caching Strategies

# Optimized caching for faster builds
name: Optimized Docker Build

jobs:
  build:
    steps:
      - uses: actions/checkout@v4
      
      # Cache Docker layers
      - name: Cache Docker layers
        uses: actions/cache@v3
        with:
          path: /tmp/.buildx-cache
          key: ${{ runner.os }}-buildx-${{ github.sha }}
          restore-keys: |
            ${{ runner.os }}-buildx-
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
        with:
          driver-opts: |
            image=moby/buildkit:latest
            network=host
      
      - name: Build with cache
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          cache-from: type=local,src=/tmp/.buildx-cache
          cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
      
      # Update cache
      - name: Move cache
        run: |
          rm -rf /tmp/.buildx-cache
          mv /tmp/.buildx-cache-new /tmp/.buildx-cache

# GitHub Actions cache (gha) is simpler and recommended
# Uses built-in GitHub Actions cache service
- name: Build with GitHub Actions cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max
Docker layer caching strategies

Image Tagging Strategies

# Tagging strategies in CI/CD

# 1. Git SHA (uniquely identifiable)
TAG_SHA=$(git rev-parse --short HEAD)
docker tag myapp myapp:$TAG_SHA

# 2. Semantic version (from git tag)
TAG_VERSION=$(git describe --tags --abbrev=0)
docker tag myapp myapp:$TAG_VERSION

# 3. Branch name
docker tag myapp myapp:develop
docker tag myapp myapp:main

# 4. Date-based
docker tag myapp myapp:$(date +%Y%m%d-%H%M%S)

# 5. Build number
docker tag myapp myapp:build-$BUILD_NUMBER

# Multiple tags on same image
docker tag myapp myapp:latest
docker tag myapp myapp:1.2.3
docker tag myapp myapp:git-sha-abc123
docker push myapp --all-tags

# GitHub Actions metadata action handles this automatically
# https://github.com/docker/metadata-action
Image tagging strategies in CI/CD

Vulnerability Scanning

# Vulnerability scanning in CI/CD
name: Security Scan

on:
  push:
    branches: [main]
  schedule:
    - cron: '0 2 * * *'  # Daily scan

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build image
        run: docker build -t myapp:scan .
      
      # Trivy scan (open source)
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:scan'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
          ignore-unfixed: true
      
      # Docker Scout (GitHub native)
      - name: Docker Scout
        uses: docker/scout-action@v1
        with:
          command: cve
          image: myapp:scan
          only-severities: critical,high
          exit-code: true
      
      # Upload scan results
      - name: Upload Trivy results to GitHub Security tab
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'
      
      # Fail on critical vulnerabilities
      - name: Check for critical vulnerabilities
        run: |
          if grep -q '"severity": "CRITICAL"' trivy-results.sarif; then
            echo "Critical vulnerabilities found!"
            exit 1
          fi
Container vulnerability scanning in CI/CD

Pushing to Different Registries

# Multi-registry push
name: Push to Multiple Registries

jobs:
  push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      # Docker Hub
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      
      # GitHub Container Registry
      - name: Login to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      # Amazon ECR
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: us-east-1
      
      - name: Login to Amazon ECR
        uses: aws-actions/amazon-ecr-login@v2
      
      # Azure Container Registry
      - name: Login to Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}
      
      - name: Login to ACR
        uses: azure/docker-login@v1
        with:
          login-server: myregistry.azurecr.io
          username: ${{ secrets.ACR_USERNAME }}
          password: ${{ secrets.ACR_PASSWORD }}
      
      # Build and push to all registries
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ secrets.DOCKER_USERNAME }}/myapp:latest
            ghcr.io/${{ github.repository }}/myapp:latest
            ${{ steps.ecr.outputs.registry }}/myapp:latest
            myregistry.azurecr.io/myapp:latest
Pushing to multiple container registries

4. Complete Project: Production Build Pipeline

# .github/workflows/production-build.yml
name: Production Build Pipeline

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

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

jobs:
  test-build:
    name: Test Build
    runs-on: ubuntu-latest
    if: github.event_name == 'pull_request'
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Build test image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          load: true
          tags: ${{ env.IMAGE_NAME }}:test
          cache-from: type=gha
      
      - name: Test image
        run: |
          docker run --rm ${{ env.IMAGE_NAME }}:test npm test
          docker run --rm ${{ env.IMAGE_NAME }}:test npm run lint

  security-scan:
    name: Security Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build image
        run: docker build -t ${{ env.IMAGE_NAME }}:scan .
      
      - name: Run Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.IMAGE_NAME }}:scan
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
      
      - name: Upload scan results
        uses: github/codeql-action/upload-sarif@v2
        with:
          sarif_file: 'trivy-results.sarif'

  build-and-push:
    name: Build and Push
    runs-on: ubuntu-latest
    needs: [security-scan]
    if: github.event_name == 'push'
    permissions:
      contents: read
      packages: write
    
    steps:
      - uses: actions/checkout@v4
      
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      
      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}
      
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: |
            ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
            ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,format=short
            type=raw,value=latest,enable={{is_default_branch}}
          flavor: |
            latest=false
      
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          build-args: |
            VERSION=${{ github.ref_name }}
            COMMIT=${{ github.sha }}
      
      - name: Image digest
        run: |
          echo "Images pushed:"
          echo "${{ steps.meta.outputs.tags }}" | tr ' ' '\n'

  sbom:
    name: Generate SBOM
    runs-on: ubuntu-latest
    needs: build-and-push
    steps:
      - name: Generate SBOM
        uses: anchore/sbom-action@v0
        with:
          image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
          format: spdx-json
          output-file: sbom.spdx.json
      
      - name: Upload SBOM
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.spdx.json

  notify:
    name: Notify
    runs-on: ubuntu-latest
    needs: build-and-push
    if: success()
    steps:
      - name: Slack notification
        uses: slackapi/slack-github-action@v1
        with:
          payload: |
            {
              "text": "✅ New image built: ${{ env.IMAGE_NAME }}:${{ github.sha }}",
              "blocks": [{
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*Build Successful*\nRepository: ${{ github.repository }}\nTag: ${{ github.sha }}\nImage: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest"
                }
              }]
            }
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK }}
Complete production build pipeline

5. Common Errors & Solutions

6. Summary Checklist

7. Next Steps

Next lesson: CI/CD Lesson 7.3: Deployment Strategies

Gataya Med

DevOps Engineer & Backend Developer. Sharing insights on cloud, automation, and scalable systems.

Comments (0)

Sarah Chen February 14, 2025

This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!

Reply

Leave a Comment