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
Comments (0)
This is exactly what I needed! The initContainer approach solved our migration issues completely. Thanks for the detailed guide!
ReplyLeave a Comment