Deploy Bluesky PDS | (Just Updated) Self-Host AT Protocol Server, Nothing to Configure
Bluesky PDS self-hosted AT Protocol server, accounts and blobs on a volume
Just deployed
/pds
Deploy and Host Bluesky PDS on Railway
Built for an LLM agent to deploy and use. The deploy form has no fields. A Bluesky Personal Data Server (the AT Protocol server that stores your accounts, posts and media) comes up with a generated admin password, a volume that keeps every account across redeploys, and a public HTTPS address. The contract below lists every address, variable name and one script that creates an account and publishes a post.
Bluesky PDS here is the official ghcr.io/bluesky-social/pds image, version 0.4.5051, the open-source server that lets you host your own Bluesky or AT Protocol account instead of living on someone else's. It follows the same public protocol as bsky.social, so the app, feeds and other servers can read your account.
About Hosting Bluesky PDS
This template runs one service from a wrapper image pinned by digest. The wrapper makes the PDS listen on Railway's port, takes its hostname from the service's public domain, and keeps a stable PLC rotation key (the key that can recover your account identity), so nothing needs to be typed in. Accounts, repositories and uploaded blobs live on the volume mounted at /pds. The admin password PDS_ADMIN_PASSWORD and the token-signing secret PDS_JWT_SECRET are generated at deploy time. Account creation is invite-only; an agent mints invite codes through the admin API.
New handles default to subdomains of the server hostname (NAME.YOUR-RAILWAY-DOMAIN). A Railway domain has no wildcard DNS, so those subdomains do not resolve on their own; the account works on this server, but for handles that other servers and the Bluesky app can resolve, attach a custom domain and use handles on a domain you own (a _atproto TXT record or /.well-known/atproto-did on that domain).
Creating an account registers its DID with the public PLC directory, and the image announces the server to bsky.network so the relay can crawl it. That is the normal behaviour of every PDS.
Agent contract
- Public URL:
PDS_PUBLIC_URL(https://plus the service'sRAILWAY_PUBLIC_DOMAIN) - Private URL:
http://pds.railway.internal:8080(alsoPDS_PRIVATE_URL) - Credentials:
PDS_ADMIN_PASSWORD(generated, 32 characters); account passwords are chosen by whoever creates the account - Auth: admin calls use HTTP basic auth with user
adminand password$PDS_ADMIN_PASSWORD; account calls useAuthorization: Bearerfollowed by theaccessJwtreturned bycreateSession - Health:
GET /xrpc/_healthreturns 200 with{"version":"0.4.5051"}
# needs: PDS_PUBLIC_URL and PDS_ADMIN_PASSWORD from the pds service variables
HOST="${PDS_PUBLIC_URL#https://}"
curl -s "$PDS_PUBLIC_URL/xrpc/_health"; echo
curl -s -o /dev/null -w "no admin auth: %{http_code}\n" -X POST -H 'content-type: application/json' -d '{"useCount":1}' "$PDS_PUBLIC_URL/xrpc/com.atproto.server.createInviteCode"
CODE=$(curl -s -u "admin:$PDS_ADMIN_PASSWORD" -X POST -H 'content-type: application/json' -d '{"useCount":1}' "$PDS_PUBLIC_URL/xrpc/com.atproto.server.createInviteCode" | sed 's/.*"code":"\([^"]*\)".*/\1/')
curl -s -X POST -H 'content-type: application/json' -d "{\"email\":\"agent@example.com\",\"handle\":\"agent.$HOST\",\"password\":\"agent-password-1\",\"inviteCode\":\"$CODE\"}" "$PDS_PUBLIC_URL/xrpc/com.atproto.server.createAccount" | sed 's/"\(accessJwt\|refreshJwt\)":"[^"]*"/"\1":"..."/g'; echo
JWT=$(curl -s -X POST -H 'content-type: application/json' -d "{\"identifier\":\"agent.$HOST\",\"password\":\"agent-password-1\"}" "$PDS_PUBLIC_URL/xrpc/com.atproto.server.createSession" | sed 's/.*"accessJwt":"\([^"]*\)".*/\1/')
curl -s -X POST -H "authorization: Bearer $JWT" -H 'content-type: application/json' -d "{\"repo\":\"agent.$HOST\",\"collection\":\"app.bsky.feed.post\",\"record\":{\"\$type\":\"app.bsky.feed.post\",\"text\":\"hello from my own PDS\",\"createdAt\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}}" "$PDS_PUBLIC_URL/xrpc/com.atproto.repo.createRecord"; echo
curl -s -G --data-urlencode "repo=agent.$HOST" --data-urlencode "collection=app.bsky.feed.post" "$PDS_PUBLIC_URL/xrpc/com.atproto.repo.listRecords"; echo
A fresh deploy prints {"version":"0.4.5051"}, then no admin auth: 401, then the new account (handle, did:plc:..., and its DID document), then the new record's at:// URI with "validationStatus":"valid", and finally the record listed back with the text hello from my own PDS. The script creates a real account and registers its DID in the public PLC directory; delete the account afterwards with com.atproto.server.deleteAccount if you do not want to keep it.
Why Deploy Bluesky PDS on Railway?
Railway is a singular platform to deploy your infrastructure stack. Railway will host your infrastructure so you don't have to deal with configuration, while allowing you to vertically and horizontally scale it.
By deploying Bluesky PDS on Railway, you are one step closer to supporting a complete full-stack application with minimal burden. Host your servers, databases, AI agents, and more on Railway.
- Agent-ready - no deployer fields, a contract block and one working create-account-and-post script.
- Nothing to configure - the admin password, JWT secret and rotation key are generated, the hostname comes from the Railway domain.
- Persistent - a volume at
/pdsholds accounts and blobs, the rotation key stays stable across redeploys, and the image is pinned by digest. - Health-checked - Railway restarts the server if
/xrpc/_healthstops answering.
Common Use Cases
- Own your Bluesky identity - host your account on a server you control and move it between servers without losing followers.
- Agent and bot accounts - give an AI agent its own AT Protocol repository and post to the network through the standard API.
- Community or organisation PDS - host accounts for a team or a project under your own domain.
- AT Protocol development - run a real PDS to test feeds, labelers and clients against.
Dependencies for Bluesky PDS Hosting
- None. Email is optional; set
PDS_EMAIL_SMTP_URLandPDS_EMAIL_FROM_ADDRESSif you want verification and reset mails.
Deployment Dependencies
Implementation Details
| Variable | Purpose |
|---|---|
PDS_ADMIN_PASSWORD | Generated password for the admin API (basic auth user admin). |
PDS_JWT_SECRET | Generated secret that signs session tokens. |
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX | Generated 64-character hex key that can recover account identities. Back it up. |
PDS_HOSTNAME | The service's public domain; used in handles and DIDs. |
PDS_PUBLIC_URL | HTTPS address of the server. |
PDS_PRIVATE_URL | Address other services in the project use. |
The PDS listens on Railway's PORT (8080). Changing PDS_HOSTNAME after accounts exist changes the issuer their sessions were signed for; attach a custom domain before creating accounts you want to keep.
Template Content
