anishfyi.com Projects All writings

October 20, 20258 min readDocker, DevOps, Django

Dockerizing a Django App for Dev and Production

A Docker setup for Django that stays fast in local development and lean in production.

"Works on my machine" is a backend problem, not a joke. Docker makes the environment part of the repository, so the Python version, the system libraries, and the way the app starts are the same for every developer and for production. Here is a setup that stays fast locally and lean in production.

A Dockerfile that builds small

A multi-stage build keeps the dependency install in its own layer and ships a final image with only what runtime needs.

FROM python:3.12-slim AS base
ENV PYTHONUNBUFFERED=1 PYTHONDONTWRITEBYTECODE=1
WORKDIR /app

FROM base AS deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

FROM deps AS final
COPY . .
RUN python manage.py collectstatic --noinput
CMD ["gunicorn", "trampoline.wsgi", "--bind", "0.0.0.0:8000"]

Layer order is a cache strategy

Requirements are copied and installed before the application code. A change to a view does not invalidate the dependency layer, so a rebuild skips the slow install entirely. Copy the slow-changing things first.

Compose for local development

Locally the app needs a database and a cache beside it. Compose brings them up together, and a volume mount means a code change is picked up without a rebuild.

services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    depends_on:
      - db
      - redis
  db:
    image: postgres:16
  redis:
    image: redis:7

Config comes from the environment

Settings read from the environment, never from a value baked into the image. The same image then runs in development, staging, and production with nothing changed but the variables passed to it.

One image, different commands

There is no need for separate dev and prod images. Build one, and let the command differ: the development server locally, Gunicorn in production. Same code, same dependencies, fewer surprises.

Keep the image lean

A .dockerignore keeps the local virtualenv, the Git history, and editor files out of the build context. A slim base image and a final stage without build tools keep the shipped image small, which means faster pulls and a smaller surface to worry about.

Do not run as root

Add an unprivileged user and switch to it before the final command. If the container is ever compromised, an attacker lands as a limited user instead of root. It is one line, and it is worth it.

Working on something in this territory? Book thirty minutes.