07 Aug 2026

feedDjango community aggregator: Community blog posts

Issue 349: Django 6.1 and a DSF Executive Director

News

Call for applicants for a Django Executive Director

The DSF is hiring its first Executive Director to run fundraising, operations, staff, and legal/compliance for the Foundation. The role is US-based (no visa sponsorship). Applications (resume, optional cover letter, and a vision statement) are due September 14, 2026.

Django 6.1 released

Django 6.1 introduces model field fetch modes, database-level delete options for ForeignKey.on_delete, and dictionary-based email settings. Django 6.0 is now out of mainstream support and receives security and data loss fixes only until April 2027, so plan your upgrade before then.

Thank you to release manager Jacob Walls and the entire Django team on another happy feature release.


Releases

Django security releases issued: 6.0.8 and 5.2.17

The releases address high-severity spatial lookup flaws that could write files or make network requests, plus denial-of-service risks in language and geometry handling and potential XSS from unsafe admin URLField values. Upgrade to Django 6.0.8 or 5.2.17 as soon as possible.

Python 3.15.0 candidate 1 is here!

Python 3.15.0 candidate 1 is available for testing, giving projects a release candidate against which to check compatibility and build wheels.

Python 3.14.7 and 3.13.15 are now available!

Python 3.14.7 and 3.13.15 are available as bug-fix releases, so update your installations.


Wagtail CMS News

An agent-heavy roadmap for 2026

A standards-based look at what agent readiness requires, without the usual hype.


Sponsored Link

Find and fix Python errors faster - for free - with Honeybadger

When something breaks in production, logs tell you something happened. Honeybadger tells you why.

Honeybadger filters out noise and transforms your Python logs into context-rich issues so you can stop guessing and ship the fix faster.


Django Fellow Reports

Django Fellow Report - Jacob

Highlights this week included clearing release blockers for 6.1 and making the parallel test runner more fault tolerant. That means lots of tickets triaged, reviewed, and authored!

Django Fellow Report - Natalia

This week I prioritized time-sensitive security work πŸ₯·, including finalizing patches, preparing and validating backports, and sending pre-notifications ahead of the release. Alongside that, I focused on Tim's "Sprint quickstart" PR πŸƒβ€βž‘οΈ and attended meetings 🎧. Otherwise, I spent time preparing my DjangoCon US talk (it is coming together well, even if I am a bit wary of expectations around my htmx expertise 🎀).

Django Fellow Report - Sarah

Focus of the week was mostly around helping finalize the security release and reviewing the GSOC Selenium to Playwright migration (which is looking in good shape!).


Articles

Django Claude Skills

Mariatta built a set of Django house rules for Claude to follow in her own projects, covering formatting (black, isort, djlint, flake8), full test coverage as a merge requirement, has_perm() over group-membership checks for permissions, and a single Markdown email template that renders both text and HTML. She's explicit that these are personal conventions, not Django community consensus, and that a project's own style wins when contributing elsewhere. She published it as an Astro static site rather than plain Markdown, since she'd rather read the rules in a browser than as agent-only files.

What I love about Django

The best parts of Django are the ones you stop noticing - a tour of the abstractions that have given Buttondown the most leverage over the years.

Django Doesn't Have to Feel Old: Modernize Your Frontend with Vite

A walkthrough of wiring django-vite into a Django project: hot module replacement so CSS edits show up without a full page reload, explicit imports instead of global script-tag collisions, and a vite_asset template tag that resolves to the dev server or hashed production files depending on DEBUG.

Django 6 isn't a revolution. And that's exactly why I like it.

An experienced developer's take on years of building applications with Django and why the latest update continues a philosophy that many modern frameworks seem to have forgotten.

Python: how time-machine is O(1) where freezegun is O(n)

The comparison explains why time-machine avoids freezegun's O(n) slowdown when mocking dates and times, building on benchmarks that found it 100 to 200 times faster across two project sizes.

