Skip to content

Running a PDS

A Colibri community is a set of gated spaces, all authored by the community’s own DID, plus a personal space for every member (see Spaces and Communities). Those spaces have to live on a real PDS, and it has to run the spaces alpha image, ghcr.io/bluesky-social/atproto:pds-spaces-alpha, because com.atproto.space and com.atproto.simplespace don’t exist on a stable PDS release.

PDS_URL in .env is one of the six variables the AppView refuses to boot without. PDS_ADMIN_PASSWORD is separate and optional: without it the AppView still serves reads, and creating or repairing a community’s credentials fails.

The AppView repository ships a compose overlay that adds a spaces-alpha PDS and its own private PLC directory, so nothing a development community mints reaches the public PLC directory:

Terminal window
docker compose -f docker-compose.yml -f docker-compose.pds.yml up

The same PDS and PLC pair, run ephemeral, back pnpm test:integration: see docker-compose.integration.yml.

ghcr.io/bluesky-social/atproto:pds-spaces-alpha is published for amd64 only, so the overlay pins platform: linux/amd64 rather than letting Docker infer it. On an arm64 host, such as an Apple Silicon Mac, that means the PDS runs under emulation. It works, but expect it to be the slowest part of your local stack.

There is no current PLC directory image published anywhere. Every ghcr.io/bluesky-social/atproto:plc-* tag on any registry dates from 2023 and predates the plc_operation format the spaces-alpha PDS emits, so pointing it at one of those fails every account creation with Not a valid operation.

docker/plc in the AppView repository builds @did-plc/server from source instead, the same package atproto’s own dev-env runs against, so this is what upstream tests with even though its published version hasn’t moved since 2023.

That directory keeps no state by default: with no DATABASE_URL its operation log lives in memory, so it starts empty and forgets everything when the container stops. The plain dev overlay gives it a DATABASE_URL instead, so a restart doesn’t orphan every community whose data the PDS still holds. Either way it’s a private directory: a DID minted there doesn’t exist anywhere else, so nothing outside your own stack can resolve it.

docker-compose.pds.yml points the AppView at the local PDS and PLC and picks a test handle domain, so you don’t set these yourself:

PDS_URL=http://pds:3001
PLC_URL=http://plc:2582
COMMUNITY_HANDLE_DOMAIN=test

It also sets JETSTREAM_ENABLED=false, since the AppView only consumes Jetstream for identity and account events on the wider network, and a throwaway local stack has no need for that traffic.

The PDS itself needs PDS_JWT_SECRET and PDS_PLC_ROTATION_KEY in your .env, both development-only variables read by the overlay rather than the AppView. .env.example carries a working default for the JWT secret and the openssl command for the rotation key:

Terminal window
openssl ecparam -name secp256k1 -genkey -noout -outform DER \
| tail -c +8 | head -c 32 | xxd -p -c 32

The PDS runs on the port it publishes, 3001, because it advertises http://localhost:3001 in every DID document it writes, and anything outside the Compose network resolves space hosts out of those documents.

Terminal window
# the PDS is up
curl http://127.0.0.1:3001/xrpc/_health
# so is its PLC directory
curl http://127.0.0.1:2582/_health
  • Spaces: what a space is and how access to one is decided.
  • Communities: the six spaces a community is made of.
  • Self-Hosting: running a PDS for real users, with real TLS and DNS.