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=1makes logs appear immediately indocker logs.- Wheels are compiled in the
buildstage; the runtime image only needs runtime libraries (libpq5for psycopg, notlibpq-devand 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.txtis 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:
- WhiteNoise — serve static files from the app container itself; simplest. Run
collectstaticduring the build:
RUN python manage.py collectstatic --noinput
(The build needs settings that don’t require a database or secrets at build time.)
- 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_HEADERin Django,ProxyFixin Flask,--proxy-headersin Uvicorn). - Log to stdout/stderr.
Checklist
- Slim base, compilers only in a build stage
-
requirements.txtcopied 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.