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 image | Size | What you get |
|---|---|---|
python:3.12 | ~900 MB | Full Debian with build tools, compilers, everything |
python:3.12-slim | ~130 MB | Stripped Debian, just enough to run Python |
python:3.12-alpine | ~50 MB | Alpine 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.
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.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.
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.
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_cacheWithout 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.
.dockerignore file. It generates the Dockerfile, and nothing else. You have to create .dockerignore yourself. Every time.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 appuserThis 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.
Common mistakes in AI-generated Dockerfiles
Here is a checklist for reviewing any AI-generated Dockerfile:
| Mistake | Why it matters | Fix |
|---|---|---|
Uses python:latest | Unstable, huge image | python:3.12-slim |
Single COPY . . before pip install | Breaks layer caching | Copy requirements.txt first |
No --no-cache-dir | Adds ~50MB of useless pip cache | Add --no-cache-dir flag |
No .dockerignore | Bloated build context, risk of secret leaks | Create .dockerignore |
| Runs as root | Security vulnerability | adduser + USER |
Uses CMD uvicorn main:app (shell form) | Breaks signal handling | Use JSON array format |
Quick reference
| Instruction | Purpose |
|---|---|
FROM python:3.12-slim | Set the base image |
WORKDIR /app | Set the working directory |
COPY requirements.txt . | Copy deps file (for cache layer) |
RUN pip install --no-cache-dir -r requirements.txt | Install dependencies |
COPY . . | Copy application code |
EXPOSE 8000 | Document the port (does not publish it) |
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"] | Set the start command |
# 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"]