Skip to main content
  1. Blog/

Self-hosting Bluesky PDS

Table of Contents
Finding this helpful?
Please consider leaving a small gesture of your appreciation.

Introduction #

There seems to be an ongoing mass-migration away from Twitter/X at the moment, with Bluesky being a popular alternative. While I can’t say I’m a particularly avid user of Twitter/X (my last tweet was over 2 years ago!), the nerd-value of a decentralised social media platform did pique my interest. Rather than choosing the easy option of signing up for a handle on the Bluesky web app, I thought I’d venture down the self-hosted route - the end goal being running my own Personal Data Server off this domain.

A huge thanks to this writeup that was posted a few weeks ago with some helpful pointers - I’ve built off that work for my own setup.

Part 1: Docker #

As I’ve mentioned in previous posts, I’m still running Docker Swarm for the majority of my Homelab needs, although a move to Kubernetes may be on the cards in the near future. I’m assuming you have your own Swarm up and running, and Traefik running and exposed on your root domain. My post about using Traefik with Cloudflare Tunnels may be useful if you need help setting that up.

The Bluesky PDS does have a compose.yaml example all ready to go, but given that I use Traefik as my reverse proxy and Renovate for container updates, I didn’t need the extra services included, so I set about making my own

version: '3.9'

services:
  pds:
    container_name: pds
    image: ghcr.io/bluesky-social/pds:0.4.67
    restart: unless-stopped
    networks:
      - bluesky
      - traefik
    env_file:
      - stack.env
    deploy:
      labels:
        - traefik.enable=true

        - traefik.http.middlewares.bluesky-pds-header.headers.customrequestheaders.Host="{host}"

        - traefik.http.routers.bluesky-pds.rule=Host(`yourdomain.com`) && PathPrefix(`/xrpc`)
        - traefik.http.routers.bluesky-pds.entrypoints=web,websecure
        - traefik.http.routers.bluesky-pds.tls=true
        - traefik.http.routers.bluesky-pds.priority=1000
        - traefik.http.routers.bluesky-pds.middlewares=bluesky-pds-header

        - traefik.http.routers.bluesky-did.rule=Host(`yourdomain.com`) && Path(`/.well-known/atproto-did`)
        - traefik.http.routers.bluesky-did.entrypoints=web,websecure
        - traefik.http.routers.bluesky-did.tls=true
        - traefik.http.routers.bluesky-did.priority=1000
        - traefik.http.routers.bluesky-did.middlewares=bluesky-pds-header

        - traefik.http.services.bluesky-pds.loadbalancer.server.port=3000

        - traefik.docker.network=traefik
    volumes:
      - pds:/opt/pds

volumes:
  pds: # Insert your persistant storage options here

networks:
  traefik:
    external: true
  bluesky:

This will run the PDS and expose only the /xrpc and /.well-known/atproto-did paths on your root domain - I did this because I host other things on the same domain I want to use as a handle, and therefore the priority for each router needs to be set suitably high. Also of note is the customRequestHeaders element - after banging my head against the desk for many hours, I finally realised that although I could correctly connect a test WebSocket connection, I would still get the dreaded Invalid Handle from Bluesky unless the Host header was passed through. Final note is that you’ll likely need to disable FastProxy on Traefik if you’re using it, although that may have been resolved by this PR which made it into release v3.2.1

You’ll also need a stack.env file - here’s mine:

PDS_HOSTNAME=<YOUR DOMAIN>
PDS_SERVICE_HANDLE_DOMAINS=.<YOUR DOMAIN>
PDS_JWT_SECRET=<INSERT SECRET HERE>
PDS_ADMIN_PASSWORD=<INSERT ANOTHER SECRET HERE>
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<INSERT KEY HEX HERE>
PDS_DATA_DIRECTORY=/opt/pds
PDS_BLOBSTORE_DISK_LOCATION=/opt/pds/blocks
PDS_BLOB_UPLOAD_LIMIT=52428800
PDS_DID_PLC_URL=https://plc.directory
PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
PDS_REPORT_SERVICE_URL=https://mod.bsky.app
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
PDS_CRAWLERS=https://bsky.network
PDS_EMAIL_SMTP_URL=smtp://smtp-relay.gmail.com/
PDS_EMAIL_FROM_ADDRESS=<YOUR GMAIL ADDRESS HERE>
LOG_ENABLED=true

You can generate the needed secrets and key using the following commands (from the official PDS install script):

$ openssl rand --hex 16
$ openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32

As I’m using GMail, I’ve chosen to use their SMTP relay, but you could use something like Resend instead.

Part 2: Set up identity #

Unfortunately, the current version of Bluesky PDS doesn’t allow us to create our handle at the root domain (yourdomain.com) - instead it will only accept someone.yourdomain.com or similar. As such, we need to create an account that satisfies this requirement, and then use one of our exposed XRPC methods to modify it.

The PDS does have some helpful administration tools, however these aren’t yet included in the Docker image, so we need to grab them separately.

$ git clone https://github.com/bluesky-social/pds/ bluesky-pds
$ cd bluesky-pds/pds-admin

Now we have the tools available, we can create the account.

PDS_ENV_FILE=/path/to/stack.env ./account.sh create <YOUR EMAIL ADDRESS> me.<YOUR DOMAIN HERE>

You’ll then be given the Decentralised Identifier and password for your new account - make a careful note of these!

We can then edit the account to set the handle to our root domain.

curl
  --fail
  --silent
  --show-error
  --request POST
  --user "admin:<YOUR ADMIN PASSWORD HERE>"
  --header "Content-Type: application/json"
  --data "{\"did\": \"<YOUR DID HERE>\",\"handle\":\"<YOUR DOMAIN HERE>\"}"
  "https://<YOUR DOMAIN HERE>/xrpc/com.atproto.admin.updateAccountHandle"

Part 3: DNS #

Confession time - I’m not 100% sure this is necessary if you have Traefik serving the correct /.well-known/atproto-did path above. The Bluesky Debug tool seems to suggest that both DNS and HTTP verification are necessary, but I may well be wrong. Still, it can’t hurt to have both available - right?!

To satisfy the DNS verification requirements, you’ll need to add a TXT record called _atproto which contains the DID we were given above. You did write this down, right?!

Once that’s done, you should be able to use the Bluesky Debug tool against your domain, and have your DID returned for both. If there are any warnings or errors, you should fix these before continuing any further.

It’s also worth checking that you can successfully connect via WebSocket - this tool is useful for doing so, checking against wss://yourdomain.com/xrpc/com.atproto.sync.subscribeRepos?cursor=0. Note that making a connection doesn’t necessarily guarantee success! As mentioned in Part 1, the correct Host header needs to be passed through your reverse proxy as well.

Part 4: Federation #

If the tests above have all worked, it’s time to get your new PDS instance noticed by the network! Until relatively recently, this required the blessing of the PDS Administractors Discord, but that requirement has now been lifted, and you can request a crawl using one of the administration tools we downloaded earlier

$ PDS_ENV_FILE=/path/to/stack.env ./request-crawl.sh

Part 5: Start posting #

Once part of the network, you should be able to see requests being logged by the PDS Docker container. You can then use any Bluesky application to log in using your domain, handle and password. I’ve been using their own hosted web version until I get around to hosting my own version.

Success! You’re now up and running on your own self-hosted social media network. Why not celebrate by following me and dropping me a message!


Comments

You can use your Bluesky account to reply to this post.