Containerizing your FastAPI applications with Docker provides consistency, portability, and scalability for your Python web services. In this hands-on tutorial, we'll walk through the process of containerizing a FastAPI application, from writing the Dockerfile to orchestrating with Docker Compose.

Table of Contents #
- Introduction
- Understanding Container Basics
- Container Lifecycle
- Prerequisites
- Project Setup
- Understanding the FastAPI Application
- Creating the Dockerfile
- Building and Running the Docker Image
- Configuring Docker Compose
- Development Workflow with Volume Mounting
- Optimizing the Docker Image
- Common Issues and Troubleshooting
- Cleanup
- Conclusion
Introduction #
FastAPI is a modern, high-performance web framework for building APIs with Python. When combined with Docker, it creates a powerful foundation for developing, testing, and deploying web services. In this tutorial, we'll containerize a simple FastAPI application, demonstrating Docker best practices along the way.
This guide is based on our Lab1 FastAPI Example from the Docker Practical Guide repository.
Understanding Container Basics #
Before diving into containerizing our FastAPI application, let's establish a fundamental understanding of what containers are and how they work.
What is a Container? #
A container is a lightweight, standalone, and executable software package that includes everything needed to run an application: code, runtime, system tools, libraries, and settings. Containers isolate software from its surroundings, ensuring it works uniformly across different environments.
┌─────────────────────────────────────────────┐
│ Container │
│ │
│ ┌─────────────┐ │
│ │ Application │ │
│ └─────────────┘ │
│ │
│ ┌─────────────┐ ┌──────────────┐ │
│ │ Libraries │ │ Dependencies │ │
│ └─────────────┘ └──────────────┘ │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Container Runtime │ │
│ └──────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────┘
Containers vs. Virtual Machines #
Unlike virtual machines (VMs), which virtualize an entire operating system, containers virtualize at the operating system level, sharing the host OS kernel but maintaining isolation:
┌─────────────────────────────────────────────┐
│ Containers vs. VMs │
│ │
│ ┌───────┐ ┌───────┐ ┌───────┐ │
│ │ App A │ │ App B │ │ App A │ │
│ ├───────┤ ├───────┤ ├───────┤ │
│ │ Bins/ │ │ Bins/ │ │ OS │ │
│ │ Libs │ │ Libs │ ├───────┤ │
│ └───────┘ └───────┘ │ VM │ │
│ │ │ │ Tools │ │
│ ┌───────────────────┐ └───────┘ │
│ │ Docker Engine │ │ │
│ └───────────────────┘ ┌───────┐ │
│ ┌───────────────────┐ │Hyper- │ │
│ │ Host OS │ │visor │ │
│ └───────────────────┘ └───────┘ │
│ ┌───────────────────┐ ┌───────┐ │
│ │ Hardware │ │ Host │ │
│ └───────────────────┘ │ OS │ │
│ └───────┘ │
└─────────────────────────────────────────────┘
Key advantages of containers include:
- Lightweight: Containers share the host OS kernel and use fewer resources than VMs
- Portable: Run consistently across different environments
- Fast: Start, stop, and deploy quickly (seconds vs. minutes for VMs)
- Efficient: Higher density on the same hardware compared to VMs
- Isolated: Applications run in isolated environments without interfering with each other
Container Lifecycle #
Understanding the container lifecycle is crucial for effective containerization. A Docker container progresses through several states from creation to removal:
┌───────────────────────────────────────────────────────┐
│ Container Lifecycle │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │
│ │ Image │────►│ Created │────►│ Running │ │
│ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └────┬────┘ │
│ │ │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ │ │ │ │ │ │
│ │ Removed │◄────│ Stopped │◄────│ Paused │ │
│ │ │ │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │
└───────────────────────────────────────────────────────┘
1. Image Creation (Build) #
The first step is building a Docker image using a Dockerfile. This image contains all the application code, dependencies, and configuration:
docker build -t my-app .
2. Container Creation #
Once an image is built, a container can be created from it, but it doesn't start running yet:
docker create --name my-container my-app
3. Running State #
When a container is started, it enters the running state where the application processes execute:
docker start my-container
# Or create and start in one command
docker run -d --name my-container my-app
4. Paused State #
A running container can be paused, which suspends all processes inside it:
docker pause my-container
docker unpause my-container
5. Stopped State #
Containers can be stopped, which terminates the processes but preserves the container's file system state:
docker stop my-container
6. Removed State #
Finally, containers can be removed, which deletes the container instance (but not the image):
docker rm my-container
Understanding this lifecycle helps manage containerized applications effectively, especially when dealing with data persistence, container orchestration, and scaling.
Prerequisites #
To follow along with this tutorial, you'll need:
- Docker installed on your system
- Basic familiarity with Python and FastAPI
- A code editor of your choice
Project Setup #
Let's first take a look at the structure of our FastAPI project:
fastapi_example/
├── app/
│ ├── __init__.py
│ └── main.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml
Here's what each file does:
app/main.py
: Contains our FastAPI application coderequirements.txt
: Lists Python dependenciesDockerfile
: Instructions for building our Docker imagedocker-compose.yml
: Configuration for Docker Compose
Understanding the FastAPI Application #
Before containerizing, let's understand the FastAPI application we're working with. Here's a simplified version of our app/main.py
:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
async def root():
return {"message": "Hello World"}
@app.get("/items/{item_id}")
async def read_item(item_id: int):
return {"item_id": item_id}
This creates a basic API with two endpoints:
/
: Returns a simple "Hello World" message/items/{item_id}
: Returns the item ID passed in the URL
Our requirements.txt
file contains the necessary dependencies:
fastapi>=0.68.0
uvicorn>=0.15.0
Creating the Dockerfile #
Now, let's create a Dockerfile to containerize our FastAPI application:
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set the working directory in the container
WORKDIR /app
# Copy the requirements file to the working directory
COPY requirements.txt .
# Install any needed packages specified in requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application code
COPY . .
# Make port 8000 available to the world outside this container
EXPOSE 8000
# Command to run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Let's break down this Dockerfile:
FROM python:3.9-slim
: Uses the official Python image with a slim variant to reduce sizeWORKDIR /app
: Sets the working directory inside the containerCOPY requirements.txt .
: Copies only the requirements file first (for better layer caching)RUN pip install...
: Installs Python dependenciesCOPY . .
: Copies the rest of the application codeEXPOSE 8000
: Documents that the container listens on port 8000CMD [...]
: Specifies the command to run when the container starts
Building and Running the Docker Image #
With our Dockerfile in place, we can build and run our Docker image:
# Build the Docker image
docker build -t fastapi-app .
# Run the container
docker run -d -p 8000:8000 --name fastapi-container fastapi-app
After running these commands, you can access your FastAPI application at http://localhost:8000
. You should see the FastAPI default documentation page.
To test the API endpoints:
- Visit
http://localhost:8000/
for the "Hello World" message - Visit
http://localhost:8000/items/42
to see the item with ID 42 - Visit
http://localhost:8000/docs
for the interactive API documentation
Configuring Docker Compose #
While running containers with docker run
works for simple cases, Docker Compose provides a better way to manage your application, especially when it grows to include multiple services. Here's our docker-compose.yml
file:
version: "3"
services:
api:
build: .
ports:
- "8000:8000"
volumes:
- ./app:/app/app
command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
This configuration:
- Builds the image from the current directory
- Maps port 8000 from the container to the host
- Mounts the
./app
directory to/app/app
in the container for live code changes - Runs uvicorn with the
--reload
flag to automatically restart on code changes
To run the application with Docker Compose:
docker-compose up
To run it in detached mode (in the background):
docker-compose up -d
To stop and remove the containers:
docker-compose down
Development Workflow with Volume Mounting #
One of the key benefits of using Docker during development is the ability to have a consistent environment while still being able to make changes to your code without rebuilding the container.
The volume mount in our Docker Compose file (./app:/app/app
) allows us to edit the FastAPI application code on our local machine and see the changes immediately, thanks to the --reload
flag in the uvicorn command.
This creates an efficient development workflow:
- Make changes to your FastAPI code in your favorite editor
- Save the file
- uvicorn automatically reloads inside the container
- Refresh your browser to see the changes
This way, you get the best of both worlds: a consistent Docker environment that matches production, and the quick feedback loop of local development.
Optimizing the Docker Image #
While our basic Dockerfile works, there are several optimizations we can make for a production-ready image:
# Use an official Python runtime as a parent image
FROM python:3.9-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH="/app"
# Set the working directory in the container
WORKDIR /app
# Copy only requirements first for better caching
COPY requirements.txt .
# Install dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy the rest of the application
COPY . .
# Run as a non-root user for security
RUN adduser --disabled-password --gecos "" appuser
USER appuser
# Expose the port the app runs on
EXPOSE 8000
# Command to run the application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]
Key optimizations include:
- Setting Python environment variables for better performance
- Running as a non-root user for improved security
- Structuring the Dockerfile to take advantage of Docker's layer caching
Common Issues and Troubleshooting #
When containerizing FastAPI applications, you might encounter a few common issues:
1. Container not accessible from host #
If you can't access your FastAPI application from your browser, make sure:
- You're using
--host 0.0.0.0
(not127.0.0.1
) in the uvicorn command - You've correctly mapped the ports in
docker run
or Docker Compose - The container is actually running (
docker ps
)
2. Changes not reflecting in the container #
If your code changes aren't being reflected:
- Check that your volume mounts are correct
- Ensure the
--reload
flag is set in the uvicorn command - Verify that you're editing the correct files in the mounted path
3. Permission issues with mounted volumes #
If you encounter permission issues with mounted volumes:
- Check ownership of the files on your host machine
- Adjust the user in the Dockerfile or run the container with appropriate user mapping
4. Dependency issues #
If you're facing dependency issues:
- Make sure your
requirements.txt
file is complete - Consider pinning exact versions to ensure reproducibility
- Use a multi-stage build if you have compilation dependencies
Cleanup #
After you've finished working with your containerized FastAPI application, it's good practice to clean up your Docker resources to free up system resources and avoid potential conflicts in future projects.
Stopping and Removing Containers #
# Stop the running container
docker stop fastapi-container
# Remove the container
docker rm fastapi-container
# Alternative: force removal of a running container
docker rm -f fastapi-container
If you're using Docker Compose, you can stop and remove all services with one command:
# Stop and remove containers, networks, and volumes defined in docker-compose.yml
docker-compose down
# To also remove volumes (if you defined any)
docker-compose down -v
Removing Images #
If you no longer need the Docker image:
# Remove the FastAPI application image
docker rmi fastapi-app
# Force removal if the image is being used by a stopped container
docker rmi -f fastapi-app
Cleaning Up Unused Resources #
To clean up your Docker environment more thoroughly:
# Remove all stopped containers
docker container prune
# Remove all unused images
docker image prune
# Remove all unused volumes
docker volume prune
# Remove all unused networks
docker network prune
# Or remove all unused resources at once
docker system prune
For a complete reset of your Docker environment (use with caution):
# Remove all containers, networks, images, and volumes (with confirmation prompt)
docker system prune -a --volumes
This comprehensive cleanup ensures that you start fresh for your next Docker project and prevents your system from accumulating unused Docker resources.
Conclusion #
In this tutorial, we've walked through the process of containerizing a FastAPI application with Docker, from creating a basic Dockerfile to optimizing it for production. We've also explored how to use Docker Compose for a better development experience with volume mounting for live code changes.
Containerizing your FastAPI applications provides numerous benefits:
- Consistency: The same environment from development to production
- Portability: Run anywhere Docker is installed
- Isolation: Application dependencies are isolated from the host system
- Scalability: Easily scale your application with container orchestration
By following these practices, you can create efficient, secure, and portable FastAPI applications that are easy to develop, test, and deploy in any environment.
In the next article in our Docker Practical Guide series, we'll explore Docker volumes in depth to learn how to persist data across container lifecycles. Stay tuned!
Are you using Docker with FastAPI or other Python web frameworks? Share your experiences and any tips you've discovered in the comments below!