Home | Blag Index


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!

Django Development the Right Way

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


Home | Blag Index

This site is Copyleft Wayne Werner - BY-NC-SA 4.0