Containerization, Multi-Stage Dockerfiles & CI/CD Deployment
Deploying Python applications in production requires building secure, lightweight, and fast-starting container images (Docker / OCI). Understanding Multi-Stage Docker builds, Docker layer caching optimization, security hardening (non-root execution users, minimal base images like python:3.11-slim), and production entrypoints (gunicorn / uvicorn) is essential for cloud-native deployment.
This chapter details Multi-Stage Dockerfile architecture, Docker layer cache optimization, container security hardening, and production WSGI/ASGI entrypoint configurations.
1. Multi-Stage Dockerfile Architecture for Python
Standard Docker builds leave heavy C compilers (gcc), build dependencies, and build caches inside the final production container image, inflating image sizes to 1GB+ and increasing attack surface.
Multi-Stage Builds separate the build environment from the final runtime container:
Multi-Stage Docker Build Architecture:
[ Stage 1: Builder (python:3.11-slim + gcc + uv) ]
βββ Install build tools
βββ Compile C-extensions & install dependencies into virtualenv (.venv)
βββ [ Build Complete ]
|
v (Copy ONLY .venv directory!)
[ Stage 2: Production Runtime (python:3.11-slim + Non-Root User) ]
βββ Clean OS base image (Zero gcc! Zero build dependencies!)
βββ Copy pre-built .venv from Stage 1
βββ Set USER appuser & ENTRYPOINT ["uvicorn", ...]
(Final Image Size: <150MB!)2. Optimized Multi-Stage Dockerfile Implementation
# ==========================================
# STAGE 1: Builder Stage
# ==========================================
FROM python:3.11-slim AS builder
# Install uv package manager
COPY --from=ghcr.io/astral-sh/uv:latest /uv /bin/uv
WORKDIR /app
# Enable bytecode compilation for fast startup
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy
# Step 1: Copy ONLY dependency manifests to leverage Docker layer caching!
COPY pyproject.toml uv.lock ./
# Step 2: Install dependencies into isolated virtual environment (.venv)
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
# Copy application source code and install project
COPY src/ ./src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
# ==========================================
# STAGE 2: Minimal Production Runtime
# ==========================================
FROM python:3.11-slim AS runtime
# Security Hardening: Create non-root system user
RUN groupadd -r appgroup && useradd -r -g appgroup -s /bin/false appuser
WORKDIR /app
# Copy pre-built virtual environment and app from Builder stage
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src
# Set virtual environment environment path
ENV PATH="/app/.venv/bin:$PATH"
# Switch to non-root user
USER appuser
# Health Check
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
EXPOSE 8000
ENTRYPOINT ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]3. Docker Layer Cache Optimization
Docker caches build layers sequentially based on file modification hashes.
CRITICAL DOCKER CACHING RULE: Copy and install
pyproject.toml/uv.lockBEFORE copying application source code!
If COPY . . is placed before uv sync, changing a single line of application source code invalidates the Docker cache for the dependency installation step, forcing Docker to re-download and re-install all Python dependencies on every build!
4. Container Security Hardening Checklist
- Never Run as Root: Always define a dedicated non-root user (
USER appuser). Running as root allows container-escape vulnerabilities to compromise the host kernel. - Minimal Base Image: Use
python:X.Y-slim(or Distroless images). Avoidalpinefor Python applications because Alpineβsmusllibc requires compiling all C-extensions from source, degrading build performance and stability. - Signal Forwarding (
execform): Always specifyENTRYPOINTusing JSON array syntax (ENTRYPOINT ["uvicorn", ...]). String syntax (ENTRYPOINT uvicorn ...) spawns a shell process (/bin/sh -c) as PID 1, which swallowsSIGTERMshutdown signals from Kubernetes!