5.3 · Docker for Python

Level 5 · Pro level & industrialization

5.3Docker for Python

Goal: Containerise a Python app (web/data) with Docker.
Estimated time: 15 min · self-paced reading · If you only have 5 min: read “The essentials in 30s” + the diagram.

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.

Common misinterpretation: “Docker replaces venv, and localhost as host is enough.” Doubly wrong. Venv isolates Python dependencies on your machine; Docker isolates the whole application (Python, system, dependencies) in a portable box — both are often used (venv in dev, Docker to ship). And in a container, localhost is reachable only from inside the container itself: the server must listen on 0.0.0.0.
Containerised Python Same image, from PC to server slim + cache Dependenciesbefore codeUSER + .dockerignore Non-root,lean contextCompose (api+db) Full stackwith one command
Same image, from PC to server
Text version of the diagram (SEO / screen readers / translation): 1) Dockerfile: Nabil starts from a slim base in Algiers, copies requirements then installs before the code (cache), excludes .venv via .dockerignore, creates a non-root USER. 2) Build/run: the image freezes the app and its environment, the container listens on 0.0.0.0. 3) Compose: api + db launched together, DATABASE_URL as a variable. Lesson: the environment travels with the code.

🔎 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

  1. Install Docker Desktop. Prepare a small Python app (e.g. a script or a FastAPI API) with its requirements.txt.
  2. Write a Dockerfile: base image “FROM python:3.12-slim”, copy the code, “RUN pip install -r requirements.txt”, launch command (CMD).
  3. 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).
  4. Edit the code, rebuild, rerun: measure reproducibility — the container runs identically everywhere.
You containerise a Python application with a Dockerfile and run it in a container: the key to reliable, portable deployment.

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.

Caution: Never put secrets (passwords, keys, tokens) in the Dockerfile or the image: pass them via the environment at launch. A shared image keeps all its content — an embedded secret stays readable there.

✓ 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?

Section exit checklist
  • I can explain image vs container in one sentence.
  • My Dockerfile builds a running image.
  • I know why secrets do not belong there.
Self-rating 1-5: is my image rebuildable identically?would a colleague start my stack with one command?

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.

Check your understanding

Which problem does Docker mainly solve?

Additional resources (optional, to go further)

Level 5 examFresh, harder questions to test what you've learned