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