Escribo Python y lo despliego en Docker. Me gusta Python: es simple y las cosas salen rápido. Lo que no es, es una buena opción cuando quieres un distribuible pequeño, y las imágenes lo delatan.

Es muy habitual ver un proyecto Python empaquetado en una imagen Docker de varios cientos de MB. Incluso en proyectos diminutos. Aquí va un truco para reducirlas.

Supongamos que queremos empaquetar un proyecto con la estructura de siempre:

$ 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

El plan:

  1. Escribir un Dockerfile multietapa.

  2. Partir de una imagen base de Python.

  3. Añadir una etapa que construya los wheels, con los flags de optimización de Python activados.

  4. Construir la capa final e instalar esos wheels sin pasar por el índice de PyPI.

Que se traduce en este 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"]

Solo hay dos líneas que merecen una mirada de cerca.

La primera construye los wheels, tanto de las dependencias como de nuestro propio proyecto:

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/

La segunda instala únicamente los wheels que acabamos de construir. Con --no-index, pip no puede ir a PyPI a descargar nada más:

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

Así que la imagen final solo lleva el código generado por los wheels, compilado con los flags de optimización de Python. El compilador y todo lo demás de la etapa builder se queda fuera.