DEV Community

Cover image for I Dockerized My First FastAPI Application — From Python Code to Container
Chetan Inaganti
Chetan Inaganti

Posted on

I Dockerized My First FastAPI Application — From Python Code to Container

I Dockerized My First FastAPI Application — From Python Code to Container

In my previous article, I explained the Docker concepts that confused me when I first started learning:

  • Images
  • Containers
  • Ports
  • Volumes
  • Dockerfiles

But understanding Docker is one thing.

Actually putting an application inside a container is another.

So this time, I decided to take a small FastAPI application and Dockerize it from scratch.

Here's exactly what I did.


What We're Building

The application is intentionally simple.

It's a small FastAPI API with a health-check endpoint.

Our final architecture will look like this:

                FastAPI Application
                        │
                        ↓
                    Dockerfile
                        │
                  docker build
                        │
                        ↓
                  Docker Image
                        │
                  docker run
                        │
                        ↓
                  Docker Container
                        │
                    Port 8000
                        │
                        ↓
                  http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

Nothing complicated.

The goal is to understand the workflow.


Step 1 — Create the FastAPI Application

First, I created a simple Python project.

The structure looks like this:

fastapi-docker/
│
├── app.py
├── requirements.txt
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

My app.py contains:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def root():
    return {
        "message": "Hello from FastAPI running inside Docker!"
    }


@app.get("/health")
def health():
    return {
        "status": "healthy"
    }
Enter fullscreen mode Exit fullscreen mode

This gives us two endpoints:

GET /
GET /health
Enter fullscreen mode Exit fullscreen mode

Step 2 — Add the Dependencies

Next, I created requirements.txt.

fastapi
uvicorn[standard]
Enter fullscreen mode Exit fullscreen mode

These are the two main packages we need.

FastAPI provides the web framework.

Uvicorn runs the application.


Step 3 — Test It Locally

Before introducing Docker, I wanted to make sure the application itself worked.

I installed the dependencies:

pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

Then started the application:

uvicorn app:app --reload
Enter fullscreen mode Exit fullscreen mode

The API was available at:

http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

And the interactive API documentation was available at:

http://localhost:8000/docs
Enter fullscreen mode Exit fullscreen mode

This is an important step.

If the application doesn't work before Docker, putting it inside a container won't magically fix it.


Step 4 — Create the Dockerfile

Now comes the interesting part.

I created a file called:

Dockerfile
Enter fullscreen mode Exit fullscreen mode

with:

FROM python:3.12

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

Let's break this down.


FROM

FROM python:3.12
Enter fullscreen mode Exit fullscreen mode

This tells Docker to start with the official Python 3.12 image.

Instead of manually installing Python inside our container, we use an existing base image.


WORKDIR

WORKDIR /app
Enter fullscreen mode Exit fullscreen mode

This creates and switches to /app inside the container.

Our application will live there.


COPY requirements.txt

COPY requirements.txt .
Enter fullscreen mode Exit fullscreen mode

We copy the dependency file into the container first.


RUN pip install

RUN pip install --no-cache-dir -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

This installs our Python dependencies while the image is being built.


COPY the Application

COPY . .
Enter fullscreen mode Exit fullscreen mode

Now we copy the rest of our project into the container.


EXPOSE

EXPOSE 8000
Enter fullscreen mode Exit fullscreen mode

This documents that our application uses port 8000.

One important detail:

EXPOSE does not publish the port to our computer.

We'll do that when we run the container.


CMD

CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]
Enter fullscreen mode Exit fullscreen mode

This is the command that starts our FastAPI application when the container runs.

The important part here is:

--host 0.0.0.0
Enter fullscreen mode Exit fullscreen mode

The application needs to listen on all interfaces inside the container so that Docker can forward traffic to it.


Step 5 — Build the Docker Image

Now we're ready to build the image.

From the project directory:

docker build -t fastapi-docker .
Enter fullscreen mode Exit fullscreen mode

Let's break that command down:

docker build
Enter fullscreen mode Exit fullscreen mode

Build an image.

-t fastapi-docker
Enter fullscreen mode Exit fullscreen mode

Give the image a name.

.
Enter fullscreen mode Exit fullscreen mode

Use the current directory as the build context.

After the build completes, I can check my images:

docker images
Enter fullscreen mode Exit fullscreen mode

I should see something similar to:

REPOSITORY       TAG       IMAGE ID
fastapi-docker   latest    ...
Enter fullscreen mode Exit fullscreen mode

We now have our own Docker image.


Step 6 — Run the Container

Now we can create a container from our image.

docker run -d -p 8000:8000 --name fastapi-app fastapi-docker
Enter fullscreen mode Exit fullscreen mode

Here's what each part means:

-d
Enter fullscreen mode Exit fullscreen mode

Run the container in the background.

-p 8000:8000
Enter fullscreen mode Exit fullscreen mode

Map:

Host port       Container port
   8000    →        8000
Enter fullscreen mode Exit fullscreen mode
--name fastapi-app
Enter fullscreen mode Exit fullscreen mode

Give the container a name.

fastapi-docker
Enter fullscreen mode Exit fullscreen mode

Use our Docker image.


Step 7 — Check the Container

Let's make sure it's running:

docker ps
Enter fullscreen mode Exit fullscreen mode

We should see something similar to:

