Files
bluesky-pds/README.md
2026-02-21 23:48:21 -05:00

4.2 KiB

bluesky-pds

  • Category: Apps
  • Status: 0
  • Image: ghcr.io/bluesky-social/pds
  • Healthcheck: Yes
  • Backups: No
  • Email: No
  • Tests: No
  • SSO: No

About

A Bluesky PDS (Personal Data Server) is a self-hosted server for the AT Protocol, allowing you to own your social data and federate with the Bluesky network.

Basic usage

  1. Set up Docker Swarm and abra
  2. Deploy coop-cloud/traefik
  3. abra app new bluesky-pds (do not use --secrets yet, see below)
  4. abra app config YOURAPPDOMAIN - set DOMAIN to something that resolves to your Docker swarm box
  5. Generate the PLC rotation key and create secrets (see below)
  6. abra app deploy YOURAPPDOMAIN
  7. Verify the PDS is running: curl https://YOURAPPDOMAIN/xrpc/_health

Generating 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 v1

The 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 32

Then store it as a secret:

abra app secret insert YOURAPPDOMAIN pds_plc_rotation_key v1 <THE_KEY_HEX>

Account management

Create an account on your PDS:

abra app run YOURAPPDOMAIN app -- \
  goat pds admin account create \
    --admin-password "$(abra app secret get YOURAPPDOMAIN pds_admin_password v1)" \
    --handle user.YOURAPPDOMAIN \
    --email user@example.com \
    --password yourpassword

Create an invite code:

abra app run YOURAPPDOMAIN app -- \
  goat pds admin account create-invite \
    --admin-password "$(abra app secret get YOURAPPDOMAIN pds_admin_password v1)"

Handle configuration

User handles on a PDS can work in two ways:

  1. 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).

  2. Domain handles (e.g. user.com): Users can use their own domain as a handle by adding a DNS TXT record at _atproto.user.com with the value did=did:plc:<their-did>. This works without any additional server configuration.

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:

  1. Traefik receives TLS connections on port 443 and does TCP passthrough (no TLS termination) for traffic matching DOMAIN and *.DOMAIN, forwarding the raw TLS stream to Caddy.
  2. 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.
  3. 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.

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>