Difficulty: Advanced
Docker containers package your Flask application with all its dependencies into a portable, reproducible unit. Whether deploying to AWS, Google Cloud, or your own servers, Docker ensures your application runs the same way everywhere. Containers isolate your app from the host system and other containers.
A Dockerfile defines how to build your application image. It starts from a base Python image, copies your code, installs dependencies, and specifies how to run the application. Each instruction creates a layer in the image. Layers are cached, so unchanged layers are not rebuilt.
Multi-stage builds keep your production image small. The first stage installs all build dependencies and your Python packages. The second stage copies only the installed packages and your application code, leaving behind build tools. This can reduce your image size by 50% or more.
Docker Compose orchestrates multiple containers. A typical Flask deployment needs at least the web application and a database. Docker Compose defines both services, their configuration, networking, and volumes in a single docker-compose.yml file.
Environment variables are the standard way to configure containerized applications. Never bake secrets into Docker images. Instead, pass them at runtime via environment variables or Docker secrets. The Dockerfile should only define non-sensitive defaults.
Health checks ensure your container is actually serving traffic, not just running. Docker can periodically check an endpoint and restart the container if it fails. Define a simple /health endpoint in your Flask app and configure it in the Dockerfile or docker-compose.yml.
For production, always use a non-root user in your container, pin your base image version (not 'latest'), use .dockerignore to exclude unnecessary files, and keep images as small as possible by using slim or alpine base images.
# Dockerfile
FROM python:3.12-slim AS builder
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc libpq-dev && rm -rf /var/lib/apt/lists/*
# Install Python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# Production stage
FROM python:3.12-slim
WORKDIR /app
# Install runtime dependencies only
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 && rm -rf /var/lib/apt/lists/*
# Copy installed packages from builder
COPY --from=builder /install /usr/local
# Create non-root user
RUN useradd -m appuser
USER appuser
# Copy application code
COPY . .
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"
CMD ["gunicorn", "-c", "gunicorn.conf.py", "app:create_app()"]
Multi-stage build: the builder installs dependencies, the production image copies them. Non-root user for security. Health check monitors the app. The CMD runs Gunicorn with the factory pattern.
# docker-compose.yml
version: '3.8'
services:
web:
build: .
ports:
- '8000:8000'
environment:
- DATABASE_URL=postgresql://user:password@db:5432/myapp
- SECRET_KEY=${SECRET_KEY}
- FLASK_CONFIG=production
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:16-alpine
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U user -d myapp']
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres_data:
Docker Compose runs the Flask app and PostgreSQL together. The web service depends on db and waits for its health check. Volumes persist database data. Environment variables configure the app.
# .dockerignore
venv/
__pycache__/
*.pyc
*.pyo
.git/
.gitignore
.env
.env.*
instance/
tests/
*.md
.pytest_cache/
htmlcov/
.coverage
docker-compose*.yml
Dockerfile
.dockerignore
node_modules/
.dockerignore works like .gitignore for Docker builds. It prevents unnecessary files from being copied into the image, reducing build time and image size. Exclude tests, docs, venv, and sensitive files.
from flask import Flask, jsonify
from app.extensions import db
@app.route('/health')
def health_check():
"""Health check endpoint for Docker/load balancers."""
try:
# Check database connectivity
db.session.execute(db.text('SELECT 1'))
return jsonify({
'status': 'healthy',
'database': 'connected'
})
except Exception as e:
return jsonify({
'status': 'unhealthy',
'database': str(e)
}), 503
# Docker HEALTHCHECK calls this endpoint
# Load balancers use it to route traffic only to healthy instances
A health check endpoint verifies the app can connect to its dependencies. Return 200 for healthy, 503 for unhealthy. Docker restarts unhealthy containers. Load balancers route traffic away from unhealthy instances.
Docker, Dockerfile, Docker Compose, Container, Multi-stage Build, Environment Variables