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 --versionWindows 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
└── DockerfileSample 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.1Basic 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-app5. 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-slimLayer 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
.dockerignoreto 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 --build7. 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 1Environment 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:app8. 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.pyDjango 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 pytest10. Container Security
Security Best Practices
- Use official base images
- Keep images minimal
- Run as non-root user
- Scan images for vulnerabilities
- Use multi-stage builds
- Pin dependency versions
- 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-appConclusion
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.