Skip to content

Commit 1525a3b

Browse files
authored
Speedup the building of Docker images (#18038)
This is a split off #18033 This uses a few tricks to speed up the building of docker images: - This switches to use `uv pip install` instead of `pip install`. This saves a bunch of time, especially when cross-compiling - I then looked at what packages were not using binary wheels: I upgraded MarkupSafe to have binaries for py3.12, and got back to Python 3.12 because hiredis didn't have builds for py3.13 with the version we were using - The generation of the requirements.txt is arch-agnostic, so I've switched this one to run on the build architecture, so that both arch can share it - The download of runtime depdendencies can be done on the build architecture through manual `apt-get download` plus `dpkg --extract` - We were using -slim images, but still installed a bunch of -dev dependencies. Turns out, all the dev dependencies were already installed in the non-slim image, which saves a bunch of time as well
1 parent 0fad0a7 commit 1525a3b

File tree

3 files changed

+150
-135
lines changed

3 files changed

+150
-135
lines changed

changelog.d/18038.docker

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Speed up the building of the Docker image.

docker/Dockerfile

+86-83
Original file line numberDiff line numberDiff line change
@@ -20,45 +20,16 @@
2020
# `poetry export | pip install -r /dev/stdin`, but beware: we have experienced bugs in
2121
# in `poetry export` in the past.
2222

23+
ARG DEBIAN_VERSION=bookworm
2324
ARG PYTHON_VERSION=3.12
25+
ARG POETRY_VERSION=1.8.3
2426

2527
###
2628
### Stage 0: generate requirements.txt
2729
###
28-
# We hardcode the use of Debian bookworm here because this could change upstream
29-
# and other Dockerfiles used for testing are expecting bookworm.
30-
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bookworm AS requirements
31-
32-
# RUN --mount is specific to buildkit and is documented at
33-
# https://github.com/moby/buildkit/blob/master/frontend/dockerfile/docs/syntax.md#build-mounts-run---mount.
34-
# Here we use it to set up a cache for apt (and below for pip), to improve
35-
# rebuild speeds on slow connections.
36-
RUN \
37-
--mount=type=cache,target=/var/cache/apt,sharing=locked \
38-
--mount=type=cache,target=/var/lib/apt,sharing=locked \
39-
apt-get update -qq && apt-get install -yqq \
40-
build-essential curl git libffi-dev libssl-dev pkg-config \
41-
&& rm -rf /var/lib/apt/lists/*
42-
43-
# Install rust and ensure its in the PATH.
44-
# (Rust may be needed to compile `cryptography`---which is one of poetry's
45-
# dependencies---on platforms that don't have a `cryptography` wheel.
46-
ENV RUSTUP_HOME=/rust
47-
ENV CARGO_HOME=/cargo
48-
ENV PATH=/cargo/bin:/rust/bin:$PATH
49-
RUN mkdir /rust /cargo
50-
51-
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
52-
53-
# arm64 builds consume a lot of memory if `CARGO_NET_GIT_FETCH_WITH_CLI` is not
54-
# set to true, so we expose it as a build-arg.
55-
ARG CARGO_NET_GIT_FETCH_WITH_CLI=false
56-
ENV CARGO_NET_GIT_FETCH_WITH_CLI=$CARGO_NET_GIT_FETCH_WITH_CLI
57-
58-
# We install poetry in its own build stage to avoid its dependencies conflicting with
59-
# synapse's dependencies.
60-
RUN --mount=type=cache,target=/root/.cache/pip \
61-
pip install --user "poetry==1.3.2"
30+
### This stage is platform-agnostic, so we can use the build platform in case of cross-compilation.
31+
###
32+
FROM --platform=$BUILDPLATFORM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-${DEBIAN_VERSION} AS requirements
6233

6334
WORKDIR /synapse
6435

@@ -75,41 +46,30 @@ ARG TEST_ONLY_SKIP_DEP_HASH_VERIFICATION
7546
# Instead, we'll just install what a regular `pip install` would from PyPI.
7647
ARG TEST_ONLY_IGNORE_POETRY_LOCKFILE
7748

49+
# This silences a warning as uv isn't able to do hardlinks between its cache
50+
# (mounted as --mount=type=cache) and the target directory.
51+
ENV UV_LINK_MODE=copy
52+
7853
# Export the dependencies, but only if we're actually going to use the Poetry lockfile.
7954
# Otherwise, just create an empty requirements file so that the Dockerfile can
8055
# proceed.
81-
RUN if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
82-
/root/.local/bin/poetry export --extras all -o /synapse/requirements.txt ${TEST_ONLY_SKIP_DEP_HASH_VERIFICATION:+--without-hashes}; \
56+
ARG POETRY_VERSION
57+
RUN --mount=type=cache,target=/root/.cache/uv \
58+
if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
59+
uvx --with poetry-plugin-export==1.8.0 \
60+
poetry@${POETRY_VERSION} export --extras all -o /synapse/requirements.txt ${TEST_ONLY_SKIP_DEP_HASH_VERIFICATION:+--without-hashes}; \
8361
else \
84-
touch /synapse/requirements.txt; \
62+
touch /synapse/requirements.txt; \
8563
fi
8664

8765
###
8866
### Stage 1: builder
8967
###
90-
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bookworm AS builder
91-
92-
# install the OS build deps
93-
RUN \
94-
--mount=type=cache,target=/var/cache/apt,sharing=locked \
95-
--mount=type=cache,target=/var/lib/apt,sharing=locked \
96-
apt-get update -qq && apt-get install -yqq \
97-
build-essential \
98-
libffi-dev \
99-
libjpeg-dev \
100-
libpq-dev \
101-
libssl-dev \
102-
libwebp-dev \
103-
libxml++2.6-dev \
104-
libxslt1-dev \
105-
openssl \
106-
zlib1g-dev \
107-
git \
108-
curl \
109-
libicu-dev \
110-
pkg-config \
111-
&& rm -rf /var/lib/apt/lists/*
68+
FROM ghcr.io/astral-sh/uv:python${PYTHON_VERSION}-${DEBIAN_VERSION} AS builder
11269

70+
# This silences a warning as uv isn't able to do hardlinks between its cache
71+
# (mounted as --mount=type=cache) and the target directory.
72+
ENV UV_LINK_MODE=copy
11373

11474
# Install rust and ensure its in the PATH
11575
ENV RUSTUP_HOME=/rust
@@ -119,7 +79,6 @@ RUN mkdir /rust /cargo
11979

12080
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path --default-toolchain stable --profile minimal
12181

122-
12382
# arm64 builds consume a lot of memory if `CARGO_NET_GIT_FETCH_WITH_CLI` is not
12483
# set to true, so we expose it as a build-arg.
12584
ARG CARGO_NET_GIT_FETCH_WITH_CLI=false
@@ -131,8 +90,8 @@ ENV CARGO_NET_GIT_FETCH_WITH_CLI=$CARGO_NET_GIT_FETCH_WITH_CLI
13190
#
13291
# This is aiming at installing the `[tool.poetry.depdendencies]` from pyproject.toml.
13392
COPY --from=requirements /synapse/requirements.txt /synapse/
134-
RUN --mount=type=cache,target=/root/.cache/pip \
135-
pip install --prefix="/install" --no-deps --no-warn-script-location -r /synapse/requirements.txt
93+
RUN --mount=type=cache,target=/root/.cache/uv \
94+
uv pip install --prefix="/install" --no-deps -r /synapse/requirements.txt
13695

13796
# Copy over the rest of the synapse source code.
13897
COPY synapse /synapse/synapse/
@@ -146,41 +105,85 @@ ARG TEST_ONLY_IGNORE_POETRY_LOCKFILE
146105
# Install the synapse package itself.
147106
# If we have populated requirements.txt, we don't install any dependencies
148107
# as we should already have those from the previous `pip install` step.
149-
RUN --mount=type=cache,target=/synapse/target,sharing=locked \
108+
RUN \
109+
--mount=type=cache,target=/root/.cache/uv \
110+
--mount=type=cache,target=/synapse/target,sharing=locked \
150111
--mount=type=cache,target=${CARGO_HOME}/registry,sharing=locked \
151112
if [ -z "$TEST_ONLY_IGNORE_POETRY_LOCKFILE" ]; then \
152-
pip install --prefix="/install" --no-deps --no-warn-script-location /synapse[all]; \
113+
uv pip install --prefix="/install" --no-deps /synapse[all]; \
153114
else \
154-
pip install --prefix="/install" --no-warn-script-location /synapse[all]; \
115+
uv pip install --prefix="/install" /synapse[all]; \
155116
fi
156117

157118
###
158-
### Stage 2: runtime
119+
### Stage 2: runtime dependencies download for ARM64 and AMD64
159120
###
121+
FROM --platform=$BUILDPLATFORM docker.io/library/debian:${DEBIAN_VERSION} AS runtime-deps
160122

161-
FROM docker.io/library/python:${PYTHON_VERSION}-slim-bookworm
123+
# Tell apt to keep downloaded package files, as we're using cache mounts.
124+
RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache
162125

163-
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
164-
LABEL org.opencontainers.image.documentation='https://github.com/element-hq/synapse/blob/master/docker/README.md'
165-
LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git'
166-
LABEL org.opencontainers.image.licenses='AGPL-3.0-or-later'
126+
# Add both target architectures
127+
RUN dpkg --add-architecture arm64
128+
RUN dpkg --add-architecture amd64
167129

130+
# Fetch the runtime dependencies debs for both architectures
131+
# We do that by building a recursive list of packages we need to download with `apt-cache depends`
132+
# and then downloading them with `apt-get download`.
168133
RUN \
169134
--mount=type=cache,target=/var/cache/apt,sharing=locked \
170135
--mount=type=cache,target=/var/lib/apt,sharing=locked \
171-
apt-get update -qq && apt-get install -yqq \
172-
curl \
173-
gosu \
174-
libjpeg62-turbo \
175-
libpq5 \
176-
libwebp7 \
177-
xmlsec1 \
178-
libjemalloc2 \
179-
libicu72 \
180-
libssl-dev \
181-
openssl \
182-
&& rm -rf /var/lib/apt/lists/*
136+
apt-get update -qq && \
137+
apt-get install -y --no-install-recommends rsync && \
138+
apt-cache depends --recurse --no-recommends --no-suggests --no-conflicts --no-breaks --no-replaces --no-enhances --no-pre-depends \
139+
curl \
140+
gosu \
141+
libjpeg62-turbo \
142+
libpq5 \
143+
libwebp7 \
144+
xmlsec1 \
145+
libjemalloc2 \
146+
libicu \
147+
| grep '^\w' > /tmp/pkg-list && \
148+
for arch in arm64 amd64; do \
149+
mkdir -p /tmp/debs-${arch} && \
150+
cd /tmp/debs-${arch} && \
151+
apt-get download $(sed "s/$/:${arch}/" /tmp/pkg-list); \
152+
done
153+
154+
# Extract the debs for each architecture
155+
# On the runtime image, /lib is a symlink to /usr/lib, so we need to copy the
156+
# libraries to the right place, else the `COPY` won't work.
157+
# On amd64, we'll also have a /lib64 folder with ld-linux-x86-64.so.2, which is
158+
# already present in the runtime image.
159+
RUN \
160+
for arch in arm64 amd64; do \
161+
mkdir -p /install-${arch}/var/lib/dpkg/status.d/ && \
162+
for deb in /tmp/debs-${arch}/*.deb; do \
163+
package_name=$(dpkg-deb -I ${deb} | awk '/^ Package: .*$/ {print $2}'); \
164+
echo "Extracting: ${package_name}"; \
165+
dpkg --ctrl-tarfile $deb | tar -Ox ./control > /install-${arch}/var/lib/dpkg/status.d/${package_name}; \
166+
dpkg --extract $deb /install-${arch}; \
167+
done; \
168+
rsync -avr /install-${arch}/lib/ /install-${arch}/usr/lib; \
169+
rm -rf /install-${arch}/lib /install-${arch}/lib64; \
170+
done
171+
172+
173+
###
174+
### Stage 3: runtime
175+
###
176+
177+
FROM docker.io/library/python:${PYTHON_VERSION}-slim-${DEBIAN_VERSION}
178+
179+
ARG TARGETARCH
180+
181+
LABEL org.opencontainers.image.url='https://matrix.org/docs/projects/server/synapse'
182+
LABEL org.opencontainers.image.documentation='https://github.com/element-hq/synapse/blob/master/docker/README.md'
183+
LABEL org.opencontainers.image.source='https://github.com/element-hq/synapse.git'
184+
LABEL org.opencontainers.image.licenses='AGPL-3.0-or-later'
183185

186+
COPY --from=runtime-deps /install-${TARGETARCH} /
184187
COPY --from=builder /install /usr/local
185188
COPY ./docker/start.py /start.py
186189
COPY ./docker/conf /conf

0 commit comments

Comments
 (0)