Dockerizing a Python App
The Node Dockerfile you already know
Section titled “The Node Dockerfile you already know”If you have containerized a Node/TypeScript app before, you know the pattern: copy package.json, run npm ci, copy source, build, and run. Python’s Dockerfile follows the same multi-stage logic — but with a few Python-specific best practices you need to internalize.
Side-by-side: Node vs Python Dockerfile
Section titled “Side-by-side: Node vs Python Dockerfile”# ---- Node / TypeScript multi-stage Dockerfile ----FROM node:20-alpine AS depsWORKDIR /appCOPY package*.json ./RUN npm ci --only=production
FROM node:20-alpine AS builderWORKDIR /appCOPY package*.json ./RUN npm ciCOPY . .RUN npm run build
FROM node:20-alpine AS runnerWORKDIR /appENV NODE_ENV=productionCOPY --from=deps /app/node_modules ./node_modulesCOPY --from=builder /app/dist ./distRUN addgroup -S appgroup && adduser -S appuser -G appgroupUSER appuserEXPOSE 3000CMD ["node", "dist/main.js"]# ---- Python multi-stage Dockerfile ----FROM python:3.12-slim AS builderWORKDIR /app
# Install build tools, then create venv inside the imageRUN python -m venv /app/.venvENV PATH="/app/.venv/bin:$PATH"
COPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txt
FROM python:3.12-slim AS runnerWORKDIR /app
# Copy the pre-built venv from builderCOPY --from=builder /app/.venv /app/.venvENV PATH="/app/.venv/bin:$PATH"
# Copy application sourceCOPY src/ ./src/
# Run as non-root userRUN addgroup --system appgroup && adduser --system --ingroup appgroup appuserUSER appuser
EXPOSE 8000CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]The full Python Dockerfile explained
Section titled “The full Python Dockerfile explained”Here is the production-grade Dockerfile with comments:
# syntax=docker/dockerfile:1
# ---- Stage 1: Build / dependency installation ----FROM python:3.12-slim AS builder
WORKDIR /app
# PYTHONDONTWRITEBYTECODE: don't write .pyc files (saves space)# PYTHONUNBUFFERED: log output immediately (12-Factor: log to stdout)ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1
# Create the virtual environment INSIDE the image# This lets us copy the entire /app/.venv to the final stageRUN python -m venv /app/.venvENV PATH="/app/.venv/bin:$PATH"
# Copy dependency manifest first (Docker layer caching)COPY pyproject.toml uv.lock* requirements.txt* ./
# Install dependencies — no cache to keep image smallRUN pip install --no-cache-dir --upgrade pip && \ pip install --no-cache-dir -r requirements.txt
# ---- Stage 2: Runtime image ----FROM python:3.12-slim AS runner
WORKDIR /app
# Copy the fully-populated venv from builderCOPY --from=builder /app/.venv /app/.venvENV PATH="/app/.venv/bin:$PATH" \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1
# Copy application source (after deps — better cache hit on rebuilds)COPY src/ ./src/
# Create and switch to a non-root user (security best practice)RUN addgroup --system appgroup && \ adduser --system --ingroup appgroup --no-create-home appuserUSER appuser
# Expose the port the app listens onEXPOSE 8000
# CMD uses exec form (no shell wrapper — signals go directly to process)CMD ["uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]Why venv inside the Docker image?
Section titled “Why venv inside the Docker image?”Run this in your terminal to build and run:
docker build -t my-python-app .docker run -p 8000:8000 my-python-app
# Or with docker compose:# docker compose up --build.dockerignore
Section titled “.dockerignore”Like .gitignore for Docker. Always create one to avoid copying your venv, cache, and secrets into the image:
.venv/__pycache__/*.pyc*.pyo.pytest_cache/.mypy_cache/.ruff_cache/.env.env.**.log