RSS Amplifier

Cory Dransfeldt • Posts · Jul 17, 2026

Self hosting a PDS isn't hard (so far)

0
Sign in to vote or save

Cory Dransfeldt · Cory Dransfeldt

Due to my compulsive need to self-host whatever I possibly can1, I decided to set up a self-hosted PDS. All that's on it so far is my Bluesky profile data, but it was a surprisingly painless process.

Why bother? Well, I get to own my identity (DID) and my data. The relay and AppView remain Bluesky's, but the authoritative copy of my account is mine. I already syndicate from my site to Bluesky and controlling the PDS is the last part of this infrastructure I can reasonably control.

I've been bolting features on to my own CLI to manage my infrastructure for a bit2. I use Hetzner for hosting and it wraps their hcloud CLI for my own convenience. I spun up a CX23 box with backups turned on and then ran another CLI command to bootstrap the server.

The server is running only two things: the canonical ghcr.io/bluesky-social/pds container maintained by Bluesky and Caddy. I use forwardemail.net for transactional emails.

With the server provisioned, I updated the _atproto record for my domain and added a pds A record. I use DNSControl to manage DNS records, so this only took a few minutes.

The migration

Migrating my data from Bluesky's PDS to my own was the only part of this process that made me a little nervous. I needed to preserve my DID, followers and just over 1,000 account records. I used Bluesky's goat tool to manage this process.

  1. I generated a recovery key using goat key generate --type k256
  2. Logged in to my old (Bluesky) account: goat account login -u <username> -p <app-password> --auth-factor-token <email-code>
  3. Created a PDS invite for myself using my CLI (or SSH into the box and run pdsadmin create-invite-code).
  4. Ran the migration — it pauses and emails a PLC confirmation code that you paste into the prompt:
goat account migrate \
    --pds-host <host> \
    --new-handle <username> \
    --new-password "<password>" \
    --invite-code "<pds-invite-code>"
  1. Added my recovery key to the identity so it's my highest-priority rotation key (this also emails a confirmation token): goat account plc add-rotation-key <recovery-pubkey> --first

It ran faster than I expected and worked without any issues (shocking, I know). I syndicated a post, opened my profile in an incognito window and it loaded without issue. I also verified that DID doc #atproto_pds pointed at my new host and threw my private recovery key in my password manager.

Cleanup

The last step was to add support for a custom PDS URL to my site and the PWA I built myself to browse Mastodon and Bluesky simultaneously. This consisted of a migration to update the database schema to save the URL and the corresponding field in the UI. The app passwords I use for both also needed to be recreated on the new PDS which I did from the aforementioned CLI.

I have yet another server running which, at this point, I'm starting to collect like trading cards. My ATProto data sits on its own server, free to be used with any apps that support it (for my purposes that's only Bluesky — for now). It was a surprisingly straightforward process and I'm thankful for that.

I'm still getting my head around ATProto, but I appreciate that you can own your own data, if not all of the application infrastructure. I was used to the Mastodon mental model where, while you can migrate between instances, not all data comes with you. You own the identity but, perhaps, not all of the content. You can't own everything in either case, for now, but it feels like we're heading in the right direction.


  1. Except email. ↩︎

  2. More on that in a future post, probably. ↩︎

Read the original on coryd.dev

Comments

Nothing yet. Say the first thing.

    Sign in to join the conversation.