5.3Docker for Python
The essentials in 30 seconds
- End of “it works on my machine”: the container ships the app with its environment and runs identically everywhere.
- Dependencies before code: slim base, requirements copied and installed first — the cache survives changes.
- Dev vs prod: bind mount + reload in dev, copied code + workers in prod — the same base image.
Running mini-case — Nabil containerises in Algiers
Nabil's async pipeline runs perfectly in Algiers — but crashes on his colleague's machine (different Python version, missing dependencies, unreachable port). He writes a Dockerfile: slim base, requirements installed before the code, non-root user, launch on all interfaces. Then a Compose with the database. A follow-up to cases 5.1-5.2 — Nabil makes his analysis portable.
Follow-up to cases 5.1-5.2 — Nabil moves from “works on mine” to “same everywhere”.
1. The Dockerfile for Python that works
Docker packs the Python app with its full environment (Python version, dependencies, system) into a container that runs identically everywhere. The recipe (Dockerfile): python:3.12-slim base, COPY requirements.txt then pip install before the code (the build cache survives changes), a non-root USER, a CMD that launches. .dockerignore excludes .venv, __pycache__ and .git: the context shrinks, the build speeds up. Nabil no longer debates versions: the environment travels with the code.
Reflex: anything living on your machine and missing from the image is a future crash elsewhere — the image must contain everything, dependencies included.
2. Dev vs prod: two faces
Same base image, two launches. In dev: bind mount of the code + --reload (FastAPI/Flask) — you edit, it reloads with no rebuild. In prod: code copied into the image, gunicorn/uvicorn server in multi-worker mode, config via -e variables, listening on 0.0.0.0 (never localhost, unreachable in a container). Nabil develops in bind and ships copied: two faces, one recipe.
To avoid: mixing the two — reload in prod, or frozen copied code in dev forcing a rebuild at every comma.
3. Compose for the full app
A real app is several containers: api (Dockerfile build) + bdd (database + persistent volume) on one network, DATABASE_URL as a variable, depends_on + retry at startup. Compose describes all of it and brings up the local stack identical to CI with one command. Nabil adds the database to Compose: his colleague starts in minutes, with the same versions.
The level 3 of the Docker guide details each line: here, keep the move — one command, the whole stack, same everywhere.
🔎 Concrete example — Nabil's API in a container
Generic recipe: slim, cache, non-root, open listening.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m app && chown -R app:app /app
USER app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Reading: the requirements cached before the changing code — the cache survives commas.
Lesson: --host 0.0.0.0 mandatory in a container, never localhost.
Section vocabulary
- Docker
- A tool that packs the app and its environment into a portable container.
Simple example: Nabil's analysis runs the same in Algiers and on his colleague's machine.
What it is not: a venv recreated by hand on each machine.
Reflex: does my app still depend on my machine?
- Image
- A frozen template of the app + dependencies, built from the Dockerfile.
Simple example: Nabil builds a versioned image of each release.
What it is not: the running container.
Reflex: can I rebuild this image identically?
- Dockerfile
- Build recipe: slim base, dependencies, code, launch.
Simple example: Nabil's readable recipe, requirements before code.
What it is not: a manual Python install on a server.
Reflex: are dependencies installed before the code?
- .dockerignore
- Excludes the unneeded from the context (.venv, caches, .git) to slim the build.
Simple example: Nabil excludes .venv and __pycache__: faster build.
What it is not: copying .venv into the image.
Reflex: what is bloating my context?
- Compose
- Describes several containers (api + db) launched together.
Simple example: Nabil brings up api and database with a single command.
What it is not: a single isolated container with no database.
Reflex: can a newcomer start with one command?
In practice — Containerising a Python application
- Install Docker Desktop. Prepare a small Python app (e.g. a script or a FastAPI API) with its requirements.txt.
- Write a Dockerfile: base image “FROM python:3.12-slim”, copy the code, “RUN pip install -r requirements.txt”, launch command (CMD).
- Build the image: “docker build -t mon-app .”, then run a container: “docker run mon-app” (with -p to expose a port for an API).
- Edit the code, rebuild, rerun: measure reproducibility — the container runs identically everywhere.
Ready-to-say script — proposing Docker to the team
“Our analysis works on my machine in Algiers but not elsewhere: it is an environment problem, not a code problem — Python version, dependencies. I suggest a slim Dockerfile with requirements installed before the code, a non-root USER, plus a Compose with the database. Everyone will run the same image, and secrets will stay out of the image, in environment variables.”
Say in a meeting to get containerisation adopted without aiming for DevOps expertise.
Copy-paste template — generic Python Dockerfile + Compose
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
RUN useradd -m app && chown -R app:app /app
USER app
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# --- docker-compose.yml (generic stack) ---
# services:
# api:
# build: .
# ports: ["8000:8000"]
# environment:
# - DATABASE_URL=postgresql://db:5432/app
# db:
# image: postgres:16
# volumes: ["pgdata:/var/lib/postgresql/data"]
Solo / tight budget
Nabil alone: a slim Dockerfile with no Compose is enough — the image already freezes Python and dependencies.
Small business / small team
Compose api + db: every colleague starts the full stack without installing Python or the database.
SME in Algiers
Nabil shares the image in Algiers: everyone tests the same version, end of machine gaps.
Starting team
Pair up: one writes the Dockerfile, the other checks USER, .dockerignore and 0.0.0.0 before adoption.
✓ Best practices
- slim + cache: requirements first, code second.
- Systematic non-root USER; .dockerignore against .venv and caches.
- 0.0.0.0 in a container, never localhost.
✗ Common pitfalls
- Copying .venv into the image (heavy, not portable).
- Listening on localhost: the service stays unreachable.
- Mixing dev (bind + reload) and prod (copied + workers).
Key takeaways
- Docker packs the app + its environment: end of “it works on my machine”.
- Image = frozen template; container = running instance.
- The Dockerfile describes the image build recipe.
- build (construct) then run (launch): Docker's basic cycle.
48h challenge — apply tomorrow
Write the Dockerfile of your app (template above) and run it in a container: does it answer the same as on your machine?
- I can explain image vs container in one sentence.
- My Dockerfile builds a running image.
- I know why secrets do not belong there.
Frequently asked questions
Does Docker replace virtual environments (venv)?
They operate at different levels. venv isolates a project's Python dependencies on your machine. Docker isolates the whole application (Python, system, dependencies) in a portable container. In practice, a venv in development and Docker to ship/deploy are often combined — complementary, not competing.
Is Docker needed for a beginner?
Not to learn the language. But as soon as you deploy a real application, work in a team or target the cloud, Docker becomes an almost unavoidable standard. It is a highly valued skill: tackling it at pro level is the right time.
Which problem does Docker mainly solve?