Fast API - Running in Docker

Fast API - Running in Docker

Introduction

This article will setup a basic Hello World FastAPI project running in Docker. The purpose is to start off small before starting on a full real world project.

What and Why

Docker is a containerization platform. Docker combines application source code and dependencies so you can run your code in any environment. The container engine runs directly on your Operating System, so containers can be scaled directly on your host... assuming it can handle the compute and storage load.

There are so many resources for Docker, including Docker's home site. No quick and easy way to learn Docker. If you need a further deep dive take a look at Docker's Getting Started pages.

You can also follow our SQL Server on macOS using Docker article to get Docker Desktop installed if you don't have it already.

Code Repo

Code for this article can be found in the following GitHub Repo - Article 5.

Setup FastAPI

The API code is a simple example for now. Our focus is on the detail of the DockerFile and configuration.

Our FastAPI project will only have one route/ endpoint to return Hello World.

# app.py
from fastapi import FastAPI

app = FastAPI()

@app.get("/")
async def root():
    return { "message": "Hello world" }

Our python requirements.txt file only includes the basics, FastAPI and our web server, Uvicorn:

fastapi>=0.68.0,<0.69.0
uvicorn>=0.15.0,<0.16.0

Setup Docker

# Using Python 3.9 
FROM python:3.9

# Setup working directory
RUN mkdir code
WORKDIR /code

# Copy requirements file to our working directory
COPY ./requirements.txt /code/requirements.txt

# Install packages - Use cache dependencies 
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# Copy our code over to our working directory
COPY ./ /code/app

# Run our project exposed on port 80
CMD ["uvicorn", "app.app:app", "--host", "0.0.0.0", "--port", "80"]

Now let's start decoding the DockerFile.

FROM

This statement is your starting command which initializes a new build and sets the base image that your environment will be working with. A Dockerfile has to start with a FROM statement.

Containers work in a layered environment, so picture this as your most fundamental dev layer of your container.

In our example, we're specifying that we're using the Python3.9 image. The image is pulled from the default Docker registry where sharable images are hosted. The default registry is used when the registry URL is not specified.

WORKDIR

The next step is setting the container image's working directory. The default value is / but it is best practice to set a working directory. In our example, we're creating a directory in our image and setting our working directory to: /code.

COPY and RUN Dependencies

The Copy statement is straight forward, we're just copying our local file into our working directory or to any other directory in our Docker container image.

We're following best practice to copy the requirements.txt file first before the rest of our project files, let me explain why. The reason we're only working with dependencies is back to the fact that Docker builds images in layers. This package dependency layer is usually not changed very often so Docker will cache this layer and avoid re-running the installation of all dependencies each build.

The pip install command is executed next by the RUN command. Pip is updated and then installs requirements with the --no-cache-dir feature used. We don't need pip to cache its dependency installations, we'll let Docker be in charge of that. The install will run the installation of dependencies the first time, then every next image build if no new packages are included or changed in the requirement.txt file then Docker will reuse the cached version.

COPY App

Now copying the application source files are at the end of the Dockerfile. These files tend to change often so this layer will create itself each time when copying over application files into our container's working directory.

CMD

The final step will execute the application. We're working with the commands that we normally start a FastAPI project with but now in the form of comma separated parameters. We're executing the app object in the app file on localhost port 80.

Build Image

Now that we know what's going on in our Dockerfile, we use a specific command to build an image using the file. The command below will build our image. The -t argument will specify the name of our image.

docker build -t fastdockerimage .

The first time you build your image a long list of installation steps are are shown.

docker-new-build.png

The next time, cached layers are used and you notice a much faster build time. The cached layers are used. The lengthy python downloads are avoided. The cached lines are shown in the screenshot below.

docker-new-build-cached.png

RUN Container

If you're using Docker Desktop, you can see your newly created image as shown below.

docker-image.png

With our new Docker image we can now create and run a Docker Container.

docker run -d --name fastdockercontainer -p 80:80 fastdockerimage

We're running the container and specifying the container name as fastdockercontainer running on exposed port 80 (mapped to the internal container port 80) from on our new image called fastdockerimage. The -d argument indicates detached mode, so you can close your terminal and the terminal will still run. The Container ID is also printed to the terminal.

docker-build-container.png

Docker desktop now shows our running container.

docker-container.png

We can hit the container on localhost port 80 as usual. The application is not just running on a local Uvicorn server on our OS but in fact running on an isolated container environment.

docker-localhost.png

This article was meant to clarify the basics of running a FastAPI project in Docker. Future articles will build on these foundational steps to build a real world production level project.