Docker para Python en 2026: la guía de compilación moderna de varias etapas de uv +
Abra cualquier tutorial "Dockerize your Python app" de hace cinco años y encontrará las mismas cuatro líneas: FROM python:3.11-slim, COPY requisitos.txt., RUN pip install -r requisitos.txt, COPY. .. Funciona. También incluye una imagen de más de 200 MB, se reconstruye lentamente porque cada instalación de dependencia es un caché completo que se pierde en el momento en que cambia una línea de código de la aplicación y arrastra silenciosamente un compilador de C y archivos de encabezado que su contenedor en ejecución nunca volverá a tocar una vez finalizada la compilación.
Ya nada de eso es necesario. Entre uv (el administrador de paquetes basado en Rust y el instalador de Astral), montajes de caché de BuildKit e imágenes base sin distribución, el valor predeterminado de 2026 para un contenedor Python de producción es una compilación de varias etapas que incluye solo un intérprete de Python, su entorno virtual y su código, nada más. Esta guía explica el patrón antiguo, el patrón nuevo y un Dockerfile que puede copiar en un proyecto real hoy.
El viejo patrón y por qué es caro
DOCKERFILEFROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install -r requirements.txt COPY . . CMD ["python", "app.py"]
Este patrón tiene tres costos que se agravan a medida que crece el proyecto. Primero, python:3.13-slim ya se entrega con aproximadamente 150 a 200 MB comprimidos antes de agregar una sola dependencia, porque "slim" todavía incluye un espacio de usuario completo de Debian, apt y las extensiones C de la biblioteca estándar de Python. En segundo lugar, cada capa RUN pip install se almacena en caché mediante el hash de contenido de Docker de las capas anteriores, por lo que se cambia una línea en app.py antes de COPY. . no invalida nada en sentido ascendente, pero un cambio en requirements.txt invalida todo el paso de instalación y vuelve a descargar cada rueda. En tercer lugar, si alguna dependencia necesita una compilación nativa (una rueda de manylinux faltante, una versión de paquete inusual), pip silenciosamente incorpora herramientas de compilación que luego permanecen en la imagen final para siempre, porque solo hay una etapa para desecharlas.
El nuevo patrón: una etapa de construcción uv más una etapa final sin distribución
La solución es una compilación de varias etapas: una etapa que tiene un compilador, uv y su archivo de bloqueo, y una segunda etapa, mucho más pequeña, que solo copia el entorno virtual terminado y su árbol de fuentes. La etapa final nunca ve pip, uv, apt o un compilador de C.
DOCKERFILEFROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]
gcr.io/distroless/python3-debian12 no tiene shell, administrador de paquetes ni pip, que es exactamente lo que se desea en producción: no queda nada en el contenedor que un atacante que obtenga la ejecución de código pueda usar para incorporar más herramientas. La imagen final son los paquetes del sitio de su entorno virtual más su código, que para un servicio FastAPI típico ocupa menos de 50 MB en lugar de más de 200 MB.
Montajes de caché de BuildKit: nunca vuelvas a descargar la misma rueda dos veces
La línea --mount=type=cache,target=/root/.cache/uv está haciendo el verdadero trabajo aquí. Sin él, cada docker build en un corredor nuevo vuelve a descargar cada rueda de PyPI, porque el caché de capas solo sobrevive si las capas anteriores, incluido COPY uv.lock, tienen bytes idénticos. Un montaje de caché de BuildKit es un volumen persistente fuera del gráfico de capas: sobrevive entre compilaciones en el mismo host o, con un backend de caché de registro, entre ejecutores de CI, incluso cuando cambia el archivo de bloqueo. Combínelo con uv sync --frozen, que se niega a tocar el archivo de bloqueo y falla estrepitosamente si pyproject.toml y uv.lock no están sincronizados, en lugar de resolver silenciosamente un árbol de dependencia diferente al que ejecutaron sus pruebas.
Dividir el paso uv sync --no-install-project del uv sync final es la otra mitad del truco: la primera llamada instala solo dependencias de terceros (almacenadas en caché siempre que uv.lock no cambie), y la segunda llamada, se ejecuta después de COPY. /app, instala tu propio proyecto sin retocar paquetes de terceros. Cambie una línea de código de la aplicación y la capa de dependencia permanecerá almacenada en caché.
Dependencias nativas: numpy, almohada y el problema de manylinux
Paquetes como numpy y Pillow incluyen muchas ruedas Linux preconstruidas para las plataformas comunes, por lo que en linux/amd64 o linux/arm64 uv sync normalmente solo descarga una rueda y nunca toca un compilador. El modo de falla aparece cuando estás en una arquitectura poco común, fijas una versión muy nueva antes de que las ruedas se pongan al día o dependes de un paquete que solo incluye una distribución fuente. En ese caso, la etapa de construcción necesita herramientas de construcción; la etapa final nunca debe ver:
DOCKERFILEFROM python:3.13-slim AS builder RUN apt-get update && apt-get install -y --no-install-recommends build-essential \ && rm -rf /var/lib/apt/lists/* # uv + sync steps as in the stage above
build-essential (encabezados gcc, make, libc) vive solo en la etapa de construcción. La etapa final sin distribución copia el .venv ya compilado, nunca el compilador que lo creó. Si desea practicar la creación y el envío de un servicio contenedorizado real de principio a fin, el curso FastAPI explica exactamente este tipo de API, desde el primer punto final hasta una implementación en forma de producción.
Python administrado por uv: deja de depender del intérprete que envíe la imagen base
python:3.13-slim le ofrece cualquier versión de parche que Debian haya decidido lanzar esa semana. En su lugar, uv puede administrar el intérprete por sí mismo: uv python install 3.13.1 descarga una compilación CPython específica y reproducible del proyecto python-build-standalone y la fija en .python-version, de modo que su etapa de compilación y su computadora portátil ejecuten exactamente el mismo intérprete hasta la versión del parche. Combinado con UV_PYTHON_DOWNLOADS=never en el Dockerfile, por lo que una versión mal fijada falla en la compilación en voz alta en lugar de descargar silenciosamente algo diferente, esto cierra la brecha "funciona en mi máquina, falla en el corredor" que una etiqueta de imagen base flotante deja abierta.
Depurando un contenedor sin shell
El único costo real de distroless: no hay shell, por lo que docker exec -it container sh no funciona. Para la depuración local, cree el mismo Dockerfile con un destino que se detenga en la etapa del constructor (docker build --target=builder -t myapp:debug .), que todavía tiene el espacio de usuario completo y un shell de python:3.13-slim. Mantenga ese objetivo de depuración fuera de su proceso de implementación; existe para su terminal, no para producción.
La plantilla de un solo archivo
DOCKERFILEFROM python:3.13-slim AS builder COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy UV_PYTHON_DOWNLOADS=never WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --frozen --no-install-project --no-dev COPY . /app RUN --mount=type=cache,target=/root/.cache/uv uv sync --frozen --no-dev FROM gcr.io/distroless/python3-debian12 COPY --from=builder /app /app WORKDIR /app ENV PATH="/app/.venv/bin:$PATH" ENTRYPOINT ["/app/.venv/bin/python", "-m", "app"]
Copie esto, cambie el nombre del módulo ENTRYPOINT para que coincida con el diseño de su proyecto, y tendrá una compilación que es más pequeña, más rápida en compilaciones repetidas y más difícil de atacar que el patrón pip install -r requisitos.txt que la mayoría de los tutoriales aún enseñan. ¿Listo para practicar el backend que va dentro de este contenedor? Comience con FastAPI.
Preguntas frecuentes
¿Necesito habilitar BuildKit para que funcione el montaje de caché?
Sí, --mount=type=cache es una característica exclusiva de BuildKit. Ha sido el constructor predeterminado en Docker Desktop y en las versiones recientes de Docker Engine, pero en ejecutores de CI más antiguos es posible que necesite configurar DOCKER_BUILDKIT=1 explícitamente, o docker buildx build en lugar de docker build simple.
¿Por qué no usar python:3.13-alpine para una imagen más pequeña?
Musl libc de Alpine rompe la suposición de la rueda manylinux en la que se basan numpy, Pillow y la mayoría de los paquetes compilados, por lo que se pierden las ruedas prediseñadas y se compila desde el código fuente o se producen errores sutiles en el tiempo de ejecución. distroless-glibc evita esa trampa.
¿Uv reemplaza completamente a pip y Poetry?
Para la mayoría de los proyectos nuevos, sí. uv se resuelve e instala más rápido, lee el mismo pyproject.toml y genera un archivo de bloqueo, pero si un equipo ya depende de los complementos de Poetry o de un flujo de trabajo que uv aún no cubre, no hay urgencia de migrar a mitad del proyecto. Pruebe primero uv en un nuevo servicio.
¿Qué pasa si mi imagen base necesita un parche de seguridad después de haberla creado?
Reconstruir. Fijar un resumen, no solo una etiqueta, para python:3.13-slim en la etapa de creación, y reconstruir según un cronograma, semanalmente es razonable para la mayoría de los equipos, de modo que una imagen base parcheada fluya hacia una nueva compilación en lugar de permanecer obsoleta durante meses.
¿Puedo usar este patrón para un proyecto Django en lugar de FastAPI?
Sí, a Dockerfile no le importa qué marco ejecute; solo cambia el ENTRYPOINT final (por ejemplo, gunicorn myproject.wsgi:application). El curso FastAPI es una forma rápida de practicar el patrón de principio a fin si está iniciando un nuevo servicio.
¿Está listo para producción sin distribución o todavía es experimental?
Es ampliamente utilizado en producción; Google lo mantiene y muchos equipos lo incluyen como su base Python predeterminada. La desventaja de aceptar entrar es la falta de un caparazón, que es una elección de seguridad deliberada, no un error.