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