Data-Dive

Using Caddy as a reverse proxy to serve a Python app via gunicorn

· mc51

Context

In this how-to we look at using Caddy as a reverse proxy for serving a Python app via gunicorn. Gunicorn is a Python WSGI server that I’ve enjoyed using for some of my projects. It’s simple, compatible with many Python web frameworks and fairly light and fast. I’ve recently switched to Caddy as my webserver and I’m not looking back. Caddy is written in Go, so it can be safer than other servers (Go is memory safe, while C is not) while still being quite performant. Moreover, it’s simple to run and configure and supports https by default.
Let’s look at the stack that I’ve used for https://bikeradar.cc, my latest project. The app itself is written in Python and using Dash. The app is served via gunicorn, which sits behind caddy. Hence, caddy is serving the site to clients, thereby acting as a reverse proxy:

Architecture diagram for bikeradar.cc Dash App
Figure 1. Architecture diagram for bikeradar.cc Dash App

This setup doesn’t seem to be very widespread. Consequently, there aren’t a lot of resources describing how to set things ups with this stack. Let’s change this.

Setup

The main file of our Dash app is in app.py. There, we have this along the other code for Dash:

from dash import Dash

app = Dash(__name__)
server = app.server

Gunicorn is serving the Dash app on localhost port 5555 using 4 workers with:

.venv/bin/gunicorn app:server -w 4 -b localhost:5555

This works by running the app via calling the server method in our app.py file. In theory, it would be possible to let gunicorn serve our app not only to localhost but on our public network interface. Clients would communicate with gunicorn directly and the setup would be simpler. However, this is not recommended. One should instead use a webserver as a revere proxy and put gunicorn “behind it”. The main reasons for this is that gunicorn itself lacks some functionalities that come with a “proper” webserver. Those are things like caching, load balancing and the ability to deal with slow clients. Thus, while for testing taking the shortcut is viable, don’t expose gunicorn publicly without putting a proper webserver in front of it!
Because we’re doing things the right way, let’s setup caddy as a reverse proxy now. For this, we edit /etc/caddy/Caddyfile which is the main configuration file. We add something along those lines:

www.bikeradar.cc, bikeradar.cc {
  log
  encode zstd gzip
  tls /etc/letsencrypt/live/bikeradar.cc/fullchain.pem /etc/letsencrypt/live/bikeradar.cc/privkey.pem
  reverse_proxy localhost:5555
  file_server
}

In result, when clients hit https://bikeradar.cc caddy will proxy the connection through to localhost:5555. There, gunicorn is listening for connections and will serve our Dash app. The connection between client and caddy is using https, so it’s encrypted. Hence, we have to specify the appropriate certificates in the tls option.
That’s it. We made our Python Dash app available for clients in a scalable and secure way!