Switching off of Twitter is all the rage these days. I opted to switch off of my own fediverse instance, satania.social (a lovely Akkoma instance running in Kubernetes) instead! I'm really enjoying the vibe that is Bluesky right now, it feels very reminiscent of my early Twitter days. However, I wanted to dig into atproto a bit, DIDs (fancy decentralized IDs, really) and get my own PDS (Personal Data Server) running! For those who aren't familiar with Bluesky PDS, you can think of it essentially as its own Mastodon/Misskey instance but with most users accessing it through Bluesky apps. It's very similar to Plex nowadays (if you use app.plex.tv) in that regard. Here's how I got my instance set up, at the end of this you'll have a Bluesky PDS with the following:
- Blobs (media) stored on S3 (I'll be routing this through Tailscale and using Minio)
- SMTP through Mailgun
Requirements
I started with an already existing Ubuntu 24.04.1 VM with Docker already installed. My machine is, currently, running in GCP with a dual NICs, though it didn't change how I set this up all that much. I'm using GCP compute because I have a decently priced 3 year commitment that had space.
Using the Installer
The easiest way to get start is to run the installer provided by Bluesky. However, as of this post, it does not support Ubuntu 24.04.1 (somehow!). Luckily, only a small modification needs to be made to support this and a PR already has it! So, we download that first:
wget -O installer.sh https://raw.githubusercontent.com/bluesky-social/pds/ee92b84ce2def43ddcc516c81d30a753a7af647f/installer.sh
# Consider checking the script before running it.
# e.g., vim installer.sh
sudo bash installer.sh
From there it'll immediately ask you to provide a domain name. Before doing this, make sure your DNS provider (I use Cloudflare) is configured to point the following domains to your server (the script also tells you this!)
example.com
*.example.com
I used pds.rgst.io
, and created the following records in Cloudflare:
A pds.rgst.io GCP_IP
CNAME *.pds.rgst.io pds.@
Before entering in the domain (example here: pds.rgst.io
), check that the DNS changes have propagated with dig
:
$ dig x.pds.rgst.io
; <<>> DiG 9.10.6 <<>> x.pds.rgst.io
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 1000
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;x.pds.rgst.io. IN A
;; ANSWER SECTION:
x.pds.rgst.io. 300 IN A GCP_IP
;; Query time: 127 msec
;; SERVER: 100.100.100.100#53(100.100.100.100)
;; WHEN: Sat Jan 11 22:27:24 PST 2025
;; MSG SIZE rcvd: 58
Look for your IP where GCP_IP
is, if it looks correct, you can enter your domain in and continue on-wards!
At the end, it'll ask you if you'd like to create a user account. You can go ahead and enter N
, as we'll be migrating your account over.
You can always create a new user at any time by running
pdsadmin create-invite-code
and using it to sign-up online!Testing the PDS
At this point, you should have a working PDS! If you try accessing it through curl
or your favorite web browser, you'll see a boring, but reassuring:
$ curl -L http://pds.rgst.io
This is an AT Protocol Personal Data Server (PDS): https://github.com/bluesky-social/atproto
Most API routes are under /xrpc/%
This means its accessible, nice work!
Configuring S3
Now, we can switch our blob storage to be on S3. If you'd rather leave storage as is, which is local file directory, you can of course skip this step.
You'll need to open up /pds/pds.env
with your favorite editor and add the following:
# Configure these!
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=YOUR_ACCESS_KEY_ID
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=YOUR_SECRET_ACCESS_KEY
# Note: I use and IP and http here because I'm using Minio
# over Tailscale, which handles network encryption for me.
# You probably want to use TLS (https) here.
PDS_BLOBSTORE_S3_ENDPOINT=http://YOUR_MINIO_IP:9000
PDS_BLOBSTORE_S3_BUCKET=YOUR_BUCKET_NAME
# Probably don't need to configure these :)
PDS_BLOBSTORE_S3_REGION=auto
PDS_BLOBSTORE_S3_FORCE_PATH_STYLE=true
PDS_BLOBSTORE_S3_UPLOAD_TIMEOUT_MS=60000
PDS_BLOB_UPLOAD_LIMIT=52428800
Make sure to also remove PDS_BLOBSTORE_DISK_LOCATION
as that will conflict with the new settings we added above.
Once done, you'll need to cd /pds
and run docker compose up -d
for these changes to take effect. Run docker compose logs -f pds
for a quick sanity check. If anything is wrong with your syntax it'll let you know! Note that it won't let you know if things like credentials are incorrect or addresses, so make sure you're decently sure before continuing forward, or opt to create a test user (see callout above) to test your changes!
Configuring SMTP
I used Mailgun, so keep that in mind with my settings here, but any SMTP providing provider should work!
You'll want to open up /pds/pds.env
(again) with your favorite editor and add the following:
# Email Settings
## Note that mailgun says the user should be the FULL
## email address (e.g., [email protected]).
PDS_EMAIL_SMTP_URL=smtps://YOUR_USER@YOUR_DOMAIN:[email protected]:465/
PDS_EMAIL_FROM_ADDRESS=YOUR_FROM_ADDRESS
Once again, cd /pds
and run docker compose up -d
for the changes to take effect. Check docker compose logs -f pds
to see if anything is broken.
Migrating the Account
Now that we have SMTP and S3 configured, we're ready to do the migration! This next part requires a decent bit of data gathering, so hang tight. First, you'll (unfortunately) need a working Go toolchain. For the casual user, try brew install golang
(macOS) or sudo apt install golang
(Ubuntu). Now, we can run goat.
We're going to create an info.env
file where we're going to store a lot of data for the purposes of running the migration (without putting creds into your history
):
# This should be the handle of your current old handle. For most
# this is probably your bsky.app handle. Do NOT include the @
OLDHANDLE="YOUR_OLD_HANDLE"
# This should be your password used to log in. Note that app
# passwords do NOT work
OLDPASSWORD="YOUR_OLD_PASSWORD"
# Must be a URL
NEWPDSHOST="https://YOUR_PDS_DOMAIN"
# If you're using a custom domain (e.g., jaredallard.dev) you
# should have NAME be "temp". You'll change this through the
# UI later.
NEWHANDLE="NAME.YOUR_PDS_DOMAIN"
NEWPASSWORD="YOUR_OLD_PASSWORD"
NEWEMAIL="YOUR_EMAIL"
Make sure to read the above comments to understand what each value is! Now we can export this into our shell:
set -a; source info.env; set +a
Now, we'll log into bsky.app to get a PLC token, which we'll use to give authenticity to our new account that goat will make for us later.
# Allows us to run "goat" comands as just "goat". This will also
# JIT compile it for us.
alias goat="go run github.com/bluesky-social/indigo/cmd/goat@latest"
# Login to your bsky.app account.
goat account login -u "$OLDHANDLE" -p "$OLDPASSWORD"
## If you get an error about 2FA, check your email and run the
## command again, but with the auth token set as well
goat account login -u "$OLDHANDLE" -p "$OLDPASSWORD" \
--auth-factor-token YOUR_TOKEN_HERE
If all went well, there should be no output! This means we can get the PLC token.
goat account plc request-token
This should send you an email, which will give you a token. Note this is NOT the same as a 2FA token, though it looks very similar. Keep that token handy as you'll need it shortly.
Switch over to your PDS and create an invite code, so that goat can create a new account during the migration:
# On your PDS host machine
sudo pdsadmin create-invite-code
Take note of the token and switch back to your machine that contains info.env
(if you need to export it again, go re-run the set
commands above!). It's time to migrate the account!
###
# !!! IMPORTANT !!!
#
# Make sure to replace the "YOUR" values below!
###
goat account migrate \
--pds-host $NEWPDSHOST \
--new-handle $NEWHANDLE \
--new-password $NEWPASSWORD \
--new-email $NEWEMAIL \
--plc-token YOUR_PLC_CODE_HERE \
--invite-code YOUR_INVITE_CODE_HERE
Once you press enter, your should see a long list of "migrating" output! If all goes well, you should see an "account migrated" message. To ensure this was successful, log into bsky.app as normal and you should see an "account deactivated" message. If you don't, you must deactivate this account AFTER you've confirmed the PDS one is activated (see the next section!)
Troubleshooting
If the above migration command fails (like it did for me because of S3 configuration issues 😔), you can restart the process (Note: This applies to anything outside of the migration phase, otherwise you may be stuck). Switch over to your PDS and run the following:
# If you get an error about "column" not found, run the following:
# sudo apt install bsdmainutils
$ sudo pdsadmin account list
Handle Email DID
jaredallard.dev [email protected] did:plc:xyz
Take note of the "DID" value and run the following:
sudo pdsadmin account delete YOUR_DID_HERE
You should be able to restart the migration! You may need to rotate the invite code and PLC token, as those are one-time use.
Validating
Now that our account is migrated and (hopefully!) our old bsky.app handle is deactivated, you can log in to your PDS through bsky.app and any of the clients. To do this, you'll need to select your PDS on ever login like so:
Once your enter your PDS url, select "Done" and you're good to go!
Hopefully this was helpful and you didn't run into any issues. As always, if you have questions/need help, leave a comment or reach out @jaredallard.dev on Bluesky! Special thanks to the following articles which helped me do this: