Skip to content

Commit 84719b5

Browse files
enkoclaude
andcommitted
perf(ci): Optimize Docker builds with native ARM runners and improved caching
- Split multi-platform builds to run on native runners (ubuntu-24.04 for amd64, ubuntu-24.04-arm for arm64) instead of emulated QEMU builds - Add registry-based caching alongside GHA cache for better cache hits - Use architecture-specific cache mount IDs to prevent cache collisions - Add separate manifest creation job to combine platform-specific images - Add build dependencies (python3, make, g++) to backend Dockerfile for native module compilation (cpu-features, ssh2) This should significantly reduce ARM64 build times from ~5 minutes (emulated) to ~30 seconds (native), and improve overall cache hit rates. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent fb62e6c commit 84719b5

File tree

4 files changed

+133
-28
lines changed

4 files changed

+133
-28
lines changed

.github/workflows/release.yml

Lines changed: 106 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,25 +81,52 @@ jobs:
8181
echo "new_release_published=false" >> "$GITHUB_OUTPUT"
8282
fi
8383
84-
# Job 3: Build and push Docker images (parallel matrix)
85-
docker:
86-
name: Build ${{ matrix.image }} Image
84+
# Job 3: Build Docker images per platform (native runners for speed)
85+
docker-build:
86+
name: Build ${{ matrix.image }} (${{ matrix.arch }})
8787
needs: release
8888
if: needs.release.outputs.new_release_published == 'true'
89-
runs-on: ubuntu-latest
89+
runs-on: ${{ matrix.runner }}
9090
strategy:
9191
fail-fast: false
9292
matrix:
9393
include:
94+
# nginx - amd64
95+
- image: nginx
96+
dockerfile: docker/Dockerfile.nginx.prod
97+
platform: linux/amd64
98+
runner: ubuntu-24.04
99+
arch: amd64
100+
# nginx - arm64
94101
- image: nginx
95102
dockerfile: docker/Dockerfile.nginx.prod
96-
context: .
103+
platform: linux/arm64
104+
runner: ubuntu-24.04-arm
105+
arch: arm64
106+
# backend - amd64
97107
- image: backend
98108
dockerfile: docker/Dockerfile.backend.prod
99-
context: .
109+
platform: linux/amd64
110+
runner: ubuntu-24.04
111+
arch: amd64
112+
# backend - arm64
113+
- image: backend
114+
dockerfile: docker/Dockerfile.backend.prod
115+
platform: linux/arm64
116+
runner: ubuntu-24.04-arm
117+
arch: arm64
118+
# sabredav - amd64
119+
- image: sabredav
120+
dockerfile: docker/Dockerfile.sabredav.prod
121+
platform: linux/amd64
122+
runner: ubuntu-24.04
123+
arch: amd64
124+
# sabredav - arm64
100125
- image: sabredav
101126
dockerfile: docker/Dockerfile.sabredav.prod
102-
context: .
127+
platform: linux/arm64
128+
runner: ubuntu-24.04-arm
129+
arch: arm64
103130
steps:
104131
- name: Checkout code
105132
uses: actions/checkout@v4
@@ -122,36 +149,95 @@ jobs:
122149
with:
123150
images: ghcr.io/${{ github.repository }}-${{ matrix.image }}
124151
tags: |
125-
type=semver,pattern={{version}},value=v${{ needs.release.outputs.new_release_version }}
126-
type=semver,pattern={{major}}.{{minor}},value=v${{ needs.release.outputs.new_release_version }}
127-
type=semver,pattern={{major}},value=v${{ needs.release.outputs.new_release_version }}
128-
type=raw,value=latest
152+
type=raw,value=${{ needs.release.outputs.new_release_version }}-${{ matrix.arch }}
129153
130154
- name: Build and push Docker image
131-
id: push
155+
id: build
132156
uses: docker/build-push-action@v6
133157
with:
134-
context: ${{ matrix.context }}
158+
context: .
135159
file: ${{ matrix.dockerfile }}
136160
push: true
137161
tags: ${{ steps.meta.outputs.tags }}
138162
labels: ${{ steps.meta.outputs.labels }}
139-
cache-from: type=gha,scope=${{ matrix.image }}
140-
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
141-
platforms: linux/amd64,linux/arm64
163+
platforms: ${{ matrix.platform }}
142164
provenance: mode=max
143165
sbom: true
144166
build-args: ${{ matrix.image == 'nginx' && format('VITE_SENTRY_DSN={0}', secrets.VITE_SENTRY_DSN) || '' }}
167+
# Multi-layer caching strategy for maximum cache hits
168+
cache-from: |
169+
type=gha,scope=${{ matrix.image }}-${{ matrix.arch }}
170+
type=registry,ref=ghcr.io/${{ github.repository }}-${{ matrix.image }}:buildcache-${{ matrix.arch }}
171+
cache-to: |
172+
type=gha,mode=max,scope=${{ matrix.image }}-${{ matrix.arch }}
173+
type=registry,ref=ghcr.io/${{ github.repository }}-${{ matrix.image }}:buildcache-${{ matrix.arch }},mode=max
174+
175+
# Job 4: Create and push multi-arch manifests
176+
docker-manifest:
177+
name: Create ${{ matrix.image }} Manifest
178+
needs: [release, docker-build]
179+
if: needs.release.outputs.new_release_published == 'true'
180+
runs-on: ubuntu-latest
181+
strategy:
182+
fail-fast: false
183+
matrix:
184+
image:
185+
- nginx
186+
- backend
187+
- sabredav
188+
steps:
189+
- name: Login to GitHub Container Registry
190+
uses: docker/login-action@v3
191+
with:
192+
registry: ghcr.io
193+
username: ${{ github.actor }}
194+
password: ${{ secrets.GITHUB_TOKEN }}
195+
196+
- name: Create and push manifest
197+
env:
198+
VERSION: ${{ needs.release.outputs.new_release_version }}
199+
IMAGE: ghcr.io/${{ github.repository }}-${{ matrix.image }}
200+
run: |
201+
# Create manifest for version tag
202+
docker manifest create ${IMAGE}:${VERSION} \
203+
${IMAGE}:${VERSION}-amd64 \
204+
${IMAGE}:${VERSION}-arm64
205+
206+
# Annotate with architecture info
207+
docker manifest annotate ${IMAGE}:${VERSION} ${IMAGE}:${VERSION}-amd64 --arch amd64
208+
docker manifest annotate ${IMAGE}:${VERSION} ${IMAGE}:${VERSION}-arm64 --arch arm64
209+
210+
# Push version manifest
211+
docker manifest push ${IMAGE}:${VERSION}
212+
213+
# Create and push additional tags
214+
for TAG in "${VERSION%.*}" "${VERSION%%.*}" "latest"; do
215+
docker manifest create ${IMAGE}:${TAG} \
216+
${IMAGE}:${VERSION}-amd64 \
217+
${IMAGE}:${VERSION}-arm64
218+
docker manifest annotate ${IMAGE}:${TAG} ${IMAGE}:${VERSION}-amd64 --arch amd64
219+
docker manifest annotate ${IMAGE}:${TAG} ${IMAGE}:${VERSION}-arm64 --arch arm64
220+
docker manifest push ${IMAGE}:${TAG}
221+
done
222+
223+
- name: Get manifest digest
224+
id: digest
225+
env:
226+
IMAGE: ghcr.io/${{ github.repository }}-${{ matrix.image }}
227+
VERSION: ${{ needs.release.outputs.new_release_version }}
228+
run: |
229+
DIGEST=$(docker manifest inspect ${IMAGE}:${VERSION} -v | jq -r '.Descriptor.digest')
230+
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
145231
146232
- name: Attest Docker image
147233
if: ${{ github.event.repository.visibility == 'public' }}
148234
uses: actions/attest-build-provenance@v2
149235
with:
150236
subject-name: ghcr.io/${{ github.repository }}-${{ matrix.image }}
151-
subject-digest: ${{ steps.push.outputs.digest }}
237+
subject-digest: ${{ steps.digest.outputs.digest }}
152238
push-to-registry: true
153239

