Using Caddy as a reverse proxy to serve a Python app via gunicorn
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:
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!