Core Dispatch #9

Python 3.15.0 release candidate 1 is out, followed by maintenance releases Python 3.14.7 and 3.13.15. The latest Core Dispatch also tracks two new PEPs entering the queue.

Devtools must be open source

Agents can now rebase your local tweaks against upstream automatically, so personalizing a tool costs a prompt instead of an ongoing maintenance burden. That's the case for why closed-source tools like Claude Code can't be personalized the way open alternatives like Codex or Pi can.


DjangoCon US

Save 10% on DjangoCon US 2026 registration

DjangoCon US 2026 runs August 24 to 28 in Chicago, now under three weeks away. Register through our link to take 10% off. Time is running out to get your ticket. Get yours before they are gone!


Events

Chicago Like a Local: Things to Do During DjangoCon US 2026 (Part 1)

Conference chair Keanya Phelps rounds up Italian beef at Mr. Beef and Al's, ramen and vinyl records at Wax in West Town, shuffleboard at Electric Shuffle, and rooftop lake views at Offshore on Navy Pier. She also flags the free Chicago House Music Festival in Millennium Park, August 27 to 30, which overlaps the last days of the conference.


Django Job Board

A few openings this week, including Django and Python Software Foundation roles.

Executive Director at Django Software Foundation πŸ†•

Security Developer at Python Software Foundation

Senior Full Stack Engineer at Hive Collective

Senior Backend Engineer at MyOme

Python + TypeScript Engineers at Fusionbox


Videos

PyCon US 2026

PyCon US 2026 videos are up.


Projects

wemake-services/django-modern-rest

Modern REST framework for Django with types and async support!

wsvincent/django-skills

Unofficial Django skills based on Django docs and community best-practices.

07 Aug 2026 3:00pm GMT

03 Aug 2026

feedDjango community aggregator: Community blog posts

Running Headscale on my own infra (and finally killing my WireGuard setup)

Hello everyone πŸ‘‹

This one started in the dumbest possible way: I wanted to check on my 3D printer from outside my house.

I have a Bambu printer running in LAN-only mode, and I use OctoApp to control it from my phone. Works great at home. Useless the moment I leave. The usual answer is OctoEverywhere, which is a lovely project, but it means my printer traffic goes through someone else's servers, and I already run enough infrastructure that this felt silly.

So I went looking for a way to just… have my home network with me. And I ended up rebuilding my entire remote access setup in an afternoon.

What I had before

My old setup was, in hindsight, a bit of a Rube Goldberg machine:

Two Pi-holes. Two address ranges. Two mental models of "where am I right now". It worked, but every time I added a service I had to think about which side of the tunnel it lived on.

What I actually wanted was much simpler: when I'm away, I want my phone to behave exactly like it's sitting on my home WiFi. Same IPs, same DNS, no difference at all.

That's a mesh VPN, and the nicest one is Tailscale.

I don't trust free

Tailscale is excellent. I want to say that first, because what follows is going to sound like criticism and it isn't. The client is great, the free tier is generous, and for most people it's the right answer.

But every time I look at a free tier this good, I catch myself doing the same mental arithmetic: someone is paying for this, and it isn't me. Tailscale is a venture-funded company. Free tiers built on top of venture funding have a well-documented lifecycle, and the last chapter is rarely "and it stayed free and generous forever". The limits shrink, or a device cap appears that you're already over, or the company gets acquired by someone with different ideas.

I'm not predicting that Tailscale does any of this. I have no reason to think they will. But I built my remote access on a WireGuard tunnel I control, and moving to something where a company I don't control holds the keys to my entire home network felt like a downgrade, even if the software is better.

I've also been burned recently enough that I'm not in a trusting mood. I wrote a whole angry post a few months ago about paying $100 a month for a service and getting less than 24 hours of notice before it changed underneath me. That was a paid tier. If that's what happens when I'm a customer, I'm not going to build my house on a free one.

