bluesky-pds
- Category: Apps
- Status: 0
- Image: ghcr.io/bluesky-social/pds
- Healthcheck: Yes
- Backups: No
- Email: No
- Tests: No
- SSO: No
Quickstart
-
setup a server with abra and deploy coop-cloud/traefik
-
abra app new bluesky-pds(do not use--secretsyet, see below) -
Generate secrets:
The JWT secret and admin password can be generated automatically:
abra app secret generate YOURAPPDOMAIN pds_jwt_secret v1 abra app secret generate YOURAPPDOMAIN pds_admin_password v1The PLC rotation key is a secp256k1 private key and must be generated manually:
openssl ecparam --name secp256k1 --genkey --noout --outform DER | \ tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32Then store it as a secret:
abra app secret insert YOURAPPDOMAIN pds_plc_rotation_key v1 <THE_KEY_HEX> -
abra app deploy YOURAPPDOMAIN
Verify the PDS is running: curl https://YOURAPPDOMAIN/xrpc/_health
Account management
Create an account on your PDS (use the admin password you stored during secret generation):
abra app run YOURAPPDOMAIN app -- \
goat pds admin account create \
--admin-password YOUR_ADMIN_PASSWORD \
--handle user.YOURAPPDOMAIN \
--email user@example.com \
--password yourpassword
Create an invite code:
abra app run YOURAPPDOMAIN app -- \
goat pds admin create-invites \
--admin-password YOUR_ADMIN_PASSWORD
Usage
Once you've created an account (see above), you can log in with any ATProto-compatible client:
- Open bsky.app (or another client like Graysky, Sky.app, etc.)
- On the login screen, tap Hosting provider (or Custom PDS depending on the client)
- Enter your PDS hostname:
YOURAPPDOMAIN - Log in with the handle and password you used when creating the account
Your handle will be user.YOURAPPDOMAIN by default (a subdomain handle). You
can switch to a custom domain handle — see Handle configuration below.
Handle configuration
User handles on a PDS can work in two ways:
-
Subdomain handles (e.g.
user.pds.example.com): The default. Requires a wildcard DNS record (*.pds.example.com) pointing to your server. TLS is handled automatically by the Caddy sidecar (see below). -
Domain handles (e.g.
user.com): Users can use their own domain as a handle by adding a DNS TXT record at_atproto.user.comwith the valuedid=did:plc:<their-did>. This works without any additional server configuration.
DNS setup
At minimum, create an A record pointing your PDS domain to your server:
pds.example.com A <server-ip>
For subdomain handles, also add a wildcard record:
*.pds.example.com A <server-ip>
TLS architecture (Caddy sidecar)
This recipe uses a Caddy sidecar for TLS instead of letting Traefik terminate
TLS directly. This is needed because Bluesky subdomain handles require TLS
certificates for each user.pds.example.com subdomain, and Traefik cannot issue
on-demand per-subdomain certificates.
The architecture:
- Traefik receives TLS connections on port 443 and does TCP passthrough
(no TLS termination) for traffic matching
DOMAINand*.DOMAIN, forwarding the raw TLS stream to Caddy. - Caddy terminates TLS using on-demand certificates — it automatically obtains a Let's Encrypt certificate for each subdomain the first time a connection arrives, using the TLS-ALPN-01 challenge.
- Caddy reverse proxies the decrypted HTTP traffic to the PDS on port 3000.
This matches how the upstream PDS is
designed to work (it ships with Caddy), adapted for Co-op Cloud's Traefik-based
routing. The PDS exposes a /tls-check endpoint that Caddy consults before
issuing a certificate, preventing abuse.
Note: The first request to a new subdomain handle may take 10-30 seconds while Caddy obtains the TLS certificate from Let's Encrypt. Subsequent requests are instant.
No changes to the Traefik recipe are needed — the TCP passthrough is configured
entirely via deploy labels on the Caddy service in this recipe's compose.yml.
About
A Bluesky PDS (Personal Data Server) is a self-hosted server for ATProto. This is a co-op cloud recipe for a PDS as implemented by bluesky, although other pds implementations exist such as cocoon, tranquil-pds, pegasus and rsky-pds.
❃
recipe maintained by @notplants