Skip to content

Containerizing Python Applications

This guide walks through the process of containerizing Python applications using Docker.

1. Introduction to Docker for Python

Docker allows you to package your Python application along with its dependencies into a standardized container that can run consistently across different environments.

Benefits of Containerization

  • Consistency: Eliminates "works on my machine" problems
  • Isolation: Applications run in their own environment
  • Portability: Run anywhere Docker is supported
  • Scalability: Easy to scale horizontally
  • Version control: Container images are versioned

2. Installing Docker

Linux

bash
# Update package index
sudo apt update

# Install dependencies
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common

# Add Docker's official GPG key
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# Set up stable repository
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io

# Add your user to the docker group
sudo usermod -aG docker $USER

# Activate changes to groups
newgrp docker

# Verify installation
docker --version

Windows and macOS

Download and install Docker Desktop from Docker's official website.

3. Creating a Basic Python Dockerfile

Sample Application Structure

my-python-app/
├── app.py
├── requirements.txt
└── Dockerfile

Sample Python Application (app.py)

python
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from a containerized Python application!"

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 5000))
    app.run(host='0.0.0.0', port=port)

Sample Requirements File (requirements.txt)

flask==2.0.1

Basic Dockerfile

dockerfile
FROM python:3.9-slim

WORKDIR /app

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

COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

4. Building and Running Your Docker Image

bash
# Build the Docker image
docker build -t my-python-app .

# Run the container
docker run -p 5000:5000 my-python-app

# Run in detached mode
docker run -d -p 5000:5000 --name my-running-app my-python-app

# View logs
docker logs my-running-app

# Stop the container
docker stop my-running-app

5. Optimizing Docker Images for Python

Multi-stage Builds

dockerfile
# Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

# Install build dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    && rm -rf /var/lib/apt/lists/*

# Install Python dependencies
COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt

# Final stage
FROM python:3.9-slim

WORKDIR /app

# Copy wheels from build stage
COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .

# Install Python dependencies
RUN pip install --no-cache /wheels/*

# Copy application code
COPY . .

# Create non-root user
RUN useradd -m appuser
USER appuser

EXPOSE 5000

CMD ["python", "app.py"]

Using Specific Python Version Tags

dockerfile
FROM python:3.9.16-slim

Layer Caching for Dependencies

dockerfile
FROM python:3.9-slim

WORKDIR /app

# Copy and install requirements first (will be cached)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy the rest of the application (changes more frequently)
COPY . .

EXPOSE 5000

CMD ["python", "app.py"]

Best Practices

  • Use .dockerignore to exclude unnecessary files
  • Set specific version numbers in requirements.txt
  • Use a non-root user for security
  • Enable pip caching

6. Docker Compose for Development

Docker Compose File (docker-compose.yml)

yaml
version: "3"

services:
  web:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - .:/app
    environment:
      - FLASK_ENV=development
      - FLASK_DEBUG=1

  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=app_db

  redis:
    image: redis:6

volumes:
  postgres_data:

Running with Docker Compose

bash
# Start all services
docker compose up

# Start in detached mode
docker compose up -d

# View logs
docker compose logs

# Stop all services
docker compose down

# Rebuild containers
docker compose up --build

7. Production Considerations

Using Gunicorn for Production

dockerfile
FROM python:3.9-slim

WORKDIR /app

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

COPY . .

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:app"]

Health Checks

dockerfile
HEALTHCHECK --interval=30s --timeout=30s --start-period=5s --retries=3 \
  CMD curl -f http://localhost:8000/ || exit 1

Environment Variables

dockerfile
ENV APP_ENV=production \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=off \
    PIP_DISABLE_PIP_VERSION_CHECK=on

# Later in your Dockerfile
CMD gunicorn --bind 0.0.0.0:${PORT:-8000} app:app

8. Django Application Example

Django Project Structure

django-app/
├── Dockerfile
├── docker-compose.yml
├── requirements.txt
├── manage.py
└── myproject/
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    ├── asgi.py
    └── wsgi.py

Django Dockerfile

dockerfile
FROM python:3.9-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /code

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

COPY . .

RUN python manage.py collectstatic --noinput

EXPOSE 8000

CMD ["gunicorn", "--bind", "0.0.0.0:8000", "myproject.wsgi:application"]

Django Docker Compose

yaml
version: "3"

services:
  web:
    build: .
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/code/staticfiles
    expose:
      - 8000
    depends_on:
      - db
    environment:
      - DATABASE_URL=postgres://postgres:postgres@db:5432/postgres
      - SECRET_KEY=your_secret_key
      - DEBUG=False

  db:
    image: postgres:13
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_USER=postgres
      - POSTGRES_DB=postgres

  nginx:
    image: nginx:1.21
    ports:
      - 80:80
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - static_volume:/home/app/staticfiles
    depends_on:
      - web

volumes:
  postgres_data:
  static_volume:

Nginx Configuration for Django

nginx
# nginx/conf.d/default.conf
server {
    listen 80;
    server_name localhost;

    location /static/ {
        alias /home/app/staticfiles/;
    }

    location / {
        proxy_pass http://web:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

9. Continuous Integration with Docker

GitHub Actions Example (.github/workflows/docker-build.yml)

yaml
name: Docker Build

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v1

      - name: Build and test
        uses: docker/build-push-action@v2
        with:
          context: .
          push: false
          load: true
          tags: app:test

      - name: Run tests
        run: |
          docker run --rm app:test pytest

10. Container Security

Security Best Practices

  1. Use official base images
  2. Keep images minimal
  3. Run as non-root user
  4. Scan images for vulnerabilities
  5. Use multi-stage builds
  6. Pin dependency versions
  7. Don't store secrets in images

Security Scanning

bash
# Install Trivy
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
    -v $HOME/.cache:/root/.cache/ aquasec/trivy:latest image my-python-app

Conclusion

Containerizing Python applications with Docker provides a consistent, portable, and scalable deployment solution. By following the practices in this guide, you can create optimized containers for both development and production environments. The next steps would be to explore container orchestration platforms like Kubernetes or Docker Swarm to manage containerized applications in production.