The recipe for one small web service, why its lines are in this order, and what must never go into an image.
The example service in code/ship-it is tiny. It has two web addresses: /health returns {"status":"ok"}, and /echo takes a name and answers with a greeting. The greeting word comes from a setting called APP_GREETING. Its four tests pass, and here is the real output of running it and calling it with curl:
$ curl-slocalhost:8011/health
{"status":"ok"}$ curl-s-XPOSTlocalhost:8011/echo-H'content-type: application/json'-d'{"name":"Asha"}'{"message":"Hello, Asha!"}$ curl-s-XPOSTlocalhost:8011/echo-H'content-type: application/json'-d'{"name":""}'{"detail":[{"type":"string_too_short","loc":["body","name"],"msg":"String should have at least 1 character","input":"","ctx":{"min_length":1}}]}
The Dockerfile that packages it:
FROMpython:3.12-slimENVPYTHONDONTWRITEBYTECODE=1\PYTHONUNBUFFERED=1WORKDIR/app# 1) Dependencies first: this layer is cached until requirements.txt changes.COPYrequirements.txt.
RUNpipinstall--no-cache-dir-rrequirements.txt
# 2) Then the code, which changes often.COPYapp.py.
# Run as an ordinary user, not root.RUNuseradd--create-homeappuser
USERappuserEXPOSE8000HEALTHCHECK--interval=30s--timeout=3s\CMDpython-c"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"CMD["uvicorn","app:app","--host","0.0.0.0","--port","8000"]
Instruction
What it does and why
FROM python:3.12-slim
Start from a small official Python image. Name the version so builds are repeatable.
ENV PYTHONUNBUFFERED=1
Print logs immediately instead of holding them back. Containers rely on what is written to the output.
WORKDIR /app
The folder inside the image where the next steps happen.
COPY requirements.txt . then RUN pip install
Install the libraries before copying the code. The slow step is then reused on most builds.
COPY app.py .
Copy only what the service needs.
RUN useradd and USER appuser
Run as an ordinary user. If the app is ever compromised, the attacker is not the all-powerful root user.
HEALTHCHECK
Lets Docker see whether the app is actually answering, not only running.
CMD [...]
The command that starts the service. The list form passes signals (such as "stop") straight to the program.
Why does the order matter? Docker saves each step as a layer. When you rebuild, it reuses every layer from the top until it meets a line whose inputs changed, and rebuilds that line and everything below it.
Order matters: put what changes rarely at the top and what changes often at the bottom.
Your code changes many times a day, but your library list only now and then. So the code is copied last. If it were copied first, every code edit would reinstall every library.
No secrets in images
Anything you COPY or ENV into an image can be read from it
Every layer is stored in the image, and anyone who gets the image can read them. Never COPY a .env file or write a key with ENV. Pass secrets when the container runs: docker run -e APP_LLM_API_KEY ..., the environment: section in Compose, or the hosting platform's secret settings. Module 6 explains why the code should read all its settings from the environment; this is where that pays off, because the same image then runs everywhere.
A .dockerignore file lists files that are never sent to the build in the first place. The example's file contains: