GitHub Actions for building, pushing and tagging docker images.
Build and push Docker images with native or cross-platform support.
Combine platform-specific Docker images into a single multi-arch manifest.
Read and validate Docker configuration files (.docker-config.json). This is a reusable action that centralizes config reading logic used by both build and manifest actions. Can also be used independently in custom workflows.
Key benefit: Repositories with multiple services/images can use the same workflow for all builds by simply specifying different config files. See Multi-Service Repositories below.
Build Docker images for multiple architectures (AMD64, ARM64) using native runners for optimal performance.
flowchart TD
A[AMD64 Runner<br/>actions-docker/build] --> C[AMD64 Digest]
B[ARM64 Runner<br/>actions-docker/build] --> D[ARM64 Digest]
C --> E[actions-docker/manifest]
D --> E
E --> F[Multi-arch Image<br/>linux/amd64 + linux/arm64]
- Native Builds Performance: Build each platform on native runners (ARM64 on ARM, AMD64 on AMD) for maximum performance
- Explicit Control: Explicit manifest creation gives you full control over the multi-arch image
- Multiple Tags: Apply multiple tags (e.g., version + latest) in one step
- Clean Workflow: Separate build and manifest creation concerns
jobs:
build-amd64:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build AMD64
uses: open-turo/actions-docker/build@v1
id: build
with:
dockerhub-user: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
# Omit image-version to push by digest only (no tags)
# Or provide image-version: 1.0.0-amd64 for architecture-specific tags
image-platform: linux/amd64
push: true
outputs:
digest: ${{ steps.build.outputs.digest }}
build-arm64:
runs-on: ubuntu-arm-runner # Native ARM64 runner
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build ARM64
uses: open-turo/actions-docker/build@v1
id: build
with:
dockerhub-user: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
# Omit image-version to push by digest only (no tags)
# Or provide image-version: 1.0.0-arm64 for architecture-specific tags
image-platform: linux/arm64
push: true
outputs:
digest: ${{ steps.build.outputs.digest }}
create-manifest:
needs: [build-amd64, build-arm64]
runs-on: ubuntu-latest
steps:
- name: Create multi-arch manifest
uses: open-turo/actions-docker/manifest@v1
with:
image-name: myorg/myimage
dockerhub-user: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}
tags: 1.0.0,latest
sources: |
myorg/myimage@${{ needs.build-amd64.outputs.digest }}
myorg/myimage@${{ needs.build-arm64.outputs.digest }}- Build Phase: Each platform (AMD64, ARM64) is built separately on native runners
- Push Phase: Each platform-specific image is pushed to the registry
- Digest-only push: Omit
image-versionto push without creating tags (cleanest approach) - Architecture-tagged push: Provide
image-versionwith arch suffix (e.g.,1.0.0-amd64) for intermediate tags
- Digest-only push: Omit
- Manifest Phase: The manifest action combines the platform images using their digests into a single multi-arch manifest
- Pull Phase: When users pull the image, Docker automatically selects the appropriate platform-specific image
- Digest-only Builds: When
image-versionis omitted, images are pushed by digest only without creating tags. This is the cleanest approach for multi-arch workflows as only the final manifest creates user-facing tags. - Digests vs Tags: Using digests (e.g.,
@sha256:...) is more reliable than tags as it ensures you're referencing the exact image that was built - Multiple Tags: You can create multiple tags for the same manifest (e.g.,
1.0.0andlatest) by providing a comma-separated list - Native Performance: Cross-platform builds using QEMU are significantly slower than native builds. Use native runners when possible.
Repositories with multiple services can use the same workflow for all builds by using different config files. Each service has its own .docker-config.json specifying image name, dockerfile path, target, and suffix.
# .github/workflows/build-services.yaml
jobs:
build:
strategy:
matrix:
service: [api, worker, frontend]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build ${{ matrix.service }}
uses: open-turo/actions-docker/build@v1
with:
docker-config-file: services/${{ matrix.service }}/.docker-config.json
dockerhub-user: ${{ secrets.DOCKER_USERNAME }}
dockerhub-password: ${{ secrets.DOCKER_PASSWORD }}Each service maintains its own config:
services/api/.docker-config.jsonservices/worker/.docker-config.jsonservices/frontend/.docker-config.json
Benefits: No workflow duplication, consistent build process, easy to add new services.