# 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](https://github.com/bluesky-social/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: ```bash 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: ```bash openssl ecparam --name secp256k1 --genkey --noout --outform DER | \ tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32 ``` Then store it as a secret: ```bash abra app secret insert YOURAPPDOMAIN pds_plc_rotation_key v1 ``` ## Account management Create an account on your PDS: ```bash 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: ```bash 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:`. 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](https://github.com/bluesky-social/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 ``` For subdomain handles, also add a wildcard record: ``` *.pds.example.com A ``` [`abra`]: https://git.coopcloud.tech/coop-cloud/abra [`coop-cloud/traefik`]: https://git.coopcloud.tech/coop-cloud/traefik