I thought Dockerizing a FastAPI app would take an afternoon. It took three days, and I learned more about Linux, networking, and environment management than I expected.
The naive Dockerfile I started with
My first attempt looked like this:
FROM python:3.11
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]
This works. But it's terrible. Every code change rebuilds the entire pip install layer because I copied source before installing dependencies.
Mistake 1: Layer order matters
Docker caches layers. If you copy requirements.txt first, install dependencies, then copy your code — pip only reruns when the requirements actually change.
FROM python:3.11-slim
WORKDIR /app
# Dependencies layer (cached until requirements change)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Code layer (rebuilds on every change — fast)
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Mistake 2: Running as root
By default containers run as root. That's a security problem. Adding USER nobody before the CMD line is a two-second fix with real impact.
What I'd do differently
- Use multi-stage builds to keep images small
- Never hardcode secrets — use environment variables or Docker secrets
- Add a
.dockerignorefrom the start (no.git, no__pycache__) - Pin your base image tag —
python:3.11-slimchanges over time
The full source for this project is on GitHub. Next post: setting up docker-compose for local development with hot reload.