Skip to content

Troubleshooting ​

This document covers common issues and solutions when building multi-architecture images.

Build Issues ​

Version Detection Fails ​

Symptom:

Error: tag is empty
Latest release:

Causes:

  1. GitHub API rate limiting
  2. Repository doesn't have releases
  3. Release tag format unexpected
  4. Network connectivity issues

Solutions:

  1. Check repository has releases:

    sh
    curl -s https://api.github.com/repos/owner/repo/releases/latest | jq -r '.tag_name'
  2. Add authentication to avoid rate limits:

    yaml
    - name: Get latest release
      run: |
        TAG=$(curl -s \
          -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
          https://api.github.com/repos/owner/repo/releases/latest | jq -r '.tag_name')
  3. Alternative: Use git tags:

    yaml
    - name: Get latest tag
      run: |
        TAG=$(git ls-remote --tags --refs --sort='version:refname' \
          https://github.com/owner/repo.git | tail -n1 | cut -d'/' -f3)
  4. Check tag format:

    sh
    # Releases page shows: v1.2.3
    # But API returns: 1.2.3 (without 'v')
    # Adjust parsing accordingly
    TAG=$(curl ... | jq -r '.tag_name' | sed 's/^v//')

Image Already Exists Check Fails ​

Symptom:

  • Builds run even when image exists
  • Or builds never run even for new versions

Solutions:

  1. Verify registry path:

    sh
    # Check exact path
    docker manifest inspect ghcr.io/this-is-tobi/mirror/myapp:v1.0.0
  2. Check authentication:

    yaml
    - name: Check image exists
      env:
        GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      run: |
        TOKEN=$(printf '%s' "$GH_TOKEN" | base64 -w0)
        STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
          -H "Authorization: Bearer ${TOKEN}" \
          https://ghcr.io/v2/${{ inputs.NAMESPACE }}/myapp/manifests/${{ steps.release.outputs.tag }})

    Encode with printf and base64 -w0, not echo ... | base64. echo appends a newline to the token before encoding, and base64 wraps its output at 76 columns — harmless for a short token, but GITHUB_TOKEN is being migrated to a ~520 character format that encodes to several wrapped lines and produces a malformed Authorization header. See the changelog.

  3. Alternative method using docker:

    yaml
    - name: Check image exists
      run: |
        if docker manifest inspect ghcr.io/.../myapp:${TAG} > /dev/null 2>&1; then
          echo "image-status=200" >> $GITHUB_OUTPUT
        else
          echo "image-status=404" >> $GITHUB_OUTPUT
        fi

Docker Build Fails on ARM64 ​

Symptom:

exec format error

or

Error: failed to solve: process "/bin/sh -c go build" did not complete successfully

Causes:

  1. QEMU not set up correctly
  2. Binary has wrong architecture
  3. Using emulation instead of cross-compilation

Solutions:

  1. Enable QEMU:

    yaml
    - name: Set up QEMU
      uses: docker/setup-qemu-action@v3
  2. Use cross-compilation:

    dockerfile
    FROM --platform=$BUILDPLATFORM golang:1.24 AS builder
    ARG TARGETOS
    ARG TARGETARCH
    ENV GOOS=${TARGETOS}
    ENV GOARCH=${TARGETARCH}
    RUN go build -o /app/binary
  3. Verify binary architecture:

    yaml
    - name: Check binary
      run: |
        file ./binary
        # Should show: ELF 64-bit LSB executable, ARM aarch64
        # Not: ELF 64-bit LSB executable, x86-64
  4. Use buildx properly:

    yaml
    - uses: docker/build-push-action@v6
      with:
        platforms: linux/arm64
        # NOT: platform: linux/arm64 (singular, wrong!)

Binary Compilation Fails ​

Symptom:

Error: GOARCH not set correctly
Error: undefined reference for ARM64

Solutions:

  1. Verify environment variables:

    yaml
    - name: Debug
      run: |
        echo "GOOS=${GOOS}"
        echo "GOARCH=${GOARCH}"
        echo "ARCH=${{ matrix.images.arch }}"
  2. Check Dockerfile sets variables:

    dockerfile
    ARG TARGETARCH
    ENV GOARCH=${TARGETARCH}
    
    # Or explicitly:
    RUN GOARCH=arm64 go build ...
  3. Use correct GOARCH values:

    • amd64 → GOARCH=amd64
    • arm64 → GOARCH=arm64
    • arm/v7 → GOARCH=arm GOARM=7
  4. Disable CGO if needed:

    dockerfile
    ENV CGO_ENABLED=0
    RUN go build -o /app/binary

Build Cache Issues ​

Symptom:

  • Cache not being used
  • Cache growing indefinitely
  • "no space left on device"

Solutions:

  1. Implement cache rotation:

    yaml
    - uses: docker/build-push-action@v6
      with:
        cache-from: type=local,src=/tmp/.buildx-cache
        cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
    
    - name: Move cache
      run: |
        rm -rf /tmp/.buildx-cache
        mv /tmp/.buildx-cache-new /tmp/.buildx-cache
  2. Clear cache manually:

    sh
    # In GitHub Actions
    # Go to: Settings → Actions → Caches
    # Delete old caches
  3. Use registry cache (more robust):

    yaml
    cache-from: type=registry,ref=ghcr.io/.../myapp:buildcache
    cache-to: type=registry,ref=ghcr.io/.../myapp:buildcache,mode=max

Manifest Issues ​

Manifest Creation Fails ​

Symptom:

Error: manifest unknown
Error: digest not found

Solutions:

  1. Verify all builds succeeded:

    yaml
    # Check build job completed for all matrix entries
    needs: [infos, build]
  2. Check digest artifacts:

    yaml
    - name: Download digests
      uses: actions/download-artifact@v4
      with:
        pattern: digests-myapp-*
        merge-multiple: true
    
    - name: Debug
      run: |
        ls -la /tmp/digests/myapp/
        cat /tmp/digests/myapp/*
  3. Verify digest format:

    sh
    # Digests should be sha256:xxxxx (without sha256: in filename)
    # Filename: abc123def456
    # Full digest: sha256:abc123def456
  4. Check GHCR authentication:

    yaml
    - name: Login to GHCR
      uses: docker/login-action@v3
      with:
        registry: ghcr.io
        username: ${{ github.actor }}
        password: ${{ secrets.GITHUB_TOKEN }}

Multi-Arch Manifest Missing Platform ​

Symptom:

sh
docker manifest inspect ghcr.io/.../myapp:latest
# Shows only amd64, missing arm64

Solutions:

  1. Check both builds completed:

    sh
    # In GitHub Actions, verify:
    # - build (myapp, amd64) ✅
    # - build (myapp, arm64) ✅
  2. Verify digest upload:

    yaml
    - name: Upload digest
      uses: actions/upload-artifact@v4
      with:
        name: digests-myapp-${{ matrix.images.arch }}  # Include arch!
        path: /tmp/digests/myapp/*
  3. Check merge download pattern:

    yaml
    - name: Download digests
      uses: actions/download-artifact@v4
      with:
        pattern: digests-myapp-*  # Wildcard matches all arches
        merge-multiple: true
  4. Verify imagetools command:

    yaml
    - name: Create manifest
      run: |
        docker buildx imagetools create \
          -t ghcr.io/.../myapp:latest \
          $(printf 'ghcr.io/.../myapp@sha256:%s ' *)
        # Should process 2+ digests

Manifest Inspect Shows Wrong Architecture ​

Symptom:

sh
# ARM64 image shows amd64 in manifest

Solutions:

  1. Check binary architecture:

    yaml
    - name: Verify binary
      run: |
        file ./binary
        # ARM64 should show: ARM aarch64
  2. Verify platform flag:

    yaml
    - uses: docker/build-push-action@v6
      with:
        platforms: ${{ matrix.images.platforms }}  # linux/arm64
  3. Check Dockerfile base image:

    dockerfile
    # Wrong: Forces amd64
    FROM alpine:latest
    
    # Correct: Respects target platform
    FROM --platform=$TARGETPLATFORM alpine:latest

Matrix Issues ​

Matrix Empty or Not Loading ​

Symptom:

  • Build job doesn't run
  • "Matrix is empty" error

Solutions:

  1. Validate JSON:

    sh
    cat apps/myapp/matrix.json | jq .
    # Should show prettified JSON
    # Errors indicate invalid JSON
  2. Check file path:

    yaml
    - name: Read matrix
      run: |
        # Verify path matches directory structure
        cat apps/myapp/matrix.json | jq -c .
  3. Debug matrix output:

    yaml
    - name: Read matrix
      id: matrix
      run: |
        MATRIX=$(cat apps/myapp/matrix.json | jq -c .)
        echo "matrix=${MATRIX}" >> $GITHUB_OUTPUT
        echo "Matrix content: ${MATRIX}"  # Debug

Wrong Number of Build Jobs ​

Symptom:

  • Expected 4 builds, only 2 run
  • Or unexpected number of jobs

Solutions:

  1. Count matrix entries:

    sh
    cat apps/myapp/matrix.json | jq 'length'
  2. Check for duplicates:

    sh
    cat apps/myapp/matrix.json | jq 'group_by(.component, .arch) | map(select(length > 1))'
  3. Verify MULTI_ARCH filtering:

    yaml
    if [ "${{ inputs.MULTI_ARCH }}" = "true" ]; then
      echo "matrix=$(cat apps/myapp/matrix.json | jq -c .)"
    else
      echo "matrix=$(cat apps/myapp/matrix.json | jq -c '[.[] | select(.arch == "amd64")]')"
    fi

Matrix Values Not Accessible ​

Symptom:

Error: matrix.images.component is undefined

Solutions:

  1. Check matrix variable name:

    yaml
    strategy:
      matrix:
        images: ${{ fromJSON(needs.infos.outputs.matrix) }}
    
    # Access with: matrix.images.component
    # NOT: matrix.component
  2. Verify fromJSON:

    yaml
    # Correct:
    images: ${{ fromJSON(needs.infos.outputs.matrix) }}
    
    # Wrong:
    images: ${{ needs.infos.outputs.matrix }}  # Missing fromJSON!

GitHub Actions Issues ​

Workflow Not Triggering ​

Symptom:

  • Workflow doesn't run on schedule
  • Manual trigger doesn't work

Solutions:

  1. Check workflow syntax:

    sh
    yamllint .github/workflows/myapp.yml
    # Or use actionlint
    actionlint .github/workflows/myapp.yml
  2. Verify cron syntax:

    yaml
    on:
      schedule:
        - cron: '0 */6 * * *'  # ✅ Correct (every 6 hours)
        # Not: cron: '0 1 * * * *'  # ❌ Wrong (6 fields)
  3. Check repository settings:

    • Settings → Actions → General
    • Ensure Actions are enabled
    • Verify workflow permissions
  4. Default branch:

    • Scheduled workflows only run on default branch
    • Merge to main/master first

Permission Denied Pushing to GHCR ​

Symptom:

Error: denied: permission_denied

Solutions:

  1. Check workflow permissions:

    yaml
    permissions:
      contents: read
      packages: write  # Required!
  2. Verify token scope:

    yaml
    - name: Login to GHCR
      uses: docker/login-action@v3
      with:
        password: ${{ secrets.GITHUB_TOKEN }}  # Use this
        # Not: password: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
  3. Check package visibility:

    • Package must be public or user must have write access
    • Settings → Packages → Change visibility

Artifacts Not Found ​

Symptom:

Error: Artifact 'digests-myapp-arm64' not found

Solutions:

  1. Verify artifact upload:

    yaml
    - name: Upload digest
      uses: actions/upload-artifact@v4
      with:
        name: digests-myapp-${{ matrix.images.arch }}
        path: /tmp/digests/myapp/*
        if-no-files-found: error  # Will fail if no files
  2. Check path exists:

    yaml
    - name: Export digest
      run: |
        mkdir -p /tmp/digests/myapp
        echo "Created directory"
        ls -la /tmp/digests/myapp/
  3. Verify download pattern:

    yaml
    - name: Download digests
      uses: actions/download-artifact@v4
      with:
        pattern: digests-myapp-*  # Must match upload name
        merge-multiple: true

Testing & Debugging ​

Test Build Locally ​

Test Docker build:

sh
# Build for amd64
docker buildx build \
  --platform linux/amd64 \
  --tag myapp:test-amd64 \
  .

# Build for arm64
docker buildx build \
  --platform linux/arm64 \
  --tag myapp:test-arm64 \
  .

Test cross-compilation:

sh
# Create test Dockerfile
cat > Dockerfile.test << 'EOF'
FROM --platform=$BUILDPLATFORM golang:1.24 AS builder
WORKDIR /build
ARG TARGETOS
ARG TARGETARCH
ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH}
RUN echo "Building for ${GOOS}/${GOARCH}"
EOF

docker buildx build \
  --platform linux/arm64 \
  --file Dockerfile.test \
  .

Debug Workflow Variables ​

Add debug step:

yaml
- name: Debug variables
  run: |
    echo "Component: ${{ matrix.images.component }}"
    echo "Arch: ${{ matrix.images.arch }}"
    echo "Platform: ${{ matrix.images.platforms }}"
    echo "Tag: ${{ needs.infos.outputs.tag }}"
    echo "Matrix: ${{ needs.infos.outputs.matrix }}"

Inspect Built Images ​

Check image details:

sh
# Inspect manifest
docker manifest inspect ghcr.io/this-is-tobi/mirror/myapp:latest

# Pull specific architecture
docker pull --platform linux/arm64 ghcr.io/.../myapp:latest

# Check binary architecture
docker run --rm --platform linux/arm64 ghcr.io/.../myapp:latest sh -c 'file /app/binary'

# Test execution
docker run --rm --platform linux/arm64 ghcr.io/.../myapp:latest version

Enable Debug Logging ​

In GitHub Actions:

  1. Repository Settings → Secrets → Variables
  2. Add variable: ACTIONS_STEP_DEBUG = true
  3. Re-run workflow

In Dockerfile:

dockerfile
# Add debug output
RUN echo "GOOS=${GOOS}" && \
    echo "GOARCH=${GOARCH}" && \
    ls -la /build/

Performance Issues ​

Build Takes Too Long ​

Solutions:

  1. Enable build cache:

    yaml
    cache-from: type=local,src=/tmp/.buildx-cache
    cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
  2. Use multi-stage builds:

    dockerfile
    FROM golang:1.24 AS builder
    # Build steps
    
    FROM alpine:latest
    COPY --from=builder /app/binary /app/binary
  3. Reduce layer count:

    dockerfile
    # Combine RUN commands
    RUN apt-get update && \
        apt-get install -y pkg1 pkg2 && \
        rm -rf /var/lib/apt/lists/*
  4. Use cross-compilation instead of QEMU:

    • QEMU emulation is 5-10x slower
    • Cross-compilation is near-native speed

Too Many Parallel Builds ​

Issue: GitHub Actions free tier limited to 20 concurrent jobs

Solutions:

  1. Reduce matrix size temporarily:

    yaml
    # Test with amd64 only first
    echo "matrix=$(cat matrix.json | jq -c '[.[] | select(.arch == "amd64")]')"
  2. Add build dependencies:

    yaml
    strategy:
      max-parallel: 5  # Limit concurrent jobs
  3. Upgrade to GitHub Pro:

    • Increases concurrent job limit

Common Error Messages ​

"exec format error" ​

Meaning: Trying to run binary for wrong architecture

Fix: Verify binary architecture matches platform

"no space left on device" ​

Meaning: Runner disk full (usually cache)

Fix: Implement cache rotation

"manifest unknown" ​

Meaning: Image/tag doesn't exist in registry

Fix: Check image path, verify push succeeded

"failed to solve: process did not complete successfully" ​

Meaning: Command failed during Docker build

Fix: Check specific command output, enable debug logging

"resource not accessible by integration" ​

Meaning: Missing permissions

Fix: Add required permissions in workflow file

Getting Help ​

Check Workflow Logs ​

  1. Go to GitHub Actions tab
  2. Select failed workflow run
  3. Expand failed job
  4. Review step output
  5. Look for error messages

Useful Debug Commands ​

yaml
# List files
- run: ls -laR /tmp/digests/

# Check environment
- run: env | sort

# Test registry access
- run: |
    docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }}
    docker images

# Verify buildx
- run: docker buildx ls

Resources ​

Reporting Issues ​

When reporting an issue, include:

  1. Workflow file (relevant sections)
  2. Matrix.json configuration
  3. Error message (exact text)
  4. Workflow run URL
  5. Steps to reproduce
  6. Expected vs actual behavior