Read and check a Dockerfile
A Docker image packages the operating system, Python, the locked packages and your code, so the server runs exactly what was built. Docker can't run in a browser, so this lesson reads real Dockerfiles and checks them with Python: the rules a reviewer applies, turned into a linter.
Read and diagnose: this can't run in the browser, so you'll read real code and output and answer questions about it.
- Read a Dockerfile: each instruction makes a layer, and layers are reused until something they depend on changes
- Parse instructions, joining
\continuation lines and skipping comments - Check the rules that make images reproducible, fast to rebuild and safe to run
About this lesson: Docker builds and runs containers on a computer with Docker installed; it can't run inside this page. The Dockerfiles below are real, and what you run is Python that reads them, the same kind of check ("lint") a CI pipeline runs before building.
A Dockerfile is a list of instructions. FROM python:3.12.7-slim starts from a published base image; WORKDIR /app sets the folder later instructions work in; COPY copies files from your project into the image; RUN runs a command while building (installing packages); USER switches to another account; CMD says what to run when a container starts. Each instruction creates a layer, and Docker reuses (caches) a layer as long as the instruction and everything it copies are unchanged.
That gives the review rules. Pin the base image (python:3.12.7-slim, not python or python:latest), or the next build quietly gets a different Python. Install dependencies before copying the code: copy just the lock file, run pip install, and only then COPY . ., so editing one source file doesn't reinstall every package. Don't run as root: create a user and switch to it before CMD. And write CMD in exec form, a JSON array (["python", "-m", "market.server"]), so the program receives stop signals directly instead of through a shell.
Run each example to see what it prints. Running examples doesn't count toward progress.
DOCKERFILE = """ # Runtime Market API FROM python:3.12.7-slim WORKDIR /app COPY requirements.lock . RUN pip install --no-cache-dir \\ --require-hashes -r requirements.lock COPY . . RUN useradd --create-home market USER market CMD ["python", "-m", "market.server"] """ def instructions(text): result = [] pending = "" for line in text.splitlines(): stripped = line.strip() if not pending and (not stripped or stripped.startswith("#")): continue if stripped.endswith("\\"): pending += stripped[:-1].strip() + " " continue full = pending + stripped pending = "" keyword, _, args = full.partition(" ") result.append((keyword.upper(), args.strip())) return result for keyword, args in instructions(DOCKERFILE): print(f"{keyword:8} {args}")
Checking exec form means reading CMD's argument as JSON. Shell form is plain text, and JSON says so:
import json cmd_args = "python /app/main.py" print(json.loads(cmd_args))
import json def instructions(text): result = [] pending = "" for line in text.splitlines(): stripped = line.strip() if not pending and (not stripped or stripped.startswith("#")): continue if stripped.endswith("\\"): pending += stripped[:-1].strip() + " " continue keyword, _, args = (pending + stripped).partition(" ") pending = "" result.append((keyword.upper(), args.strip())) return result def lint(text): found = [] steps = instructions(text) keywords = [k for k, _ in steps] image = steps[0][1] if steps and steps[0][0] == "FROM" else "" if ":" not in image or image.endswith(":latest"): found.append("pin the base image to a version") if "WORKDIR" not in keywords: found.append("set a WORKDIR") copy_all = [i for i, (k, a) in enumerate(steps) if k == "COPY" and a.split()[0] == "."] installs = [i for i, (k, a) in enumerate(steps) if k == "RUN" and "pip install" in a] if copy_all and installs and copy_all[0] < installs[0]: found.append("install dependencies before copying the code") if "USER" not in keywords: found.append("run as a non-root USER") cmd = [a for k, a in steps if k == "CMD"] try: exec_form = bool(cmd) and isinstance(json.loads(cmd[-1]), list) except json.JSONDecodeError: exec_form = False if not exec_form: found.append("write CMD as a JSON array") return found first_draft = """ FROM python:latest COPY . /app RUN pip install -r /app/requirements.lock CMD python /app/main.py """ for finding in lint(first_draft): print("-", finding)
When to use it: in CI before docker build, and when reviewing someone's Dockerfile. Tools such as hadolint do this with many more rules; the ideas are the same. Mistakes to avoid: latest or untagged base images; COPY . . before installing dependencies; secrets copied into the image (the Secure Python module: they stay in its layers); running as root; and shell-form CMD.
- A Dockerfile's instructions build layers; a changed layer rebuilds everything after it.
- Review rules: pinned base image, WORKDIR, dependencies installed (from the lock) before the code is copied, a non-root USER, exec-form CMD.
- A linter is ordinary Python: parse instructions (comments,
\continuations), then check the rules. - Next: CI runs your checks and tests on every push and turns the result into pass or fail.