Self-Hosting a Bluesky Personal Data Server (PDS)

Build an AT Protocol / BlueSky Personal Data Server in 20 minutes.

Self-Hosting a Bluesky Personal Data Server (PDS)
Photo by Andra C Taylor Jr / Unsplash

The AT Protocol that powers Bluesky is designed for federation—your data doesn't have to live on Bluesky's servers. You can run your own Personal Data Server (PDS), hosting your account on infrastructure you control while remaining fully connected to the broader network.

Self-hosting a PDS means your posts, follows, and profile data live on your server. You own it completely. If Bluesky disappeared tomorrow, your data would still exist, and you could connect to any other AT Protocol network.

This guide covers deploying your own PDS from scratch, without relying on automated migration tools that can fail unpredictably.

What a PDS Does

A Personal Data Server is your agent in the AT Protocol network:

  • Stores your repository: All your posts, likes, follows, and profile data
  • Manages your identity: Holds your signing keys and handles authentication
  • Communicates with the network: Syncs with relays and app views
  • Serves your data: Other servers fetch your content from your PDS

The AT Protocol architecture separates concerns: PDSs store data, Relays aggregate and distribute it, and App Views index and serve it to users. You're replacing only the PDS component—your posts still appear on bsky.app and other clients through the existing infrastructure.

Prerequisites

You'll need:

  • A VPS with at least 1GB RAM (2GB recommended)
  • Ubuntu 24.04
  • A domain name you control
  • SSH access with root privileges
  • Basic Linux administration knowledge

The official installer assumes a fresh server not running other web services on ports 80/443. If you need to share with existing services, you'll need to configure reverse proxying manually.

DNS Configuration

Before installation, configure DNS:

Required:

  • pds.yourdomain.com - A record pointing to your server's IP
  • *.pds.yourdomain.com - Wildcard A record for user handles (same IP)

The wildcard record is necessary because each account on your PDS gets a subdomain handle like user.pds.yourdomain.com.

Verify DNS is working:

dig pds.yourdomain.com A
dig test.pds.yourdomain.com A

Both should return your server's IP address.

Installation

SSH into your server and download the installer:

wget https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh
sudo bash installer.sh

The installer prompts for:

  1. Hostname: Your PDS domain (e.g., pds.yourdomain.com)
  2. Admin email: For Let's Encrypt certificates and notifications
  3. Create initial account: You can create your first user during installation or skip and do it later

The script:

  • Installs Docker and Docker Compose
  • Pulls the PDS container image
  • Configures Caddy for TLS termination
  • Sets up automatic certificate renewal
  • Creates the initial configuration

Installation takes several minutes. When complete, your PDS is running.

Verifying the Installation

Check that services are running:

docker compose -f /pds/compose.yaml ps

You should see containers for the PDS and Caddy (web server) both running.

Test the PDS endpoint:

curl https://pds.yourdomain.com/xrpc/_health

A successful response indicates the PDS is operational.

Creating Accounts

If you didn't create an account during installation, generate an invite code:

sudo /usr/local/bin/pdsadmin create-invite-code

Then create an account through the Bluesky app or web interface using your PDS as the hosting provider:

  1. Go to bsky.app or open the Bluesky app
  2. Choose "Sign up"
  3. Select "Custom" for hosting provider
  4. Enter your PDS URL: https://pds.yourdomain.com
  5. Use the invite code you generated

Configuration

The PDS configuration lives in /pds/pds.env. Key settings:

# View current configuration
sudo cat /pds/pds.env

PDS_HOSTNAME: Your PDS domain (set during installation)

PDS_ADMIN_EMAIL: Administrator contact

PDS_BLOB_UPLOAD_LIMIT: Maximum upload size in bytes (default 52428800 = 50MB)

Some PDS instances increased this to 100MB (104857600) for larger media uploads.

You must have an SMTP server configured to get your 2FA codes and PLC tokens. You can edit the /pds/pds.env file and adding the required information.

PDS_EMAIL_FROM_ADDRESS="test@example.com"
PDS_EMAIL_SMTP_URL="smtp://test@example.com:STRONG-PASSWORD@mail.example.com:587"

After changing configuration:

cd /pds
sudo docker compose down
sudo docker compose up -d

Automatic Updates

Set up automatic PDS updates with a cron job:

sudo crontab -e

Add:

0 3 * * * /usr/local/bin/pdsadmin update

This checks for and applies updates daily at 3 AM.

Using a Custom Handle

By default, accounts get handles like user.pds.yourdomain.com. You can use a custom domain as your handle instead.

