Skip to content

GitHub Actions CI

Building and pushing images by hand is error-prone. A CI pipeline ensures that every merge to your main branch produces a tested, tagged, and published image — without manual steps.

GitHub Actions is the most common choice for projects hosted on GitHub. The Docker team publishes four official actions that make this straightforward:

ActionPurpose
docker/login-actionAuthenticate to any registry
docker/setup-buildx-actionEnable BuildKit (multi-platform, cache mounts)
docker/metadata-actionGenerate tags and labels from Git metadata
docker/build-push-actionBuild and push in a single step

Registry credentials must never appear in your repository. Store them as encrypted repository secrets:

  1. Go to your repository → Settings → Secrets and variables → Actions → New repository secret.
  2. Add DOCKERHUB_USERNAME and DOCKERHUB_TOKEN (use a Docker Hub access token, not your password).

The workflow below references these as ${{ secrets.DOCKERHUB_USERNAME }} and ${{ secrets.DOCKERHUB_TOKEN }} — GitHub Actions substitutes the values at runtime and masks them from logs.

Create .github/workflows/docker.yml in your repository:

name: Build and push Docker image
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # needed if pushing to GHCR
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: acme/api-server
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-,format=short
- name: Build and push
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

To push to GitHub Container Registry, change the login step and the image name:

- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

And update the images: value in metadata-action:

images: ghcr.io/${{ github.repository }}

GITHUB_TOKEN is automatically provided by GitHub Actions — no manual secret needed.

Given a git tag v2.1.0, the metadata action generates:

acme/api-server:2.1.0
acme/api-server:2.1
acme/api-server:latest
acme/api-server:sha-abc1234

On a plain branch push (no semver tag), only the sha- tag and branch name are emitted — latest is only set on a tagged release.

The cache-from: type=gha and cache-to: type=gha,mode=max lines use GitHub Actions cache to store BuildKit layers between runs. This can cut build times by 70-90% on unchanged layers.

  • push: ${{ github.event_name != 'pull_request' }} — build on every PR for validation, but only push on merges to main.
  • setup-buildx-action enables BuildKit features (layer cache, multi-platform, --mount in RUN).
  • Never put credentials directly in the YAML file. Always use secrets.*.
Why is `push: ${{ github.event_name != 'pull_request' }}` used in build-push-action?
What does docker/metadata-action generate from a git tag like v2.1.0?
Where should Docker Hub credentials be stored in a GitHub Actions workflow?
What is the purpose of docker/setup-buildx-action in a CI workflow?