
Self-hosting Bluesky PDS from Scratch
Yulei ChenBluesky 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:
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:
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:
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:
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:
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:
sudo apt install docker-ce docker-ce-cli \
containerd.io docker-buildx-plugin docker-compose-plugin -y
Check installation:
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:
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]:
- Log into your domain registrar's dashboard (where you purchased your domain)
- Navigate to the DNS settings or DNS management section
- 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
- Type:
- 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
- Type:
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:
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:
yourdomain.com {
reverse_proxy localhost:3000
}
Restart Caddy to load the config:
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:
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:
# 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:
sudo nano .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:
sudo nano compose.yml
Copy/paste the following content into 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:
sudo docker compose up -d
You can watch the logs to see when it's ready:
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:
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:
- Go to pdsls.dev
- Enter your handle or DID (the one
pdsadminprinted in Step 7) into the search field - Confirm that the resolved PDS endpoint matches your domain
- 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.