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.lock BEFORE 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

  1. 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.
  2. Minimal Base Image: Use python:X.Y-slim (or Distroless images). Avoid alpine for Python applications because Alpine’s musl libc requires compiling all C-extensions from source, degrading build performance and stability.
  3. Signal Forwarding (exec form): Always specify ENTRYPOINT using JSON array syntax (ENTRYPOINT ["uvicorn", ...]). String syntax (ENTRYPOINT uvicorn ...) spawns a shell process (/bin/sh -c) as PID 1, which swallows SIGTERM shutdown signals from Kubernetes!
Display Options
Appearance
Text Size
100%