So: I like the idea, I don't trust the arrangement. Which is exactly the situation self-hosting exists for.

Enter Headscale.

What Headscale actually is

Headscale is an open source implementation of the Tailscale control server. Your devices still run the official Tailscale client, they just point at your server instead of Tailscale's.

The important thing to understand is what the control server does and doesn't do. It handles key exchange, device registration, ACLs, and DNS settings. It does not sit in the middle of your traffic. Once two devices know about each other, they talk directly. So Headscale being a small container on a cheap VPS is completely fine.

I put it on the same Hetzner box that was already running WireGuard, because it already has a public IP and I was going to be deleting the WireGuard side anyway.

The Docker Compose setup

Headscale is CLI-first, which is fine, but I got tired of typing docker exec headscale headscale ... within about four minutes. So I also added Headplane, a web UI for it.

Directory structure first:

mkdir -p ~/headscale/config/{headscale,headplane}
cd ~/headscale
wget -O config/headscale/config.yaml https://raw.githubusercontent.com/juanfont/headscale/main/config-example.yaml
wget -O config/headplane/config.yaml https://raw.githubusercontent.com/tale/headplane/main/config.example.yaml

And the compose file:

services:
 headplane:
 image: ghcr.io/tale/headplane:latest
 container_name: headplane
 restart: unless-stopped
 ports:
 - "3003:3000"
 volumes:
 - ./config/headplane/config.yaml:/etc/headplane/config.yaml
 - ./config/headplane/lib:/var/lib/headplane
 # Shared path to the Headscale config. This has to match
 # `headscale.config_path` in the Headplane config.
 - ./config/headscale/config.yaml:/etc/headscale/config.yaml
 - /var/run/docker.sock:/var/run/docker.sock:ro

 headscale:
 image: headscale/headscale:latest
 container_name: headscale
 restart: unless-stopped
 command: serve
 labels:
 # Absolutely necessary for Headplane to find Headscale.
 me.tale.headplane.target: headscale
 ports:
 - "8083:8080"
 volumes:
 # Same host path in both containers. This matters.
 - ./config/headscale/config.yaml:/etc/headscale/config.yaml
 - ./config/headscale/lib:/var/lib/headscale

Nginx handles TLS and public exposure, same as every other service on that box. If you're not putting a firewall in front of these, bind the ports to 127.0.0.1 instead ("127.0.0.1:8083:8080"), otherwise Docker happily opens them to the internet and walks around UFW while it's at it.

Note the me.tale.headplane.target label on the Headscale container. That's how Headplane finds it, and it's how Headplane restarts Headscale when you change DNS settings from the UI. Note also that both containers mount the Headscale config from the same host path. Headplane reads it directly, so if the paths drift you get a UI that shows you stale settings.

Headscale config

The parts of config/headscale/config.yaml that matter:

server_url: https://headscale.example.com
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090

database:
 type: sqlite3
 sqlite:
 path: /var/lib/headscale/db.sqlite

dns:
 magic_dns: true
 base_domain: ts.example.com
 override_local_dns: true
 nameservers:
 global:
 - 1.1.1.1

Leave nameservers.global pointing at a public resolver for now. We'll swap it for the Pi-hole later, once the Pi-hole has joined the network and has an address to point at.

base_domain has to be different from your server_url domain, otherwise Headscale refuses to start.

Headplane config

In config/headplane/config.yaml:

server:
 host: "0.0.0.0"
 port: 3000
 base_url: "https://headplane.example.com"
 cookie_secret: "<32 char random string>"
 cookie_secure: true
 data_path: "/var/lib/headplane"

headscale:
 url: "http://headscale:8080"
 config_path: "/etc/headscale/config.yaml"
 api_key: ""

Generate the cookie secret with openssl rand -hex 32.

The url is the internal Docker address: container name, container port. Not whatever port you mapped on the host. The two containers talk over the compose network.

Then start Headscale on its own, create a user and an API key:

