From: wayne+blog@waynewerner.com To: everyone.everywhere.all.at.once Date: Tue, 22 Apr 2025 19:26:38 -0500 Subject: Django Development the Right Way™
Coming back to Django after more than a decade, and I Have Opinions!
I've uh, written about Django before, which was a good idea but as it turns out, I was wrong on the internet.
Not necessarily hugely wrong but wrong in some pretty essential ways. Unlike with Flask, where static resources are going to be much less involved, and the middleware patterns seem pretty established... Django static resources make the entire process very weird. Also when you reverse-proxy it ends out making everything all kinds of confusing, at least if you mount your page under multiple endpoints under a single domain.
If you follow the Django tutorial, it teaches you to do something like this:
graph TD
subgraph djangotutorial
proj-t(mysite /) --> posts
proj-t(mysite /) --> settings.py
polls-t@{ shape: docs, label: "polls/templates/polls/*.html" }
posts(/posts) --> polls-t
end
And if you just use the baked in server, it's going to look like this:
graph TD
subgraph manage["manage.py runserver"]
runserver -- settings.STATIC_ROOT --> static@{ shape: docs, label: "/some/path/to/static" }
runserver --> app(asgi/wsgi)
app -.-> static
posts -.-> static
app ---> posts
posts --> polls-t@{ shape: docs, label: "polls/templates/polls/*.html" }
end
And this kind of works OK except for when you go to deploy your site everything is wrong.
I get that the goal for the tutorial is probably just get things started; but there's also a lack of documentation on how to actually run things in a production-y envirornment. Which makes some sense but I hate it. So I'm going to make a guide for local development that works just like your deploy will.
It's actually a lot easier than it sounds it just takes a little bit of (actually pretty simple) setup to look like this:
graph TD
web(Internet)
subgraph webserver["Caddy"]
direction LR
caddy --> static@{ shape: docs, label: "/your/static/" }
end
subgraph gunicorn
direction LR
guni["gunicorn w/uvicorn"] --> app.asgi
end
subgraph project
direction LR
proj[youproject] --> app_one
proj[yourproject] --> app_two
end
caddy -- reverse_proxy /yoursite* --> guni
guni --> project
web --> caddy
The first thing you're going to need is Caddy. If you have it installed on your platform, great! If not, you can probably just get it from the Caddy releases page on GitHub.
You can run as admin but in case you don't, all you need to do is tell Caddy to run on a different port, and then you tell it where your static files live. If you want to enable browsing for debugging, you can! But you don't have to. Here's your Caddyfile:
{
http_port 8888
}
localhost:2015 {
file_server browse
handle_path /static* {
root * /path/to/your/static/
}
reverse_proxy /yourproject* {
to 127.0.0.1:9998
header_up +X-Forwarded-Prefix /yourproject
header_up X-Real-IP {http.request.remote}
}
}
Honestly I don't think the Forwarded-Prefix does anything. In production you'd
probably want to change out the X-Real-IP
but for local that's super
reasonable.
You'll start caddy with
caddy run --config YourCaddyfile
It might ask you for root credentials to install Caddy's localhost CA. If you
don't do that then you'll need to accept the cert in your browser. Or you can
do http://localhost:2015 {
instead and then it will only serve HTTP. Or
http://localhost:2016, localhost:2015
if you want to serve on HTTP and
https. Also remember to <ip>:<port>
, that will be important in a minute.
Definitely note whatever you set as /path/to/your/static
-- that needs to
match the STATIC_ROOT
in your Django settings.py
. the handle_path /static*
matches with your STATIC_URL = '/static/'
. It's a good idea to not add the
trailing slash on handle_path
or something unfortunate will happen but I
don't remember what. I think maybe it's more when you're passing things in for
the reverse_proxy
but in any case it's not going to hurt your static any.
Now you should be able to follow along with my other post and but just launch your server with
uv run gunicorn yourapp.asgi:application -k uvicorn_worker.UvicornWorker --reload --bind 127.0.0.1:9998
And you should be good to go!
~Wayne
^C