Self-Hosting a Bluesky PDS with Dokku

Last updated: November 15, 2024

The official Bluesky documentation on self-hosting a PDS is a little thin.

In addition, the installation instructions make several assumptions that don’t quite match my setup.

To get things running, I packaged up the PDS in a Dockerfile that can be deployed to services like Heroku, GCP Cloud Run, AWS Fargate, etc.

I happen to use Dokku, a “personal” Heroku clone, to host a variety of low-traffic services on a tiny cloud server. I wanted to install my Bluesky PDS as a Dokku app on that same server.

Here are my notes on how to do that.

A word about Dokku

There are a lot of open-source projects that promise a Heroku-like experience. Dokku is the first I’ve run across that (a) delivers near enough to that promise (b) is easy to install and use, and (c) seems to mostly “just work”.

Unlike Heroku, Dokku does not have a web-based UI. All interactions are through a reasonably well-documented CLI.

If you haven’t used Dokku before, see the well-written installation instructions.

Getting your PDS up and running

Clone my bluesky-pds-docker repository to your local machine:

git clone https://github.com/davepeck/bluesky-pds-docker.git bluesky-pds-docker
cd bluesky-pds-docker

Create a new Dokku app for your Bluesky PDS:

dokku apps:create bsky  # or whatever name you'd like

This will also create a dokku remote in your Git repository; at this point, so long as you’re running dokku commands from the same directory, you don’t need to specify the app name.

Set up a persistent volume for your app:

dokku storage:ensure-directory bluesky-data
# I find it awkward that Dokku lets you ensure-directory with just a name,
# but seems to require you to mount it with a full path. Oh well.
dokku storage:mount /var/lib/dokku/data/storage/bluesky-data:/pds

Create a JWT secret for your PDS:

openssl rand --hex 16

You can also create an admin password this way too. (Or use your favorite password manager.)

Create a private key for DID PLC key rotation:

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

Set up your app’s environment variables:

dokku config:set \
  # Replace these with your own values.
  PDS_HOSTNAME=bsky.your-dokku-domain.com \
  PDS_ADMIN_PASSWORD=your-admin-password \
  PDS_JWT_SECRET=your-jwt-secret \
  PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=your-plc-rotation-key \
  # Everything after this is default configuration.
  PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks \
  PDS_DATA_DIRECTORY=/pds \
  PDS_BLOB_UPLOAD_LIMIT=52428800 \
  PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app \
  PDS_BSKY_APP_VIEW_URL=https://api.bsky.app \
  PDS_CRAWLERS=https://bsky.network \
  PDS_DID_PLC_URL=https://plc.directory \
  PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac \
  PDS_REPORT_SERVICE_URL=https://mod.bsky.app \

Now go ahead and deploy your app:

git push dokku main

Once the app is deployed, you’ll want to make sure your outbound port 80 is open. You can do this with a simple Dokku command:

dokku ports:add http:80:3000

Finally, let’s enable HTTPS. First, let’s install the Let’s Encrypt Dokku plugin:

dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git

Now enable Let’s Encrypt for your app:

dokku letsencrypt:enable

After a moment, your PDS should be up and running. You can access it at https://bsky.your-dokku-domain.com. You should see a simple web page that says “This is an AT Protocol Personal Data Server (PDS)”.

Creating a Bluesky account

Once your PDS is up and running, you’ll need to create a Bluesky account.

Log in to your PDS:

dokku run

Now create a new account:

./pdsadmin.sh account create

IMPORTANT. When creating this account, you’ll be asked for a new Bluesky handle. Annoyingly, when you first set up your handle, it has to be styled as a “subdomain” of your Bluesky server. Don’t worry, we will fix this, but for now, if your server is bsky.your-dokku-domain.com, your handle should be something like <your-name>.bsky.your-dokku-domain.com.

Assuming all goes well, you should see your account:

./pdsadmin.sh account list
Handle        Email                  DID
<your-handle> <your-email>           <some-plc-did-that-got-generated-for-you>

Signing in to Bluesky

Okay, it’s time to sign in to Bluesky!

Go to https://bsky.app and click “Sign in”.

Click “Hosting Provider”, select “Custom”, and enter your PDS domain name. Click “Done”.

Now, enter your current Bluesky handle (the one with the subdomain) and your PDS admin password. Click “Sign in”.

You’re in!

Fixing your Bluesky handle

Now that you’re signed in, you can change your Bluesky handle if you like.

Under your user icon, click “Settings”. Under “Advanced”, click “Change handle”. Choose a new handle and then use one of the two provided methods (adding a DNS TXT record, or adding a .well-known file) to verify that you own the domain.

It can take 15-30 minutes for the change to take effect. Once it does, you’ll be able to sign in to Bluesky using your new handle and your PDS admin password.

You can also log in to your PDS to verify that your handle was successfully changed:

dokku run
./pdsadmin.sh account list

Have fun!

Validating your Bluesky admin email

When you log in, Bluesky will nag you to validate your email.

Alas, as of this writing (November 16, 2024) this feature appears to be broken for those of us who self-host their own PDS. I’ve talked to several others and none of us have ever received a validation email.

There don’t appear to be any adverse side effects from leaving your email unvalidated. But hopefully this will be fixed soon.

Adapting for Heroku, GCP Cloud Run, AWS Fargate, etc.

While I focus on Dokku here, my approach could easily be adapted to other hosting environments, including Heroku, Google Cloud Run, or AWS Fargate. If you manage to get Bluesky running on one of these platforms, please let me know!

One useful thing to know is that Blueky’s PDS implementation supports both local storage (which is what I used) and S3-compatible storage. If you’re using Heroku or AWS, you probably want to store your data in S3. Just set the PDS_BLOBSTORE_DISK_LOCATION environment variable to an s3:// URL and you should be good to go.