Skip to content

Commit f8668b1

Browse files
authored
Multiarch native build (#270)
* Build multiarch images with native workers (removes QEMU) * Switch to registry based cache for better docker buildx support
1 parent 46deed9 commit f8668b1

File tree

2 files changed

+148
-34
lines changed

2 files changed

+148
-34
lines changed

.github/workflows/ci-pipeline.yml

Lines changed: 134 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,30 @@ env:
2424
jobs:
2525
test:
2626
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
packages: write
2730
steps:
31+
# GitHub gives only repository complete in <owner>/<repo> format.
32+
# Need some manual shenanigans
33+
# Set IMAGE_NAME so we can push to <owner>/<repo>/<image>
34+
# Transform os/arch to os-arch for suffix target
35+
- name: Set ENV variables
36+
run: |
37+
echo "IMAGE_NAME=${GITHUB_REPOSITORY#$GITHUB_REPOSITORY_OWNER/}" >> $GITHUB_ENV
38+
2839
- name: Checkout repository
2940
uses: actions/checkout@v4
3041

42+
# Login against a Docker registry
43+
# https://github.com/docker/login-action
44+
- name: Log into registry ${{ env.REGISTRY }}
45+
uses: docker/[email protected]
46+
with:
47+
registry: ${{ env.REGISTRY }}
48+
username: ${{ github.actor }}
49+
password: ${{ secrets.GITHUB_TOKEN }}
50+
3151
# This might be unnecessary as tests are not
3252
# multiplatform
3353
- name: Setup Docker buildx
@@ -43,17 +63,18 @@ jobs:
4363
load: true
4464
target: dev
4565
tags: ${{ env.TEST_TAG }}
46-
cache-from: type=gha
47-
cache-to: type=gha,mode=max
66+
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-cache:tests
67+
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-cache:tests,mode=max
4868

4969
# This is a barrier check to make sure we push a functional
5070
# docker image, we can avoid linting
5171
- name: Run tests in the test image
5272
run: |
5373
docker run --rm ${{ env.TEST_TAG }} make ci-test
5474
55-
build:
56-
runs-on: ubuntu-latest
75+
# Inspired to https://docs.docker.com/build/ci/github-actions/multi-platform/#distribute-build-across-multiple-runners
76+
build-arch:
77+
runs-on: ${{ matrix.arch.runner }}
5778
needs: test
5879
permissions:
5980
contents: read
@@ -63,24 +84,120 @@ jobs:
6384
id-token: write
6485
strategy:
6586
matrix:
87+
arch:
88+
- platform: linux/amd64
89+
runner: ubuntu-latest
90+
- platform: linux/arm64
91+
# There is no latest for ARM yet
92+
# https://docs.github.com/en/actions/using-github-hosted-runners/using-github-hosted-runners/about-github-hosted-runners#supported-runners-and-hardware-resources
93+
runner: ubuntu-24.04-arm
6694
docker_target:
6795
- migrations
6896
- http
6997
- socketio
7098
- dramatiq
7199
steps:
72100
# GitHub gives only repository complete in <owner>/<repo> format.
73-
# Need some manual sheanigans
101+
# Need some manual shenanigans
74102
# Set IMAGE_NAME so we can push to <owner>/<repo>/<image>
103+
# Transform os/arch to os-arch for suffix target
75104
- name: Set ENV variables
76105
run: |
77106
echo "IMAGE_NAME=${GITHUB_REPOSITORY#$GITHUB_REPOSITORY_OWNER/}" >> $GITHUB_ENV
107+
platform=${{ matrix.arch.platform }}
108+
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
78109
79110
- name: Checkout repository
80111
uses: actions/checkout@v4
81112

82-
- name: Set up QEMU
83-
uses: docker/setup-qemu-action@v3
113+
# Install the cosign tool
114+
# https://github.com/sigstore/cosign-installer
115+
- name: Install cosign
116+
uses: sigstore/[email protected]
117+
118+
- name: Setup Docker buildx
119+
uses: docker/[email protected]
120+
121+
# Login against a Docker registry
122+
# https://github.com/docker/login-action
123+
- name: Log into registry ${{ env.REGISTRY }}
124+
uses: docker/[email protected]
125+
with:
126+
registry: ${{ env.REGISTRY }}
127+
username: ${{ github.actor }}
128+
password: ${{ secrets.GITHUB_TOKEN }}
129+
130+
# We extract metadata without tags for single image
131+
- name: Extract Docker metadata
132+
id: meta
133+
uses: docker/[email protected]
134+
with:
135+
# list of Docker images to use as base name for tags
136+
# <registry/<owner>/<repo_name>/<repo_name>-<target>
137+
images: |
138+
${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-${{ matrix.docker_target }}
139+
140+
141+
# This build an image WITHOUT tags and outputs the digests, so that we can aggragate them later
142+
- name: Build and push production image
143+
id: build-and-push
144+
uses: docker/[email protected]
145+
with:
146+
context: .
147+
target: ${{ matrix.docker_target }}
148+
platforms: ${{ matrix.arch.platform }}
149+
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
150+
tags: ${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-${{ matrix.docker_target }}
151+
labels: ${{ steps.meta.outputs.labels }}
152+
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-cache:buildcache-${{ matrix.docker_target }}-${{ env.PLATFORM_PAIR }}
153+
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-cache:buildcache-${{ matrix.docker_target }}-${{ env.PLATFORM_PAIR }},mode=max
154+
155+
- name: Export digest
156+
run: |
157+
mkdir -p ${{ runner.temp }}/digests/${{ matrix.docker_target }}
158+
digest="${{ steps.build-and-push.outputs.digest }}"
159+
touch "${{ runner.temp }}/digests/${{ matrix.docker_target }}/${digest#sha256:}"
160+
161+
- name: Upload digest
162+
uses: actions/upload-artifact@v4
163+
with:
164+
name: digests-${{ env.PLATFORM_PAIR }}
165+
path: ${{ runner.temp }}/digests/${{ matrix.docker_target }}/*
166+
if-no-files-found: error
167+
retention-days: 1
168+
169+
170+
aggregate-manifests:
171+
runs-on: ubuntu-latest
172+
needs: build-arch
173+
permissions:
174+
contents: read
175+
packages: write
176+
# This is used to complete the identity challenge
177+
# with sigstore/fulcio when running outside of PRs.
178+
id-token: write
179+
strategy:
180+
matrix:
181+
docker_target:
182+
- migrations
183+
- http
184+
- socketio
185+
- dramatiq
186+
187+
steps:
188+
# GitHub gives only repository complete in <owner>/<repo> format.
189+
# Need some manual sheanigans
190+
# Set IMAGE_NAME so we can push to <owner>/<repo>/<image>
191+
- name: Set ENV variables
192+
run: |
193+
echo "IMAGE_NAME=${GITHUB_REPOSITORY#$GITHUB_REPOSITORY_OWNER/}" >> $GITHUB_ENV
194+
195+
- name: Download digests
196+
uses: actions/download-artifact@v4
197+
with:
198+
path: ${{ runner.temp }}/digests/${{ matrix.docker_target }}
199+
pattern: digests-*
200+
merge-multiple: true
84201

85202
# Install the cosign tool
86203
# https://github.com/sigstore/cosign-installer
@@ -115,20 +232,15 @@ jobs:
115232
type=raw,value={{branch}}-latest
116233
type=raw,value={{branch}}-{{date 'YYYYMMDDHHmmss'}}
117234
118-
# Build and push Docker image with Buildx
119-
# https://github.com/docker/build-push-action
120-
- name: Build and push production image
121-
id: build-and-push
122-
uses: docker/[email protected]
123-
with:
124-
context: .
125-
target: ${{ matrix.docker_target }}
126-
platforms: linux/amd64,linux/arm64
127-
push: true
128-
tags: ${{ steps.meta.outputs.tags }}
129-
labels: ${{ steps.meta.outputs.labels }}
130-
cache-from: type=gha
131-
cache-to: type=gha,mode=max
235+
- name: Create manifest list and push
236+
working-directory: ${{ runner.temp }}/digests/${{ matrix.docker_target }}
237+
run: |
238+
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
239+
$(printf '${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-${{ matrix.docker_target }}@sha256:%s ' *)
240+
241+
- name: Inspect image
242+
run: |
243+
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.REGISTRY_PATH }}/${{ env.IMAGE_NAME }}-${{ matrix.docker_target }}:${{ steps.meta.outputs.version }}
132244
133245
#TODO: Implement signature using generated key: https://docs.sigstore.dev/signing/quickstart/#signing-with-a-generated-key
134246

@@ -141,12 +253,11 @@ jobs:
141253
env:
142254
# https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable
143255
TAGS: ${{ steps.meta.outputs.tags }}
144-
DIGEST: ${{ steps.build-and-push.outputs.digest }}
145256
# This step uses the identity token to provision an ephemeral certificate
146257
# against the sigstore community Fulcio instance.
147258
run: |
148259
images=""
149260
for tag in ${TAGS}; do
150-
images+="${tag}@${DIGEST} "
261+
images+="${tag}@$(docker buildx imagetools inspect --format '{{json .Manifest.Digest}}' ${tag} | xargs) "
151262
done
152263
cosign sign --yes ${images}

Dockerfile

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,22 +14,25 @@ RUN mkdir /venv && chown nonroot:nonroot /venv
1414
ENV PATH="/venv/bin:$PATH"
1515

1616
# Install necessary runtime libraries (e.g. libmysql)
17-
RUN apt-get update \
17+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
18+
--mount=type=cache,target=/var/lib/apt,sharing=locked \
19+
apt-get update \
1820
&& apt-get install -y --no-install-recommends \
19-
make \
20-
&& rm -rf /var/lib/apt/lists/*
21+
make
2122

2223
FROM base AS base_builder
2324
ENV UV_PROJECT_ENVIRONMENT=/venv
2425
# Enable bytecode compilation
2526
ENV UV_COMPILE_BYTECODE=1
27+
ENV UV_LINK_MODE=copy
2628

2729
# Install build system requirements (gcc, library headers, etc.)
2830
# for compiled Python requirements like psycopg2
29-
RUN apt-get update \
31+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
32+
--mount=type=cache,target=/var/lib/apt,sharing=locked \
33+
apt-get update \
3034
&& apt-get install -y --no-install-recommends \
31-
build-essential gcc git \
32-
&& rm -rf /var/lib/apt/lists/*
35+
build-essential gcc git
3336

3437
COPY --from=ghcr.io/astral-sh/uv:0.6.3 /uv /uvx /bin/
3538

@@ -44,7 +47,7 @@ COPY --chown=nonroot:nonroot Makefile .
4447
# Dev image, contains all files and dependencies
4548
FROM base_builder AS dev
4649
COPY --chown=nonroot:nonroot . .
47-
RUN --mount=type=cache,target=~/.cache/uv \
50+
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,sharing=locked,uid=$UID,gid=$GID \
4851
make dev-dependencies
4952

5053
# Note that opentelemetry doesn't play well together with uvicorn reloader
@@ -53,22 +56,22 @@ CMD ["uvicorn", "http_app:create_app", "--host", "0.0.0.0", "--port", "8000", "-
5356

5457
# Installs requirements to run production dramatiq application
5558
FROM base_builder AS dramatiq_builder
56-
RUN --mount=type=cache,target=~/.cache/uv \
59+
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,sharing=locked,uid=$UID,gid=$GID \
5760
uv sync --no-dev --no-install-project --frozen --no-editable
5861

5962
# Installs requirements to run production http application
6063
FROM base_builder AS http_builder
61-
RUN --mount=type=cache,target=~/.cache/uv \
64+
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,sharing=locked,uid=$UID,gid=$GID \
6265
uv sync --no-dev --group http --no-install-project --frozen --no-editable
6366

6467
# Installs requirements to run production socketio application
6568
FROM base_builder AS socketio_builder
66-
RUN --mount=type=cache,target=~/.cache/uv \
69+
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,sharing=locked,uid=$UID,gid=$GID \
6770
uv sync --no-dev --group socketio --no-install-project --frozen --no-editable
6871

6972
# Installs requirements to run production migrations application
7073
FROM base_builder AS migrations_builder
71-
RUN --mount=type=cache,target=~/.cache/uv \
74+
RUN --mount=type=cache,target=/home/nonroot/.cache/uv,sharing=locked,uid=$UID,gid=$GID \
7275
uv sync --no-dev --group migrations --no-install-project --frozen --no-editable
7376

7477
# Create the base app with the common python packages

0 commit comments

Comments
 (0)