If you're using Docker for your FastAPI applications, you know it's great for consistency. But sometimes, those Docker images can get pretty big. Let's talk about how to make them smaller using a feature called multi-stage builds.
A Docker image is a neat package that holds everything your app needs to run. This includes your code, all its dependencies (like FastAPI and Uvicorn), any special settings (environment variables), and configuration files. Think of it as a ready-to-go snapshot of your application.
When you tell Docker to run an image, it creates a container. This container is a self-contained little world for your app. The cool part is that this container acts the same whether it's on your laptop, a testing server, or in production. This consistency is a big help for developers.
When you build a Docker image, you often need tools and files just for the build process itself – like compilers or development libraries. If these stick around in your final image, they make it larger than it needs to be. Bigger images take longer to upload, download, and start.
Multi-stage builds let you use multiple FROM
statements in your Dockerfile. Each FROM
instruction can use a different base image and starts a new "stage" of the build.
Here's the clever bit: you can copy files from an earlier stage to a later one. This means you can have one stage (the "builder" stage) where you install all your dependencies and compile your code. Then, you can have a final, clean stage that only copies the necessary bits (your compiled code and runtime dependencies) from the builder stage. The build tools and temporary files from the builder stage get left behind.
Let's say you have a basic FastAPI application. You'll also have a requirements.txt
file listing your Python dependencies. I will use python:3.13.3-alpine3.21
as the base image, which is a slim version of Python on Alpine Linux, known for its small size and efficiency.
Here’s a Dockerfile that uses a multi-stage build:
# ------------- Alpine Linux Base -------------------------- # ────── Stage 1: Build all dependencies (Builder Stage) ────── FROM python:3.13.3-alpine3.21 AS builder # Install OS packages needed to compile some Python packages RUN apk add --no-cache \ build-base \ libffi-dev \ openssl-dev \ musl-dev WORKDIR /build # Copy requirements and install Python dependencies into a specific prefix COPY requirements.txt . RUN pip install \ --prefix=/install \ --no-cache-dir \ -r requirements.txt # Copy your application code COPY app ./app # Compile Python code to .pyc files (optional, but can speed up app start) RUN python -m compileall -q app # ────── Stage 2: Runtime only (Final Stage) ────── FROM python:3.13.3-alpine3.21 # Install only the minimal runtime OS libraries needed # ffmpeg is included here as an example; only add what your app truly needs at runtime RUN apk add --no-cache \ libffi \ openssl \ ffmpeg WORKDIR /app # Copy the installed Python packages from the builder stage COPY --from=builder /install /usr/local # Copy the (compiled) application code from the builder stage COPY --from=builder /build/app ./app # Command to run the FastAPI app using Gunicorn # Cloud Run and similar platforms will set the $PORT environment variable. CMD exec gunicorn --bind :$PORT --workers 1 --worker-class uvicorn.workers.UvicornWorker --threads 4 -t 3600 app.main:app
Stage 1 (Builder Stage):
FROM python:3.13.3-alpine3.21 AS builder
: We start with a Python image based on Alpine Linux (which is known for being small) and name this stage builder
.RUN apk add ...
: We install system packages like build-base
(compilers, etc.) that are needed to install some Python packages from requirements.txt
. These won't be in our final image.WORKDIR /build
: Sets the working directory for this stage.COPY requirements.txt .
: Copies your Python dependencies list.RUN pip install --prefix=/install ...
: This is key. Instead of a virtual environment that we copy, we tell pip to install packages into a specific directory (/install
). This makes it easy to copy them to the next stage.COPY app ./app
: Copies your FastAPI application code.RUN python -m compileall -q app
: This pre-compiles your Python code into bytecode (.pyc
files). This is an optional optimization.Stage 2 (Final Stage):
FROM python:3.13.3-alpine3.21
: We start fresh with another Alpine-based Python image. This will be the base for our final, slim image.RUN apk add ...
: We install only the runtime system libraries. For a basic FastAPI app, you might only need libffi
and openssl
. ffmpeg
is just an example; only include what your app actually uses when it's running.WORKDIR /app
: Sets the working directory for the final image.COPY --from=builder /install /usr/local
: This is the magic. We copy the Python packages that were installed into /install
in the builder
stage directly into a standard location (/usr/local
) in our final image.COPY --from=builder /build/app ./app
: We copy our application code (which might include the .pyc
files if you ran compileall
) from the builder
stage.CMD exec gunicorn ...
: This is the command that runs your FastAPI application using Gunicorn. The $PORT
variable is typically provided by hosting platforms like Cloud Run.The Dockerfile provided actually uses python:3.13.3-alpine3.21
and installs packages to a specific prefix (/install
) rather than creating and copying a whole virtual environment. This prefix approach is quite effective for multi-stage builds.
To build your Docker image, navigate to the directory containing your Dockerfile
and app
folder, then run:
docker build -t my-fastapi-app .
To run your container (assuming your app inside the container listens on the port specified by $PORT
, and you want to map it to port 8000 on your host):
docker run -e PORT=8000 -p 8000:8000 my-fastapi-app
If your CMD
expects $PORT
to be set (like for Cloud Run), you can pass it with -e PORT=8000
. If Gunicorn is hardcoded to a port, adjust accordingly.
You should notice that the image my-fastapi-app
is significantly smaller than if you had built it in a single stage with all the build tools.
We touched on this before, but smaller images have several advantages:
Multi-stage builds are a straightforward way to make your FastAPI Docker images much smaller. By separating build-time needs from runtime needs, you create lean, efficient images. This means faster deployments, potentially quicker application starts, and a more secure setup. Give it a try with your next FastAPI project!
Enjoyed this blog post? Check out these related posts!
Full-Text Search: Using the Trigram Tokenizer Algorithm to Match Peoples Names
Leveraging Full Text Search and Trigram Tokenization for Efficient Name Matching
Read More..
How to Set Up a Custom Domain for Your Google Cloud Run service
A Step-by-Step Guide to Mapping Your Domain to Cloud Run
Read More..
Optimizing Reflex Performance on Google Cloud Run
A Comparison of Gunicorn, Uvicorn, and Granian for Running Reflex Apps
Read More..
Have a project in mind? Send me an email at hello@davidmuraya.com and let's bring your ideas to life. I am always available for exciting discussions.