GitHub Actions: Docker Build and Push
GitHub Actions reference: Docker build and push
TL;DR
- Bottom line: Use Docker's official GitHub Actions (
docker/build-push-action@v6,docker/metadata-action@v5,docker/setup-buildx-action@v3) to build, tag, and push container images to any registry with layer caching and multi-platform support. - Key tool/command:
docker/build-push-action@v6withpush: trueand tags fromdocker/metadata-action@v5. - Watch out for: Not setting up Buildx before building -- without it you cannot use build cache or multi-platform builds.
- Works with: Any Dockerfile, any OCI-compliant registry, linux/amd64 and linux/arm64 platforms.
Constraints
- NEVER hardcode registry credentials in workflow files -- use GitHub Secrets or
GITHUB_TOKEN. - Always use
docker/setup-buildx-action@v3beforedocker/build-push-action-- Buildx is required for layer caching and multi-platform builds. - Set
packages: writepermission explicitly when pushing to GHCR. - Pin Docker action versions to major tags (e.g.,
@v6) -- never use@main. - Multi-platform builds with QEMU are 3-10x slower -- use native runners for production speed.
- Docker Hub rate limits: 100 pulls/6h anonymous, 200/6h authenticated.
Quick Reference
| Action | Version | Purpose | Key Inputs |
|---|---|---|---|
docker/setup-qemu-action | @v3 | Enable multi-platform via QEMU | platforms |
docker/setup-buildx-action | @v3 | Create Buildx builder | driver-opts |
docker/login-action | @v3 | Authenticate to registry | registry, username, password |
docker/metadata-action | @v5 | Generate tags/labels from Git | images, tags |
docker/build-push-action | @v6 | Build and push image | context, push, tags, platforms, cache |
| GHCR login | — | GitHub Container Registry | registry: ghcr.io, GITHUB_TOKEN |
| Docker Hub login | — | Docker Hub | DOCKERHUB_TOKEN secret |
| ECR login | @v2 | AWS ECR | AWS credentials |
| Cache (GHA) | — | GitHub Actions cache backend | type=gha, mode=max |
| Cache (registry) | — | Registry as cache | type=registry, ref=... |
Decision Tree
START
├── Single platform (amd64 only)?
│ ├── YES → setup-buildx + login + metadata + build-push (no QEMU)
│ └── NO ↓
├── Multi-platform (amd64 + arm64)?
│ ├── YES → Add setup-qemu + platforms: linux/amd64,linux/arm64
│ └── NO ↓
├── Push to GHCR?
│ ├── YES → login with GITHUB_TOKEN, registry: ghcr.io
│ └── NO ↓
├── Push to Docker Hub?
│ ├── YES → login with DOCKERHUB_TOKEN
│ └── NO ↓
├── Push to AWS ECR?
│ ├── YES → aws-actions/configure-aws-credentials + amazon-ecr-login
│ └── NO ↓
└── DEFAULT → Build without push (CI validation): push: false
Step-by-Step Guide
1. Create the workflow file
Create .github/workflows/docker.yml for building and pushing Docker images. [src1]
name: Docker Build and Push
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
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 }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- uses: docker/build-push-action@v6
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
Verify: Push to main → image appears at ghcr.io/<owner>/<repo>:main.
2. Configure semantic versioning tags
Use metadata-action to auto-tag from Git. [src5]
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
Verify: Push tag v1.2.3 → image gets tags: 1.2.3, 1.2, and SHA.
3. Add multi-platform support
Enable QEMU emulation for multi-arch builds. [src4]
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
Verify: docker manifest inspect shows both amd64 and arm64.
4. Enable GitHub Actions cache
Use GHA cache backend for faster builds. [src1]
- uses: docker/build-push-action@v6
with:
cache-from: type=gha
cache-to: type=gha,mode=max
Verify: Second build shows "importing cache manifest" and completes faster.
Code Examples
Complete GHCR workflow with caching and semver
# .github/workflows/docker.yml
name: Docker Build & Push
on:
push:
branches: [main]
tags: ['v*.*.*']
pull_request:
branches: [main]
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=
- uses: docker/build-push-action@v6
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
Multi-platform build with QEMU
# .github/workflows/docker-multiplatform.yml
name: Multi-Platform Docker Build
on:
push:
tags: ['v*.*.*']
permissions:
contents: read
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- uses: docker/build-push-action@v6
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
Anti-Patterns
Wrong: Building without Buildx
# ❌ BAD — docker build without Buildx cannot use layer caching
- run: docker build -t myimage:latest .
- run: docker push myimage:latest
Correct: Using Docker official actions with Buildx
# ✅ GOOD — Buildx enables caching, multi-platform, advanced features
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myimage:latest
cache-from: type=gha
cache-to: type=gha,mode=max
Wrong: Hardcoding image tags
# ❌ BAD — hardcoded tags don't track versions
- uses: docker/build-push-action@v6
with:
tags: ghcr.io/myorg/myapp:latest
Correct: Using metadata-action for dynamic tags
# ✅ GOOD — generates tags from Git refs automatically
- uses: docker/metadata-action@v5
id: meta
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=semver,pattern={{version}}
type=ref,event=branch
- uses: docker/build-push-action@v6
with:
tags: ${{ steps.meta.outputs.tags }}
Wrong: Pushing images on pull requests
# ❌ BAD — PR builds should validate, not push
- uses: docker/build-push-action@v6
with:
push: true
Correct: Conditional push based on event type
# ✅ GOOD — only push on push events
- uses: docker/build-push-action@v6
with:
push: ${{ github.event_name != 'pull_request' }}
Common Pitfalls
- Missing packages:write permission: GHCR push fails with 403. Fix: add
permissions: packages: write. [src3] - GHA cache exceeding limit: 10 GB limit per repo. Fix: use
mode=maxsparingly or switch to registry cache. [src1] - QEMU builds extremely slow: ARM64 under emulation takes 5-20 min. Fix: use native arm64 runners or matrix strategy. [src4]
- Image not visible in GHCR: New packages default to private. Fix: change visibility in Package Settings. [src3]
- Docker Hub rate limits: Anonymous pulls limited to 100/6h. Fix: add login-action even for pull-only workflows. [src6]
- Build context too large: Entire repo sent as context. Fix: use
.dockerignore. [src2]
Diagnostic Commands
# Check Docker version on runner
docker version
# Inspect Buildx builders
docker buildx ls
# Check image manifest (multi-platform)
docker manifest inspect ghcr.io/owner/repo:tag
# List GHCR packages via CLI
gh api user/packages?package_type=container
# View GHA cache usage
gh cache list
# Debug Buildx build locally
docker buildx build --progress=plain --no-cache .
Version History & Compatibility
| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| docker/build-push-action@v6 | Current | Requires Buildx v0.15+ | Update setup-buildx to @v3 |
| docker/build-push-action@v5 | Deprecated | — | Replace @v5 with @v6 |
| docker/metadata-action@v5 | Current | New tag type syntax | Review tag configuration |
| docker/login-action@v3 | Current | — | — |
| docker/setup-buildx-action@v3 | Current | — | — |
| GHCR (ghcr.io) | Stable | — | Replaced docker.pkg.github.com |
When to Use / When Not to Use
| Use When | Don't Use When | Use Instead |
|---|---|---|
| Building container images in GitHub CI | Building in GitLab CI | GitLab CI kaniko or DinD |
| Pushing to GHCR, Docker Hub, or ECR | Need image scanning, signing, SBOM | Tekton or Harbor |
| Need multi-platform images | Need GPU-accelerated builds | Self-hosted runners |
| Simple container workflows | Need 50+ image variants | Custom build matrix |
Important Caveats
- GHA cache has a 10 GB limit per repository shared across all workflows. Old entries are evicted LRU.
- GHCR packages inherit repository visibility by default but can be changed independently.
- Multi-platform images built with QEMU may exhibit subtle behavior differences vs. native builds.
docker/build-push-actionbuilds on the runner, not inside a container.- The
latesttag should only be applied to production-ready releases.