HomeBlog

Slimmer FastAPI Docker Images with Multi-Stage Builds

7 min read

David Muraya Blog Header Image for Slimmer FastAPI Docker Images with Multi-Stage Builds

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.

First, What's Docker and a Docker Image?

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.

The Problem: Bulky Images

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.

The Solution: Multi-Stage Builds

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.

How to Use Multi-Stage Builds for FastAPI

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

Breaking Down the Dockerfile:

  • 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.

Build and Run Your Image

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.

Why Slim Docker Images Are Good

We touched on this before, but smaller images have several advantages:

  • Faster Pushes and Pulls: Smaller images upload to container registries (like Docker Hub or Google Container Registry) more quickly and download faster when you deploy or scale your application.
  • Quicker Startup Times: Especially on serverless platforms like Cloud Run, smaller images can lead to faster "cold starts" because there's less data to pull and initialize.
  • Reduced Storage Costs: Less space used in your container registry and on your host machines.
  • Smaller Attack Surface: Fewer packages and libraries in your final image mean fewer potential security vulnerabilities.

Conclusion

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!

Related Blog Posts

Enjoyed this blog post? Check out these related posts!

Full-Text Search: Using the Trigram Tokenizer Algorithm to Match Peoples Names

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

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

Optimizing Reflex Performance on Google Cloud Run

A Comparison of Gunicorn, Uvicorn, and Granian for Running Reflex Apps

Read More..

Function Calling in Google Gemma3

Function Calling in Google Gemma3

Understanding Function Calling in Google Gemma3

Read More..

Contact Me

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.

© 2025 David Muraya. All rights reserved.