CONTAINER ID   IMAGE            STATUS          PORTS
abc123         fastapi-docker   Up 10 seconds   0.0.0.0:8000->8000/tcp
Enter fullscreen mode Exit fullscreen mode

This is the point where the whole Docker concept starts to become real.

We have:

Python Code
     ↓
FastAPI
     ↓
Dockerfile
     ↓
Docker Image
     ↓
Docker Container
     ↓
Port 8000
     ↓
Browser
Enter fullscreen mode Exit fullscreen mode

Step 8 — Test the API

Now open:

http://localhost:8000
Enter fullscreen mode Exit fullscreen mode

We should get:

{
    "message": "Hello from FastAPI running inside Docker!"
}
Enter fullscreen mode Exit fullscreen mode

We can also test:

http://localhost:8000/health
Enter fullscreen mode Exit fullscreen mode

which returns:

{
    "status": "healthy"
}
Enter fullscreen mode Exit fullscreen mode

And one of my favorite FastAPI features:

http://localhost:8000/docs
Enter fullscreen mode Exit fullscreen mode

opens the interactive Swagger UI.

Now we're running an API inside a Docker container.


Step 9 — Look at the Container Logs

If something goes wrong, logs are one of the first places to look.

docker logs fastapi-app
Enter fullscreen mode Exit fullscreen mode

This shows the output generated by the application.

You can also follow the logs:

docker logs -f fastapi-app
Enter fullscreen mode Exit fullscreen mode

This is particularly useful when debugging applications running inside containers.


Step 10 — Stop and Remove the Container

When we're finished:

docker stop fastapi-app
Enter fullscreen mode Exit fullscreen mode

The container is now stopped.

To remove it:

docker rm fastapi-app
Enter fullscreen mode Exit fullscreen mode

Notice that removing the container does not necessarily mean removing the image.

Our image still exists:

docker images
Enter fullscreen mode Exit fullscreen mode

We can use that image to create another container.


One Image, Multiple Containers

This is another Docker concept that became clearer after doing this project.

We can have:

             FastAPI Image
                  │
          ┌───────┼───────┐
          ↓       ↓       ↓
      Container Container Container
          1       2       3
Enter fullscreen mode Exit fullscreen mode

The image is the template.

The containers are the running instances.


The Final Project Structure

After everything is set up:

fastapi-docker/
│
├── app.py
├── requirements.txt
└── Dockerfile
Enter fullscreen mode Exit fullscreen mode

And the Docker workflow is:

             Dockerfile
                  │
                  ↓
          docker build
                  │
                  ↓
          Docker Image
                  │
                  ↓
           docker run
                  │
                  ↓
        Docker Container
                  │
                  ↓
          Port 8000
                  │
                  ↓
          FastAPI API
Enter fullscreen mode Exit fullscreen mode

What Actually Changed?

Before Docker:

My Computer
│
├── Python
├── FastAPI
├── Uvicorn
└── Application
Enter fullscreen mode Exit fullscreen mode

After Docker:

Docker Container
│
├── Python
├── FastAPI
├── Uvicorn
└── Application
Enter fullscreen mode Exit fullscreen mode

The application and its environment are now packaged together.

That is one of the biggest reasons Docker is useful.


A Small Lesson I Learned

One thing I learned from this experiment is that Docker isn't really about memorizing commands.

It's about understanding the relationship between the pieces.

At first, these commands looked unrelated:

docker build
docker images
docker run
docker ps
docker logs
docker stop
Enter fullscreen mode Exit fullscreen mode

After actually building an application, they started to form a workflow:

Build
  ↓
Image
  ↓
Run
  ↓
Container
  ↓
Inspect
  ↓
Debug
  ↓
Stop
Enter fullscreen mode Exit fullscreen mode

That mental model is much more useful than memorizing commands individually.


What's Missing From This Setup?

This is still a very simple application.

A real application would probably need:

  • Environment variables
  • A database
  • Docker Compose
  • A .dockerignore
  • Better dependency management
  • Production configuration
  • Health checks
  • Logging
  • Reverse proxy
  • HTTPS

And that's exactly where I want to go next.


What's Next?

The next step is to move beyond a single container.

Imagine:

              FastAPI
                 │
        ┌────────┼────────┐
        ↓        ↓        ↓
    PostgreSQL  Redis    Nginx
Enter fullscreen mode Exit fullscreen mode

Now we have multiple services that need to communicate with each other.

That's where Docker Compose becomes useful.


Final Thoughts

Docker made much more sense to me after I stopped learning it only through commands and actually put an application inside a container.

The process was surprisingly straightforward:

Write the application
        ↓
Create requirements.txt
        ↓
Write Dockerfile
        ↓
Build image
        ↓
Run container
        ↓
Map the port
        ↓
Access the API
Enter fullscreen mode Exit fullscreen mode

If you're learning Docker, I would recommend doing the same thing.

Don't just memorize:

docker run
Enter fullscreen mode Exit fullscreen mode

Build something small.

Break it.

Read the logs.

Fix it.

Then build it again.

That's when Docker starts to become a tool rather than just another technology you're trying to learn.


What About You?

Have you Dockerized a project before?

If you're learning Docker right now, what's the part you're struggling with?

Let me know in the comments.


Related Articles

Part 1: From Windows to WSL (Ubuntu) + Docker

Part 2: Docker for Beginners: Images, Containers, Ports, and Volumes Explained

Part 3: I Dockerized My First FastAPI Application

More coming as I continue building and learning.

Top comments (0)