FastAPI Deployment Made Easy with Docker and Fly.io

By on 18 March 2025

For the PDM program I worked on a FastAPI project to track books using the Google Book API and also provide AI powered recommendations using Marvin AI. As the project came closer to deployment, I knew that I wanted to try out containerization for a reliable and repeatable way to deploy. I chose Docker due to its widespread use, open-source nature, and consistent behavior across environments.If you’re new to Docker or looking for a straightforward guide to deploying a FastAPI app with Docker and Fly.io, this post is for you.


FastAPI Set Up for Docker

Before deploying the app, we need to containerize it using Docker. To do this we need to start with creating a Dockerfile, which defines how the project will be packaged and run inside of the container. 

Project Structure

A clean project structure will improve the build efficiency, dependency management, security, and easier maintainability and readability. Here is the structure I used for my FastAPI project:

$ tree
.
├── Dockerfile
├── LICENSE
├── Procfile
├── README.md
├── alembic.ini
├── app.py
├── auth.py
├── config.py
├── db.py
├── docker-compose.yml
├── fly.toml
├── heroku.yml
├── main.py
├── migrations
│   ├── README
│   ├── env.py
│   ├── script.py.mako
│   └── versions
│       ├── 2ab73586da75_initial_migration.py
│       ├── 2e3f7780d24b_update_user_book_status_models.py
│       ├── 56b69f39cacf_add_book_index.py
│       └── bcc627763cfc_add_rate_limit_table.py
├── models.py
├── pages
│   ├── ai_recommendations.py
│   ├── login.py
│   ├── saved_books.py
│   └── signup.py
├── pyproject.toml
├── requirements.txt
├── screenshots
│   ├── ai_recommendation.png
│   ├── book_search.png
│   └── saved_books.png
├── services
│   ├── __init__.py
│   ├── google_books.py
│   └── marvin_ai.py
├── styles
│   └── global.css
├── tests
│   ├── __init__.py
│   ├── conftest.py
│   └── test_main.py
└── wait-for-it.sh

8 directories, 38 files

Writing the Dockerfile:

The Dockerfile is the key component that tells Docker how to set up the environment for the FastAPI app. 

# Use the full Python image instead of slim to avoid missing system dependencies
FROM python:3.11

# Set environment variables
ENV PYTHONUNBUFFERED=1 \
   UV_NO_INDEX=1 \
   DEBIAN_FRONTEND=noninteractive

# Set the working directory inside the container
WORKDIR /app

# Install uv globally first
RUN pip install --no-cache-dir uv

# Install dependencies globally (avoiding virtual env issues)
COPY pyproject.toml ./
RUN uv pip install --system -r pyproject.toml

# Copy the rest of the application code
COPY . .

# Expose the FastAPI default port
EXPOSE 8000

# Copy wait-for-it.sh into the container (if needed)
COPY wait-for-it.sh /usr/local/bin/wait-for-it
RUN chmod +x /usr/local/bin/wait-for-it

# Run the application
CMD ["sh", "-c", "/usr/local/bin/wait-for-it db:5432 -- uv run uvicorn main:app --host 0.0.0.0 --port 8000"]

I initially tried using python: 3.11-slim to keep the container lightweight but ran into some missing system dependencies. After researching my issues, I decided to go with the full image, which solved the problems I was having. Optimizing the image is a future goal of the project.

To ensure the database is ready before starting the FastAPI app, we use a small script called wait-for-it.sh. This utility blocks the container’s startup until a specified host and port (in our case, the database) becomes available. It’s a lightweight, reliable way to avoid race conditions where the app tries to connect to the database before it’s fully up—something that can often happen in Dockerized deployments where services start concurrently.

Creating a .dockerignore File

After the Dockerfile is done, we need to set up a .dockerignore file. This is used to keep the Docker image  small and clean. It works just like a .gitignore file, as we want to exclude files that aren’t relevant to the container plus exclude any secret keys, environment variables, etc. 

__pycache__/
*.pyc
*.pyo
*.sqlite3
.env
migrations/**/__pycache__/
tests/__pycache__/
.vscode/
.idea/
*.swp
*.swo
.DS_Store

With the Dockerfile and Dockerignore files ready, we are set to build the image locally!

Building and Running the Docker Container Locally

Now we need to build the container and test the FastAPI project before deploying it. This will ensure that the project will run smoothly in a Dockerized environment. 

Building the Image

First step is to build the Docker image. A Docker image is a blueprint for containers. This image will package the code, dependencies, and environment configurations. To build the image, we need to run the below command in the root of the project (where the Dockerfile is located):

docker build -t read-radar-api .

  • docker build tells Docker to build an image
  • -t read-radar-api will assign a tag (read-radar-api) to the image for reference.
  • . specifies the current directory as the build context. 

Running the Container

Once the image is successfully built we can run the container. Use the below command to start:

docker run -p 8000:8000 read-radar-api

  • docker run will start a new container
  • -p 8000:8000 will map port 8000 on your machine to port 8000 inside of the container
  • read-radar-api is the name of the Docker image just built

