Skip to main content
Nami Writes - Nami's Blog

Self-hosting Bluesky PDS at Home

I'm writing this blog post to jot down why and what I did to self-host Bluesky PDS at my homelab (a server at home).

Moving away from centralization (like Twitter) #

I had been having an urge to self-host Bluesky PDS at Home. I've been hearing good things about Bluesky, but then I don't want to contribute to the centralization / platformism by joining their instance. It's still too fresh in my memory what happened to Twitter.

Bringing data closer to me #

In organizing my digital life, I've been working on to bring data closer to me. Self-hosting Bluesky PDS sounded like a perfect opportunity for me.

Prerequisites #

I had the following before starting this project.

  1. A server running at home with Docker. In my case, I ran off from my OpenMediaVault machine (MacBook Air). But looking back, I'd place this in a more isolated environment, since it's a public-facing service. (A dedicated RaspberryPi or a proxmox VM may be a good idea...)
  2. A domain you own
  3. Cloudflare-account to manage Cloudflare Tunnels

Where I found information #

I did not sit down to come up all of these from my head. I followed The Robbie Davis's excellent tutorial for self-hosting a Bluesky PDS. I mostly followed it, except the part about the reverse proxy (since I'm using a Cloudflare Tunnel).

Part 1. Configure Docker Compose #

Based on the Robbie Davis's, I configured the docker compose file and the .env file as below. I edited the following parts:

  1. I re-named the service and container as bluesky-pds instead of pds so that I can easily identify them later (e.g., when doing docker ps)
  2. I moved all the environment variables into the compose file (under environment:). Then, I refer to them by environment variable interpolation. I also specify the default values where appropriate, such as for the PDS data directory

Example compose.yml #