docker compose up -d headscale
docker exec headscale headscale users create myuser
docker exec headscale headscale apikeys create --expiration 90d

Paste that key into api_key and bring up Headplane. The key is only shown once, so if you lose it, just make another one. apikeys list only shows the prefix.

Nginx

Two vhosts, nothing exotic. But the Headscale one needs WebSocket passthrough, and this is the first place I lost time (more on that below):

location / {
 proxy_pass http://127.0.0.1:8083;
 proxy_http_version 1.1;
 proxy_set_header Upgrade $http_upgrade;
 proxy_set_header Connection $connection_upgrade;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto https;
 proxy_redirect off;
 proxy_read_timeout 5m;
}

Headplane is a boring proxy block, no special headers needed. Certbot for certs, as usual.

Pi-hole as the tailnet DNS

This is my favourite part of the whole setup, and the reason I bothered.

Join your home Pi-hole to the network like any other device:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com
tailscale ip -4

Take that 100.x.x.x address, put it in Headscale's dns.nameservers.global, and restart the container.

Now every device on the tailnet uses my home Pi-hole for all DNS. Which means:

Neither of those is new, I had both with the old WireGuard setup. The difference is that I now get them from one Pi-hole instead of two, and I didn't have to think about it. The Pi-hole joined the tailnet, I pointed Headscale at it, and every device I add from now on inherits both for free.

override_local_dns: true is what forces this. Without it, your phone keeps using whatever DNS the carrier hands it, the tunnel works fine, and your internal domains silently fail to resolve.

The subnet router

A mesh VPN only connects the devices that are on it. My printer can't run a Tailscale client. Neither can most of the stuff I care about.

The fix is a subnet router: one device on your LAN that advertises the whole network to everyone else. I used the Pi-hole box, since it was already joined:

sudo tailscale up --login-server https://headscale.example.com \
 --advertise-routes=192.168.0.0/24

Two things have to happen after this or nothing works, and I hit both.

First, IP forwarding:

echo 'net.ipv4.ip_forward = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
echo 'net.ipv6.conf.all.forwarding = 1' | sudo tee -a /etc/sysctl.d/99-tailscale.conf
sudo sysctl -p /etc/sysctl.d/99-tailscale.conf

Second, and this is the one that got me: routes are double opt-in. The node advertises, and then the control server has to approve. Advertising alone does nothing.

docker exec headscale headscale nodes list-routes
docker exec headscale headscale nodes approve-routes --identifier 1 --routes 192.168.0.0/24

The moment I ran that second command, everything worked. Printer, internal domains, random machines on my LAN, all reachable from my phone on cellular.

Clients

You don't need a special app. The official Tailscale clients all support custom control servers.

On Android, install from Play Store or F-Droid, go to "Accounts", tap the kebab menu, and select "Use an alternate server". Enter your Headscale URL and log in.

On iOS it's slightly more buried: install from the App Store, tap the account icon, "Log in", then use the options menu to pick "Use custom coordination server".

macOS is the one with a real trap. Use the standalone build, not the Mac App Store one, because the App Store version is sandboxed and fights you on custom control servers. brew install tailscale works, or grab the standalone package from Tailscale's download page. Then:

sudo tailscale up --login-server https://headscale.example.com
tailscale set --accept-routes

That --accept-routes is easy to forget. Without it the client joins fine and then can't see anything on your LAN.

The gotchas that cost me the most time

Four things ate most of my afternoon. All of them had error messages that pointed somewhere other than the actual problem.

Headscale listening on 127.0.0.1 inside its own container

Headplane kept logging this:

Error while validating API key: [object Object]

I regenerated that API key three times. The key was fine. The real problem was one line above in the Headscale logs:

INF listening and serving HTTP on: 127.0.0.1:8080

127.0.0.1 inside a container means that container only. Not the compose network, not the other container sitting right next to it. Headplane literally could not open a connection, so it couldn't validate anything, and reported that as an auth failure.

