Serving a FastAPI app with Poetry and Docker
June 27, 2024
Creating the server
The FastAPI-Poetry-Docker stack is my go-to for deploying backend applications. It's great because FastAPI gives you a nice OpenAPI framework out of the box, Poetry makes dependencies easy, and Docker standardizes the deployment process. But deploying to Docker can be surprisingly tricky, especially when you're using Poetry.
First, let's create a FastAPI app using Poetry. The first step is to create our Poetry project:
poetry new serverNext, let's install FastAPI and Uvicorn:
poetry add fastapi 'uvicorn[standard]'
poetry installThen, let's create a simple FastAPI app in server/server/main.py:
import fastapi
app = fastapi.FastAPI()
@app.get("/")
def index():
return {"message": "Hello, World!"}Now, let's run our FastAPI app using Uvicorn:
poetry run uvicorn server.main:app --reloadIf you open your browser and navigate to http://localhost:8000, you should see the message {"message":"Hello, World!"}.
Creating the Dockerfile
Now we get to the tricky part: creating a Dockerfile that will build and run our app. Let's start by creating a Dockerfile in the root of our project and selecting an image to build off of:
FROM python:3.11.8-bullseye AS python-baseNext, we'll set a few environment variables and create a working directory:
# Set environment variables for Python, Pip, Poetry and project directories
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PROJECT_DIR="/code"
# Add Poetry to the PATH
ENV PATH="$POETRY_HOME/bin:$PROJECT_DIR/.venv/bin:$PATH"With that set up, we can start installing a few dependencies for Python:
FROM python-base AS production
# Install system dependencies
RUN buildDeps="build-essential" \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
vim \
netcat \
&& apt-get install -y --no-install-recommends $buildDeps \
&& rm -rf /var/lib/apt/lists/*Now, let's install Poetry. To install our packages, we won't use Poetry directly—instead, we'll export our Poetry dependencies to a requirements.txt file and install them using uv. We use uv because it's much faster than Poetry:
# Set Poetry and uv versions
ENV POETRY_VERSION=1.8.3
ENV UV_VERSION=0.2.17
# Install Poetry - respects $POETRY_VERSION & $POETRY_HOME
RUN curl -sSL https://install.python-poetry.org | python3 - && chmod a+x /opt/poetry/bin/poetry
RUN poetry self add poetry-plugin-export
RUN pip install uv==$UV_VERSIONNow, let's export the dependencies and install them.
# Install package dependencies with uv
COPY poetry.lock pyproject.toml ./
RUN poetry export -f requirements.txt --output requirements.txt
RUN poetry run uv pip install -r requirements.txt# Set working directory and copy package files
WORKDIR $PROJECT_DIR
COPY server ./server
EXPOSE 8000We'll leave the entrypoint blank so we can specify it in our docker-compose file instead.
# Set default command to run the application
CMD [""]Putting it all together, our Dockerfile should look like this:
FROM python:3.11.8-bullseye AS python-base
# Set environment variables for Python, Pip, Poetry and project directories
ENV PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PIP_NO_CACHE_DIR=off \
PIP_DISABLE_PIP_VERSION_CHECK=on \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_HOME="/opt/poetry" \
POETRY_VIRTUALENVS_IN_PROJECT=true \
POETRY_NO_INTERACTION=1 \
PROJECT_DIR="/code"
# Add Poetry to the PATH
ENV PATH="$POETRY_HOME/bin:$PROJECT_DIR/.venv/bin:$PATH"
FROM python-base AS production
# Install system dependencies
RUN buildDeps="build-essential" \
&& apt-get update \
&& apt-get install --no-install-recommends -y \
curl \
vim \
netcat \
&& apt-get install -y --no-install-recommends $buildDeps \
&& rm -rf /var/lib/apt/lists/*
# Set Poetry and uv versions
ENV POETRY_VERSION=1.8.3
ENV UV_VERSION=0.2.17
# Install Poetry - respects $POETRY_VERSION & $POETRY_HOME
RUN curl -sSL https://install.python-poetry.org | python3 - && chmod a+x /opt/poetry/bin/poetry
RUN poetry self add poetry-plugin-export
RUN pip install uv==$UV_VERSION
# Install package dependencies with uv
COPY poetry.lock pyproject.toml ./
RUN poetry export -f requirements.txt --output requirements.txt
RUN poetry run uv pip install -r requirements.txt
# Set working directory and copy package files
WORKDIR $PROJECT_DIR
COPY server ./server
EXPOSE 8000
# Set default command to run the application
CMD [""]Building the image
To build our Docker image, we can run:
docker build . \
--network host \
-t serverThe --network host flag is important because it allows the Docker container to access the host's network and download dependencies. The -t server flag tags our image with the name server.
Running the image
Now let's create a docker-compose file to run our app:
version: "3.8"
services:
server:
network_mode: host
container_name: server
command: poetry run uvicorn server.main:app --port 8000 --host 0.0.0.0
build:
context: .
dockerfile: Dockerfile
network: host
image: server
restart: always
init: trueTo run our app, we can use:
docker compose upOr, if you prefer to run it in the background:
docker compose up -dIf you open your browser again and navigate to http://localhost:8000, you should see the message {"message":"Hello, World!"}.
Bonus: Setting up SSL with Cloudflare
To set up SSL, we can use Cloudflare's origin certificates. First, we need to generate a certificate. We can do this by navigating to the SSL/TLS tab in Cloudflare under a domain and clicking on "Origin Server." Clicking the "Create Certificate" button will generate a certificate that we can use to secure our server.
That will give us a certificate and a private key. We can download these into cert.pem and key.pem, respectively, under the /server directory.
We'll want to rebuild our Dockerfile to include these certificates. We'll run the build command again:
docker build . \
--network host \
-t serverNext, we'll modify our entrypoint to serve our app on port 443 (HTTPS) and use the certificates:
version: "3.8"
services:
server:
network_mode: host
container_name: server
command: poetry run uvicorn server.main:app --port 443 --host 0.0.0.0 --ssl-keyfile /code/server/key.pem --ssl-certfile /code/server/cert.pem
build:
context: .
dockerfile: Dockerfile
network: host
image: server
restart: always
init: trueNow, if you navigate to https://localhost, you should get an authentication error. You'll want to deploy your app to a server with a domain name and set up Cloudflare to proxy your server. Typically, you'll want to add an A record to your domain pointing to your server's IP address and set up Cloudflare to proxy your server. Once that's done, you should be able to access your server over HTTPS.