services:
  bluesky-pds:
    container_name: bluesky-pds
    image: ghcr.io/bluesky-social/pds:latest
    restart: unless-stopped
    ports:
      - 3000:3000
    volumes:
      - ./pds:/pds
    environment:
      - PDS_HOSTNAME=${PDS_HOSTNAME}
      - PDS_JWT_SECRET=${PDS_JWT_SECRET}
      - PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
      - PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=${PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX}
      - PDS_EMAIL_SMTP_URL=${PDS_EMAIL_SMTP_URL}
      - PDS_EMAIL_FROM_ADDRESS=${PDS_EMAIL_FROM_ADDRESS}
      - PDS_MODERATION_EMAIL_SMTP_URL=${PDS_MODERATION_EMAIL_SMTP_URL}
      - PDS_MODERATION_EMAIL_ADDRESS=${PDS_MODERATION_EMAIL_ADDRESS}
      - PDS_DATA_DIRECTORY=${PDS_DATA_DIRECTORY:-/pds}
      - PDS_BLOBSTORE_DISK_LOCATION=${PDS_BLOBSTORE_DISK_LOCATION:-/pds/blocks}
      - PDS_DID_PLC_URL=${PDS_DID_PLC_URL:-https://plc.directory}
      - PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL:-https://api.bsky.app}
      - PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID:-did:web:api.bsky.app}
      - PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL:-https://mod.bsky.app}
      - PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID:-did:plc:ar7c4by46qjdydhdevvrndac}
      - PDS_CRAWLERS=${PDS_CRAWLERS:-https://bsky.network}
      - LOG_ENABLED=${LOG_ENABLED:-true}

Example .env #

Below is the example .env file. You need to specify your own values here.

PDS_HOSTNAME #bluesky.yourdomain.com
PDS_JWT_SECRET #openssl rand --hex 16
PDS_ADMIN_PASSWORD #openssl rand --hex 16
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX #openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32
PDS_EMAIL_SMTP_URL #smtp://[email protected]:[email protected]:587
PDS_EMAIL_FROM_ADDRESS #[email protected]
PDS_MODERATION_EMAIL_SMTP_URL #smtp://[email protected]:[email protected]:587
PDS_MODERATION_EMAIL_ADDRESS #[email protected]

For my SMTP, I used Postmark. So the SMTP URL looked something like username:[email protected]:587

Then, you can run docker copose up to start the service. To check, you can access the port 3000 of the server. If you see the following message, you are successfully running the Bluesky PDS.

A screenshot of the Bluesky PDS page

The next step is to expose the service to the Internet, safely!

Part 2. Exposing your Bluesky PDS via a Cloudflare Tunnel #

I usually use Cloudflare tunnels to expose my self-hosted services from my homelab.

Side note: Some people avoid Cloudflare because all data will pass through Cloudflare, and we need to trust Cloudflare to do good. For my use case, the security and privacy risk of me trying to expose my home lab directly to the internet myself is higher than Cloudflare messing up, so I use Cloudflare Tunnels.

If you are interested in learning more about Cloudflare Tunnels, check out Syntax.fm's YouTube/Podcast or the blog post by I's FOSS.

Making sure that your domain is managed by Cloudflare #

  1. Login or sign-up to Cloudflare.
  2. Check if you have your domain added to the account. If not, click "+ Add a domain" to add your domain, and follow the instructions.

Create a Cloudflare tunnel #

  1. Go to Cloudflare Zero Trust (one.dash.cloudflare.com)

  2. Go to Networks → Tunnels
    A screenshot of Cloudflare Dashboard showing Tunnels menu item in Networks group

  3. Click "Create a tunnel"
    A screenshot of Cloudflare website showing the '+ Create a tunnel' button

  4. Select Cloudflared
    A screenshot of Cloudflare Tunnel showing the 'Select Cloudflared' button

  5. Name your tunnel
    A screenshot of Cloudflare Tunnel page, showing the textbox labeled 'Name your tunnel'

  6. Choose Docker as your environment, and copy the command to run a connector
    A screenshot of Cloudflare Tunnel to select an environment with Docker highlighted A screenshot of Cloudflare Tunnel showing a docker command to run cloudflared

    You can start the connector (`cloudflared`) by running the command. I usually modify the command and add a daemon flag (`-d`) so that it runs in the background, that is: `docker run -d cloudflare/cloudflared:latest ...` Click "Next"
  7. Configure the public hostname and service

    A screenshot of Cloudflare Tunnel showing a screen to add a public hostname

    a. Public hostname: I wanted to host my Bluesky PDS instance on bluesky.namisunami.com. So I set up the following.

    Subdomain bluesky
    Domain namisunami.com
    Path (Empty)

    b. Service:

    Type HTTP
    URL Set the URL to be the IP address of the server plus the port number 3000. (A local IP address usually starts with 192.168 or 10) Something like: 192.168.99.99:3000
  8. Click "Save tunnel"

  9. Go to the public hostname that you configured in above to check if you see the message "This is an AT Protocol Personal Data Server ..."

Part 3. Creating an Account #

Now the server is running! The next step is to create an account on your Bluesky PDS.

But you will need an invitation code from your Bluesky PDS.

Get an invitation code #

To get an invitation code, you can make a POST request to PDS. You need to use the admin password that you set in your .env file.

You can use curl to make a post request like below. Replace the admin password (PDS_ADMIN_PASSWORD) and hostname (YOUR_PUBLIC_HOSTNAME) with yours

curl -X POST \
 -u admin:PDS_ADMIN_PASSWORD \
 -H 'Content-Type:application/json' \
 -d '{"useCount": 1}' \
 https://YOUR_PUBLIC_HOSTNAME/xrpc/com.atproto.server.createInviteCode

The response will look something like:

{ "code": "bluesky-namisunami-com-abcde-fghij" }

Create an account on your PDS #

  1. Open a Blusky client (I used my iOS app)

    A screenshot of Bluesky iOS app startup screen with Create account & Sign in buttons

    Select Create account

  2. Change hosting provider to the domain that you set
    A screenshot of Bluesky create-account screen with hosting provider set to bluesky.namisunami.com

  3. Copy and paste the invite code
    A screenshot of Bluesky create-account screen with an invite code

  4. Fill in the rest of the information about your account. Click "Next"

  5. Set the username. By default, the handle will look like @your_username.yourdomain.com. In my case, I wanted my handle to be my top domain name, that is @namisunami.com. I will describe the process in the next step. For now, you can put a temporary username here, and you can change it later. Click "Next"
    A screenshot of Bluesky for choosing your username

Changing the handle #

By default, your handle will show as invalid.
A screenshot of Nami's Bluesky account showing an Invalid Handle warning

It's because Bluesky looks for a TXT record for the domain to validate the handle.

To change the handle and set up properly:

  1. Go to Settings → Account → Handle
    A screenshot of Bluesky's change handle view

  2. Select "I have my own domain →"
    A screenshot of Bluesky's change handle view to specify own domain's handle

  3. Fill in the handle that you want to use. Then you have the information to set up the TXT record for your domain. You can do so via Cloudflare's DNS settings (Cloudflare docs).
    A screenshot of Cloudflare's DNS setting

  4. Once you set up your DNS record on Cloudflare, click "Verify DNS Record". Then click "Update to YOUR_HANDLE" to complete the setup.
    A screenshot of Bluesky client showing a green checkmark with 'Domain verified' text, with a button labeled, 'updated to namisunami.com'

Hope you were able to host your own Bluesky PDS!

If you were, please follow me on @namisunami.com on Bluesky

References #