Self-hosting the Bluesky PDS with nginx

I’ve been taken by Bluesky. I was a big Twitter user in the pre-Musk days, and deleted my account the day he bought the platform. Some people I know stuck around, ranging from journalists to friends, and I never understood why. You knew exactly where he wanted to take things.

I tried Mastodon a little bit, but the poor adoption meant it was always quite quiet. It was at least nice to be able to follow Jeff Minter again and see what his sheep are up to, but otherwise it just wasn’t the noisy bazaar I wanted.

I heard James O’Brien start talking about BSky and his radio show and that settled it. If the platform is easy enough for a non-techie journalist to get on and start seeing it as “the new place”, that signals that adoption is gonna be good.

SO - I like self hosting, so I looked at what it took to self-host my Bluesky identity with the PDS and lo and behold - it’s easy as heck.

I won’t repeat too much of what I found in other places - this excellent guide from Ben Harri introduced me to the basics. I didn’t want to run the PDS in docker so this was a great starting point. I got it running but this made my identity joe.pds.mydomain.com. That’s a lot of dots. I wanted it at the top level and that needed a bit more screwing around.

Another post I found detailed running the PDS on a top-level domain. Combining the knowledge from that and Ben’s blog, I had the basics down and I had the PDS running - outside of docker, managed by systemd and proxied to through nginx.

The final piece of the puzzle was oauth. The PDS comes with a complete oauth implementation, but accessing it - especially when running it on your top-level domain alongside other services - is not well documented. Blueview was the first app I found with oauth login and it just…didn’t work. I looked in my nginx log and saw a 404 for /.well-known/oauth-protected-resource. Okay, no big deal - let’s just proxy that to the PDS. Still no luck.

Hunting down the 404s, looking at logs, trying to grasp the sparse documentation, I eventually ended up with the following location block in my nginx config:

        location ~ ^/(\.well-known/(oauth-|atproto-did)|@atproto|oauth|xrpc) {
                proxy_pass http://localhost:3002;
                proxy_set_header Host $host;
        }

So what’s going on here? The PDS and ATProto use a mix of requests to /.well-known. If I was running JUST the pds, I’d be able to just proxy /.well-known through to my PDS daemon and we’d be okay. But that directory provides verification services for Let’s Encrypt, Matrix, and probably other stuff. There’s also requests to paths outside of /.well-known, such as the oauth flow. xrpc is vitally important, as proxying THAT to the wrong place will break your Bsky timeline entirely.

With this location block I have a fully working PDS on my domain, complete with oauth. My bsky identity exists entirely on my server, and I just let ATProto in to take a look around.

The best of both worlds: own your bits, but also join the flock.

Domain handles are important!

Journalists, politicians, activists, anyone. Everyone should be using a domain handle if they can. I was a little disheartened to see that while LBC uses a domain handle, their journalists (like the aforementioned James O’Brien) do not. A perfect implementation of Bsky sees figures like this with domain handles, ie jamesobrien.lbc.co.uk, as a form of decentralised verification. As it is, there’s quite a few impersonators out there.

It’s up to us, the users of this new network, to expunge dangerous impersonation before it takes root. Domain-based trust is good. It works. We literally got rid of the whole concept of paying for a TLS certificate because Let’s Encrypt could come up with robust domain ownership verification. Bsky’s implementation works just as well.

Use it.