In this, I’m gonna talk about how to deploy a Python FastAPI SSL With Traefik and Docker Compose using Let’s Encrypt. Without further ado, let’s dive into it.
Note: You can use any application to set up SSL not just the FastAPI application with these steps.
Before that, you might also check out my other article on Configure a Reverse Proxy with Nginx (Python).
Scenario
Let’s assume that you wish to use Docker to execute a Python application that was created with FastAPI. The application utilizes Let’s Encrypt for SSL certificates and is protected by a reverse proxy. Also, the configuration of certificates must be automatic as well.
Traefik
Traefik is a cloud-native application, a modern reverse proxy.
Being “cloud-native” refers to Traefik’s ability to quickly and simply integrate out of the box with cloud platforms like Docker and Kubernetes.
So, Traefik stands out from other NGINX-based solutions thanks to its automated support for Let’s Encrypt certificates. (See NGINX for further information.)
FastAPI
An asynchronous Python framework called FastAPI is used to create APIs. It is becoming more and more well-liked as a cutting-edge substitute for Flask and Django’s REST framework, particularly for performance-demanding, I/O-bound applications.
Requirements
- Knowledge of FastAPI
- Basic understanding of how SSL works
- Basic understanding of docker and docker-compose
- Knowledge of reverse proxy (NGINX, Traefik, etc.)
Set Up FastAPI SSL With Traefik and Docker Compose
Development environment
Setting up the project
To start off create a new project folder and initialize a Python virtual environment into it:
mkdir api-service && cd $_
python3 -m venv .venv
source .venv/bin/activate
Once done create a Docker Compose file for later use:
touch docker-compose.yml
Also, make sure to initialize a Git repo in this folder:
git init
Don’t forget to use a .gitignore
for Python.
Create a remote repo somewhere (GitLab, GitHub, etc.)
Now, let’s start a FastAPI project:
FastAPI app for development
Let’s start off with a minimal configuration for FastAPI to run our API in development.
To start and run the project you need the followings:
- FastAPI, the API framework
- Uvicorn, an ASGI server for running FastAPI
- Gunicorn for running Uvicorn
In api-service
with the Python virtual environment active, create a new folder for our API code and move into it:
mkdir -p services/api && cd $_
Now install Python dependencies with:
pip install "fastapi[all]"
When the dependencies are ready to populate a requirements.txt
with:
pip freeze > requirements.txt
In this file leave only the following dependencies:
fastapi[all]
(Your version numbers and packages may vary. If you’re using a specific version. Also, packages uvicorn, gunicorn are all included within fastapi[all] package. You can install them individually as well.)
Now create a new Python file named main.py
with the following code:
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def root():
return {"Hello": "World"}
Here we import FastAPI, and we also create a root route for sending back a Hello World to our users.
This example is intentionally minimal to keep things easy to understand. With the code in place let’s now move to the Docker section.
FastAPI, Docker, and Docker Compose for development
Let’s prepare the Docker environment for running FastAPI in development.
Make sure you’re still in services/api
and create a new Dockerfile:
touch Dockerfile
In this Dockerfile we prepare a Docker image with code and dependencies for running our FastAPI app:
FROM python:3.10
WORKDIR /api
COPY ./requirements.txt /api/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /api/requirements.txt
COPY ./ /api/
EXPOSE 8000
CMD ["uvicorn", "--reload", "--host", "0.0.0.0", "--port", "8000", "app.main:app"]
An explanation for this file is out of the scope of this tutorial. Make sure to refer to the official documentation if you need guidance.
Shot-Interruption: Checkout my other article on Linting Docker Image with Dockle.
(Use multi-stage Docker builds as a potential optimization for bigger projects.)
Now let’s go to the Docker Compose file.
Open up the docker-compose.yml
you created earlier (should be in the root project folder) and configure it as follows:
version: "3.3"
services:
api:
build: ./services/api/
restart: always
extra_hosts:
- host.docker.internal:host-gateway
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.localhost)
- traefik.http.services.app-http.loadbalancer.server.port=8000
traefik:
image: traefik:v2.9
ports:
- 80:80
- 443:443
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.toml:/etc/traefik/traefik.toml
Here we create a “api” service, basically our FastAPI app, we expose it on port 8000, and we launch it with Gunicorn and Uvicorn. (In development, you could also use just Uvicorn alone).
Extra Tips
Since I’m using MySQL database in host machine, I used
extra_hosts:
parameter, with database host ashost.docker.internal
. (You can ignore this block) To know more checkout this article. Which explains how the host and docker applications can be connected. Eg. (Ignore this in your case)
DB_HOST='host.docker.internal'
DB_NAME='example_db'
DB_USER='root'
Easy Peasy!
DB_PASSWORD='password@123'
Now,
Let’s also take a closer look at labels
:
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.localhost`)
- traefik.http.services.app-http.loadbalancer.server.port=8000
These labels will be read later by Traefik. In particular: traefik.enable=true
ensures that Traefik sees our container and routes traffic to it. You can omit this directive if in the Traefik configuration you use exposedByDefault = true
.
traefik.http.routers.app-http
defines a so-called router for Traefik. The complete configuration is:
traefik.http.routers.app-http.rule=Host(`api.localhost`)
This line means: create a router called app-http
, and route traffic to it when the host header matches api.localhost
.
Line traefik.http.services.app-http.loadbalancer.server.port=8000
is used to “expose” the internal Docker service port to the Traefik instance. All incoming requests to the URL defined before will be forwarded to the Docker service on the Port mentioned within this label.
In theory, these configurations can live in the Traefik configuration file, but here we take advantage of Traefik auto discovering capabilities to attach our container to Traefik from the Docker Compose file.
Traefik configuration for development
After setting up the Python app it’s now time to meet Traefik, the proxy.
Move to the root project folder api-service
and create a new folder for Traefik:
mkdir -p services/traefik && cd $_
Once inside the new folder create a configuration file for Traefik:
touch traefik.toml
Now configure traefik.toml
as follows:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[accessLog]
[providers]
[providers.docker]
exposedByDefault = false
Now, in this we define an entrypoint for Traefik to listen on port 80:
[entryPoints]
[entryPoints.web]
address = ":80"
Then we enable the Traefik dashboard over HTTP (This is mostly only for development!):
[entryPoints]
[entryPoints.web]
address = ":80"
Next up we enable the debug, and the access log:
[entryPoints]
[entryPoints.web]
address = ":80"
[log]
level = "DEBUG"
[accessLog]
Finally, we enable a so-called provider:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[accessLog]
[providers]
[providers.docker]
exposedByDefault = false
With the Docker provider enabled Traefik is able to see our containers. exposedByDefault = false
means that only container that is explicitly labeled with traefik.enable=true
in the labels
section of Docker Compose is seen by Traefik.
Also, in this configuration file for Traefik we:
- redirect HTTP connections to HTTPS.
- configure a resolver for requesting SSL certificates with Let’s Encrypt.
Optionally you can enable the Traefik dashboard. Refer to the documentation for more.
Once this file is in place we can finally create a Docker Compose configuration for development.
Setting up Traefik with Docker Compose for development
After configuring Traefik for development let’s go to the Docker Compose file again.
Open up the docker-compose.yml
(should be in the root project folder) and configure it as follows:
version: "3.3"
services:
api:
build: ./services/api/
restart: always
extra_hosts:
- host.docker.internal:host-gateway
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.localhost)
- traefik.http.services.app-http.loadbalancer.server.port=8000
traefik:
image: traefik:v2.9
ports:
- 80:80
- 443:443
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.toml:/etc/traefik/traefik.toml
Here we come full circle: we wire up Traefik and FastAPI together.
In Docker Compose we declare two volumes for Traefik:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.dev.toml:/etc/traefik/traefik.toml
The first volume makes Traefik aware of Docker containers, while the second volume mounts in traefik.toml
, the configuration file, inside the Traefik container.
With the configuration in place, we’re now ready to test things out.
Testing FastAPI and Traefik in development
To recap, for running the development environment you should have:
- a configuration file for Traefik at
services/traefik/traefik.toml
. - a configuration file for Docker Compose in the root project folder
docker-compose.yml
.
Once everything is in place, to test our development environment launch Docker Compose in the main project folder:
docker-compose up --build
Then from another terminal run the following curl call:
curl -H Host:api.localhost <http://0.0.0.0>
You should see the following output:
{"Hello":"World"}
That’s it for production! now you can change your codes on your own.
Production environment
FastAPI, Docker, and Docker Compose for production
For our FastAPI app, there’s nothing special to do at this stage. A "Hello world"
is enough to keep things going.
Open up or create(touch docker-compose.prod.yml) docker-compose.prod.yml
, and give it the following configuration:
version: "3.3"
services:
api:
build: ./services/api/
restart: always
extra_hosts:
- host.docker.internal:host-gateway
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.example.com`)
- traefik.http.routers.app-http.tls=true
- traefik.http.routers.app-http.tls.certresolver=letsencrypt
- traefik.http.services.app-http.loadbalancer.server.port=8000
Again, traefik.enable=true
ensures that Traefik sees our container and routes traffic to it.
In the router’s configuration instead, we say: create a router called fastapi, and route traffic to it when the Host header matches subdomain.example.com
.
The subdomain or the domain you’ll choose must appear in your configuration. Here subdomain.example.com
is just an example.
Note that to make this work with your domain, you must point the appropriate A records to the IP address of your production host.
Let’s now configure Let’s Encrypt with Traefik.
Preparing the Traefik configuration for production with Let’s Encrypt
What really shines in Traefik is the ability to automatically request Let’s Encrypt certificates.
To activate this capability we must follow these steps:
- configuring a certificate resolver in Traefik’s static configuration.
- associating the router with the certificate resolver.
To do so, create a production configuration file in services/traefik
:
touch traefik.prod.toml
Now configure traefik.prod.toml
as follows:
[entryPoints]
[entryPoints.web]
address = ":80"
[entryPoints.web.http]
[entryPoints.web.http.redirections]
[entryPoints.web.http.redirections.entryPoint]
to = "websecure"
scheme = "https"
[entryPoints.websecure]
address = ":443"
[accessLog]
[providers]
[providers.docker]
exposedByDefault = false
[certificatesResolvers.letsencrypt.acme]
email = "sagar.budhathoki@cloudyfox.com"
storage= "acme.json"
[certificatesResolvers.letsencrypt.acme.httpChallenge]
entryPoint = "web"
In this configuration file for Traefik we:
- redirect HTTP connections to HTTPS.
- configure a resolver for requesting SSL certificates with Let’s Encrypt.
Optionally you can enable the Traefik dashboard. Refer to the documentation for more.
Once this file is in place we can finally create a Docker Compose configuration for production.
Setting up Traefik with Docker Compose for production
As the last step, we need to complete docker-compose.prod.yml
. For the web service we add two more configuration directives needed by Traefik:
version: "3.3"
services:
api:
build: ./services/api/
restart: always
extra_hosts:
- host.docker.internal:host-gateway
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.example.com`)
- traefik.http.routers.app-http.tls=true
- traefik.http.routers.app-http.tls.certresolver=letsencrypt
- traefik.http.services.app-http.loadbalancer.server.port=8000
traefik:
image: traefik:v2.9
ports:
- 80:80
- 443:443
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.toml:/etc/traefik/traefik.toml
- traefik-public-certificates:/certificates
volumes:
traefik-public-certificate
Here we have still the FastAPI app enabled for Traefik, the same host subdomain.example.com
, and in addition we:
- enable TLS for the Docker Compose web service
- enable the Let’s Encrypt resolver
Finally, we configure the Traefik service:
version: "3.3"
services:
api:
build: ./services/api/
restart: always
extra_hosts:
- host.docker.internal:host-gateway
labels:
- traefik.enable=true
- traefik.http.routers.app-http.rule=Host(`api.example.com`)
- traefik.http.routers.app-http.tls=true
- traefik.http.routers.app-http.tls.certresolver=letsencrypt
- traefik.http.services.app-http.loadbalancer.server.port=8000
traefik:
image: traefik:v2.9
ports:
- 80:80
- 443:443
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.toml:/etc/traefik/traefik.toml
- traefik-public-certificates:/certificates
volumes:
traefik-public-certificate
Notice how we use 80 and 443 for production so that Traefik can listen for connections. Notice also that we use the production configuration file traefik.prod.toml
mounted in the container:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- $PWD/services/traefik/traefik.prod.toml:/etc/traefik/traefik.toml
- traefik-public-certificates:/certificates
volumes:
traefik-public-certificate
With everything in place, commit the changes and push them to the remote repo.
Testing FastAPI and Traefik in production
To recap, for running the production environment you should:
- have a configuration file for Traefik at
services/traefik/traefik.prod.toml
. - have a configuration file for Docker Compose in the root project folder
docker-compose.prod.yml
. - Clone the remote repo on the machine where Docker and Docker Compose are installed.
Once everything is in place, to test the production environment launch Docker Compose in the main project folder:
docker-compose -f docker-compose.prod.yml up --build
You should now be able to access subdomain.example.com
in your browser, or with curl:
curl <https://api.example.com>
You should see again the following output:
{"Hello":"World"}
The only difference now is the SSL certificate configured almost magically for us by Traefik!
That’s it!
Conclusion
In this tutorial, you learned how to deploy a FastAPI app with Docker, Traefik, and Let’s Encrypt for both development as well as production environment.
With the same steps, you can virtually secure any application with Traefik as an SSL.
Thank you!