Building & Pushing Docker Images in CI
IntermediateA production Docker build pipeline builds multi-arch images, uses layer caching, scans for CVEs, and pushes to a registry with semantic version tags — all automated on every commit.
Overview
Building Docker images in CI is one of the most common pipeline tasks. The naive approach (docker build + docker push) works but is slow and produces untagged images. A production pipeline uses Docker Buildx for multi-platform builds (Intel servers + Apple Silicon), GitHub Actions Cache for layer caching (90%+ cache hit rate), docker/metadata-action for automatic image tagging from git tags and branches, and a security scanner like Trivy before pushing to the registry.
Complete Docker Build & Push Pipeline
The docker/build-push-action handles everything: multi-platform, caching, tagging, and pushing in one step.
# .github/workflows/docker.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
tags: ['v*.*.*'] # trigger on semver tags like v1.2.3
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} # owner/repo → ghcr.io/owner/repo
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # push to GitHub Container Registry
security-events: write # upload Trivy SARIF to Security tab
steps:
- uses: actions/checkout@v4
# Enable Docker Buildx (multi-platform builder)
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to GitHub Container Registry
- name: Login to GHCR
if: github.event_name != 'pull_request' # don't push on PRs
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} # auto-provided, no setup needed
# Generate image tags and labels from git metadata
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch # main → :main
type=ref,event=pr # PR #123 → :pr-123
type=semver,pattern={{version}} # v1.2.3 → :1.2.3
type=semver,pattern={{major}}.{{minor}} # v1.2.3 → :1.2
type=sha,prefix=sha- # :sha-abc1234 (always)
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
# Build and push with layer caching
- name: Build and push
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64 # multi-arch
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha # GitHub Actions cache
cache-to: type=gha,mode=maxImage Security Scanning with Trivy
Scan images for CVEs before pushing to production registries. Trivy checks OS packages and app dependencies against vulnerability databases.
# Security scan with Trivy — added to the docker.yml above
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: 'sarif' # structured format for GitHub
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1' # fail pipeline on CRITICAL/HIGH CVEs
- name: Upload Trivy scan results to GitHub Security tab
if: always() # upload even if scan failed
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-results.sarif'
# Push image digest to summary for traceability
- name: Image digest
run: echo "${{ steps.build.outputs.digest }}" >> $GITHUB_STEP_SUMMARY
# Resulting image tags after pushing v1.2.3 tag:
# ghcr.io/org/repo:1.2.3
# ghcr.io/org/repo:1.2
# ghcr.io/org/repo:latest
# ghcr.io/org/repo:sha-abc1234
# Pull the image
# docker pull ghcr.io/org/repo:1.2.3
# docker pull ghcr.io/org/repo@sha256:<digest> ↠immutableRegistry Choices & OIDC Authentication
Different registries suit different use cases. OIDC (keyless) authentication with AWS ECR or GCP GAR eliminates long-lived credentials from GitHub secrets.
# AWS ECR with OIDC (no long-lived AWS keys needed)
jobs:
build:
permissions:
id-token: write # required for OIDC token
contents: read
steps:
- name: Configure AWS credentials (OIDC)
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-ecr
aws-region: us-east-1
# No access key/secret — OIDC token proves GitHub identity
- name: Login to Amazon ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Build and push to ECR
uses: docker/build-push-action@v5
with:
push: true
tags: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-app:latest
# Registry comparison:
# Docker Hub → public default, rate-limited, good for OSS
# GitHub GHCR → integrated with GitHub Actions, free for public
# AWS ECR → native for ECS/EKS, IAM-controlled
# Google GAR/GCR → native for GKE, VPC Service Controls
# Harbor (self-host) → full control, on-prem, scanning built-in
# AWS IAM role trust policy (allows GitHub Actions to assume role):
# {
# "Principal": {"Federated": "arn:aws:iam::123:oidc-provider/token.actions.githubusercontent.com"},
# "Condition": {"StringEquals": {"token.actions.githubusercontent.com:sub": "repo:org/repo:ref:refs/heads/main"}}
# }Key Points to Remember
- 1docker/metadata-action generates correct image tags automatically from git refs — no manual tag scripting.
- 2type=sha always produces a unique immutable tag per commit — essential for production traceability.
- 3GitHub Actions Cache (type=gha) stores Docker layer cache — 60-90% speedup on repeated builds.
- 4OIDC authentication avoids storing long-lived cloud credentials in GitHub secrets.
- 5Trivy scan before push — exit-code: 1 blocks the push if CRITICAL/HIGH CVEs are found.
- 6Multi-arch builds (linux/amd64,linux/arm64) let dev Macs (M1/M2) and prod Linux servers use the same image.
Interview Questions
Sign in to ask AriaHow do you avoid storing AWS credentials in GitHub Secrets for CI/CD?
How do you implement image tagging in GitHub Actions?
Ask Aria about Building & Pushing Docker Images in CI
Your personal AI tutor — ask anything about this concept
Revision Status
Personal Notes
Sign in to save personal notes for this topic.
Discussion
Sign in to join the discussion.