For each python app I build, the server deployment always seems to be the most difficult part! As well as Python we need some kind of python server (uWSGI) and a web server AND getting the SSL certs…. I guess that is why there are platforms that help to abstract this away. I generally prefer the control and cost of using my own VPS, but I setting up nginx.conf and uWSGI.ini are not something I’m doing daily are there are a dizzying array of options! SSL certificate renewal is much better with Let’s encrypt these days, but certbot is just an extra thing to setup as well, and all of these seem to need slightly different config to previous projects so it’s ahrd to reuse what I did before.
Enter Caddy Server.
My most recent project was to deploy a single webhook endpoint built using FastAPI, and I didn’t want the previous hassle in deploying a server and provisioning SSL certificates – so I experimented with Caddy & Gunicorn. The two main benefits of Caddy are the much simpler config file, and the fact that it automatically obtains and renews SSL certificates for the domain (using Let’s Encrypt) – no setup required! Apparently it’s not as performance optimised as Nginx for high traffic sites, but I’m accepting a few webhooks per day not hosting billions of requests!
At its simplest the Caddyfile (config file for Caddy, equivalent to nginx.conf) is just:
webhook.example.com { reverse_proxy python-webhook-app:8000 }
In this example webhook.example.com is the domain name, and python-webhook-app is the docker container name (this can be replaced by localhost if not using docker). I tend to use docker combined with Ansible for server provisioning which also means I can use (almost) the same environment locally for development using docker. It’s important to see the context here and what id required to productionize a Python web app, so here’s my docker setup:
docker-compose.yml
version: "3.9" services: fastapi: build: . # uses dockerfile in current folder (see below) restart: unless-stopped container_name: python-webhook-app volumes: - .:/home/webhook_app # Mount local repo into container ports: - "8000:8000" networks: - webnet caddy: image: caddy:2.7 restart: unless-stopped container_name: caddy-server ports: - "443:443" - "80:80" volumes: - ./Caddyfile.prod:/etc/caddy/Caddyfile - caddy_data:/data networks: - webnet volumes: caddy_data: networks: webnet:
Docker compose creates two containers, one is for the Caddy server which uses the Caddyfile described above, which acts as a reverse proxy to the python-webhook-app container. The caddy_data volume mounts a folder on the host file system, so that Caddy can store certificates here (otherwise it would create a new certificate each time the docker container is run, and Let’s Encrypt restricts certificate renewals to 5 per domain per week). This docker setup should be the same for most kinds of Python web app, whether that is FastAPI, Django, flask or similar (you may add other containers such as for celery).
dockerfile
FROM python:3.11.8-bookworm RUN apt-get update && \ apt-get install -y nano && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* COPY requirements.txt /home/webhook_app/requirements.txt RUN pip3 install --no-cache-dir -r /home/webhook_app/requirements.txt WORKDIR /home/webhook_app CMD ["gunicorn", "server:app", "-k", "uvicorn.workers.UvicornWorker", "--bind", "0.0.0.0:8000"]
This dockerfile builds the docker container (python-webhook-app) for the python app, using python:3.11.8-bookworm, and running the command to start the python web app using gunicorn.
I did end up making this a bit more complicated as I have a local and a production Caddyfile and I added a few other things:
webhook.example.com { reverse_proxy python-webhook-app:8000 log { # logging! output stdout format json } encode gzip # compress files to save bandwidth header { # security hardening Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" X-Content-Type-Options "nosniff" Referrer-Policy "no-referrer" } }
In conclusion, Caddy and Gunicorn are much simpler! No need to mess about setting up certbot for SSL certificates, much simpler config for the server compared to Nginx, and no config needed at all for Gunicorn, in comparison to uWSGI. This will be my go to production server setup for Python from now on – watch out for a future post on using these for serving a Django project.
Leave a Reply