ATProto PDS
Bluesky is built on the open ATProto “atmosphere” network. The Personal Data Server is the network unit that serves as a repository for user data, identity, and media on the network.
Why Self Host?
- Open Social by Dan Abramov
- Protocols, Not Platforms by Mike Masnick
- Information Civics by Paul Frazee
- Social media’s next evolution a conversation about Blacksky with Rudy Fraser
This Site
The PDS is essentially a public database. This site generates some of it’s content from that database. Post Everywhere, Showcase on Own Site?
Resources
- PDS on Github - the software to run your own PDS
- pdsls.dev - handy tool for browsing your PDS
- @me - explore atproto records visually
- pdsadmin-web - a web based tool for performing some basic admin functions
- PDS Moover - migrate accounts between PDS
- ATP airport - PLC recovery keys, migration, backups
- compare hoses - compare atproto relays
- Independent PDS Directory
- did:web - tool to create
did:webaccounts on a self hosted PDS
Setting up the PDS
Running the official PDS Docker container is pretty simple. It’s pretty lightweight, so you don’t need a fancy server to run it. I use the OCI Free Tier for both my server and their object storage (S3). You could definitely do this on something like a Raspberry Pi.
Before starting this process, you’ll want to set up DNS records with your domain provider and give them some time to propagate.
The Github repo has a script you can use to set all this up automatically. You probably just want to do that!
I already have a server and proxy running with some other software, so I just need to add the PDS to my existing Docker Compose stack.
The relevant portion of my Compose file:
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:latest
networks:
- public-proxy
restart: unless-stopped
volumes:
- /home/ubuntu/pds/data:/pds
env_file:
- /home/ubuntu/pds/pds.env
Running the container will create a template pds.env file at that location specified, which you can then fill out with your secrets and SMTP credentials.
OCI S3
I also set up S3 storage, which Bayley Townsend’s howto was helpful for figuring out (also the OCI S3 compatibility doc).
Getting set up with OCI S3 was, of course, not that straightforward:
- Make note of your Region Identifier ex:
us-sanjose-1 - Make a new Object Storage bucket on your account.
- On Bucket Details page, change visibility to Public
- Make note of the Namespace on the Bucket Details page
- Go to your User Profile -> User Settings. Select Tokens and Keys
- Scroll down to Customer secret keys
- Generate a Secret key it will only be shown once
- Back on the previous page, you now need to copy the Access key from
...
Now we have everything we need to add our secrets to our pds.env file:
PDS_BLOBSTORE_S3_BUCKET=<bucket name>
PDS_BLOBSTORE_S3_REGION=<region>
PDS_BLOBSTORE_S3_ENDPOINT=https://<namespace>.objectstorage.<region>.oraclecloud.com
PDS_BLOBSTORE_S3_ACCESS_KEY_ID=<accesskey>
PDS_BLOBSTORE_S3_SECRET_ACCESS_KEY=<secretkey>
PDS_BLOBSTORE_S3_FORCE_PATH_STYLE=true
Once you have done so and started the service you’ll see this message in your browser:
__ __
/\ \__ /\ \__
__ \ \ ,_\ _____ _ __ ___\ \ ,_\ ___
/'__'\ \ \ \/ /\ '__'\/\''__\/ __'\ \ \/ / __'\
/\ \L\.\_\ \ \_\ \ \L\ \ \ \//\ \L\ \ \ \_/\ \L\ \
\ \__/.\_\\ \__\\ \ ,__/\ \_\\ \____/\ \__\ \____/
\/__/\/_/ \/__/ \ \ \/ \/_/ \/___/ \/__/\/___/
\ \_\
\/_/
This is an AT Protocol Personal Data Server (aka, an atproto PDS)...
You should now be able to browse your PDS at it’s URL with pdsls. Here’s mine. If you haven’t set up an account though there won’t be any records.
Creating Accounts
The PDS container doesn’t include pdsadmin tool, so we’ll need another way to make invite codes. You can use pdsadmin-web to do so easily. Before I was aware of that tool, I did an API call via Postman:
- HTTP Request
- Type: POST
- URL:
your.pds.tld/xrpc/com.atproto.server.createInviteCode - Body: raw
{"useCount":1} - Auth: Basic Auth
- User: admin
- Pass: from your pds.env
curl -X POST -u "admin:PASSWORD" -H "Content-Type: application/json" -d '{"useCount": 1}' https://your.pds.tld/xrpc/com.atproto.server.createInviteCode
Once you have the code, you can go to the Bluesky app, enter your PDS and invite, and create an account as normal. Now, moment of truth, make a test post with some media:
You’ll get a 500 error if something goes wrong with uploading an image, meaning S3 isn’t working. Double check values in the pds.env and permissions in your OCI account.
did:web Accounts
By default, accounts are created with did:plc as the Decentralized ID. The Public Ledger of Credentials is owned and operated by Bluesky. If you want to control your DID independently, you can create your account using did:web which binds your identity to a URL you control.
Since this site exists on the web I felt it’s identity should be too, so here was the process of creating did:web:brad.quest:
Process
I had to fiddle with this a bt to get it working. I’m sure I’m doing something wrong here, but it ultimately I got the account created and activated:
- Generate a signing keypair with goat:
goat key generate - Save the secret key securely. Note the public key (without the
did:keyprefix) - Create
did.jsonand place it atyoursite.tld/.well-known/did.json:
{
"@context": [
"https://www.w3.org/ns/did/v1",
"https://w3id.org/security/multikey/v1",
"https://w3id.org/security/suites/secp256k1-2019/v1"
],
"id": "did:web:handle.tld",
"alsoKnownAs": ["at://handle.tld"],
"verificationMethod": [{
"id": "did:web:handle.tld#atproto",
"type": "Multikey",
"controller": "did:web:handle.tld",
"publicKeyMultibase": "PUBLIC_KEY_MULTIBASE"
}],
"service": [{
"id": "#atproto_pds",
"type": "AtprotoPersonalDataServer",
"serviceEndpoint": "https://your.pds.com"
}]
}
- Set up handle resolution (one of):
- HTTP: Create
yoursite.tld/.well-known/atproto-didcontainingdid:web:handle.tld - DNS:
_atproto.handle.tldTXT record containingdid=did:web:handle.tld
- HTTP: Create
- Generate a service auth token (proves you own the DID’s private key):
goat account service-auth-offline \
--atproto-signing-key "SECRET_KEY" \
--lxm com.atproto.server.createAccount \
--iss did:web:handle.tld \
--aud did:web:your.pds.com \
--duration-sec 3600
- Create the account:
goat account create \
--pds-host https://your.pds.com \
--existing-did did:web:handle.tld \
--handle handle.tld \
--email you@you.com \
--password PASSWORD \
--invite-code INVITE_CODE \
--service-auth "TOKEN_FROM_STEP_4"
- Get the PDS-assigned signing key (the PDS generates its own key for the account):
curl -s -H "Authorization: Bearer ACCESS_JWT" https://your.pds.com/xrpc/com.atproto.identity.getRecommendedDidCredentials
- To get the ACCESS_JWT, create a session first:
curl -s -X POST -H "Content-Type: application/json" \
-d '{"identifier":"handle.tld","password":"PASSWORD"}' \
https://your.pds.com/xrpc/com.atproto.server.createSession
- Update
did.jsonwith thepublicKeyMultibasefrom step 6 (strip did:key: prefix). Redeploy. - Activate the account:
curl -s -X POST -H "Authorization: Bearer ACCESS_JWT" https://your.pds.com/xrpc/com.atproto.server.activateAccount
The key from step 1 is used to create the account (proving DID ownership), but the PDS assigns its own signing key in step 6 which must be in the final did.json. This is the part that tripped me up.
Even with everything set up correctly I was stilling getting “Missing PDS” on pdsls. This was confusing because other debugging tools were showing my handle resolving correctly. The issue was my server not sending the did.json as actual application/json MIME type so I needed a quick nginx config tweak.
Migrating Accounts
I used PDS MOOver and it worked perfectly for consolidating 4 accounts on my new PDS. I migrated from Bluesky servers and my own old server. I yolo’ed it and did not follow the precautions, although that seems like a good idea. The process went like this:
Entry 31 - Migrating from the Bluesky PDS to the #BlackSky PDS with PDS MOOver #SharpieVLOG
— dapurplesharpie 🔜 #ATProto_NYC (@sharpiepls.com) Aug 10, 2025 at 4:39 PM
[image or embed]
You receive the final PLC code via email. If you are moving from an old self hosted PDS, double check that your SMTP is working properly on that server before attempting this or you won’t receive the PLC.
Further Decentralization
Clients
You don’t have to rely on Bluesky’s app or moderation policies, there are several alternatives:
Relays
The relay is the part of the atmosphere network that aggregates the records stored in all the individual PDS servers. By default, this is bsky.network but you can use a third-party relay. In my pds.env file I am using:
PDS_CRAWLERS=https://relay.xero.systems,https://relay.upcloud.world,https://bsky.network
More ATProto
Some stuff I’ve tried:
- BookHive - book tracking
- Grain - photography
- Graze - custom feed builder
- Leaflet.pub - long form writing
- recipe.exchange - here is a recipe for Cajun dirty rice
- Smoke Signal - events & RSVPs
- teal.fm - music history ala last.fm
- semble - open research
- Margin - bookmarks and web annotations
- Inspoland - visual bookmarking