Serving Django with Caddy

I’m in the progress of migrating my Django sites from NGINX to Caddy.

Three reasons:

  • Caddy automatically handles HTTPS for you. This means no more setting up certbot and making sure NGINX restarts on certificate renewal. The TLS provisioning is robust and even uses ZeroSSL as a fallback option, in case Let’s encrypt doesn’t work.
  • The configuration is much more readable.
  • It’s fast, modern and written in Go.

Let’s start. I assume you have a machine running and a domain pointing to this machine.

Install caddy#

Follow the instructions at

Instructions for debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf '' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf '' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy

Caddy Hello World#


} {
    respond "ok"

Now enable and start the caddy service with:

sudo systemctl enable caddy
sudo systemctl start caddy

And visit your domain in the browser. You should see a secured connection, responding “ok”.

To troubleshoot you can watch journalctl -f -u caddy in a different session.


Install gunicorn with sudo pip3 install gunicorn.

Now we create a gunicorn configuration in your django project folder:


bind = "localhost:3000"
workers = 4
keepalive = 5

wsgi_app = "my-django-main-app.wsgi"  # replace with your main app (contains

Then create a systemd unit:



Description=Gunicorn service


Enable and start gunicorn with:

sudo systemctl enable gunicorn
sudo systemctl start gunicorn

I use poetry to manage dependencies, but that is another topic. In this case you would poetry add gunicorn in your project and use ExecStart=/usr/local/bin/poetry run gunicorn in the systemd unit.

Reverse proxy#

Now we use caddy as a reverse proxy to pass our incoming traffic over to gunicorn.

} {
	encode zstd gzip

	handle {
		reverse_proxy localhost:3000

Restart caddy with sudo systemctl restart caddy and visit your site in the browser. This serves the page but misses your static files.

Serving static files#

We’ll collect all static files in a folder called staticfiles.

In your django project, modify your

from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

    BASE_DIR / "static",

MEDIA_URL = "/media/"

and the run the collectstatic command:

python collectstatic

Your static files (even the ones from contrib.admin) should now be copied into the staticfiles folder in your project.

So our production-ready Caddyfile is now:

} {
	encode zstd gzip

	handle_path /static/* {
		file_server {
			root "/path/to/proj/staticfiles"

	handle_path /media/* {
		file_server {
			root "/path/to/proj/media"

	handle {
		reverse_proxy localhost:3000

Restart caddy and your site is online. I know that the Caddyfile could be shortened, but I like it this way.

Thanks for reading!

© 2023 Thomas Feldmann