Skip to content

Docker Build and Push

Reusable workflow that builds a multi-platform Docker image and pushes it to GitHub Container Registry (GHCR). Each platform is built in a separate job in parallel on native runners—no emulation (e.g. QEMU): AMD64 runs on ubuntu-latest and ARM64 on ubuntu-24.04-arm. When both are done, a final job creates the multi-arch manifest. It checks out the tag for the given version, uses Docker Buildx, and publishes ghcr.io/<repo>:v<version> and ghcr.io/<repo>:latest with GitHub Actions cache.

Typically used after a release workflow (e.g. Simple Semantic Release), passing the new version.

Runners

Platform Runner Architecture
linux/amd64 ubuntu-latest AMD64 (native)
linux/arm64 ubuntu-24.04-arm ARM64 (native)

Inputs

Name Type Default Description
version string Required. Release version without the v prefix; used for the image tag and for checking out v<version>.
context string "." Docker build context path.
dockerfile string Dockerfile Path to the Dockerfile relative to the context (e.g. Dockerfile, Dockerfile.prod, docker/Dockerfile).
image-name string "" Optional suffix for the image name. When set, the image is ghcr.io/owner/repo/<image-name> (lowercase); when empty, ghcr.io/owner/repo. Use this when you have multiple images in the same repo (e.g. different Dockerfiles).
platforms string linux/amd64,linux/arm64 Comma-separated list of platforms to build.
push boolean true Whether to push the image to GHCR.

Secrets

Secret Required Description
GITHUB_TOKEN No (automatic) Default token for GHCR login. Automatically provided by Actions in the caller repo.
GHCR_TOKEN No Optional token for GHCR login. Use when the default GITHUB_TOKEN is not enough (e.g. org with SSO on packages, or restricted workflow permissions). Typically a Personal Access Token (PAT) with write:packages and SSO authorized for the organization. When set, it is used instead of GITHUB_TOKEN.

If you get 403 Forbidden when pushing, try passing a PAT as GHCR_TOKEN and authorize SSO for that PAT in the organization.

Caller Permissions

The calling workflow must set:

permissions:
  contents: read
  packages: write

Usage

Example: call after a semantic-release job and only when a new release was published:

name: Release

on:
  push:
    branches: [main]

permissions:
  contents: read
  packages: write

jobs:
  release:
    uses: AutomationDojo/reusable-cicd/.github/workflows/semantic-release_simple-release.yml@main
    secrets:
      GITHUB_APP_ID: ${{ secrets.GITHUB_APP_ID }}
      GITHUB_APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}

  docker:
    name: Build and Push Docker Image
    needs: release
    if: needs.release.outputs.new-release == 'true'
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
    permissions:
      contents: read
      packages: write

When the default token cannot push (e.g. 403 due to SSO or org settings), use a PAT with SSO authorized:

  docker:
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
    secrets:
      GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}   # PAT with write:packages + SSO authorized
    permissions:
      contents: read
      packages: write

With custom context, Dockerfile, and platforms:

  docker:
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
      context: ./app
      dockerfile: Dockerfile.prod
      platforms: linux/amd64,linux/arm64,linux/arm/v7
    permissions:
      contents: read
      packages: write

When the Dockerfile lives in a subdirectory of the context, pass the path relative to the context:

  docker:
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
      context: .
      dockerfile: docker/Dockerfile
    permissions:
      contents: read
      packages: write

Multiple images in the same repo (two or more Dockerfiles)

If you build several images from the same repo (e.g. one Dockerfile for an API, another for a worker), use image-name so each image gets a different name under your repo on GHCR. Without it, all images would be tagged as ghcr.io/owner/repo and would overwrite each other.

image-name Resulting image
(empty) ghcr.io/owner/repo:v1.0.0
api ghcr.io/owner/repo/api:v1.0.0
worker ghcr.io/owner/repo/worker:v1.0.0

Example: build two images (e.g. Dockerfile.api and Dockerfile.worker) after a release:

  release:
    uses: AutomationDojo/reusable-cicd/.github/workflows/semantic-release_simple-release.yml@main
    secrets:
      GITHUB_APP_ID: ${{ secrets.GITHUB_APP_ID }}
      GITHUB_APP_PRIVATE_KEY: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}

  docker-api:
    needs: release
    if: needs.release.outputs.new-release == 'true'
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
      dockerfile: Dockerfile.api
      image-name: api
    permissions:
      contents: read
      packages: write

  docker-worker:
    needs: release
    if: needs.release.outputs.new-release == 'true'
    uses: AutomationDojo/reusable-cicd/.github/workflows/docker-build-push.yml@main
    with:
      version: ${{ needs.release.outputs.version }}
      dockerfile: Dockerfile.worker
      image-name: worker
    permissions:
      contents: read
      packages: write