From c282a72bd35db062cec25770a42cf9c803388e44 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:22:05 +0200 Subject: [PATCH 1/5] feat: support Apple Silicon natively via multi-arch images --- README.md | 2 ++ docker-compose.mac.override.yml | 27 +++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 docker-compose.mac.override.yml diff --git a/README.md b/README.md index d8645fde..23a9cbbd 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,8 @@ Note on Docker file permissions (PUID/PGID) - `PGID=1000` - Without this, creating events, uploads, thumbnails, or logs can fail with "Permission denied". +**ARM64 (aarch64) systems:** Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.mac.override.yml](docker-compose.mac.override.yml) for a transitional fallback. + ## 🔄 Release Channels PicPeak offers two release channels for different needs: diff --git a/docker-compose.mac.override.yml b/docker-compose.mac.override.yml new file mode 100644 index 00000000..0da0013b --- /dev/null +++ b/docker-compose.mac.override.yml @@ -0,0 +1,27 @@ +# Apple Silicon (ARM64) fallback override. +# +# This file is ONLY needed for Mac users who are pulling an image tag that was +# published before multi-arch CI was rolled out (e.g. an old :latest or a +# pre-multi-arch :stable still in your local cache). +# +# Once you're on a release whose backend/frontend manifests include linux/arm64, +# you do NOT need this file — Docker selects the native arm64 image automatically. +# +# Usage (Mac Studio / Apple Silicon only): +# +# docker compose \ +# -f docker-compose.production.yml \ +# -f docker-compose.mac.override.yml \ +# up -d +# +# Linux users: do NOT load this file. Forcing platform: linux/amd64 on Linux +# ARM64 hosts (Raspberry Pi, AWS Graviton, etc.) would push you into emulation. +# +# postgres + redis are omitted — their upstream images are already multi-arch +# and run natively on Apple Silicon without any platform flag. + +services: + backend: + platform: linux/amd64 + frontend: + platform: linux/amd64 From ec2eaf76ea713300f82c73da4d94565534e78cd6 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:22:45 +0200 Subject: [PATCH 2/5] ci: build multi-arch images on every channel via native arm64 runners --- .github/workflows/docker-build.yml | 403 +++++++++++++++++++++-------- 1 file changed, 290 insertions(+), 113 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7a87dbb2..37024eb1 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -1,10 +1,22 @@ name: Build and Push Docker Images # This workflow is triggered by: -# - Push to main/develop branches (builds 'latest' or branch-tagged images) +# - Push to main/beta branches (builds 'latest'/'stable' or 'beta' tagged images) # - Version tags from Release Please (e.g., v1.2.0 -> builds versioned images) # - GitHub Releases (created by Release Please) +# - Pull requests (build verification only, no push by default) # - Manual workflow dispatch +# +# Multi-arch strategy: +# Each image (backend, frontend) is built once per architecture on a +# native runner — linux/amd64 on ubuntu-latest, linux/arm64 on +# ubuntu-24.04-arm. Each leg pushes by digest to GHCR. A follow-up +# merge job combines the digests into a multi-arch manifest and applies +# the human-readable tags. This is the pattern documented at +# https://docs.docker.com/build/ci/github-actions/multi-platform/ +# +# Native runners are used instead of QEMU because npm install under +# QEMU was previously too slow/unreliable for regular branch builds. on: push: @@ -31,47 +43,34 @@ env: FRONTEND_IMAGE_NAME: ${{ github.repository }}/frontend jobs: + # ----------------------------------------------------------------------------- + # Backend: per-arch build, then merge into a multi-arch manifest + # ----------------------------------------------------------------------------- build-backend: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write - security-events: write + # Skip the arm64 leg for PRs and verify-only manual runs to keep CI cheap. + # Building amd64 alone is enough to catch Dockerfile/build regressions. + if: matrix.platform == 'linux/amd64' || (github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.push == 'true')) steps: - name: Checkout code uses: actions/checkout@v4 - - name: Determine build context - id: context + - name: Prepare platform pair run: | - # Determine if this is a beta or stable release - if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then - echo "channel=beta" >> $GITHUB_OUTPUT - echo "is_prerelease=true" >> $GITHUB_OUTPUT - else - echo "channel=stable" >> $GITHUB_OUTPUT - echo "is_prerelease=false" >> $GITHUB_OUTPUT - fi - - - name: Determine build platforms - id: platforms - run: | - # Only build ARM64 for tagged releases (v*.*.*) - # QEMU emulation is too slow/unreliable for npm operations on regular builds - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT - echo "skip_qemu=false" >> $GITHUB_OUTPUT - else - echo "platforms=linux/amd64" >> $GITHUB_OUTPUT - echo "skip_qemu=true" >> $GITHUB_OUTPUT - fi - - - name: Set up QEMU - if: steps.platforms.outputs.skip_qemu != 'true' - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 + platform="${{ matrix.platform }}" + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -86,6 +85,102 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine if pushing + id: push-decision + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.inputs.push }}" != "true" ]]; then + echo "push=false" >> "$GITHUB_OUTPUT" + elif [[ "${{ steps.login-ghcr.outcome }}" != "success" ]]; then + echo "push=false" >> "$GITHUB_OUTPUT" + else + echo "push=true" >> "$GITHUB_OUTPUT" + fi + + - name: Extract metadata for Backend (labels only) + id: meta-backend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }} + labels: | + org.opencontainers.image.title=PicPeak Backend + org.opencontainers.image.description=PicPeak photo sharing platform backend service + org.opencontainers.image.vendor=PicPeak + maintainer=${{ github.repository_owner }} + + - name: Build Backend image (push by digest) + id: build + uses: docker/build-push-action@v5 + with: + context: ./backend + file: ./backend/Dockerfile + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta-backend.outputs.labels }} + cache-from: type=gha,scope=backend-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=backend-${{ env.PLATFORM_PAIR }} + outputs: ${{ steps.push-decision.outputs.push == 'true' && format('type=image,name={0}/{1},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY, env.BACKEND_IMAGE_NAME) || 'type=cacheonly' }} + build-args: | + CACHEBUST=${{ github.run_number }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} + VCS_REF=${{ github.sha }} + VERSION=${{ steps.meta-backend.outputs.version }} + + - name: Export digest + if: steps.push-decision.outputs.push == 'true' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest artifact + if: steps.push-decision.outputs.push == 'true' + uses: actions/upload-artifact@v4 + with: + name: digests-backend-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-backend: + needs: build-backend + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + security-events: write + # Only run when at least one digest was pushed (i.e. not on PRs without push intent). + if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' + + steps: + - name: Download digest artifacts + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-backend-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + id: login-ghcr + continue-on-error: true + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine build context + id: context + run: | + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + echo "channel=beta" >> $GITHUB_OUTPUT + echo "is_prerelease=true" >> $GITHUB_OUTPUT + else + echo "channel=stable" >> $GITHUB_OUTPUT + echo "is_prerelease=false" >> $GITHUB_OUTPUT + fi + - name: Extract metadata for Backend id: meta-backend uses: docker/metadata-action@v5 @@ -107,23 +202,15 @@ jobs: type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} - - name: Build and push Backend Docker image - uses: docker/build-push-action@v5 - with: - context: ./backend - file: ./backend/Dockerfile - # Always build; only push when registry login succeeded - push: ${{ (github.event_name != 'pull_request' || github.event.inputs.push == 'true') && steps.login-ghcr.outcome == 'success' }} - tags: ${{ steps.meta-backend.outputs.tags }} - labels: ${{ steps.meta-backend.outputs.labels }} - platforms: ${{ steps.platforms.outputs.platforms }} - cache-from: type=gha,scope=backend - cache-to: type=gha,mode=max,scope=backend - build-args: | - CACHEBUST=${{ github.run_number }} - BUILD_DATE=${{ github.event.head_commit.timestamp }} - VCS_REF=${{ github.sha }} - VERSION=${{ steps.meta-backend.outputs.version }} + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}@sha256:%s " *) + + - name: Inspect manifest + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }} - name: Run Trivy vulnerability scanner if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success' @@ -142,47 +229,32 @@ jobs: sarif_file: 'trivy-backend.sarif' category: 'backend-vulnerabilities' + # ----------------------------------------------------------------------------- + # Frontend: per-arch build, then merge into a multi-arch manifest + # ----------------------------------------------------------------------------- build-frontend: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + runner: ubuntu-latest + - platform: linux/arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} permissions: contents: read packages: write - security-events: write + if: matrix.platform == 'linux/amd64' || (github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.push == 'true')) steps: - name: Checkout code uses: actions/checkout@v4 - - name: Determine build context - id: context + - name: Prepare platform pair run: | - # Determine if this is a beta or stable release - if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then - echo "channel=beta" >> $GITHUB_OUTPUT - echo "is_prerelease=true" >> $GITHUB_OUTPUT - else - echo "channel=stable" >> $GITHUB_OUTPUT - echo "is_prerelease=false" >> $GITHUB_OUTPUT - fi - - - name: Determine build platforms - id: platforms - run: | - # Only build ARM64 for tagged releases (v*.*.*) - # QEMU emulation is too slow/unreliable for npm operations on regular builds - if [[ "${{ github.ref }}" == refs/tags/v* ]]; then - echo "platforms=linux/amd64,linux/arm64" >> $GITHUB_OUTPUT - echo "skip_qemu=false" >> $GITHUB_OUTPUT - else - echo "platforms=linux/amd64" >> $GITHUB_OUTPUT - echo "skip_qemu=true" >> $GITHUB_OUTPUT - fi - - - name: Set up QEMU - if: steps.platforms.outputs.skip_qemu != 'true' - uses: docker/setup-qemu-action@v3 - with: - platforms: arm64 + platform="${{ matrix.platform }}" + echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -197,6 +269,101 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Determine if pushing + id: push-decision + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.inputs.push }}" != "true" ]]; then + echo "push=false" >> "$GITHUB_OUTPUT" + elif [[ "${{ steps.login-ghcr.outcome }}" != "success" ]]; then + echo "push=false" >> "$GITHUB_OUTPUT" + else + echo "push=true" >> "$GITHUB_OUTPUT" + fi + + - name: Extract metadata for Frontend (labels only) + id: meta-frontend + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }} + labels: | + org.opencontainers.image.title=PicPeak Frontend + org.opencontainers.image.description=PicPeak photo sharing platform frontend application + org.opencontainers.image.vendor=PicPeak + maintainer=${{ github.repository_owner }} + + - name: Build Frontend image (push by digest) + id: build + uses: docker/build-push-action@v5 + with: + context: ./frontend + file: ./frontend/Dockerfile + platforms: ${{ matrix.platform }} + labels: ${{ steps.meta-frontend.outputs.labels }} + cache-from: type=gha,scope=frontend-${{ env.PLATFORM_PAIR }} + cache-to: type=gha,mode=max,scope=frontend-${{ env.PLATFORM_PAIR }} + outputs: ${{ steps.push-decision.outputs.push == 'true' && format('type=image,name={0}/{1},push-by-digest=true,name-canonical=true,push=true', env.REGISTRY, env.FRONTEND_IMAGE_NAME) || 'type=cacheonly' }} + build-args: | + CACHEBUST=${{ github.run_number }} + BUILD_DATE=${{ github.event.head_commit.timestamp }} + VCS_REF=${{ github.sha }} + VERSION=${{ steps.meta-frontend.outputs.version }} + + - name: Export digest + if: steps.push-decision.outputs.push == 'true' + run: | + mkdir -p /tmp/digests + digest="${{ steps.build.outputs.digest }}" + touch "/tmp/digests/${digest#sha256:}" + + - name: Upload digest artifact + if: steps.push-decision.outputs.push == 'true' + uses: actions/upload-artifact@v4 + with: + name: digests-frontend-${{ env.PLATFORM_PAIR }} + path: /tmp/digests/* + if-no-files-found: error + retention-days: 1 + + merge-frontend: + needs: build-frontend + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + security-events: write + if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' + + steps: + - name: Download digest artifacts + uses: actions/download-artifact@v4 + with: + path: /tmp/digests + pattern: digests-frontend-* + merge-multiple: true + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + id: login-ghcr + continue-on-error: true + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine build context + id: context + run: | + if [[ "${{ github.ref }}" == refs/tags/v*-beta* ]] || [[ "${{ github.ref }}" == refs/heads/beta ]]; then + echo "channel=beta" >> $GITHUB_OUTPUT + echo "is_prerelease=true" >> $GITHUB_OUTPUT + else + echo "channel=stable" >> $GITHUB_OUTPUT + echo "is_prerelease=false" >> $GITHUB_OUTPUT + fi + - name: Extract metadata for Frontend id: meta-frontend uses: docker/metadata-action@v5 @@ -218,23 +385,15 @@ jobs: type=raw,value=stable,enable=${{ github.ref == 'refs/heads/main' || (startsWith(github.ref, 'refs/tags/v') && steps.context.outputs.is_prerelease == 'false') }} type=raw,value=beta,enable=${{ github.ref == 'refs/heads/beta' || steps.context.outputs.is_prerelease == 'true' }} - - name: Build and push Frontend Docker image - uses: docker/build-push-action@v5 - with: - context: ./frontend - file: ./frontend/Dockerfile - # Always build; only push when registry login succeeded - push: ${{ (github.event_name != 'pull_request' || github.event.inputs.push == 'true') && steps.login-ghcr.outcome == 'success' }} - tags: ${{ steps.meta-frontend.outputs.tags }} - labels: ${{ steps.meta-frontend.outputs.labels }} - platforms: ${{ steps.platforms.outputs.platforms }} - cache-from: type=gha,scope=frontend - cache-to: type=gha,mode=max,scope=frontend - build-args: | - CACHEBUST=${{ github.run_number }} - BUILD_DATE=${{ github.event.head_commit.timestamp }} - VCS_REF=${{ github.sha }} - VERSION=${{ steps.meta-frontend.outputs.version }} + - name: Create and push multi-arch manifest + working-directory: /tmp/digests + run: | + docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \ + $(printf "${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}@sha256:%s " *) + + - name: Inspect manifest + run: | + docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }} - name: Run Trivy vulnerability scanner if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success' @@ -253,45 +412,63 @@ jobs: sarif_file: 'trivy-frontend.sarif' category: 'frontend-vulnerabilities' - # Note: The publish-manifest job is not needed since docker/build-push-action@v5 - # automatically creates multi-arch manifests when building for multiple platforms. - # The images are already properly tagged and include all architectures. - summary: - needs: [build-backend, build-frontend] + needs: [build-backend, merge-backend, build-frontend, merge-frontend] if: always() runs-on: ubuntu-latest permissions: contents: read - + steps: - name: Build Summary run: | echo "## đŸŗ Docker Build Summary" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [[ "${{ needs.build-backend.result }}" == "success" ]]; then - echo "✅ **Backend**: Successfully built" >> $GITHUB_STEP_SUMMARY + echo "✅ **Backend build (per-arch)**: Successfully built" >> $GITHUB_STEP_SUMMARY else - echo "❌ **Backend**: Build failed" >> $GITHUB_STEP_SUMMARY + echo "❌ **Backend build (per-arch)**: ${{ needs.build-backend.result }}" >> $GITHUB_STEP_SUMMARY fi - + + if [[ "${{ needs.merge-backend.result }}" == "success" ]]; then + echo "✅ **Backend manifest merge**: Successfully published" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ needs.merge-backend.result }}" == "skipped" ]]; then + echo "â„šī¸ **Backend manifest merge**: Skipped (verify-only build)" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Backend manifest merge**: ${{ needs.merge-backend.result }}" >> $GITHUB_STEP_SUMMARY + fi + if [[ "${{ needs.build-frontend.result }}" == "success" ]]; then - echo "✅ **Frontend**: Successfully built" >> $GITHUB_STEP_SUMMARY + echo "✅ **Frontend build (per-arch)**: Successfully built" >> $GITHUB_STEP_SUMMARY else - echo "❌ **Frontend**: Build failed" >> $GITHUB_STEP_SUMMARY + echo "❌ **Frontend build (per-arch)**: ${{ needs.build-frontend.result }}" >> $GITHUB_STEP_SUMMARY fi - + + if [[ "${{ needs.merge-frontend.result }}" == "success" ]]; then + echo "✅ **Frontend manifest merge**: Successfully published" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ needs.merge-frontend.result }}" == "skipped" ]]; then + echo "â„šī¸ **Frontend manifest merge**: Skipped (verify-only build)" >> $GITHUB_STEP_SUMMARY + else + echo "❌ **Frontend manifest merge**: ${{ needs.merge-frontend.result }}" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY echo "### đŸ“Ļ Images" >> $GITHUB_STEP_SUMMARY echo "- Backend: \`${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY echo "- Frontend: \`${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY - + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### đŸ—ī¸ Architectures" >> $GITHUB_STEP_SUMMARY + echo "Published manifests include both \`linux/amd64\` and \`linux/arm64\` (built natively, no QEMU)." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY echo "### đŸˇī¸ Tags" >> $GITHUB_STEP_SUMMARY echo "Images are tagged based on:" >> $GITHUB_STEP_SUMMARY echo "- Branch name (for branch pushes)" >> $GITHUB_STEP_SUMMARY - echo "- PR number (for pull requests)" >> $GITHUB_STEP_SUMMARY + echo "- PR number (for pull requests, when push is enabled)" >> $GITHUB_STEP_SUMMARY echo "- Version tags (for releases)" >> $GITHUB_STEP_SUMMARY - echo "- Short SHA with branch prefix" >> $GITHUB_STEP_SUMMARY + echo "- Short SHA" >> $GITHUB_STEP_SUMMARY echo "- \`latest\` (for main branch)" >> $GITHUB_STEP_SUMMARY + echo "- \`stable\` (for main branch and stable releases)" >> $GITHUB_STEP_SUMMARY + echo "- \`beta\` (for beta branch and pre-releases)" >> $GITHUB_STEP_SUMMARY From ede5193e5847516914924b25cdc0e070bd5a5aea Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 1 May 2026 08:53:17 +0200 Subject: [PATCH 3/5] Update docker-build.yml --- .github/workflows/docker-build.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 37024eb1..9c99c43f 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -59,9 +59,6 @@ jobs: permissions: contents: read packages: write - # Skip the arm64 leg for PRs and verify-only manual runs to keep CI cheap. - # Building amd64 alone is enough to catch Dockerfile/build regressions. - if: matrix.platform == 'linux/amd64' || (github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.push == 'true')) steps: - name: Checkout code @@ -245,7 +242,6 @@ jobs: permissions: contents: read packages: write - if: matrix.platform == 'linux/amd64' || (github.event_name != 'pull_request' && (github.event_name != 'workflow_dispatch' || github.event.inputs.push == 'true')) steps: - name: Checkout code From 3440ecc99957d158a10a07e1c0bb11e18ad8e97c Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 1 May 2026 08:59:33 +0200 Subject: [PATCH 4/5] ci: lowercase image names for GHCR compatibility on forks --- .github/workflows/docker-build.yml | 37 ++++++++++++++++++++++++++++-- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 9c99c43f..4e273b96 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -39,8 +39,11 @@ on: env: REGISTRY: ghcr.io - BACKEND_IMAGE_NAME: ${{ github.repository }}/backend - FRONTEND_IMAGE_NAME: ${{ github.repository }}/frontend + # BACKEND_IMAGE_NAME and FRONTEND_IMAGE_NAME are computed per job in the + # "Compute image names" step. GHCR requires all-lowercase repository names, + # but ${{ github.repository }} preserves the original case (e.g. "Luca-Timo/..."). + # Computing them with bash parameter expansion (${VAR,,}) keeps the workflow + # working on forks regardless of the owner's name casing. jobs: # ----------------------------------------------------------------------------- @@ -64,6 +67,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Compute image names (lowercase for GHCR) + run: | + repo_lc="${GITHUB_REPOSITORY,,}" + echo "BACKEND_IMAGE_NAME=${repo_lc}/backend" >> "$GITHUB_ENV" + echo "FRONTEND_IMAGE_NAME=${repo_lc}/frontend" >> "$GITHUB_ENV" + - name: Prepare platform pair run: | platform="${{ matrix.platform }}" @@ -148,6 +157,12 @@ jobs: if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' steps: + - name: Compute image names (lowercase for GHCR) + run: | + repo_lc="${GITHUB_REPOSITORY,,}" + echo "BACKEND_IMAGE_NAME=${repo_lc}/backend" >> "$GITHUB_ENV" + echo "FRONTEND_IMAGE_NAME=${repo_lc}/frontend" >> "$GITHUB_ENV" + - name: Download digest artifacts uses: actions/download-artifact@v4 with: @@ -247,6 +262,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 + - name: Compute image names (lowercase for GHCR) + run: | + repo_lc="${GITHUB_REPOSITORY,,}" + echo "BACKEND_IMAGE_NAME=${repo_lc}/backend" >> "$GITHUB_ENV" + echo "FRONTEND_IMAGE_NAME=${repo_lc}/frontend" >> "$GITHUB_ENV" + - name: Prepare platform pair run: | platform="${{ matrix.platform }}" @@ -330,6 +351,12 @@ jobs: if: github.event_name != 'pull_request' || github.event.inputs.push == 'true' steps: + - name: Compute image names (lowercase for GHCR) + run: | + repo_lc="${GITHUB_REPOSITORY,,}" + echo "BACKEND_IMAGE_NAME=${repo_lc}/backend" >> "$GITHUB_ENV" + echo "FRONTEND_IMAGE_NAME=${repo_lc}/frontend" >> "$GITHUB_ENV" + - name: Download digest artifacts uses: actions/download-artifact@v4 with: @@ -416,6 +443,12 @@ jobs: contents: read steps: + - name: Compute image names (lowercase for GHCR) + run: | + repo_lc="${GITHUB_REPOSITORY,,}" + echo "BACKEND_IMAGE_NAME=${repo_lc}/backend" >> "$GITHUB_ENV" + echo "FRONTEND_IMAGE_NAME=${repo_lc}/frontend" >> "$GITHUB_ENV" + - name: Build Summary run: | echo "## đŸŗ Docker Build Summary" >> $GITHUB_STEP_SUMMARY From c7ac9ddfe519930f185edb0bae18db350cc6a406 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Fri, 1 May 2026 09:38:41 +0200 Subject: [PATCH 5/5] refactor: rename mac override to amd64 override for arch accuracy --- README.md | 2 +- docker-compose.amd64.override.yml | 33 +++++++++++++++++++++++++++++++ docker-compose.mac.override.yml | 27 ------------------------- 3 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 docker-compose.amd64.override.yml delete mode 100644 docker-compose.mac.override.yml diff --git a/README.md b/README.md index 23a9cbbd..b1398621 100644 --- a/README.md +++ b/README.md @@ -97,7 +97,7 @@ Note on Docker file permissions (PUID/PGID) - `PGID=1000` - Without this, creating events, uploads, thumbnails, or logs can fail with "Permission denied". -**ARM64 (aarch64) systems:** Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.mac.override.yml](docker-compose.mac.override.yml) for a transitional fallback. +**ARM64 (aarch64) systems:** Pre-built images include native `linux/arm64`, no platform flags or emulation needed. If you're on an older image tag that's still amd64-only, see [docker-compose.amd64.override.yml](docker-compose.amd64.override.yml) for a transitional fallback. ## 🔄 Release Channels diff --git a/docker-compose.amd64.override.yml b/docker-compose.amd64.override.yml new file mode 100644 index 00000000..bda75227 --- /dev/null +++ b/docker-compose.amd64.override.yml @@ -0,0 +1,33 @@ +# Force linux/amd64 for the PicPeak backend and frontend images. +# +# When you need this: +# You're on an ARM64 host (Apple Silicon Mac, Raspberry Pi, AWS Graviton, +# Ampere, etc.) AND the image tag you're pulling does NOT yet have a +# linux/arm64 variant — for example, an older :latest or a pre-multi-arch +# :stable still cached locally. +# +# When you do NOT need this: +# Once you're on a release whose backend/frontend manifests include both +# linux/amd64 and linux/arm64, Docker selects the native arm64 image +# automatically. Drop this override. +# +# Usage: +# +# docker compose \ +# -f docker-compose.production.yml \ +# -f docker-compose.amd64.override.yml \ +# up -d +# +# Performance note: forcing amd64 on an ARM64 Linux host invokes qemu-user +# emulation, which is significantly slower (especially for the backend's +# Node.js workloads). Apple Silicon uses Rosetta 2, which is faster but still +# slower than native arm64. Prefer pulling a multi-arch tag whenever possible. +# +# postgres + redis are intentionally omitted — their upstream images are +# already multi-arch and pull natively on every supported architecture. + +services: + backend: + platform: linux/amd64 + frontend: + platform: linux/amd64 diff --git a/docker-compose.mac.override.yml b/docker-compose.mac.override.yml deleted file mode 100644 index 0da0013b..00000000 --- a/docker-compose.mac.override.yml +++ /dev/null @@ -1,27 +0,0 @@ -# Apple Silicon (ARM64) fallback override. -# -# This file is ONLY needed for Mac users who are pulling an image tag that was -# published before multi-arch CI was rolled out (e.g. an old :latest or a -# pre-multi-arch :stable still in your local cache). -# -# Once you're on a release whose backend/frontend manifests include linux/arm64, -# you do NOT need this file — Docker selects the native arm64 image automatically. -# -# Usage (Mac Studio / Apple Silicon only): -# -# docker compose \ -# -f docker-compose.production.yml \ -# -f docker-compose.mac.override.yml \ -# up -d -# -# Linux users: do NOT load this file. Forcing platform: linux/amd64 on Linux -# ARM64 hosts (Raspberry Pi, AWS Graviton, etc.) would push you into emulation. -# -# postgres + redis are omitted — their upstream images are already multi-arch -# and run natively on Apple Silicon without any platform flag. - -services: - backend: - platform: linux/amd64 - frontend: - platform: linux/amd64