After running this command, logs from Uvicorn should be available. This means that your FastAPI app is running inside the container!

You can test running the app by going to http://localhost:8000/docs in your browser. If working correctly, you’ll see the Swagger UI for the FastAPI interactive docs. 

Using Docker Compose for PostgreSQL

For my project I used PostgreSQL for my database and we will also need to get that set up before deploying to Fly.io. 

Docker Compose makes a PostgreSQL database simple to set up. Just create a docker-compose.yml file in the root of the project directory:

version: "3.8"
services:
 db:
   image: postgres:15
   container_name: postgres_db
   restart: always
   environment:
     POSTGRES_USER: ${POSTGRES_USER}
     POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
     POSTGRES_DB: ${POSTGRES_DB}
   volumes:
     - postgres_data:/var/lib/postgresql/data
   ports:
     - "5432:5432"
 web:
   build: .
   container_name: fastapi_app
   depends_on:
     - db
   environment:
     DATABASE_URL: ${DATABASE_URL}
   ports:
     - "8000:8000"
   volumes:
     - .:/app
   command: ["/usr/local/bin/wait-for-it", "db:5432", "--", "uv", "run", "python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]
volumes:
 postgres_data:

Once that file is set up, we can have Docker spin up both services using:

docker-compose up -d

This will have both our FastAPI running at the localhost and PostgreSQL will be available at  postgresql://myuser:mypassword@localhost:5432/mydatabase (change the user, password, and database to match yours)

Once both are running successfully, we are ready to move onto deploying to Fly.io.


Deploying to Fly.io

Why Fly.io?

Containerization is great, but without deploying it to the cloud, others won’t be able to access the app. There are a variety of options to deploy like Fly.io or Heroku. I chose Fly.io because I was having issues with Heroku that required me to contact support. But using Fly.io was pretty simple, supporting Docker containers out of the box, and is relatively cheap with them changing to a pay-as-you-go system. You can also set up credits, so that you don’t run the risk of getting a super high bill. 

Setting Up Fly.io

The CLI is a tool that is needed to deploy and it can be downloaded with the following command:

curl -L https://fly.io/install.sh | sh

Then just restart the terminal and login. It will guide you through creating an account if you don’t have one yet:

flyctl auth login

Once logged in, we can initialize the Fly.io app. Once in your FastAPI project folder, run the following command:

flyctl launch

This will detect the Dockerfile and create a fly.toml configuration file. 

Setting Up PostgreSQL on Fly.io

Fly.io provides a managed PostgreSQL service, which makes the choice of database pretty easy if you are planning on using Fly.io like I did. 

To set up the database just use the following command:

flyctl postgres create

Just like initializing the app, it’ll ask you for a region for the database. Fly.io will then provision a PostgreSQL database and then provide a database connection URL. It’s super important to save this URL!

Now that the FastAPI app and PostgreSQL database are set up in Fly.io, we need to connect them! It’s a simple command that will link the PostgreSQL instance to the FastAPI app and then store the database connection URL as a Fly.io secret. Run the following command:

flyctl postgres attach --app read-radar-api

Deploying the App

Now we can deploy the FastAPI container to Fly.io! Just do the following command:

flyctl deploy

This will build the Docker image, push it to Fly.io’s container registry, and deploy it in the chosen region. Once it successfully completes, Fly.io will provide the public URL for the API:

https://read-radar-api.fly.dev/docs

Applying Database Migrations

Once deployed, we will need to apply database migrations in the deployed container. I’ve used Alembic for this and it makes it a simple process. First, we need to open an SSH session of the Fly app:

flyctl ssh console

Then run:

alembic upgrade head

This will apply the database migrations to the deployed container.

Later I learned that Fly supports release commands that run before a new deployment becomes active, so you can automate this step by adding this to your fly.toml file:

[deploy]
  release_command = "alembic upgrade head"

Debugging & Testing

A few simple commands to check on how the app is working on Fly.io.

To check the logs for any error just run:

flyctl logs

To check the app’s status:

flyctl status

And if the app crashes, you can redeploy by doing:

flyctl deploy –remote-only


Final Thoughts

WIth that, deploying the Docker container to Fly.io is complete! The next step would be to integrate it with a frontend. For my project I used Streamlit and it was a fairly easy process since all of the logic was done on the FastAPI backend. Other future enhancements would be CI/CD for automated deployments, and using Fly.io’s auto-scaling features. 

This project was a great introduction to Docker and Fly.io! The combination of clear documentation, guidance from my coach, and help from ChatGPT got me through the tricky parts. As we enter this new age of AI, it’s powerful to ask specific questions to AI — just be sure to understand and apply its suggestions wisely. That said, having a coach to provide tailored feedback and accountability made all the difference in truly grasping the concepts.

I hope this guide will help others implement Docker and Fly.io!

Want a career as a Python Developer but not sure where to start?