ScanSkill
Sign up for daily dose of tech articles at your inbox.
Loading

Python FastAPI SSL With Traefik and Docker Compose

Python FastAPI SSL With Traefik and Docker Compose
Python FastAPI SSL With Traefik and Docker Compose

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 as host.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'

DB_PASSWORD='password@123'

Easy Peasy!

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 containersexposedByDefault = 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.ymldocker-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!

Sign up for daily dose of tech articles at your inbox.
Loading