Containers and deployment
Lesson 4 of 6
4Containers and deployment
The big picture"Works on my machine" is not deployment. Deployment is a container image holding the code and libraries at pinned versions, running the same way on any server. The secrets live outside it in environment variables. And every version has a number, so you can roll back to the previous one in a minute when the new one goes wrong.
What the picture shows: The libraries are installed before the code is copied, so a code change does not reinstall them and builds stay fast. And nothing in the image is secret or heavy: MODEL_PATH and API_KEY arrive at runtime.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
A container is a shipping box holding your program and everything it needs, so it runs on a colleague's computer and on the server exactly as it does on yours. But passwords are not shipped inside the box; the server gets them separately. And every box carries a version number, so if trouble appears you roll back.
A Dockerfile builds a layered image with pinned dependencies, and secrets are passed through environment variables or a secret store, never baked in. You tag each image with a fixed version, not latest, roll out gradually with health checks, and can rollback.
Before You Start
🌐From a notebook to a service
You have a FastAPI service with a health check and a version in every reply.
💻The command line
You run commands in a terminal and know environment variables.
The Core Idea
Deployment is a container that runs the same way on any server. Here you build settings from the environment, then the image, then keeping the weights outside it, then the version numbers that make rollback one minute.
1Settings from the environment
The first thing a service does at startup is read its settings from environment variables and check everything it needs is there. Secrets like API_KEY come from a secrets manager at runtime.
Look at the picture: an incomplete service refuses to start with a clear message. That is far better than one that starts and fails on the first request.
def load_config(env):
required = ["MODEL_PATH", "API_KEY"]
missing = [k for k in required if not env.get(k)]
if missing:
raise SystemExit(f"cannot start: missing {', '.join(missing)}")
return {
"model_path": env["MODEL_PATH"],
"api_key": env["API_KEY"],
"max_input_chars": int(env.get("MAX_INPUT_CHARS", "2000")),
"version": env.get("APP_VERSION", "dev"),
}
def show(cfg):
safe = {k: ("***" if k == "api_key" else v) for k, v in cfg.items()}
print("starting with", safe)
show(load_config({"MODEL_PATH": "/models/sentiment-v3", "API_KEY": "sk-123", "APP_VERSION": "1.4.2"}))
try:
load_config({"MODEL_PATH": "/models/sentiment-v3"})
except SystemExit as e:
print(e)
Required settings are checked first, optional ones have defaults, and the secret is masked even in the startup log. The second time the key was missing, so the service refused to start and said exactly what was missing. The platform sees the failure immediately and keeps the old version running.
2The image: a Dockerfile
The image is a package with a small OS, libraries at pinned versions, and the code. It is built once and runs the same way everywhere.
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt # pinned versions: fastapi==0.115.0, ...
COPY app/ app/
RUN useradd --create-home appuser
USER appuser # do not run as root
ENV MAX_INPUT_CHARS=2000
EXPOSE 8000
HEALTHCHECK CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/ready')"
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
requirements.txt is copied and installed before the code, so a code change does not reinstall the libraries and builds stay fast. The service runs as an ordinary user, not root. And there is no secret and no weights in the file: MODEL_PATH and API_KEY arrive at runtime.
3Weights outside the image
An image holding a ten gigabyte model is slow to build and pull, and every small code change uploads all of it again.
Look at the picture: the image holds only the code, and the weights live in file storage at a path carrying their version. The service loads them at startup from MODEL_PATH.
That way you change code and model independently, and roll back either one alone.
4Versions and rollback
Every image has a fixed tag like 1.4.2, and the model has its own version too. The latest tag is banned in production.
Look at the picture: rollback is not rebuilding old code but running an existing image by its tag.
docker build -t sentiment-api:1.4.2 . docker run -d --name api -p 8000:8000 \ -e MODEL_PATH=s3://models/sentiment/v3 \ -e APP_VERSION=1.4.2 \ --env-file /secure/api.env \ sentiment-api:1.4.2 # 1.4.2 misbehaves in production: go back to the previous tag docker rm -f api && docker run -d --name api -p 8000:8000 ... sentiment-api:1.4.1
The image tag matches APP_VERSION, so it shows in every reply and log line. And rollback is not rebuilding old code but running an existing image. On real platforms this is one button or one command, provided you never deleted the old images.
How It Works, Step by Step: how to ship a version and roll it back
Build the image with a fixed tag such as sentiment-api:1.4.2, not latest.
Run it, passing settings and secrets as environment variables from the secrets manager, and the weights path with its version.
The service checks the required settings at startup and refuses to start with a clear message if something is missing.
If the new version misbehaves, run the previous image by its tag, or point MODEL_PATH back at the previous model.
Worked example
The first thing a service does at startup: read its settings from the environment, make sure everything it needs is there, and refuse to start with a clear message if something is missing. A service that starts incomplete and fails on the first request is far worse than one that refuses to start.
pip install fastapi with no version in requirements.txt. A month later you build the same code and get a new library version that changed its behaviour, and the service breaks with no change from you. Pin every version, and upgrade deliberately with a test.
Try it without code
Myth vs Reality
| Myth | Reality |
|---|---|
| Deleting the key from the repository removes it. | It stays in Git history and in any image built with it. Secrets arrive only at runtime and never touch the image. |
pip install fastapi with no version is enough. | A month later you build the same code and get a new library version that changed behaviour. Pin every version and upgrade deliberately with a test. |
| Rolling back means rebuilding the old code. | Rolling back is running an existing image by its tag, in a minute, provided you never deleted the old images. |
Try it yourself
Where does the service's API key go?
Anything in the image or repository can be extracted and stays in history. The secret reaches only the process, at runtime.
Why not use the latest tag in production?
latest points at whatever was built last. Two servers that pulled on different days may run two different versions under the same name.
Quick review cards
1.4.2 or the commit hash), and the model with its version too. No latest in production.Check Your Understanding
The new version works but its accuracy is worse. Do you roll back the code or the model?
It depends on what changed, which is why they are separate. If only the model changed, point MODEL_PATH back to the previous version with the same image. If the code changed (such as preprocessing), run the previous image with the same model. If both lived in one image, you would have to roll back both together.
Why is requirements.txt copied and installed before the code folder in the Dockerfile?
Because Docker reuses layers that have not changed. When only the code changes, the library layer stays as it is, so nothing is reinstalled and builds stay fast.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Done with this lesson?
Sign in to save your progress.