Containerising a Django app
"It works on my machine" is the oldest problem in software, and containers are the modern answer to it. A Docker container packages your application with its exact Python version, dependencies and system libraries, so it runs identically on your laptop, in CI, and in production. For deploying Django — and for running PostgreSQL locally so development matches production — Docker has become the common baseline. This lesson is a practical, honest introduction: enough to containerise Nidaan and understand what you are doing.
What a container is, and why it helps
A container is a lightweight, isolated package containing your app and everything it needs to run — the
Python runtime, your pip dependencies, system libraries — defined by a text file (Dockerfile) and built
into an image you can run anywhere Docker runs. Unlike a virtual machine, it shares the host's kernel, so
it is fast to start and light on resources. The benefit for Django: the environment is reproducible and
explicit. No "which Python is installed here?", no "did you pip install the new dependency?", no drift
between your machine and the server — the image is the same everywhere. It also makes running dependent
services (PostgreSQL, Redis) locally trivial, so your development environment can mirror production exactly.
A Dockerfile for Django
A Dockerfile is the recipe for your image. A straightforward one for Nidaan:
FROM python:3.12-slim # a specific, slim Python base image
ENV PYTHONUNBUFFERED=1 \ # logs appear immediately, not buffered
PYTHONDONTWRITEBYTECODE=1 # no .pyc files in the container
WORKDIR /app
# Install dependencies FIRST, as their own layer, so they are cached
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Then copy the application code
COPY . .
# Collect static files at build time
RUN python manage.py collectstatic --noinput
# Run with a production server (NOT runserver)
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
Three deliberate choices worth understanding:
- A specific base image (
python:3.12-slim), notlatest— pin the Python version so the image is reproducible, andslimto keep it small. - Copy
requirements.txtand install before copying the code. Docker caches each step as a layer; if your code changes but dependencies do not, Docker reuses the cached dependency layer and rebuilds fast. Copy code first and every code change reinstalls everything — a common, slow mistake. gunicorn, notrunserver. The container runs a real production WSGI server (Gunicorn);runserveris the development server and must never face production traffic (the deploying lesson covers the server).
docker compose for the whole stack
A real Django app is not one container — it is the app plus a database plus maybe Redis. Docker
Compose describes the whole set in one docker-compose.yml and runs them together:
services:
db:
image: postgres:16
environment:
POSTGRES_DB: nidaan
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data # persist the database across restarts
web:
build: .
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000
environment:
DATABASE_URL: postgres://postgres:${DB_PASSWORD}@db:5432/nidaan
depends_on:
- db
ports:
- "8000:8000"
volumes:
pgdata:
docker compose up starts PostgreSQL and the web app together, wired so the app reaches the database at the
hostname db. This is how you run a production-matching stack locally — Postgres included — so
"develop on the same database you deploy on" (the postgres lesson) becomes effortless. Note the volume on
the database: without it, your data lives inside the container and is lost when the container is removed;
the named volume persists it.
The things people get wrong
Containers introduce their own footguns; the common ones for Django:
- Secrets baked into the image. Never
COPYa.envwith real secrets into the image, or hard-code them in the Dockerfile — the image is shareable and the secret would travel with it. Pass secrets as environment variables at run time (Composeenvironment:, or the platform's secret manager), never build them in. - Data in the container. A container's filesystem is ephemeral — remove the container and its internal files vanish. The database must use a persistent volume (above), and user-uploaded media must go to an object store (the static-files lesson), never the container's disk.
runserverin the container. Using the development server in production for "simplicity" — do not; use Gunicorn/Uvicorn.- A giant image. Copying the whole project including
.git, the venv, and node modules bloats the image; use a.dockerignore(like.gitignore) to exclude them.
Is Docker required? An honest note
No. You can deploy Django without Docker — to a platform-as-a-service that builds from your repository, or to a server you configure directly. Docker's value is reproducibility and portability: the same image runs everywhere, and the whole stack (app + Postgres + Redis) spins up with one command. That is genuinely worth it for most teams, and it is why Docker is the common baseline — but it is a means, not a requirement. If a platform gives you reproducible builds without you writing a Dockerfile, that is fine too. Use containers because they make your deploys reliable and your local environment match production — not because a checklist said to.
Check your work
What a container is. A lightweight, isolated package of your app plus its exact runtime, dependencies
and libraries, defined by a Dockerfile and built into an image that runs identically everywhere — solving
"works on my machine".
The Dockerfile choices. A pinned slim base image (reproducible, small); install dependencies before
copying code (layer caching); run Gunicorn, not runserver.
Docker Compose. Describes the whole stack (web + Postgres + Redis) in one file, run with compose up,
wired by service name — the easy way to run a production-matching stack, including Postgres, locally. Use a
volume so the database persists.
The footguns. No secrets baked into the image (pass as runtime env vars); no data in the ephemeral
container filesystem (volumes for the DB, object store for media); no runserver in production; a
.dockerignore to keep the image small.
Honest scope. Docker is not required — its value is reproducibility and portability, worth it for most teams, but a means, not an end.
Practice
- Write a
Dockerfilefor Nidaan with a pinned base image, dependency-layer caching,collectstatic, and a GunicornCMD; build the image. - Reorder the Dockerfile to copy code before installing dependencies, rebuild after a code change, and observe the slower rebuild — then fix the order.
- Write a
docker-compose.ymlwithwebanddbservices; runcompose upand confirm the app connects to PostgreSQL at hostnamedb. - Remove the database volume, restart, and watch the data vanish; add the volume back and confirm it persists.
- Add a
.dockerignoreexcluding.git, the venv and other cruft; compare image sizes. - Reason about why baking a
.envwith real secrets into an image is dangerous, and how to pass secrets instead.
Official documentation
- Docker — Best practices for Dockerfiles — Layer caching, small images, and more.
- Docker — Compose — Defining multi-container stacks.
- Gunicorn documentation — The production WSGI server the container runs.
Next: deploying, HTTPS and the launch checklist.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship