Tutorial: Learn Python Docker from Scratch (2026)
The first time I Dockerized a Python app, the setup time for new developers dropped from half a day to five minutes. Docker packages your application and its dependencies into a container that runs the same way on a developer's laptop, a CI server, and a production instance. For Python projects, Docker solves the classic 'it works on my machine' problem by locking in the Python version, system libraries, and pip packages.
This tutorial covers containerizing Python applications step by step: writing Dockerfiles that leverage multi-stage builds for small images, using Docker Compose to orchestrate multi-service stacks (app + database + cache), and optimizing images for production with .dockerignore, layer caching, and non-root users.
Writing a Python Dockerfile
A Dockerfile defines the container image. Start with a base image like python:3.12-slim (Debian-based, minimal size). Copy your requirements.txt first and run pip install — this layer is cached as long as requirements don't change. Then copy the application code. Use WORKDIR to set the working directory, and CMD or ENTRYPOINT to define the process that runs when the container starts. The -slim variants save hundreds of megabytes over full images.
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies (if needed)
RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/*
# Install Python dependencies (cached separately)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Run as non-root user
RUN useradd -m appuser && chown -R appuser /app
USER appuser
CMD ["python", "app.py"]
Multi-Stage Builds for Smaller Images
Multi-stage builds use multiple FROM statements in one Dockerfile. The first stage installs build dependencies (compilers, headers) and compiles or builds the application. The final stage copies only the runtime artifacts — no build tools. This dramatically reduces image size. For a Python app with compiled C extensions (NumPy, Pandas), the final image can be 80% smaller than a single-stage build.
# Build stage
FROM python:3.12-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt
# Final stage
FROM python:3.12-slim
WORKDIR /app
# Copy only installed packages from builder
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY app.py .
# Verify size: docker images | grep myapp
CMD ["python", "app.py"]
Docker Compose for Multi-Service Applications
Docker Compose defines multi-container applications in a YAML file. Each service specifies its build context, ports, volumes, environment variables, and dependencies. The depends_on key controls startup order. I use Compose to run the application server, PostgreSQL, Redis, and a background worker together. Named volumes persist database data across container restarts.
version: '3.9'
services:
app:
build: .
ports:
- "8000:8000"
depends_on:
- db
- redis
environment:
- DATABASE_URL=postgresql://user:pass@db:5432/myapp
- REDIS_URL=redis://redis:6379/0
volumes:
- .:/app
db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: user
POSTGRES_PASSWORD: pass
volumes:
- pgdata:/var/lib/postgresql/data
redis:
image: redis:7-alpine
volumes:
pgdata:
Optimizing Image Size and Build Speed
Small images deploy faster and have a smaller attack surface. Start with alpine or slim base images. Chain RUN commands to reduce layers. Use .dockerignore to exclude venv, __pycache__, .git, and other unnecessary files. Order Dockerfile instructions from least to most frequently changing to maximize layer caching. Use --no-cache-dir on pip to avoid caching packages in the image.
# .dockerignore
venv/
__pycache__/
*.pyc
.env
.git/
.gitignore
README.md
*.md
# Best practices summary:
# 1. Use python:3.12-slim (130MB) over python:3.12 (330MB)
# 2. Combine RUN apt-get update && apt-get install && apt-get clean
# 3. Add .dockerignore (saves build context upload time)
# 4. Pin pip package versions
# 5. Use HEALTHCHECK instruction
FROM python:3.12-alpine
RUN apk add --no-cache --virtual .build-deps gcc musl-dev && \
pip install --no-cache-dir -r requirements.txt && \
apk del .build-deps
Environment Variables and Configuration
Container configuration belongs in environment variables, not code. Pass them via docker run -e or the Compose environment block. For secrets, use Docker secrets (Swarm) or a .env file (Compose). Python apps should read config from os.environ with defaults, and fail fast if required variables are missing. The pydantic-settings library provides validated configuration from environment variables.
# docker-compose override for dev
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up
# docker-compose.dev.yml
services:
app:
environment:
- DEBUG=true
- LOG_LEVEL=DEBUG
volumes:
- .:/app # hot reload in development
# Python config loading
import os
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
database_url: str
redis_url: str = "redis://localhost:6379/0"
debug: bool = False
log_level: str = "INFO"
class Config:
env_file = ".env"
settings = Settings()
print(settings.database_url) # From env var DATABASE_URL
Production Deployment and Health Checks
Production containers need health checks, resource limits, and logging. Docker's HEALTHCHECK instruction runs a command periodically to verify the app is responding. Set memory and CPU limits in Compose or orchestration. Use Gunicorn with multiple workers for Python web apps. For logging, write to stdout/stderr — Docker captures these and sends them to the logging driver (json-file, syslog, CloudWatch).
# Dockerfile production additions
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
# docker-compose production (compose.yml)
services:
app:
build:
context: .
dockerfile: Dockerfile.prod
deploy:
replicas: 3
resources:
limits:
memory: 512M
cpus: '0.5'
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Run with: docker compose up -d --scale app=3
Frequently Asked Questions
Should I use Docker for development or just production?
Both. Docker ensures dev/prod parity. Use Docker Compose for local development with hot reload (bind mounts). The same image goes through CI to staging to production without changes.
How do I handle database migrations in Docker?
Run migrations as an init container or a separate Compose service that runs before the app starts. Use depends_on with condition: service_healthy for the database, then run python manage.py migrate.
Why is my Docker image so large?
Common culprits: using full (not slim/alpine) base images, not cleaning apt-get cache, including virtualenv in the image, and copying unnecessary files. Use docker history myimage to inspect layer sizes.
How do I debug a crashing container?
Check docker logs
Originally published on Ayodhyyya. Last updated June 1, 2026.