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@v7
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v6
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@v7
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@v4
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.*.
OptionBenefitCost
Push to Docker Hub with login-actionHuge ecosystem, pullable from anywhereHave to create and rotate DOCKERHUB_TOKEN yourself, and it’s subject to Docker Hub’s rate limits
Push to GHCR with GITHUB_TOKENNo extra secret to create, permissions tie automatically to the repositoryThe token only lives for the duration of the job, cannot be used to pull outside the workflow directly, and ties you mainly to GitHub
  • Hardcoding credentials directly in the workflow YAML instead of referencing them through secrets.*.
  • Using a plain password instead of an access token in docker/login-action.
  • Not restricting a job’s permissions:, leaving GITHUB_TOKEN with wider scope than it needs.
  • Baking credentials into a Dockerfile layer (for example through an ARG carrying a token) instead of using BuildKit build secrets.

💡 Real-world example

docker/build-push-action is the standard GitHub Action teams across the industry use to build and push images in CI — nearly every Docker-related workflow on GitHub Actions relies on it together with setup-buildx-action and metadata-action.

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?