From c228ac0f4fa6f4f2b8005c84d1b3ed120d124779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Such=C3=A1nek?= Date: Wed, 23 Sep 2026 14:56:54 +0200 Subject: [PATCH] chore: Speedup Docker build in CI --- packages/dsw-data-seeder/Dockerfile | 45 ++++++++++++++---- packages/dsw-document-worker/Dockerfile | 46 +++++++++++++++---- .../dsw-document-worker/lambda.Dockerfile | 46 +++++++++++++++---- packages/dsw-mailer/Dockerfile | 44 ++++++++++++++---- packages/dsw-tdk/Dockerfile | 39 +++++++++++++--- 5 files changed, 174 insertions(+), 46 deletions(-) diff --git a/packages/dsw-data-seeder/Dockerfile b/packages/dsw-data-seeder/Dockerfile index 87b4bf79..403298c0 100644 --- a/packages/dsw-data-seeder/Dockerfile +++ b/packages/dsw-data-seeder/Dockerfile @@ -2,6 +2,34 @@ # The default keeps a plain `docker build` working outside CI. ARG PYTHON_BASE_VERSION=4.35.0 +# The dsw-* distributions are pure Python - every one of them builds to a +# py3-none-any wheel - so they are built once on the build platform and reused +# by every target platform. Under multi-arch emulation that is the difference +# between one native build and one emulated build per architecture, and almost +# all of the emulated cost is PEP 517 build-environment setup repeated per +# package rather than the build itself. +FROM --platform=$BUILDPLATFORM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS workspace-wheels + +ARG PACKAGE_VERSION +ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION} + +# Sources only: nothing outside packages/ takes part in building a wheel, so a +# change to the docs or the mirror tooling no longer invalidates this layer. +COPY packages /app/packages + +# One pip invocation, not one per package: each still gets its own isolated +# build environment, but pip itself starts once. +RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels \ + /app/packages/dsw-command-queue \ + /app/packages/dsw-config \ + /app/packages/dsw-database \ + /app/packages/dsw-storage \ + /app/packages/dsw-data-seeder + + +# Third-party wheels. These are architecture-specific, so this stage is built +# once per target platform - but it depends only on the dependency manifests, +# so it is reused across commits. FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS builder # .dockerignore excludes .git, so the build context carries no repository and @@ -48,15 +76,6 @@ COPY packages/dsw-tdk/README.md /app/packages/dsw-tdk/ RUN uv export --locked --no-dev --no-emit-workspace --no-hashes --package dsw-data-seeder -o /app/requirements.txt \ && python -m pip wheel --no-cache-dir --wheel-dir=/app/wheels -r /app/requirements.txt -# Sources: changes on every commit, so everything below rebuilds each time. -COPY . /app - -RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-command-queue \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-config \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-database \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-storage \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-data-seeder - FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic @@ -74,7 +93,13 @@ RUN mkdir -p /home/user/data # Install Python packages COPY --from=builder --chown=user:user /app/wheels /home/user/wheels -RUN python -m pip install --break-system-packages --user --no-cache --no-index /home/user/wheels/* \ +COPY --from=workspace-wheels --chown=user:user /app/wheels /home/user/wheels + +# uv rather than pip: this unpacks and byte-compiles the whole dependency set, +# and it runs emulated on every non-native architecture. It is bind-mounted +# rather than copied so nothing of it remains in the image. +RUN --mount=from=ghcr.io/astral-sh/uv:0.12.7,source=/uv,target=/usr/local/bin/uv \ + uv pip install --prefix /home/user/.local --no-cache --no-index --compile-bytecode /home/user/wheels/* \ && rm -rf /home/user/wheels # Run diff --git a/packages/dsw-document-worker/Dockerfile b/packages/dsw-document-worker/Dockerfile index ca71a23b..472d1b7f 100644 --- a/packages/dsw-document-worker/Dockerfile +++ b/packages/dsw-document-worker/Dockerfile @@ -2,6 +2,35 @@ # The default keeps a plain `docker build` working outside CI. ARG PYTHON_BASE_VERSION=4.35.0 +# The dsw-* distributions are pure Python - every one of them builds to a +# py3-none-any wheel - so they are built once on the build platform and reused +# by every target platform. Under multi-arch emulation that is the difference +# between one native build and one emulated build per architecture, and almost +# all of the emulated cost is PEP 517 build-environment setup repeated per +# package rather than the build itself. +FROM --platform=$BUILDPLATFORM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker AS workspace-wheels + +ARG PACKAGE_VERSION +ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION} + +# Sources only: nothing outside packages/ takes part in building a wheel, so a +# change to the docs or the mirror tooling no longer invalidates this layer. +COPY packages /app/packages + +# One pip invocation, not one per package: each still gets its own isolated +# build environment, but pip itself starts once. +RUN python -m pip wheel --no-deps --wheel-dir=/app/wheels \ + /app/packages/dsw-command-queue \ + /app/packages/dsw-config \ + /app/packages/dsw-database \ + /app/packages/dsw-storage \ + /app/packages/dsw-document-worker/addons/* \ + /app/packages/dsw-document-worker + + +# Third-party wheels. These are architecture-specific, so this stage is built +# once per target platform - but it depends only on the dependency manifests, +# so it is reused across commits. FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker AS builder # .dockerignore excludes .git, so the build context carries no repository and @@ -50,15 +79,6 @@ COPY packages/dsw-tdk/README.md /app/packages/dsw-tdk/ RUN uv export --locked --no-dev --no-emit-workspace --no-hashes --package dsw-document-worker -o /app/requirements.txt \ && python -m pip wheel --wheel-dir=/app/wheels -r /app/requirements.txt -# Sources: changes on every commit, so everything below rebuilds each time. -COPY . /app - -RUN python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-command-queue \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-config \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-database \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-storage \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-document-worker/addons/* \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-document-worker FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker @@ -84,7 +104,13 @@ COPY --chown=user:user packages/dsw-document-worker/data /home/user/data # Install Python packages COPY --from=builder --chown=user:user /app/wheels /home/user/wheels -RUN python -m pip install --break-system-packages --user --no-cache --no-index /home/user/wheels/* \ +COPY --from=workspace-wheels --chown=user:user /app/wheels /home/user/wheels + +# uv rather than pip: this unpacks and byte-compiles the whole dependency set, +# and it runs emulated on every non-native architecture. It is bind-mounted +# rather than copied so nothing of it remains in the image. +RUN --mount=from=ghcr.io/astral-sh/uv:0.12.7,source=/uv,target=/usr/local/bin/uv \ + uv pip install --prefix /home/user/.local --no-cache --no-index --compile-bytecode /home/user/wheels/* \ && rm -rf /home/user/wheels # Run diff --git a/packages/dsw-document-worker/lambda.Dockerfile b/packages/dsw-document-worker/lambda.Dockerfile index 09eeb9c5..352f6887 100644 --- a/packages/dsw-document-worker/lambda.Dockerfile +++ b/packages/dsw-document-worker/lambda.Dockerfile @@ -2,6 +2,35 @@ # The default keeps a plain `docker build` working outside CI. ARG PYTHON_BASE_VERSION=4.35.0 +# The dsw-* distributions are pure Python - every one of them builds to a +# py3-none-any wheel - so they are built once on the build platform and reused +# by every target platform. Under multi-arch emulation that is the difference +# between one native build and one emulated build per architecture, and almost +# all of the emulated cost is PEP 517 build-environment setup repeated per +# package rather than the build itself. +FROM --platform=$BUILDPLATFORM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker-lambda AS workspace-wheels + +ARG PACKAGE_VERSION +ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION} + +# Sources only: nothing outside packages/ takes part in building a wheel, so a +# change to the docs or the mirror tooling no longer invalidates this layer. +COPY packages /app/packages + +# One pip invocation, not one per package: each still gets its own isolated +# build environment, but pip itself starts once. +RUN python -m pip wheel --no-deps --wheel-dir=/app/wheels \ + /app/packages/dsw-command-queue \ + /app/packages/dsw-config \ + /app/packages/dsw-database \ + /app/packages/dsw-storage \ + /app/packages/dsw-document-worker/addons/* \ + /app/packages/dsw-document-worker + + +# Third-party wheels. These are architecture-specific, so this stage is built +# once per target platform - but it depends only on the dependency manifests, +# so it is reused across commits. FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker-lambda AS builder # .dockerignore excludes .git, so the build context carries no repository and @@ -44,15 +73,6 @@ COPY packages/dsw-tdk/README.md /app/packages/dsw-tdk/ RUN uv --directory /app export --locked --no-dev --no-emit-workspace --no-hashes --package dsw-document-worker -o /app/requirements.txt \ && python -m pip wheel --wheel-dir=/app/wheels -r /app/requirements.txt -# Sources: changes on every commit, so everything below rebuilds each time. -COPY . /app - -RUN python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-command-queue \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-config \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-database \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-storage \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-document-worker/addons/* \ - && python -m pip wheel --no-deps --wheel-dir=/app/wheels /app/packages/dsw-document-worker FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-docworker-lambda @@ -77,7 +97,13 @@ COPY packages/dsw-document-worker/data ./data # Copy Python dependencies COPY --from=builder /app/wheels /tmp/wheels -RUN python -m pip install --no-cache --no-index /tmp/wheels/* \ +COPY --from=workspace-wheels /app/wheels /tmp/wheels + +# uv rather than pip: this unpacks and byte-compiles the whole dependency set, +# and it runs emulated on every non-native architecture. It is bind-mounted +# rather than copied so nothing of it remains in the image. +RUN --mount=from=ghcr.io/astral-sh/uv:0.12.7,source=/uv,target=/usr/local/bin/uv \ + uv pip install --system --no-cache --no-index --compile-bytecode /tmp/wheels/* \ && rm -rf /tmp/wheels # Copy the Lambda handler diff --git a/packages/dsw-mailer/Dockerfile b/packages/dsw-mailer/Dockerfile index 6f8c39d0..574c4f24 100644 --- a/packages/dsw-mailer/Dockerfile +++ b/packages/dsw-mailer/Dockerfile @@ -2,6 +2,34 @@ # The default keeps a plain `docker build` working outside CI. ARG PYTHON_BASE_VERSION=4.35.0 +# The dsw-* distributions are pure Python - every one of them builds to a +# py3-none-any wheel - so they are built once on the build platform and reused +# by every target platform. Under multi-arch emulation that is the difference +# between one native build and one emulated build per architecture, and almost +# all of the emulated cost is PEP 517 build-environment setup repeated per +# package rather than the build itself. +FROM --platform=$BUILDPLATFORM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS workspace-wheels + +ARG PACKAGE_VERSION +ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION} + +# Sources only: nothing outside packages/ takes part in building a wheel, so a +# change to the docs or the mirror tooling no longer invalidates this layer. +COPY packages /app/packages + +# One pip invocation, not one per package: each still gets its own isolated +# build environment, but pip itself starts once. +RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels \ + /app/packages/dsw-config \ + /app/packages/dsw-command-queue \ + /app/packages/dsw-database \ + /app/packages/dsw-storage \ + /app/packages/dsw-mailer + + +# Third-party wheels. These are architecture-specific, so this stage is built +# once per target platform - but it depends only on the dependency manifests, +# so it is reused across commits. FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS builder # .dockerignore excludes .git, so the build context carries no repository and @@ -45,14 +73,6 @@ COPY packages/dsw-tdk/README.md /app/packages/dsw-tdk/ RUN uv export --locked --no-dev --no-emit-workspace --no-hashes --package dsw-mailer -o /app/requirements.txt \ && python -m pip wheel --no-cache-dir --wheel-dir=/app/wheels -r /app/requirements.txt -# Sources: changes on every commit, so everything below rebuilds each time. -COPY . /app - -RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-config \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-command-queue \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-database \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-storage \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-mailer FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic @@ -72,7 +92,13 @@ COPY --chown=user:user packages/dsw-mailer/templates /home/user/templates # Install Python packages COPY --from=builder --chown=user:user /app/wheels /home/user/wheels -RUN python -m pip install --break-system-packages --user --no-cache --no-index /home/user/wheels/* \ +COPY --from=workspace-wheels --chown=user:user /app/wheels /home/user/wheels + +# uv rather than pip: this unpacks and byte-compiles the whole dependency set, +# and it runs emulated on every non-native architecture. It is bind-mounted +# rather than copied so nothing of it remains in the image. +RUN --mount=from=ghcr.io/astral-sh/uv:0.12.7,source=/uv,target=/usr/local/bin/uv \ + uv pip install --prefix /home/user/.local --no-cache --no-index --compile-bytecode /home/user/wheels/* \ && rm -rf /home/user/wheels # Run diff --git a/packages/dsw-tdk/Dockerfile b/packages/dsw-tdk/Dockerfile index f849b756..ce1367f6 100644 --- a/packages/dsw-tdk/Dockerfile +++ b/packages/dsw-tdk/Dockerfile @@ -2,6 +2,31 @@ # The default keeps a plain `docker build` working outside CI. ARG PYTHON_BASE_VERSION=4.35.0 +# The dsw-* distributions are pure Python - every one of them builds to a +# py3-none-any wheel - so they are built once on the build platform and reused +# by every target platform. Under multi-arch emulation that is the difference +# between one native build and one emulated build per architecture, and almost +# all of the emulated cost is PEP 517 build-environment setup repeated per +# package rather than the build itself. +FROM --platform=$BUILDPLATFORM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS workspace-wheels + +ARG PACKAGE_VERSION +ENV UV_DYNAMIC_VERSIONING_BYPASS=${PACKAGE_VERSION} + +# Sources only: nothing outside packages/ takes part in building a wheel, so a +# change to the docs or the mirror tooling no longer invalidates this layer. +COPY packages /app/packages + +# One pip invocation, not one per package: each still gets its own isolated +# build environment, but pip itself starts once. +RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels \ + /app/packages/dsw-models \ + /app/packages/dsw-tdk + + +# Third-party wheels. These are architecture-specific, so this stage is built +# once per target platform - but it depends only on the dependency manifests, +# so it is reused across commits. FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic AS builder # .dockerignore excludes .git, so the build context carries no repository and @@ -45,12 +70,6 @@ COPY packages/dsw-tdk/README.md /app/packages/dsw-tdk/ RUN uv export --locked --no-dev --no-emit-workspace --no-hashes --package dsw-tdk -o /app/requirements.txt \ && python -m pip wheel --no-cache-dir --wheel-dir=/app/wheels -r /app/requirements.txt -# Sources: changes on every commit, so everything below rebuilds each time. -COPY . /app - -RUN python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-models \ - && python -m pip wheel --no-cache-dir --no-deps --wheel-dir=/app/wheels /app/packages/dsw-tdk - FROM ghcr.io/ds-wizard/python-base:${PYTHON_BASE_VERSION}-basic @@ -64,7 +83,13 @@ WORKDIR /home/user # Install Python packages COPY --from=builder --chown=user:user /app/wheels /home/user/wheels -RUN python -m pip install --break-system-packages --user --no-cache --no-index /home/user/wheels/* \ +COPY --from=workspace-wheels --chown=user:user /app/wheels /home/user/wheels + +# uv rather than pip: this unpacks and byte-compiles the whole dependency set, +# and it runs emulated on every non-native architecture. It is bind-mounted +# rather than copied so nothing of it remains in the image. +RUN --mount=from=ghcr.io/astral-sh/uv:0.12.7,source=/uv,target=/usr/local/bin/uv \ + uv pip install --prefix /home/user/.local --no-cache --no-index --compile-bytecode /home/user/wheels/* \ && rm -rf /home/user/wheels # Run