154-
# Job 4: Build and attach frontend assets
240+
# Job 5: Build and attach frontend assets
155241
assets:
156242
name: Build Frontend Assets
157243
needs: release
@@ -216,10 +302,10 @@ jobs:
216302
tag_name: v${{ needs.release.outputs.new_release_version }}
217303
files: frontend-${{ needs.release.outputs.new_release_version }}.tar.gz
218304

219-
# Job 5: Deploy to production server
305+
# Job 6: Deploy to production server
220306
deploy:
221307
name: Deploy to Production
222-
needs: [release, docker]
308+
needs: [release, docker-manifest]
223309
if: needs.release.outputs.new_release_published == 'true'
224310
runs-on: ubuntu-latest
225311
steps:

docker/Dockerfile.backend.prod

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@
55
# Stage 1: Build dependencies and compile TypeScript
66
FROM node:24-bookworm-slim AS builder
77

8+
# Get target architecture for cache isolation
9+
ARG TARGETARCH
10+
11+
# Install build dependencies for native modules (cpu-features, ssh2, etc.)
12+
RUN apt-get update && apt-get install -y --no-install-recommends \
13+
python3 \
14+
make \
15+
g++ \
16+
&& rm -rf /var/lib/apt/lists/*
17+
818
RUN corepack enable && corepack prepare pnpm@latest --activate
919
WORKDIR /app
1020

@@ -16,8 +26,8 @@ COPY tsconfig.base.json ./
1626
COPY apps/backend/package.json ./apps/backend/
1727
COPY packages/shared/package.json ./packages/shared/
1828

19-
# Install all dependencies with cache mount
20-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
29+
# Install all dependencies with architecture-specific cache mount
30+
RUN --mount=type=cache,id=pnpm-${TARGETARCH},target=/root/.local/share/pnpm/store \
2131
pnpm install --frozen-lockfile
2232

2333
# Copy source code
@@ -33,6 +43,9 @@ RUN pnpm --filter @freundebuch/shared run build && \
3343
# Stage 2: Production runtime
3444
FROM node:24-bookworm-slim AS production
3545

46+
# Get target architecture for cache isolation
47+
ARG TARGETARCH
48+
3649
RUN corepack enable && corepack prepare pnpm@latest --activate
3750
WORKDIR /app
3851

@@ -41,8 +54,8 @@ COPY pnpm-workspace.yaml package.json pnpm-lock.yaml ./
4154
COPY apps/backend/package.json ./apps/backend/
4255
COPY packages/shared/package.json ./packages/shared/
4356

44-
# Install production dependencies only
45-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
57+
# Install production dependencies only with architecture-specific cache mount
58+
RUN --mount=type=cache,id=pnpm-${TARGETARCH},target=/root/.local/share/pnpm/store \
4659
pnpm install --frozen-lockfile --prod --ignore-scripts
4760

4861
# Copy built artifacts

docker/Dockerfile.nginx.prod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
# Stage 1: Build frontend
66
FROM node:24-bookworm-slim AS builder
77

8+
# Get target architecture for cache isolation
9+
ARG TARGETARCH
10+
811
RUN corepack enable && corepack prepare pnpm@latest --activate
912
WORKDIR /app
1013

@@ -16,8 +19,8 @@ COPY tsconfig.base.json ./
1619
COPY apps/frontend/package.json ./apps/frontend/
1720
COPY packages/shared/package.json ./packages/shared/
1821

19-
# Install dependencies with cache mount
20-
RUN --mount=type=cache,id=pnpm,target=/root/.local/share/pnpm/store \
22+
# Install dependencies with architecture-specific cache mount
23+
RUN --mount=type=cache,id=pnpm-${TARGETARCH},target=/root/.local/share/pnpm/store \
2124
pnpm install --frozen-lockfile
2225

2326
# Copy source code

docker/Dockerfile.sabredav.prod

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
# Stage 1: Install PHP dependencies
66
FROM composer:2 AS deps
77

8+
# Get target architecture for cache isolation
9+
ARG TARGETARCH
10+
811
WORKDIR /app
912
COPY apps/sabredav/composer.json apps/sabredav/composer.lock* ./
1013

11-
# Install dependencies with cache mount (ignore platform reqs - pdo_pgsql is in runtime)
12-
RUN --mount=type=cache,id=composer,target=/root/.composer/cache \
14+
# Install dependencies with architecture-specific cache mount (ignore platform reqs - pdo_pgsql is in runtime)
15+
RUN --mount=type=cache,id=composer-${TARGETARCH},target=/root/.composer/cache \
1316
composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs
1417

1518
# Stage 2: Production PHP-FPM runtime

0 commit comments

Comments
 (0)