If you want the handle @yourdomain.com:

  1. Add a TXT record to yourdomain.com:
    • Name: _atproto
    • Value: did=did:plc:your-did-string

Find your DID in your profile settings or by querying:

curl https://pds.yourdomain.com/xrpc/com.atproto.identity.resolveHandle?handle=currenthandle.pds.yourdomain.com
  1. Update your handle in the Bluesky app settings

The custom handle points to your PDS while displaying your preferred domain.

Manual Account Migration

If you have an existing Bluesky account on bsky.social and want to move it to your PDS, the process requires careful execution. Automated tools like PDSmoover exist but can fail, leaving accounts in broken states. Here's the manual approach using the goat CLI tool.

Install goat

The goat CLI is the official AT Protocol command-line tool:

# Install Go if not present
wget https://go.dev/dl/go1.22.6.linux-amd64.tar.gz
sudo tar -C /usr/local -xvf go1.22.6.linux-amd64.tar.gz
echo "export PATH=$PATH:/usr/local/go/bin" >> ~/.profile
source ~/.profile

# Install goat
go install github.com/bluesky-social/indigo/cmd/goat@latest
echo "export PATH=$PATH:$HOME/go/bin" >> ~/.profile
source ~/.profile

# Verify installation
goat --version

Migration Steps

1. Log into your existing account:

goat account login -u yourusername.bsky.social -p your-app-password

Use an app password, not your main password.

2. Request a PLC operation token:

goat account plc request-token

This sends an email to your account's registered address with a confirmation token.

3. Create an invite code on your PDS:

sudo /usr/local/bin/pdsadmin create-invite-code

4. Perform the migration:

goat account migrate \
    --pds-host https://pds.yourdomain.com \
    --new-handle yourhandle.pds.yourdomain.com \
    --new-password your-new-secure-password \
    --new-email your@email.com \
    --plc-token TOKEN-FROM-EMAIL \
    --invite-code your-invite-code

This:

  • Exports your repository from the old PDS
  • Imports it to your new PDS
  • Updates your DID document to point to your new PDS
  • Signs away the old PDS's authority

Warning: This is a one-way operation. Once your DID document is updated, you cannot return to the old PDS without another migration.

5. Verify the migration:

curl https://plc.directory/did:plc:your-did | jq

The response should show your new PDS as the service endpoint.

6. Request crawl from the relay:

After migration, request the Bluesky relay to crawl your new PDS:

curl -X POST "https://bsky.network/xrpc/com.atproto.sync.requestCrawl" \
  -H "Content-Type: application/json" \
  -d '{"hostname": "pds.yourdomain.com"}'

Federation Limits

Bluesky's relay currently rate-limits PDSs:

  • Maximum 10 accounts per PDS for federation
  • Up to 1,500 events per hour
  • Up to 10,000 events per day

These limits exist because federation is still in early stages. They're intended for developers and self-hosters, not large service providers.

Backup and Recovery

Your PDS data lives in Docker volumes. Regular backups are essential:

# Stop the PDS
cd /pds
sudo docker compose down

# Backup the data directory
sudo tar -czvf pds-backup-$(date +%Y%m%d).tar.gz /pds

# Restart
sudo docker compose up -d

Store backups off-server. Your repository contains your cryptographic signing key—losing it means losing control of your identity.

Troubleshooting

PDS not reachable:

  • Check Docker containers are running
  • Verify DNS resolution
  • Check firewall allows 80/443
  • Review Caddy logs: docker logs pds-caddy-1

Migration failures:

  • Ensure you have the correct PLC token (check email)
  • Verify invite code is valid and unused
  • Check goat version is current
  • Review error messages carefully—they usually indicate the specific failure

Account not appearing in network:

  • Request a crawl from the relay
  • Wait for propagation (can take minutes to hours)
  • Verify DID document is correctly updated

Upload failures:

  • Check PDS_BLOB_UPLOAD_LIMIT in configuration
  • Verify disk space is available

Security Considerations

Your PDS holds your private signing key. Compromise of this key means someone could post as you or take control of your identity.

  • Keep the server updated
  • Use strong passwords for server access
  • Consider firewall rules limiting SSH access
  • Monitor for unauthorized access
  • Maintain encrypted backups stored separately

Footnotes:

[1] AT Protocol Self-Hosting Guide: https://atproto.com/guides/self-hosting
[2] Bluesky PDS Repository: https://github.com/bluesky-social/pds
[3] goat CLI Documentation: https://github.com/bluesky-social/indigo
[4] PDS Account Migration: https://github.com/bluesky-social/pds/blob/main/ACCOUNT_MIGRATION.md