7 min read

Self-hosting Bluesky PDS using Docker, Cloudflare, and Reverse Proxy with SWAG

How to self-host the Bluesky PDS on Docker with SWAG and Cloudflare
Bluesky, a decentralized microblogging social platform
Bluesky, a decentralized microblogging social platform

On Feb. 22 2024 Bluesky announced that they are opening up federation and self-hosting. I wanted to wait a bit for the kinks to get ironed out. Being on the other side of this journey I wish I would have waited a bit longer, but that's where this post comes into play. I want to help other setup their own Bluesky servers as easily as possible. Bluesky has an installer, which makes some ...interesting... assumptions, but my goal was to try to setup Bluesky with the program I have and then streamline it for user consumption.


Introduction

First we need to understand what we will be hosting. We will be setting up a Personal Data Server (PDS). The PDS basically stores all your public and private data and also acts as your entryway into the Bluesky network. Here is a more detailed overview of what a PDS is and how it relates to BlueSky's architecture.

The Domain & Server

First and foremost you will need a domain name. I prefer Namecheap.com, but there are other registrars out there as well.

You will also need a server. Most people go with a Digital Ocean or Vultr server. I use a Hetzner auction server. Below are the requirements as stated by Bluesky:

Ensure that you can ssh to your server and have root access.

Server Requirements

  • Public IPv4 address
  • Public DNS name
  • Public inbound internet access permitted on port 80/tcp and 443/tcp

Server Recommendations

Operating System Ubuntu 22.04
Memory (RAM) 1 GB
CPU Cores 1
Storage 20 GB SSD
Architectures amd64, arm64
Numer of users 1-20

I would also recommend creating a seperate user with access to specific folder for where you will store the PDS data.

Once you have your domain and server, I recommend changing the nameservers over to Cloudflare. This provides protection and better configuration.

After you have transferred your nameservers (and DNS) over to cloudflare, go ahead and setup a couple A records in the DNS.

Name IPv4 Address
yourdomain.com [Server IP]
bluesky [Server IP]
*.bluesky [Server IP]

Docker

After that, SSH into your server because you will need to install docker. This is a pretty straightforward process:

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done

Then we setup Docker's apt repo

# Add Docker's official GPG key:
sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

Then finally we install docker

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

SWAG Container

We will need to setup a bare minimum of 2 containers for this. One being the reverse proxy, the other being the PDS. This section will cover setting up SWAG for a reverse proxy. We will also be using docker compose as I find it easier. Docker compose is just a yml file that is used to tell docker how to create containers.

Create a file named docker-compose.yml where ever you would like. Just remember the location.

---
version: "3"
services:
  swag:
    image: lscr.io/linuxserver/swag
    container_name: swag
    cap_add:
      - NET_ADMIN
    environment:
      - PUID=1000 #id -u
      - PGID=1000 #id -g
      - TZ=Europe/London #change
      - URL=yourdomain.url #your domain
      - SUBDOMAINS=www,bluesky
      - VALIDATION=dns
      - DNSPLUGIN=cloudflare #optional
      - PROPAGATION=20
      - EMAIL=<e-mail> #optional
    volumes:
      - /path/to/appdata/config:/config
    ports:
      - 443:443
      - 80:80 #optional
    restart: unless-stopped

Then we'll fire up the container via docker compose -f <path/to/your/docker-compose.yml> up -d

After the container is started, we'll watch the logs with docker logs swag -f. After some init steps, we'll notice that the container will give an error during validation due to wrong credentials. That's because we didn't enter the correct credentials for the Cloudflare API yet. We can browse to the location /config/dns-conf which is mapped from the host location (according to above settings) /path/to/appdata/config and edit the correct ini file for our dns provider. For Cloudflare, we'll enter our API token. The API token can be created by going to My Profile->API Tokens and creating a token with the Edit DNS permission on the DNS zones for which you wish to request certificates. In the cloudflare.ini comment out the dns_cloudflare_email and dns_cloudflare_api_key values, then uncomment dns_cloudflare_api_token and add your API token against it.

Once we enter the credentials into the ini file, we'll restart the docker container via docker restart swag and again watch the logs. After successful validation, our webserver should be up and accessible by visiting your domain name.

Now we will need to setup the reverse proxy configuration via SWAG. Where you are storing SWAG's config data /path/to/appdata/config/ find /nginx/proxy-confs and create a new file called bluesky.subdomain.conf

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

	server_name bluesky.*;
	server_name *.bluesky.yourdomain.com;

    include /config/nginx/ssl.conf;

    client_max_body_size 0;
    proxy_redirect off;
    proxy_buffering off;

    location / {
        include /config/nginx/proxy.conf;
        include /config/nginx/resolver.conf;
        set $upstream_app pds;
        set $upstream_port 3000;
        set $upstream_proto http;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;
    }
}

Then run docker restart swag and verify that there are no errors.

Bluesky PDS

This next part will cover the Bluesky PDS container and initial setup

In your docker-compose.yml add the following

  pds:
    container_name: pds
    image: ghcr.io/bluesky-social/pds:0.4
    restart: unless-stopped
    ports:
      - 3000:3000
    volumes:
        - path/to/pds/storage:/pds
    env_file:
      - /path/to/pds.env

You will need to decide where you want to store the PDS data as well as where you want to store a pds.env file to be used by the container.

In the pds.env file please enter and edit the following. # denotes a comment, or command, to help fill out this file. Your data should be directly following the =. Delete the comment after completion

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]
PDS_DATA_DIRECTORY=/pds
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
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
LOG_ENABLED=true

Then we'll fire up the new container via docker compose -f <path/to/your/docker-compose.yml> up -d

If you did everything correctly you should be able to load https://bluesky.yourdomain.com/xrpc/_health and see something like {"version":"0.4.12"}

Joining the network

Update Nov. 29th 2024: This registration instructions below are no longer required. If all is setup correctly then your PDS will federate automatically. I will leave the previous instructions intact below for posterity.

Initially to join the network you'll need to join the AT Protocol PDS Admins Discord and register the hostname of your PDS. They recommend doing so before bringing your PDS online. In the future, this registration check will not be required.

Create an invite code

You will need Postman or something similar for this step since we will be sending calls to our server's API endpoints.

Setup a POST request to https://bluesky.yourdomain.com/xrpc/com.atproto.server.createInviteCode

Under Authorization use Basic Auth. Username admin, Password is the PDS_ADMIN_PASSWORD from the pds.env file.

Under Headers add a header called Content-Type with the value application/json

Under Body user the raw data type with the JSON format option and enter the following into the body field {"useCount": 1}

Click Send and you should get your invite code as the response.

Registering

Now you should be able to register your account. Go to bsky.app and click Sign up

Click Done, enter your invite code and fill out the rest of the form

lastly you will need to add one last DNS record to cloudflare. In the Bsky.app app or website, go to Settings > Advanced > Change Handle

Add your specific values to your Cloudflare DNS, then validate using the Bsky debug website and you should be set!

If you still see “Invalid Handle” and it’s passing the DNS check, just change your handle to what it already is to revalidate.