Shipping Python APIs/
Lesson

You asked an AI to containerize your FastAPI application. It gave you a Dockerfile that builds and runs. But building and running is the lowest bar a Dockerfile can clear. The image is 1.2 GB, the containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine. runs as root, and every time you change a single line of code, DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. reinstalls all your dependencies from scratch. This lesson teaches you to read what AI generates and understand why each line matters.

The base image

Every Dockerfile starts with FROM, which sets the operating system and runtimeWhat is runtime?The environment that runs your code after it's written. Some languages need a runtime installed on the machine; others (like Go) bake it into the binary. your application will use. For Python APIs, you have three realistic choices.

Base imageSizeWhat you get
python:3.12~900 MBFull Debian with build tools, compilers, everything
python:3.12-slim~130 MBStripped Debian, just enough to run Python
python:3.12-alpine~50 MBAlpine Linux, tiny, but uses musl libc instead of glibc

python:3.12-slim hits the sweet spot. It is small enough for production and large enough that most Python packages install without trouble. Alpine looks tempting at 50 MB, but packages that depend on C extensions (like numpy, pandas, psycopg2) can fail to compile or require manual installation of system libraries.

AI pitfall
AI defaults to python:latest or python:3. Both are bad choices. python:latest points to the full Debian image (900 MB) and can change Python versions without warning. python:3 is equally unstable. Always pin to a specific minor version: python:3.12-slim.
02

Reading a basic Dockerfile line by line

Here is what a solid Dockerfile for a FastAPI application looks like:

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Seven lines. Each one has a reason.

WORKDIR /app

Sets the working directory inside the containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine.. Every subsequent COPY, RUN, and CMD executes relative to /app. Without it, everything runs in /, which is messy and makes paths unpredictable.

COPY requirements.txt . then RUN pip install

This two-step pattern is the most important optimization in a Python Dockerfile. DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. builds images in layers, and each layer is cached. If requirements.txt has not changed since the last build, Docker skips the pip install entirely, saving minutes on every rebuild.

The --no-cache-dir flag tells pip not to store downloaded wheel files. Inside a container, there is no reason to cache downloads, you will never run pip install again after the image is built. Without this flag, pip stores tens of megabytes of cache files that bloat your image for no benefit.

COPY . .

Copies the restWhat is rest?An architectural style for web APIs where URLs represent resources (nouns) and HTTP methods (GET, POST, PUT, DELETE) represent actions on those resources. of your application code. This comes after pip install so that code changes do not invalidate the dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. cache. If you copy everything before installing deps, changing a single Python file forces a full pip install, which can take several minutes.

AI pitfall
AI almost always generates the Dockerfile with a single COPY . . before RUN pip install. This means every code change triggers a complete dependency reinstall. The AI does not understand Docker layer caching because it has never waited three minutes for pip install to finish.

CMD

Sets the default command when the container starts. The JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. array format (["uvicorn", "main:app"]) is preferred over the shell format (CMD uvicorn main:app) because it runs the process directly, without wrapping it in /bin/sh. This matters for signal handling, docker stop sends SIGTERM, and your process needs to receive it directly to shut down gracefully.

03

The .dockerignore file

When you run docker build, DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. sends the entire build context (the directory you point it at) to the Docker daemonWhat is docker daemon?The background service that builds, runs, and manages Docker containers on your machine.. Without a .dockerignore, that includes your virtual environment, __pycache__, .git, test data, IDEWhat is ide?Integrated Development Environment - an application like VS Code where you write, run, and debug code. AI coding tools plug into IDEs to suggest completions. config, everything.

# .dockerignore
__pycache__
*.pyc
.venv
venv
.git
.gitignore
.env
.env.*
*.md
tests/
.pytest_cache
.mypy_cache
.ruff_cache

Without this file, your build context might be hundreds of megabytes instead of a few kilobytes. Worse, you might accidentally copy .env files with production secrets into your image, where anyone with access to the image can read them.

AI pitfall
AI never generates a .dockerignore file. It generates the Dockerfile, and nothing else. You have to create .dockerignore yourself. Every time.
04

Running as root

By default, DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. runs everything as root. This means that if an attacker exploits a vulnerability in your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses., they have root access inside the containerWhat is container?A lightweight, portable package that bundles your application code with all its dependencies so it runs identically on any machine.. Container isolation is strong, but not impenetrable, running as root makes escape exploits more dangerous.

# The fix: create and switch to a non-root user
RUN adduser --disabled-password --no-create-home appuser
USER appuser

This is a production concern, not a development one. We will cover it in detail in the production patterns lesson. For now, know that every AI-generated Dockerfile runs as root, and you need to fix it before deploying.

05

Common mistakes in AI-generated Dockerfiles

Here is a checklist for reviewing any AI-generated Dockerfile:

MistakeWhy it mattersFix
Uses python:latestUnstable, huge imagepython:3.12-slim
Single COPY . . before pip installBreaks layer cachingCopy requirements.txt first
No --no-cache-dirAdds ~50MB of useless pip cacheAdd --no-cache-dir flag
No .dockerignoreBloated build context, risk of secret leaksCreate .dockerignore
Runs as rootSecurity vulnerabilityadduser + USER
Uses CMD uvicorn main:app (shell form)Breaks signal handlingUse JSON array format
06

Quick reference

InstructionPurpose
FROM python:3.12-slimSet the base image
WORKDIR /appSet the working directory
COPY requirements.txt .Copy deps file (for cache layer)
RUN pip install --no-cache-dir -r requirements.txtInstall dependencies
COPY . .Copy application code
EXPOSE 8000Document the port (does not publish it)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]Set the start command
javascript
# Recommended Dockerfile for a FastAPI application
FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]