Owning your Bluesky identity
The AT Protocol's portability rests on a strict separation between the DID (Decentralized Identifier), the PDS that stores your data, and the network relay. Your DID doc, cryptographically signed by your server, lists the signing keys and the service endpoint holding your repository. Changing your PDS updates that document without modifying the DID itself: your followers see nothing, your profile links remain valid, and your handle stays the same.
As long as you delegate this hosting to bsky.social, portability remains theoretical. Moving it to a VPS you administer makes it real: you control the keys, you set the registration rules, and no external policy decision can deprive you of your post repository.
Benefits of a self-hosted PDS
- Your Bluesky handle becomes a subdomain of your own domain, for example
you.your-domain.com. - Your posts, likes and follows are stored in a cryptographically signed repository on your server — you are the sole owner.
- If bsky.social changes its policies, a migration costs you neither followers nor posts: the AT Protocol transfers the entire repository.
- You manage the list of admitted accounts via an invite code system: a strictly personal instance is possible.
- Watchtower monitors the PDS image and triggers updates automatically, without manual intervention.
- Caddy handles TLS and certificate renewal, including for the wildcard DNS required by handle subdomains.
- Memory usage at rest is around 512 MB: compatible with most entry-level VPS.
- The code is published under the MIT licence at github.com/bluesky-social/pds; no paid enterprise edition exists.
Minimum requirements
A VPS with at least 512 MB of RAM and 1 vCPU is enough for personal use (one to five accounts). Plan for at least 20 GB of SSD storage for media and post history. For an association-scale instance with around ten accounts, 1 GB of RAM and 40 GB of storage provide more headroom.
On the network side, ports 80 and 443 must be reachable from the outside. The PDS maintains a persistent WebSocket connection to the Bluesky network: make sure your firewall does not time out idle long-lived connections.
DNS: a wildcard A record *.your-domain.com pointing to your VPS IP is mandatory. It lets every handle subdomain resolve without a separate DNS entry per account. If your registrar does not support wildcards, manually configuring a handle via a _atproto.your-domain.com TXT record remains possible, but more cumbersome.
Required software: Docker and Docker Compose v2. No other dependency is needed; the official Compose bundles Caddy, Watchtower and the PDS in an isolated internal network.
Step-by-step installation
Clone the official repository
SSH into your VPS and clone the reference repository. The main branch is the stable branch recommended by the maintainers: git clone https://github.com/bluesky-social/pds /opt/pds. Then enter the directory with cd /opt/pds.
Create the environment file
Copy the example file from the repository: cp .env.example .env.
Set at minimum PDS_HOSTNAME (your root domain, without the wildcard, e.g. your-domain.com), PDS_JWT_SECRET and PDS_ADMIN_PASSWORD. Generate strong random values: openssl rand -hex 32 produces a 64-character string suitable for each secret.
Configure the wildcard DNS
In your registrar's or Cloudflare's interface, create an A record *.your-domain.com pointing to your VPS IP address.
Propagation usually takes a few minutes, sometimes up to 24 h. Verify with dig +short test.your-domain.com @1.1.1.1 before moving on: an empty response means the entry is not yet propagated.
Start the services
The official Compose stack starts the PDS, Caddy and Watchtower in a single command: docker compose up -d.
Caddy automatically obtains a TLS certificate for your-domain.com and *.your-domain.com. Follow the logs with docker compose logs -f caddy to confirm the challenge succeeded before creating your first account.
Create the first account
The PDS image exposes an administration command. Create your account: docker compose exec pds /pds/bin/create-account --handle you.your-domain.com --email [email protected] --password <password>.
Open the Bluesky app, choose "Custom server" (address: https://your-domain.com), enter the returned invite code and complete account creation.
Migrate from bsky.social (if applicable)
If you already have an account on bsky.social, the AT Protocol allows a lossless migration. In the Bluesky app: Settings → Manage account → Migrate account. Enter your PDS address (https://your-domain.com) and follow the steps. Your DID is updated to point to your server; your followers have nothing to do — they still see you in their feed.
Verify the installation
Open https://your-domain.com/xrpc/com.atproto.server.describeServer in a browser.
The JSON response confirms your PDS is reachable and that TLS is valid. Then search for @you.your-domain.com in the Bluesky app to verify the handle resolves correctly.
Post-installation configuration
Invite codes. By default, registrations are closed: only accounts created with an invite code can join your PDS. Generate additional codes with docker compose exec pds /pds/bin/create-invite-code. For strictly personal use, this restriction is the recommended configuration.
Monitoring with Watchtower. Watchtower is already in the official Compose stack and monitors the PDS image. As soon as a new version is published to the image registry, it pulls the image and restarts the service, without any action on your part. To receive a notification with each update, add [email protected] to your .env file.
Backups. The data/ directory (configurable via PDS_DATA_DIRECTORY) contains the SQLite database and media. Schedule regular backups: the directory is consistent when cold if you stop the services with docker compose stop before copying it.
Recommended hardening
Add PDS_REGISTRATION_DISABLED=true to .env to require an invite code for any registration. Restart the PDS with docker compose restart pds after the change.
Regularly test restoring your backups by starting an ephemeral PDS against a second directory: an untested backup is not a backup. A corrupted SQLite database with no valid restore means losing all posts.
Common troubleshooting
DNS not yet propagated. If Caddy cannot obtain a certificate, docker compose logs caddy shows failed to obtain certificate. Check wildcard resolution with dig *.your-domain.com @1.1.1.1: an empty response means the DNS entry does not exist yet or has not propagated.
Certificate error (blocked ports). If the DNS record exists but the TLS-ALPN challenge fails, a firewall is likely blocking port 80 or 443. On a VPS with ufw, check ufw status and allow both ports: ufw allow 80/tcp && ufw allow 443/tcp.
DID migration failed. If the Bluesky app returns an error during migration, confirm your PDS responds from the outside (the describeServer endpoint above). The migration contacts your server from bsky.social's servers: a timeout points to a network issue or an invalid certificate.
Handle not resolving. If @you.your-domain.com does not resolve in the Bluesky app, check that the wildcard DNS is propagated and that the /.well-known/atproto-did endpoint on your handle responds with the correct DID (curl https://you.your-domain.com/.well-known/atproto-did).
Going further
A self-hosted PDS fits naturally into an existing Docker infrastructure. The guide on deploying with Caddy covers more advanced multi-service reverse proxy scenarios on the same VPS. If you also want to host your own Git forge, the Forgejo on VPS guide follows the same Compose model. To secure administrative access to your self-hosted services without exposing them directly to the internet, the Headscale/Tailscale on VPS guide offers an alternative to a plain SSH bastion.