Construir una aplicación Python
Los wheels y el instrumental de construcción de Python hacen que valga la pena una separación limpia. Compila e instala todo en un virtualenv autocontenido en la etapa de construcción y luego copia ese único directorio sobre una base python ligera para el runtime. Los compiladores y las cabeceras que algunos paquetes necesitan en el momento de la instalación nunca llegan a la imagen que despliegas.
Estas construyen sobre ghcr.io/quenchworks/images/python:3.13. Para la resolución de dependencias está ghcr.io/quenchworks/images/uv (un instalador y resolvedor rápido) y ghcr.io/quenchworks/images/poetry.
El Dockerfile multi-etapa
# Build stage: build a venv with everything installed into it.FROM ghcr.io/quenchworks/images/python:3.13 AS buildWORKDIR /appENV VIRTUAL_ENV=/opt/venv \ PATH="/opt/venv/bin:$PATH"
RUN ["python", "-m", "venv", "/opt/venv"]COPY requirements.txt ./RUN ["pip", "install", "--no-cache-dir", "-r", "requirements.txt"]COPY . .
# Runtime stage: copy the venv onto a clean python base, run nonroot.FROM ghcr.io/quenchworks/images/python:3.13 AS runtimeWORKDIR /appENV VIRTUAL_ENV=/opt/venv \ PATH="/opt/venv/bin:$PATH" \ PYTHONUNBUFFERED=1COPY --from=build /opt/venv /opt/venvCOPY --from=build /app /appUSER 1001EXPOSE 8000CMD ["python", "-m", "app"]# Build stage: uv resolves and installs into a project venv, fast.FROM ghcr.io/quenchworks/images/uv:0.11 AS buildWORKDIR /appENV UV_PROJECT_ENVIRONMENT=/opt/venv \ UV_COMPILE_BYTECODE=1
# Install dependencies first (cached) from the lockfile, then the project.COPY pyproject.toml uv.lock ./RUN ["uv", "sync", "--frozen", "--no-install-project", "--no-dev"]COPY . .RUN ["uv", "sync", "--frozen", "--no-dev"]
FROM ghcr.io/quenchworks/images/python:3.13 AS runtimeWORKDIR /appENV PATH="/opt/venv/bin:$PATH" \ PYTHONUNBUFFERED=1COPY --from=build /opt/venv /opt/venvCOPY --from=build /app /appUSER 1001EXPOSE 8000CMD ["python", "-m", "app"]# Build stage: Poetry installs into a venv at a known path.FROM ghcr.io/quenchworks/images/poetry:2 AS buildWORKDIR /appENV POETRY_VIRTUALENVS_IN_PROJECT=true \ POETRY_NO_INTERACTION=1
COPY pyproject.toml poetry.lock ./RUN ["poetry", "install", "--only", "main", "--no-root"]COPY . .RUN ["poetry", "install", "--only", "main"]
FROM ghcr.io/quenchworks/images/python:3.13 AS runtimeWORKDIR /appENV PATH="/app/.venv/bin:$PATH" \ PYTHONUNBUFFERED=1COPY --from=build /app/.venv /app/.venvCOPY --from=build /app /appUSER 1001EXPOSE 8000CMD ["python", "-m", "app"]Qué hace cada etapa
- Construcción. Crea un virtualenv en una ruta fija e instala cada dependencia en él. Esta etapa carga el resolvedor, cualquier backend de construcción y las dependencias de construcción de los wheels. Instalar las dependencias antes de copiar el resto del código fuente mantiene esa capa cacheada cuando solo cambia tu código.
- Runtime. Empieza de nuevo desde
python:3.13, copia el venv y la aplicación, pon el venv en elPATH, baja auid 1001y arranca. Nada del conjunto de herramientas de construcción se entrega.
Siguiente
- ¿Sirves con un servidor WSGI/ASGI? Ponlo en el venv (gunicorn, uvicorn) e invócalo directamente en forma exec:
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000"]. - Fija por digest para que cada construcción ejecute exactamente la base que se escaneó y firmó.