Python apps are easy to containerise badly: huge images with compilers inside, rebuilt dependencies on every code change, the dev server in production. This guide builds a lean, production-ready image for Django, Flask or FastAPI.

.dockerignore

.git
__pycache__/
*.pyc
.venv
venv
.env
media/
staticfiles/

The Dockerfile

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS base
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# ---- Build wheels (compilers live only here) ----
FROM base AS build
RUN apt-get update && apt-get install -y --no-install-recommends build-essential libpq-dev \
 && rm -rf /var/lib/apt/lists/*
WORKDIR /wheels
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip wheel --wheel-dir /wheels -r requirements.txt

# ---- Runtime ----
FROM base
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
 && rm -rf /var/lib/apt/lists/*
RUN useradd --system --uid 10001 --create-home app
WORKDIR /app
COPY --from=build /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY --chown=app:app . .
USER app
EXPOSE 8000
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "--access-logfile", "-", "myproject.wsgi:application"]
  • PYTHONUNBUFFERED=1 makes logs appear immediately in docker logs.
  • Wheels are compiled in the build stage; the runtime image only needs runtime libraries (libpq5 for psycopg, not libpq-dev and gcc).
  • Packages with ready-made wheels (psycopg[binary], most popular libraries) don’t need compilers at all — then you can drop the build stage.
  • requirements.txt is copied before the code so dependencies stay cached.

The right server for each framework

Framework Production command
Django (WSGI) gunicorn myproject.wsgi:application -b 0.0.0.0:8000 -w 3
Flask gunicorn app:app -b 0.0.0.0:8000 -w 3
FastAPI (ASGI) uvicorn main:app --host 0.0.0.0 --port 8000 --workers 3
Django ASGI / channels gunicorn -k uvicorn.workers.UvicornWorker myproject.asgi:application

Never use manage.py runserver or flask run in production. See Gunicorn vs Uvicorn.

Workers: start with 2 × CPUs + 1 for sync workers and adjust to the container’s memory limit.

Static files (Django)

Options:

  1. WhiteNoise — serve static files from the app container itself; simplest. Run collectstatic during the build:
RUN python manage.py collectstatic --noinput

(The build needs settings that don’t require a database or secrets at build time.)

  1. A shared volume served by Nginx/Caddy — collect into a volume at startup and let the proxy serve it.

Uploaded media files must live in a volume (or object storage), never inside the image.

Migrations

Don’t run migrations automatically in every container start when you have several replicas — they’ll race. Run them as a one-off step during deployment:

docker compose run --rm web python manage.py migrate

For a single-instance app, an entrypoint script that migrates before starting is acceptable.

Compose with PostgreSQL and Redis

services:
  web:
    build: .
    image: myapp:local
    restart: unless-stopped
    env_file: .env
    ports:
      - "127.0.0.1:8000:8000"
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - media:/app/media

  worker:
    image: myapp:local
    command: celery -A myproject worker -l info
    env_file: .env
    depends_on: [db, redis]

  db:
    image: postgres:17
    environment:
      POSTGRES_DB: app
      POSTGRES_USER: app
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    volumes:
      - pg:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10

  redis:
    image: redis:7-alpine

volumes:
  pg:
  media:

The worker reuses the same image with a different command — one build, several roles. See Celery with Redis.

Settings for containers

  • Read secrets and config from environment variables (DATABASE_URL, SECRET_KEY, ALLOWED_HOSTS).
  • Behind a reverse proxy, configure proxy headers (SECURE_PROXY_SSL_HEADER in Django, ProxyFix in Flask, --proxy-headers in Uvicorn).
  • Log to stdout/stderr.

Checklist

  • Slim base, compilers only in a build stage
  • requirements.txt copied before code
  • Non-root user
  • Gunicorn/Uvicorn, not the dev server
  • Static and media handled outside the code layer
  • Migrations as a deployment step
  • Config via environment

Related: Django with Gunicorn and Nginx (without Docker), FastAPI with Uvicorn and the Dockerfile tutorial.

  • Docker
  • Python
  • Deployment