I write Python and I ship it in Docker. I like Python: it is simple and you get things done fast. What it is not is a good choice when you want a small distributable, and the images show it.

It is very common to see a Python project packaged as a Docker image of several hundred MB. Even for tiny projects. Here is a trick to get them down.

Say we want to package a project with the usual layout:

$ ls -lh demo_app/
-rw-r--r--  1 Dani  staff     1B May 25 10:31 Dockerfile
-rw-r--r--  1 Dani  staff     2B May 25 09:28 MANIFEST.in
drwxr-xr-x  2 Dani  staff    64B May 25 09:27 app
-rw-r--r--  1 Dani  staff     9B May 25 09:27 requirements.txt
-rw-r--r--  1 Dani  staff     0B May 25 09:27 setup.py

The plan:

  1. Write a multi-stage Dockerfile.

  2. Start from a Python base image.

  3. Add a stage that builds the wheels, with the Python optimization flags on.

  4. Build the final layer and install those wheels without going to the PyPI index.

Which gives us this Dockerfile:

FROM python:3.8-alpine as base

RUN apk update && \
    apk upgrade

FROM base as builder

RUN apk add --no-cache build-base && \
    python -m pip install --no-cache-dir -U pip wheel

COPY ./ /app/
RUN python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /app/requirements.txt && \
    python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels /app/

FROM base
COPY --from=builder /root/wheels /root/wheels

RUN python -m pip install --no-cache --no-index /root/wheels/*
RUN rm -rf /root/wheels

ENTRYPOINT ["my-project"]

Only two lines deserve a closer look.

The first builds the wheels, both for the dependencies and for our own project:

RUN python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels -r /app/requirements.txt && \
    python -OO -m pip wheel --no-cache-dir --wheel-dir=/root/wheels /app/

The second installs nothing but the wheels we just built. With --no-index, pip cannot reach PyPI to download anything else:

RUN python -m pip install --no-cache --no-index /root/wheels/*

So the final image only carries the code generated by the wheels, compiled with the Python optimization flags. The compiler and everything else from the builder stage stays behind.