Python Docker — Containerized Applications

Python DevOpsDockerFree Lesson

Advertisement

Python Docker — Containerized Applications

Docker packages applications with their dependencies into portable containers. This eliminates "it works on my machine" problems and enables consistent deployments.

Learning Objectives

  • Write efficient Dockerfiles for Python
  • Use multi-stage builds to reduce image size
  • Manage secrets and environment variables in containers
  • Use docker-compose for multi-service applications

Why Docker?

Imagine you built a Python app on your laptop. It works perfectly. But when you deploy it to a server, it breaks because the server has a different Python version, missing system packages, or conflicting dependencies.

Docker solves this by creating a snapshot of your entire application environment — code, dependencies, system libraries, and runtime. This snapshot (called an image) runs the same way everywhere.

Without Docker:           With Docker:
┌─────────────┐          ┌─────────────┐
│ Your Laptop │          │  Container  │
│ Python 3.11 │          │ ┌─────────┐ │
│ pandas 2.0  │   ──►   │ │ Your App│ │
│ Ubuntu 22   │          │ │ Python  │ │
│ Custom libs │          │ │ deps    │ │
└─────────────┘          │ └─────────┘ │
┌─────────────┐          │  Ubuntu 22   │
│   Server    │          └─────────────┘
│ Python 3.9  │             Same everywhere!
│ Missing pkg │
└─────────────┘

Your First Dockerfile

A Dockerfile is a recipe for building a Docker image. Each line creates a layer — a snapshot of changes.

# Dockerfile

# 1. Start from a base image (Python 3.11 slim)
FROM python:3.11-slim

# 2. Set working directory inside container
WORKDIR /app

# 3. Copy dependency file first (for caching)
COPY requirements.txt .

# 4. Install dependencies
RUN pip install --no-cache-dir -r requirements.txt

# 5. Copy application code
COPY . .

# 6. Expose port (documentation — does not actually publish)
EXPOSE 8000

# 7. Command to run when container starts
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Why This Order Matters

Layer 1: FROM python:3.11-slim          (~150MB, cached)
Layer 2: WORKDIR /app                   (tiny)
Layer 3: COPY requirements.txt .        (small file)
Layer 4: RUN pip install ...            (~200MB, SLOW — cache this!)
Layer 5: COPY . .                       (your code changes often)

If you change your code but not requirements.txt, Docker reuses layers 1-4 from cache. This makes rebuilds fast (seconds instead of minutes).


Multi-Stage Builds

Multi-stage builds separate the "build" phase from the "runtime" phase, producing much smaller images.

# Stage 1: Build
FROM python:3.11 AS builder
WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y gcc

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Stage 2: Runtime (much smaller)
FROM python:3.11-slim
WORKDIR /app

# Copy only installed packages from builder
COPY --from=builder /root/.local /root/.local
COPY . .

# Add local packages to PATH
ENV PATH=/root/.local/bin:$PATH

# Run as non-root user (security)
RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Result: Build stage might be 800MB, but final image is only ~200MB.


docker-compose.yml

For applications with multiple services (web app + database + cache), use docker-compose.

version: '3.8'

services:
  web:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql://user:pass@db:5432/mydb
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    volumes:
      - ./src:/app/src  # Hot reload in development

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: mydb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

  cache:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  pgdata:

Running with Compose

# Build and start all services
docker-compose up --build

# Start in background
docker-compose up -d

# Stop everything
docker-compose down

# View logs
docker-compose logs -f web

.dockerignore

Just like .gitignore, .dockerignore tells Docker which files to exclude when building.

__pycache__
*.pyc
*.pyo
.env
.git
.venv
venv/
*.egg-info
dist/
build/
.pytest_cache
.mypy_cache

Without this, Docker copies unnecessary files into the image, making it larger and potentially leaking secrets.


Essential Docker Commands

# Build an image
docker build -t myapp:latest .

# Run a container
docker run -p 8000:8000 myapp:latest

# Run in background
docker run -d -p 8000:8000 --name myapp myapp:latest

# View running containers
docker ps

# View logs
docker logs myapp

# Execute command in running container
docker exec -it myapp bash

# Stop and remove
docker stop myapp
docker rm myapp

# Clean up unused images
docker system prune -a

Environment Variables and Secrets

import os

# Read from environment (set by Docker)
DATABASE_URL = os.environ["DATABASE_URL"]
API_KEY = os.environ.get("API_KEY")  # Optional
DEBUG = os.environ.get("DEBUG", "false").lower() == "true"
# Set defaults in Dockerfile
ENV DEBUG=false
ENV PYTHONUNBUFFERED=1
# Override in docker-compose.yml
environment:
  - DEBUG=true
  - DATABASE_URL=postgresql://...

# Or use .env file
env_file:
  - .env

Production Best Practices

# Good production Dockerfile
FROM python:3.11-slim

# Security: don't run as root
RUN useradd --create-home --shell /bin/bash appuser
WORKDIR /home/appuser/app

# Install dependencies first (cache layer)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy code
COPY --chown=appuser:appuser . .

# Switch to non-root user
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:8000/health || exit 1

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Key Takeaways

  1. Always use slim base images to reduce size
  2. Copy requirements.txt before code for layer caching
  3. Never run containers as root in production
  4. Use multi-stage builds for smaller production images
  5. Use .dockerignore to exclude unnecessary files
  6. Use docker-compose for multi-service applications
  7. Set PYTHONUNBUFFERED=1 for proper log output
  8. Use HEALTHCHECK for container health monitoring

Advertisement

Need Expert Python Help?

Get personalized tutoring, project support, or professional consulting.

Advertisement