Set listen_addr: 0.0.0.0:8080 in the Headscale config. Your host port mapping is a separate concern and keeps working the same.

Nginx eating the WebSocket upgrade

Clients wouldn't connect, and Headscale said:

WRN no upgrade header in TS2021 request. If headscale is behind a reverse proxy,
make sure it is configured to pass WebSockets through.

At least this error tells you exactly what's wrong. My nginx config was a copy-paste of the same reverse proxy block I use for every other service on that box, which has no Upgrade handling because nothing else needs it.

The commonly recommended fix uses a map block in the http context:

map $http_upgrade $connection_upgrade {
 default upgrade;
 '' close;
}

The idea is that plain HTTP requests to the same vhost get Connection: close instead of a bogus Connection: upgrade. If you put that map somewhere nginx doesn't load it into the http context, you get unknown "connection_upgrade" variable and nothing starts.

I'll be honest: I got that error, commented the Connection line out entirely to keep moving, and it worked. It's on my list to go back and do properly, because "it worked when I removed the correctness" is not a state I enjoy leaving things in.

The routes CLI moved

Every guide and blog post out there tells you to run:

headscale routes list
headscale routes enable -r 1

On 0.29 that gives you unknown command "routes". It's now under nodes:

headscale nodes list-routes
headscale nodes approve-routes --identifier 1 --routes 192.168.0.0/24

Related: --user wants a numeric ID now, not a username. headscale users list gives you the number.

When in doubt, headscale --help is more current than anything you'll find in a search result. I lost a few minutes to blog posts written against older versions before I just asked the binary.

Pi-hole ignoring the tailnet

Tunnel up, subnet routes approved, ping 1.1.1.1 working, and google.com resolving to nothing.

Pi-hole's default interface listening behaviour doesn't answer queries arriving on the tailscale0 interface. Settings, then DNS, then set it to listen on all interfaces and permit all origins. Or scope it to the tailnet CIDR if you want to be tidier about it than I was.

The bash alias

Small thing, big quality of life improvement:

alias headscale='docker exec headscale headscale'

Now every command in every tutorial you find online works verbatim, without mentally prefixing the docker part every time. Just remember it's there if you ever install the real binary on the same box, or you'll spend an entertaining twenty minutes wondering why the local install ignores your config file.

What I still need to do

I'm not going to pretend this is finished.

The old WireGuard tunnel and its dedicated Pi-hole are still running. Headscale does everything they did, but I'm leaving them up until I've gone through the Nginx Proxy Manager config and confirmed nothing still points at a 10.0.0.x address. Deleting infrastructure at 2 AM after a successful afternoon is how you learn what depended on it.

Was it worth it?

Yes, and for reasons beyond the printer.

The setup collapsed two Pi-holes and two IP ranges into one mental model. My phone now behaves identically at home and on cellular. Every internal service I add from now on works remotely for free, without port forwarding or a new proxy host or any thought about which side of a tunnel it lives on.

And no ports are open to my LAN. The only publicly reachable thing is the Headscale control server, which hands out keys and doesn't carry traffic.

The total cost was one afternoon, most of which was spent on four error messages that pointed at the wrong thing. Hopefully this post saves you those.

If you want to check the printer thing specifically: Bambu printers in LAN-only mode expose MQTT on 8883 and FTPS on 990. There's no web UI, so don't waste time typing the IP into a browser like I did. Once you're on the tailnet, OctoApp just takes the same local IP and access code you use at home.

See you in the next one!

03 Aug 2026 5:00am GMT

31 Jul 2026

feedDjango community aggregator: Community blog posts

Chicago Like a Local: Things to Do During DjangoCon US 2026 (Part 1)

DjangoCon US 2026 is coming back to Chicago from August 23-28, and we at Caktus couldn't be more thrilled, not only to be returning as a Silver Sponsor, but also because I (Keanya) have the privilege of chairing the conference once again!

31 Jul 2026 11:35pm GMT