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

Deploy Bluesky PDS | (Just Updated) Self-Host AT Protocol Server, Nothing to Configure

/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's RAILWAY_PUBLIC_DOMAIN)
  • Private URL: http://pds.railway.internal:8080 (also PDS_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 admin and password $PDS_ADMIN_PASSWORD; account calls use Authorization: Bearer followed by the accessJwt returned by createSession
  • Health: GET /xrpc/_health returns 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 /pds holds 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/_health stops 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_URL and PDS_EMAIL_FROM_ADDRESS if you want verification and reset mails.

Deployment Dependencies

Implementation Details

VariablePurpose
PDS_ADMIN_PASSWORDGenerated password for the admin API (basic auth user admin).
PDS_JWT_SECRETGenerated secret that signs session tokens.
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEXGenerated 64-character hex key that can recover account identities. Back it up.
PDS_HOSTNAMEThe service's public domain; used in handles and DIDs.
PDS_PUBLIC_URLHTTPS address of the server.
PDS_PRIVATE_URLAddress 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

More templates in this category

View Template
Rocky Linux
Hosted Rocky Linux 9 workspace with SSH and persistent storage. 🚀

codestorm
51
View Template
Foundry Virtual Tabletop
A Self-Hosted & Modern Roleplaying Platform

Lucas
71
View Template
Letta Code Remote
Run a Letta Code agent 24/7. No inbound ports, just deploy.

Letta
51