Bluesky Personal Data Server


This is my first blog post. I bought this domain name to create a Bluesky Personal Data Server (PDS) so I thought it would be a good idea to start a blog on here as well. Makes sense my first blog post would be about how to install a PDS.

Bluesky is one of the few replacements for X/Twitter, along with Mastodon and Threads. One of the features of Bluesky is that it allows you to host what they call a Personal Data Server rather than relying on your personal data being stored in the Bluesky servers. This server contains all of your account information, and can be migrated from one server to another.

One neat benefit of hosting your own PDS is that you can have your own domain handle. For the majority of non-tech-savvy users, Bluesky handles are in the format of @username.bsky.social. If you own your domain and a PDS, you can have handles under your domain name, such as @parkerr.app—which is my handle—or @username.parkerr.app. I originally bought this domain specifically for this purpose and I’ll show you how I did it.

First, you obviously need your own domain. I’ve been using GoDaddy my whole life so I’ll continue to recommend them, however there’s plenty of registrars you can choose from and I have no strong opinions on that matter.

I decided to host this server on my own Raspberry Pi 4 and I thought this would be a good opportunity to use a Cloudflare tunnel, which allows you to tunnel your domain’s DNS traffic through Cloudflare and into your server. Think of this as a proxy that hides your machine’s public IP address from the domain’s DNS. When a DNS lookup is done for your domain, it would route to Cloudflare, which internally would route to your machine running the Clouldflare tunnel. The major benefit of using a Cloudflare tunnel is you don’t need to expose any ports on your machine.

Setting up the tunnel is fairly straightforward. When setting up your domain to use with Cloudflare, they will provide nameservers for you to use. Afterwards, all DNS settings for this domain you make should now be done through Cloudflare.

Cloudflare Nameservers

In GoDaddy, I assigned my domain these nameservers like so:

GoDaddy Nameservers

Next step is to set up the Cloudflare tunnel. To do so, go to the website dashboard in Cloudflare and click Access, and then Launch Zero Trust. Through here, you will set up the tunnel by going to Networks > Tunnels. Through here, I selected “Cloudflared” which is the software they provide for you to run on your server. After giving it a name, they’ll give you a few installation options, in which I decided to use Docker. Be sure to keep your token private. The command they provided was:

docker run cloudflare/cloudflared:latest tunnel --no-autoupdate run --token [REDACTED TOKEN]

Instead of using the docker run command, I created a docker network and moved this into a docker compose file.

docker network create cloudflared_network

docker-compose.yaml:

services:
cloudflared:
container_name: cloudflared
image: cloudflare/cloudflared:latest
restart: unless-stopped
command: tunnel run
environment:
- TUNNEL_TOKEN=[REDACTED TOKEN]
networks:
- cloudflared_network
networks:
cloudflared_network:
external: true

Installing the PDS was a bit of a mess. Bluesky offers a Docker image @bluesky-social/pds. Their docs suggest to download the install script it by running the command below:

wget https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh

This script creates a docker compose file and three services: caddy, the PDS, and watchtower. It also installs pdsadmin which is a CLI tool that interfaces with the PDS service, which runs in a caddy reverse proxy. I was getting consistent 400 and 500 errors when using the pdsadmin CLI tool. To get around this, I just set up my own nginx reverse proxy instead by adding the below to the docker-compose.yaml file we created earlier.

services:
...
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:0.4
restart: unless-stopped
volumes:
- /pds:/pds
env_file:
- /pds/pds.env
networks:
- cloudflared_network
depends_on:
- cloudflared
nginx:
container_name: nginx
image: nginx:stable-alpine3.20
volumes:
- ./nginx:/etc/nginx/conf.d
restart: unless-stopped
networks:
- cloudflared_network
depends_on:
- pds
- cloudflared

In Cloudflare Zero Trust, I added the hostname below:

Cloudflare Public Hostname

By running sudo pdsadmin account create, you can create your account. Unfortunately, this command doesn’t support root domain handles, for example, I was not able to create @parkerr.app and instead had to create a dummy account such as @dummy.parkerr.app. We will fix this later.

sudo pdsadmin account create
Enter an email address (e.g. [email protected]): [email protected]
Enter a handle (e.g. alice.parkerr.app): dummy.parkerr.app
Account created successfully!
-----------------------------
Handle : dummy.parkerr.app
DID : did:plc:22wqa4uibt4f7kguds6aycrp
Password : [REDACTED PASSWORD]
-----------------------------
Save this password, it will not be displayed again.

The DID is the account identifier for this new account. Keep note of this.

Next step is go to the Bluesky app, and sign in. For the hosting provider, because we are hosting the PDS ourselves, select Custom, and then enter the domain. In my case, it’s https://parkerr.app. Log in with the credentials provided by the sudo pdsadmin account create output.

The Bluesky app allows you to change your handle to anything that’s NOT root. For example, you can change your handle to @dummy2.parkerr.app. If you want to change it to @parkerr.app, you need to prove you own this domain.

Change Handle

The instructions there are clear enough. In Cloudflare, I added the below DNS record:

__atproto DNS Entry

After clicking Verify DNS Record on Bluesky, your handle should change. Well, it should, but it won’t, because there is a bug in the Bluesky UI. They offer another option to verify your domain by adding an entry into /.well-known/atproto-did.

Below is my nginx/default.conf file. I didn’t want to actually create this /.well-known/atproto-did file, so instead I simply return the DID string through nginx.

default.conf:

map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
server_name parkerr.app www.parkerr.app;
location /xrpc {
proxy_pass http://pds:3000;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
}
location /.well-known/atproto-did {
add_header Content-Type text/plain;
return 200 "did:plc:22wqa4uibt4f7kguds6aycrp";
}
}

We are very close to done. Last step is to now actually change our handle from @dummy.parkerr.app to @parkerr.app. Instead of using their UI to change the handle, I found a solution on GitHub. You can find your admin password in /pds/pds.env.

curl --request POST -k \
--url "https://parkerr.app/xrpc/com.atproto.admin.updateAccountHandle" \
--header "Content-Type: application/json" \
--user "admin:[REDACTIED ADMIN PASSWORD]" \
--data '{
"did": "did:plc:22wqa4uibt4f7kguds6aycrp",
"handle": "parkerr.app"
}'

If you want the ability to send verification emails, I used Resend. After configuring the required DNS entries, add your API key to the /pds/pds.env file.

Voila. Feel free to follow me on Bluesky!