Smaller Python Docker Images with Build Mounts
If you want small Docker images and fast builds, you're likely already using Docker's multi-stage builds. For Python applications, this often means one stage to download and compile any wheels and another stage to install them and add your application source code. For example:
FROM python:3.10 AS build
COPY requirements.txt .
RUN pip wheel -r requirements.txt \
--wheel-dir /whl \
--no-cache-dir
FROM python:3.10-slim
COPY --from=build /whl/*.whl /whl/
RUN pip install --no-deps /whl/*.whl
Given a small requirements.txt
, we can build this image with Docker BuildKit:
❯ cat requirements.txt
ciso8601
flask
numpy
pandas
❯ DOCKER_BUILDKIT=1 docker build --no-cache -t python-with-copy .
[+] Building 22.3s (12/12) FINISHED
=> [internal] load build definition from copy.Dockerfile 0.0s
=> => transferring dockerfile: 42B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/python:3.10-slim 0.0s
=> [internal] load metadata for docker.io/library/python:3.10 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 37B 0.0s
=> CACHED [build 1/3] FROM docker.io/library/python:3.10 0.0s
=> CACHED [stage-1 1/3] FROM docker.io/library/python:3.10-slim 0.0s
=> [build 2/3] COPY requirements.txt . 0.0s
=> [build 3/3] RUN pip wheel -r requirements.txt --wheel-dir /whl --n 9.5s
=> [stage-1 2/3] COPY --from=build /whl/*.whl /whl/ 0.1s
=> [stage-1 3/3] RUN pip install --no-deps /whl/*.whl 9.5s
=> exporting to image 2.2s
=> => exporting layers 2.2s
=> => writing image sha256:242b56c7f1b6f181986d62fcd80afde5287d5f2d8cdaddd96d 0.0s
=> => naming to docker.io/library/python-with-copy 0.0s
We can inspect the history of the image to view the sizes of each layer:
❯ docker history python-with-copy
IMAGE CREATED CREATED BY SIZE COMMENT
242b56c7f1b6 8 minutes ago RUN /bin/sh -c pip install --no-deps /whl/*.… 127MB buildkit.dockerfile.v0
<missing> 8 minutes ago COPY /whl/*.whl /whl/ # buildkit 28.9MB buildkit.dockerfile.v0
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 2 weeks ago /bin/sh -c set -ex; savedAptMark="$(apt-ma… 9.51MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_SETUPTOOLS_VER… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=21… 0B
<missing> 2 weeks ago /bin/sh -c cd /usr/local/bin && ln -s idle3… 32B
<missing> 2 weeks ago /bin/sh -c set -ex && savedAptMark="$(apt-… 29.5MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.10.0 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ENV GPG_KEY=A035C8C19219B… 0B
<missing> 4 weeks ago /bin/sh -c set -eux; apt-get update; apt-g… 3.11MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:16dc2c6d1932194ed… 80.4MB
From this, we can see that COPY /whl/*.whl /whl/
adds 28.9MB to our Docker image. We don't need the wheels in our final image, but we can't free up this space with a RUN rm -rf /whl/
instruction, so what do we do?
We can use Docker BuildKit's build mounts feature. This is most commonly used to pass secrets or SSH keys to the build, but we're interested in the bind
mount type, which "can be used to bind files from other part of the build without copying".
We use it like so:
# syntax=docker/dockerfile:1.2
FROM python:3.10 AS build
COPY requirements.txt .
RUN pip wheel -r requirements.txt \
--wheel-dir /whl \
--no-cache-dir
FROM python:3.10-slim
RUN --mount=type=bind,target=/whl,source=/whl,from=build \
pip install --no-deps /whl/*.whl
We can build this image:
❯ DOCKER_BUILDKIT=1 docker build --no-cache -t python-with-mount .
[+] Building 22.6s (15/15) FINISHED
=> [internal] load build definition from mount.Dockerfile 0.0s
=> => transferring dockerfile: 43B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> resolve image config for docker.io/docker/dockerfile:1.3 0.6s
=> CACHED docker-image://docker.io/docker/dockerfile:1.3@sha256:42399d4635edd 0.0s
=> [internal] load build definition from mount.Dockerfile 0.0s
=> [internal] load .dockerignore 0.0s
=> [internal] load metadata for docker.io/library/python:3.10-slim 0.0s
=> [internal] load metadata for docker.io/library/python:3.10 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 37B 0.0s
=> CACHED [stage-1 1/2] FROM docker.io/library/python:3.10-slim 0.0s
=> CACHED [build 1/3] FROM docker.io/library/python:3.10 0.0s
=> [build 2/3] COPY requirements.txt . 0.0s
=> [build 3/3] RUN pip wheel -r requirements.txt --wheel-dir /whl --n 9.7s
=> [stage-1 2/2] RUN --mount=type=bind,target=/whl,source=/whl,from=build 9.2s
=> exporting to image 2.1s
=> => exporting layers 2.0s
=> => writing image sha256:1d677e47533d54467300be2c1b4898cb7b5d07b539877c2f07 0.0s
=> => naming to docker.io/library/python-with-mount 0.0s
And inspect its layers:
❯ docker history python-with-mount
IMAGE CREATED CREATED BY SIZE COMMENT
1d677e47533d 59 seconds ago RUN /bin/sh -c pip install --no-deps /whl/*.… 127MB buildkit.dockerfile.v0
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["python3"] 0B
<missing> 2 weeks ago /bin/sh -c set -ex; savedAptMark="$(apt-ma… 9.51MB
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_SHA256… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_GET_PIP_URL=ht… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_SETUPTOOLS_VER… 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ENV PYTHON_PIP_VERSION=21… 0B
<missing> 2 weeks ago /bin/sh -c cd /usr/local/bin && ln -s idle3… 32B
<missing> 2 weeks ago /bin/sh -c set -ex && savedAptMark="$(apt-… 29.5MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV PYTHON_VERSION=3.10.0 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ENV GPG_KEY=A035C8C19219B… 0B
<missing> 4 weeks ago /bin/sh -c set -eux; apt-get update; apt-g… 3.11MB
<missing> 4 weeks ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ENV PATH=/usr/local/bin:/… 0B
<missing> 4 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 4 weeks ago /bin/sh -c #(nop) ADD file:16dc2c6d1932194ed… 80.4MB
As expected, the COPY
step and its 28.9MB are gone. This is about 10% of our initial image size freed up!