GitHub Actions for Docker — Build, Tag, and Push Images 2026

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Introduction

Why This Matters

Manually building and pushing Docker images is error-prone and slow. A single misconfigured tag or forgotten push can block a deployment. GitHub Actions automates the entire container pipeline — from code commit to a versioned, verified image sitting in a registry, ready to deploy.

With native integration into GitHub events (push, pull request, tag), you can build images only when needed, cache layers between runs, and push to multiple registries in one workflow.

Pushing to Docker Hub on Git Tags

The most common pattern: build and push a production image only when you create a semantic version tag like v1.2.3:

name: Build and Push to Docker Hub
 
on:
  push:
    tags:
      - 'v*'
 
jobs:
  build-push:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
 
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ secrets.DOCKER_USERNAME }}/myapp
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

The docker/metadata-action automatically derives tags from your Git ref — pushing v1.2.3 creates three tags: 1.2.3, 1.2, and sha-abc1234. This eliminates manual tagging errors entirely.

Pushing to GitHub Container Registry (GHCR)

GHCR is free for public repositories and uses your existing GITHUB_TOKEN — no extra secrets required:

name: Build and Push to GHCR
 
on:
  push:
    branches: [main]
 
jobs:
  build-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
 
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

Set permissions.packages: write at the job level — without it the GITHUB_TOKEN cannot push packages and the workflow fails.

Multi-Platform Builds for ARM and AMD64

Modern infrastructure spans x86 and ARM (Apple Silicon, AWS Graviton). Build for both platforms simultaneously with QEMU emulation:

- name: Set up QEMU
  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
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

Multi-platform builds increase build time by 2–4x. Consider building only for linux/amd64 on pull requests and doing the full multi-arch build on merge to main.

Layer Caching to Speed Up Builds

Docker layer caching with the GitHub Actions cache backend (type=gha) dramatically reduces rebuild times when only application code changes and base layers are stable:

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

For even faster caching with private registries, use type=registry to store cache in the registry itself:

cache-from: type=registry,ref=ghcr.io/myorg/myapp:cache
cache-to: type=registry,ref=ghcr.io/myorg/myapp:cache,mode=max

Building Preview Images on Pull Requests

Build images on PRs but do not push to production tags — this validates the Dockerfile without polluting your registry:

name: PR Docker Build
 
on:
  pull_request:
    branches: [main]
 
jobs:
  build:
    runs-on: ubuntu-latest
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
 
      - name: Build (no push)
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: myapp:pr-${{ github.event.number }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

Setting push: false validates the build succeeds without publishing the image.

Common Mistakes

  • Using Docker Hub password instead of an access token — Create a scoped Docker Hub access token, not your account password. Tokens can be revoked without changing your password.
  • Missing packages: write permission — GHCR pushes silently fail or throw 403 errors without this permission.
  • Not using docker/metadata-action — Hardcoding image tags leads to overwriting latest unintentionally. Let metadata-action derive tags from Git context.
  • Building on every file change — Use paths filters to skip Docker builds when only documentation or test files change.
  • Single-platform images in production — Building only for amd64 will break on ARM-based cloud instances without error until runtime.

Best Practices

  • Store DOCKER_PASSWORD as a GitHub Secret and use scoped access tokens, not account passwords.
  • Always add cache-from and cache-to with type=gha — it reduces build times by 50–80% for unchanged layers.
  • Use docker/metadata-action to derive consistent, predictable tags from Git refs automatically.
  • Separate build jobs (PR) from push jobs (merge) to keep registries clean.
  • Sign images with cosign from the Sigstore project to verify image provenance before deployment.

Key Takeaways

  • docker/setup-buildx-action must be included before docker/build-push-action — Buildx is required for cache and multi-platform support.
  • docker/metadata-action derives image tags from Git tags and branches automatically, eliminating manual tag management.
  • GITHUB_TOKEN is sufficient to push to GHCR — no extra credentials needed when packages: write permission is set.
  • GitHub Actions cache (type=gha) for Docker layer caching can reduce build times by 50–80% for stable base layers.
  • Multi-platform builds (linux/amd64,linux/arm64) require QEMU setup and increase build duration — best reserved for production merges.
  • Never store Docker Hub passwords directly — use scoped access tokens that can be revoked independently.
  • Setting push: false on PR workflows validates the Dockerfile without publishing intermediate images.
  • Semantic version tags via docker/metadata-action enable image rollback to any previous release without guessing tag formats.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading