Self-hosting Bluesky PDS from Scratch

Self-hosting Bluesky PDS from Scratch

Yulei Chen - Content-Engineerin bei sliplane.ioYulei Chen
10 min

Bluesky is an open social app, built on top of the philosophy of "file over app". You can store your own social data on your own server. No matter what happens to the social app itself, whether it goes evil or simply shuts down, you still own your data. That's cool.

In this post, I'll walk you through how to self-host a Bluesky PDS (Personal Data Server) on your own server, create an account on it, log in to Bluesky with it, and verify it using PDSls.

Prerequisites

Before we start, make sure you already have a server and a domain name. My server is an Ubuntu server from Hetzner.

Step 1: Update Your Server

On your server, update the system to ensure it has the latest security patches and updates:

Terminal
sudo apt update
sudo apt upgrade -y

Once finished, your server is ready for installing the software.

Step 2: Install and Configure UFW Firewall

Only keep necessary ports open: SSH (22), HTTP (80), HTTPS (443). Install UFW and configure the firewall as follows:

Terminal
sudo apt install ufw -y
sudo ufw allow 22    # SSH
sudo ufw allow 80    # HTTP
sudo ufw allow 443   # HTTPS
sudo ufw enable

Check your firewall configuration:

Terminal
sudo ufw status verbose
# Output:

# Status: active
# Logging: on (low)
# Default: deny (incoming), allow (outgoing), deny (routed)
# New profiles: skip

# To                         Action      From
# --                         ------      ----
# 22                         ALLOW IN    Anywhere
# 80                         ALLOW IN    Anywhere
# 443                        ALLOW IN    Anywhere
# 22 (v6)                    ALLOW IN    Anywhere (v6)
# 80 (v6)                    ALLOW IN    Anywhere (v6)
# 443 (v6)                   ALLOW IN    Anywhere (v6)

Docker can sometimes ignore UFW rules. To tackle this, verify extra settings as explained here.

Step 3: Docker Installation

Install Docker by running these commands:

Setup dependencies and Docker's GPG key:

Terminal
sudo apt install ca-certificates curl gnupg

sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
| sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Add Docker repository:

Terminal
echo \
  "deb [arch=$(dpkg --print-architecture) \
signed-by=/etc/apt/keyrings/docker.gpg] \
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 update

Install Docker Engine and compose-plugin:

Terminal
sudo apt install docker-ce docker-ce-cli \
containerd.io docker-buildx-plugin docker-compose-plugin -y

Check installation:

Terminal
sudo docker run hello-world
# Output:
# Hello from Docker!

Step 4: Install Caddy for Automatic HTTPS

Caddy simplifies HTTPS configuration since it handles SSL certificates automatically from Let's Encrypt.

Install Caddy:

Terminal
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
| sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
| sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update
sudo apt install caddy -y

Before configuring Caddy, you need to point your domain to your server's IP address. If you haven't configured DNS yet, follow these steps:

Configure DNS for Your Domain

According to [bluesky pds official documents]:

  1. Log into your domain registrar's dashboard (where you purchased your domain)
  2. Navigate to the DNS settings or DNS management section
  3. Add an A record with the following settings:
    • Type: A
    • Name: @ (for root domain, e.g. "sliplane.dev")
    • Value/Target: Your server's IPv4 address
  4. Add an A record with the following settings:
    • Type: A
    • Name: * (for all subdomains, e.g. "yulei.sliplane.dev")
    • Value/Target: Your server's IPv4 address

DNS changes can take a few minutes to several hours to propagate. You can check if your DNS is configured correctly using tools like dig or online DNS checkers. Once the DNS record is active, you can proceed with Caddy configuration.

Configure Caddy

Edit the Caddyfile configuration file:

Terminal
sudo nano /etc/caddy/Caddyfile

Enter your domain and configure the reverse proxy. Replace yourdomain.com with your actual domain name. The PDS service listens on port 3000 inside the container, so we proxy to that:

Caddyfile
yourdomain.com {
    reverse_proxy localhost:3000
}

Restart Caddy to load the config:

Terminal
sudo systemctl restart caddy

Step 5: Run bluesky-pds with Docker Compose

We're going to use Docker Compose for easier setup.

First, create a directory for bluesky-pds, then navigate to it:

Terminal
mkdir -p ~/bluesky-pds
cd ~/bluesky-pds

Before adding the compose file, generate the secrets your PDS needs. Run each command below and save its output — you'll need them for PDS_JWT_SECRET, PDS_ADMIN_PASSWORD, and PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX:

Terminal
# JWT secret
openssl rand --hex 16

# Admin password
openssl rand --hex 16

# PLC rotation key (secp256k1 private key)
openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32

Now create a .env file with your domain, email, and the secrets you just generated:

Terminal
sudo nano .env
.env
PDS_HOSTNAME=yourdomain.com
PDS_ADMIN_EMAIL=you@example.com
PDS_JWT_SECRET=<paste first generated value>
PDS_ADMIN_PASSWORD=<paste second generated value>
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<paste third generated value>
PDS_DATA_DIRECTORY=/pds
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
PDS_BLOB_UPLOAD_LIMIT=52428800
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/xrpc/com.atproto.moderation.createReport
PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
PDS_CRAWLERS=https://bsky.network

Now create the compose.yml file:

Terminal
sudo nano compose.yml

Copy/paste the following content into compose.yml:

compose.yml
services:
  pds:
    image: 'ghcr.io/bluesky-social/pds:0.4.5001'
    restart: unless-stopped
    ports:
      - '3000:3000'
    volumes:
      - pds-data:/pds
    env_file:
      - .env
    command: |
      sh -c '
        set -euo pipefail
        echo "Installing required packages and pdsadmin..."
        apk add --no-cache openssl curl bash jq coreutils gnupg util-linux-misc >/dev/null
        curl -o /usr/local/bin/pdsadmin.sh https://raw.githubusercontent.com/bluesky-social/pds/main/pdsadmin.sh
        chmod 700 /usr/local/bin/pdsadmin.sh
        ln -sf /usr/local/bin/pdsadmin.sh /usr/local/bin/pdsadmin
        echo "Creating an empty pds.env file so pdsadmin works..."
        touch ${PDS_DATA_DIRECTORY}/pds.env
        echo "Launching PDS, enjoy!..."
        exec node --enable-source-maps index.js
      '
    healthcheck:
      test:
        - CMD
        - wget
        - '--spider'
        - 'http://127.0.0.1:3000/xrpc/_health'
      interval: 5s
      timeout: 10s
      retries: 10

volumes:
  pds-data:

Start it:

Terminal
sudo docker compose up -d

You can watch the logs to see when it's ready:

Terminal
sudo docker compose logs -f pds

Wait until the logs settle and the healthcheck passes, then press Ctrl+C to exit the logs.

Step 6: Create an Account with pdsadmin

The PDS image ships with pdsadmin, a CLI for managing your server, including creating user accounts. Run it inside the running container:

Terminal
sudo docker exec -it bluesky-pds-pds-1 pdsadmin account create

If your container has a different name, check it first with sudo docker compose ps.

pdsadmin will prompt you for:

  • Email address — used for account recovery
  • Handle — your Bluesky handle, which will resolve to your own domain (e.g. eli.yourdomain.com)
  • It will then generate an invite code and a password for the new account, and print your account's DID (Decentralized Identifier)

Save the handle, password, and DID somewhere safe — you'll need them to log in.

Step 7: Sign in to Bluesky with Your PDS

Open the Bluesky app (or any AT Protocol-compatible client), and instead of signing in with the default bsky.social server, choose the option to use a custom PDS / "Other" server, then enter your domain (yourdomain.com).

Log in using the handle and password generated by pdsadmin in the previous step. If everything is configured correctly, you should land in your Bluesky timeline, now backed by your own server.

Step 8: Verify Your PDS with PDSls

PDSls is a browser-based tool for inspecting AT Protocol data — repos, records, and PDS metadata — directly from the network.

To verify your setup:

  1. Go to pdsls.dev
  2. Enter your handle or DID (the one pdsadmin printed in Step 7) into the search field
  3. Confirm that the resolved PDS endpoint matches your domain
  4. Browse your repo's collections (e.g. app.bsky.feed.post) to confirm your data is being served from your own infrastructure rather than the default Bluesky-hosted PDS

If the endpoint and DID resolve correctly, your self-hosted PDS is live and properly federated with the wider Bluesky network.

Conclusion

You now have your own Bluesky PDS running on your own server, fronted by Caddy with automatic HTTPS, with an account created through pdsadmin, and verified independently via PDSls. Your posts, likes, and follows live on infrastructure you control — if Bluesky the company ever changes direction, shuts down, or simply doesn't fit your needs anymore, your data and identity stay portable across the AT Protocol network.

Welcome to the container cloud

Sliplane makes it simple to deploy containers in the cloud and scale up as you grow. Try it now and get started in minutes!