| name | github-actions-container-build |
| description | Build multi-architecture container images in GitHub Actions. Matrix builds (public repos with native ARM64), QEMU emulation (private repos), or ARM64 larger runners (Team/Enterprise). Uses Podman rootless builds with push-by-digest pattern |
GitHub Actions Container Build
Build multi-architecture container images in GitHub Actions using Podman and native ARM64 runners.
Core Principles
Choose Your Workflow
CRITICAL: Ask these questions before generating any workflow.
Question 1: Is your GitHub repository public?
- Yes → Use
github-actions-workflow-matrix-build.yml(free standard ARM64 runners, 10-50x faster) - No → Go to Question 2
Question 2: Do you have GitHub Team/Enterprise + willing to pay for ARM64 builds?
- Yes → Use ARM64 larger runners (custom setup required, paid per minute)
- No → Use
github-actions-workflow-qemu.yml(free QEMU emulation, slower but works on free tier)
1. Push-by-Digest (2025 Best Practice - Default)
Matrix builds use push-by-digest pattern:
- Images pushed by digest without intermediate
:amd64/:arm64tags - Only tiny digest files (~70 bytes) transfer as artifacts
- Registry stays clean (no tag clutter)
- Same debug experience with
--platformflag
# Build job
- name: Push by digest
run: |
podman push \
--digestfile /tmp/digest \
localhost/build:${{ matrix.arch }} \
docker://${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# Merge job
- name: Create manifest from digests
run: |
podman manifest create "$IMAGE:latest"
podman manifest add "$IMAGE:latest" "docker://$IMAGE@${AMD64_DIGEST}"
podman manifest add "$IMAGE:latest" "docker://$IMAGE@${ARM64_DIGEST}"
podman manifest push --all "$IMAGE:latest" "docker://$IMAGE:latest"
Debug specific architecture:
podman pull --platform linux/arm64 ghcr.io/OWNER/REPO:latest
2. Matrix Builds (Public Repos)
For public repositories - use GitHub-hosted standard ARM64 runners:
- 10-50x faster builds (native vs. emulation)
- Better reliability and accuracy
- Lower CI costs
- Completely free for public repos
- Not available for private repos
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: ubuntu-24.04-arm # Standard ARM64 runner (public repos only)
3. QEMU Builds (Private Repos - Free Tier)
For private repositories on free tier - use QEMU emulation:
- Works on GitHub Free plan
- Slower (10-50x) than native ARM64 runners
- Uses
docker/setup-qemu-actionfor ARM64 emulation - Single-job pattern with
--platform linux/amd64,linux/arm64
runs-on: ubuntu-latest
steps:
- uses: docker/setup-qemu-action@v3
- run: podman build --platform linux/amd64,linux/arm64 --manifest ...
4. Podman Over Docker
Use Podman for container builds:
- Rootless by default (better security)
- No daemon required
- Native multi-arch manifest support
- OCI compliant
- Must use
podman manifest push --all(notpodman push) - Format: Use OCI (default) for modern registries; use
--format v2s2only for Quay.io or cross-registry (see references for details) - Network: Use
--network=hostflag for builds to avoid container networking SSL issues on GitHub Actions ubuntu-24.04 (see Troubleshooting section)
5. podman-static for Heredoc Support
Ubuntu 24.04's bundled podman (4.9.3) uses buildah 1.33.7 which doesn't support heredoc syntax. Install podman-static for full BuildKit compatibility:
- name: Install podman-static
run: |
ARCH=$(uname -m)
if [ "$ARCH" = "x86_64" ]; then
PODMAN_ARCH="amd64"
else
PODMAN_ARCH="arm64"
fi
curl -fsSL -o /tmp/podman-linux-${PODMAN_ARCH}.tar.gz \
https://github.com/mgoltzsche/podman-static/releases/latest/download/podman-linux-${PODMAN_ARCH}.tar.gz
cd /tmp && tar -xzf podman-linux-${PODMAN_ARCH}.tar.gz
sudo cp -f podman-linux-${PODMAN_ARCH}/usr/local/bin/* /usr/bin/
podman system migrate
Important: Install to /usr/bin/ (not /usr/local/bin/) to avoid AppArmor issues.
Quick Start
For Public Repos (Matrix Build)
Copy workflow template:
cp assets/github-actions-workflow-matrix-build.yml .github/workflows/build.ymlCustomize Containerfile path:
-f ./Containerfile.python-uv # or your ContainerfileAdd your Containerfile (see secure-container-build plugin for templates)
For Private Repos (QEMU)
Copy workflow template:
cp assets/github-actions-workflow-qemu.yml .github/workflows/build.ymlFollow steps 2-3 from above.
Workflow Structure
Matrix Build Workflow (Push-by-Digest)
- Build job (matrix): Build and push images by digest on native runners
- Merge job: Download digests, create and push multi-arch manifest
QEMU Workflow
- Single job: Build multi-arch manifest directly with
--platformflag
Multi-arch Build Approaches
| Approach | Artifact Size | Registry Overhead | Best For |
|---|---|---|---|
| Push-by-digest (default) | ~70 bytes | 1x | Production |
| Architecture tags | None | 2x (tags + manifest) | Debugging |
| OCI artifacts | Full images | 3x | Maximum privacy |
See references/github-actions-best-practices.md for detailed comparison.
ARM64 Larger Runners (Private Repos with Team/Enterprise)
For private repositories with GitHub Team or Enterprise Cloud plans:
Standard ARM64 runners (ubuntu-24.04-arm) don't work in private repos. Instead, create ARM64 larger runners:
Setup steps:
- Go to Organization Settings → Actions → Runners → New runner
- Select "Larger runners"
- Choose "Ubuntu 24.04 by Arm Limited" partner image
- Name your runner (e.g.,
my-org-arm64-runner) - Configure size (e.g., 4-core, 16GB RAM)
Update workflow to use custom runner:
strategy:
matrix:
include:
- arch: amd64
runner: ubuntu-24.04
- arch: arm64
runner: my-org-arm64-runner # Your custom ARM64 larger runner name
Cost:
- Billed per minute (not included in free minutes)
- ~37% cheaper than x64 larger runners
- Ref: Actions runner pricing
Registry Configuration
GitHub Container Registry (GHCR) - Default
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
- name: Login to GHCR
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | podman login "${{ env.REGISTRY }}" \
-u "${{ github.actor }}" \
--password-stdin
Docker Hub (Optional)
- name: Login to Docker Hub
run: |
echo "${{ secrets.DOCKERHUB_TOKEN }}" | podman login docker.io \
-u "${{ secrets.DOCKERHUB_USERNAME }}" \
--password-stdin
# Push to Docker Hub
podman manifest push --all "$IMAGE:latest" \
"docker://docker.io/${{ secrets.DOCKERHUB_USERNAME }}/app:latest"
Debugging Multi-arch Images
# Pull specific architecture
podman pull --platform linux/arm64 ghcr.io/OWNER/REPO:latest
# Inspect manifest
podman manifest inspect ghcr.io/OWNER/REPO:latest
# Verify architectures
podman manifest inspect ghcr.io/OWNER/REPO:latest | jq '.manifests[].platform'
Reference Documentation
For detailed information, see references/github-actions-best-practices.md.
Containerfile Templates
For Containerfile templates and security best practices, see the secure-container-build plugin which provides:
- Production-ready templates for Python/uv, Bun, Node.js/pnpm, Golang, and Rust
- Wolfi runtime images with non-root users
- Multi-stage build patterns
- Allocator optimization for Rust
Troubleshooting
Common Issues
Container networking SSL errors (ubuntu-24.04 runners):
- Symptom:
UNKNOWN_CERTIFICATE_VERIFICATION_ERRORor SSL certificate verification failures duringbun install,npm install,pip install, etc. inside containers - Cause: GitHub Actions ubuntu-24.04 runner image 20251208.163.1+ has container networking configuration changes that break SSL/TLS connections from inside containers
- Solution: Add
--network=hostflag topodman build:podman build \ --network=host \ --format docker \ --platform linux/${{ matrix.arch }} \ -f ./Containerfile \ . - Verification: Test repository at https://github.com/pigfoot/test-bun-ssl-issue
- GitHub Issue: https://github.com/actions/runner-images/issues/13422
- Note: This is a known issue with ubuntu-24.04 runners. The
--network=hostworkaround reduces network isolation during build but is acceptable for CI/CD use cases.
Authentication failed:
- Ensure GITHUB_TOKEN has package write permission
- Check registry URL and credentials
Manifest add failed:
- Verify architecture-specific images exist in registry
- Check digest format is correct (
sha256:...)
ARM64 runner not available:
- Standard ARM64 runners only work for public repos
- For private repos, use QEMU or larger runners
podman-static installation fails:
- Verify correct architecture detection
- Check GitHub releases for podman-static availability
AppArmor issues:
- Install binaries to
/usr/bin/not/usr/local/bin/ - Run
podman system migrateafter installation
Wrong architecture pulled:
- Always use
--platformflag when pulling - Use
--format dockerwhen building for compatibility