runtime
242 / 250
lesson 242 Application Delivery · advanced

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.

You'll be able to
  • 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
The idea

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.

example-1.py · Parse a Dockerfile into instructions
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:

example-2.py · Shell-form CMD isn't a JSON array
import json

cmd_args = "python /app/main.py"
print(json.loads(cmd_args))
example-3.py · A linter for the review rules
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.

Review
  • 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.
Practise this

Loading your progress…

To master this lesson, solve both challenges without hints or the answer, and pass the concept check.

Read and check a Dockerfile — Application Delivery · Runtime