Home/Learn/CI/CD & GitHub Actions/Building & Pushing Docker Images in CI

Building & Pushing Docker Images in CI

Intermediate
GitHub Actions

A 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 — complete build pipeline
# .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=max

Image Security Scanning with Trivy

Scan images for CVEs before pushing to production registries. Trivy checks OS packages and app dependencies against vulnerability databases.

Trivy security scanning in CI
      # 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>  ← immutable

Registry 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.

OIDC authentication for AWS ECR
# 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 Aria
1

How do you avoid storing AWS credentials in GitHub Secrets for CI/CD?

2

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.

Loading discussion…