GitHub Actions — Docker Build & Push to GHCR

Build a Docker image and push it to GitHub Container Registry (GHCR) on every push to main and on version tags — with automatic image tagging, OCI labels, and minimal required permissions.

🛠 Paste this YAML into the validator to check it instantly.

Open Validator →

Overview

This workflow uses three Docker-specific GitHub Actions maintained by Docker, Inc.: docker/login-action to authenticate with GHCR, docker/metadata-action to automatically generate image tags and OCI-compliant labels, and docker/build-push-action to build and push the image using BuildKit.

The workflow triggers on pushes to main (producing a :main tag) and on any tag matching v* (producing versioned tags like :v1.2.3, :1.2.3, and :1.2). This follows the standard semantic versioning tag strategy for container images.

GHCR is free for public repositories and included in GitHub's package storage quotas for private ones. It authenticates via secrets.GITHUB_TOKEN — no separate credentials are needed.

Save this file as .github/workflows/docker.yml. Your repository must contain a Dockerfile at the root (or specify a different context path in the build step).

Full YAML Copy-paste ready

.github/workflows/docker.yml
name: Docker Build & Push

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Log in to GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}

Key sections explained

tags: ['v*'] — version tag triggers

The tags: ['v*'] filter under the push event triggers the workflow whenever you push a Git tag that starts with v — for example, v1.0.0, v2.3.1-rc1. This is the standard convention for release workflows. The 'v*' glob pattern must be quoted in YAML because unquoted v* could be misinterpreted by some YAML parsers as an anchor reference.

When a version tag push triggers the workflow, docker/metadata-action automatically generates multiple tags following Docker Hub conventions: the full version (v1.2.3), the major.minor (1.2), and the major-only (1), plus latest. This behavior is configurable via the action's tags input.

The permissions block

The permissions block grants the job exactly the permissions it needs — no more. By default, GitHub Actions tokens have a broad set of permissions. Explicitly scoping them to contents: read (to check out the code) and packages: write (to push to GHCR) follows the principle of least privilege.

If your repository's default workflow permissions are set to "read-only" in Settings, you must explicitly include packages: write in the permissions block or the push to GHCR will fail with a 403 error. Always include this block.

GHCR login with GITHUB_TOKEN

secrets.GITHUB_TOKEN is automatically generated by GitHub for every workflow run — you don't need to create or store it yourself. Combined with github.actor as the username (the user or bot that triggered the workflow), this gives the job permission to push to GHCR packages scoped to the repository owner. No personal access token or service account is needed.

docker/metadata-action — auto-tagging

The metadata-action step extracts tags and labels from the GitHub event context. It outputs two values referenced in the build step: steps.meta.outputs.tags (a newline-separated list of fully qualified image tags) and steps.meta.outputs.labels (OCI image labels including the source repo URL, revision SHA, and creation timestamp). The id: meta key names the step so subsequent steps can reference its outputs via steps.meta.outputs.*.

context: . in the build step

context: . sets the Docker build context to the repository root — the same as running docker build . locally. This means your Dockerfile can reference any file in the repo. If your Dockerfile lives in a subdirectory (e.g. docker/Dockerfile), use context: ./docker and optionally file: ./docker/Dockerfile.

Tips & variations

Add multi-platform builds (linux/amd64 + linux/arm64)

To build for both x86 and ARM (e.g. for Apple Silicon compatibility or AWS Graviton), add a docker/setup-qemu-action and docker/setup-buildx-action step, then specify platforms in the build step:

multi-platform additions
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3
      # then in build-push-action:
      - 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 Docker layers between runs

Speed up builds by caching Docker layer data in the GitHub Actions cache:

with layer caching
      - 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

Run CI tests before pushing

Add a dependency between jobs so the image is only pushed if CI passes. Set needs: test on the build-and-push job and define a separate test job that runs your test suite first. This prevents broken images from reaching the registry.