Local PDS + Jetstream #
A docker-compose stack for developing against a corvus.run PDS without touching a real atproto network.
What runs #
| Service | Host port | Purpose |
|---|---|---|
PDS (ghcr.io/bluesky-social/pds) |
2585 |
Personal data server. PDS_DEV_MODE=true so OAuth accepts http:// origins — do not reuse this config in production. |
PLC (ghcr.io/did-method-plc/did-method-plc) |
2586 |
Local PLC directory. The PDS resolves and writes did:plc: docs against this instead of the public https://plc.directory, so dev never creates real entries upstream. |
plc-db (postgres:16-alpine) |
(internal) | Postgres for the PLC server. No host port — only the plc service connects. |
Jetstream (ghcr.io/bluesky-social/jetstream) |
6012 (WebSocket) / 6013 (metrics) |
Local firehose stand-in. The indigo relay won't probe private-IP PDSes; Jetstream subscribes directly to the PDS's com.atproto.sync.subscribeRepos. |
Ports are offset from branchline (PDS 2584 / Jetstream 6010) and cozy-corner (PDS 2583) so the stacks can co-exist.
PDS_DID_PLC_URL is set to http://plc:3000 in docker-compose.yaml (not pds.env), so even a stale env file can't aim the dev PDS at the public registry.
First-time setup #
-
Make sure Docker Desktop (or equivalent) is running.
-
Generate
docker/pds.env— the file is gitignored. Sample values (regenerate withopenssl randfor your own dev environment):PDS_HOSTNAME=localhost PDS_PORT=2585 PDS_SERVICE_DID=did:web:localhost PDS_DEV_MODE=true PDS_ADMIN_PASSWORD=<openssl rand -hex 16> PDS_JWT_SECRET=<openssl rand -hex 32> PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=<openssl rand -hex 32> PDS_DATA_DIRECTORY=/pds PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks PDS_BLOB_UPLOAD_LIMIT=52428800 # PDS_DID_PLC_URL is intentionally omitted — docker-compose.yaml pins # it to the local plc service so dev can't hit plc.directory. PDS_BSKY_APP_VIEW_URL=https://api.bsky.app PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app PDS_REPORT_SERVICE_URL=https://mod.bsky.app PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac PDS_CRAWLERS=https://bsky.network LOG_ENABLED=true -
Run
./docker/seed.sh. This tears down any previous stack (destroying IDBs), brings it back up, waits for liveness, and creates two test accounts:corvus-alice.test/alice-password-123corvus-bob.test/bob-password-123
Using it #
After ./docker/seed.sh provisions PDS accounts:
# Boot the corvus server in a separate terminal. The `dev:server` root
# script wires every endpoint at this stack — jetstream (6012), PDS (2585,
# also the handle resolver), and PLC (2586) — plus OAuth in loopback mode
# with `--oauth-allow-http`. Ops land in ./data/ops.db.
pnpm dev:server
Override individual endpoints via env vars (CORVUS_JETSTREAM_URL,
CORVUS_HANDLE_RESOLVER, CORVUS_PLC_DIRECTORY_URL, …) or pass the
corresponding CLI flags. Drop all of them and the server falls back to
bsky.network defaults — what you'd want for a production deploy.
- Editor:
pnpm --filter @corvus/editor dev→ http://127.0.0.1:5174. The editor is currently local-only (IndexedDB), no PDS sign-in required. - Reset:
./docker/seed.shagain — it tears down and recreates PDS accounts from scratch. - Stop:
docker compose -f docker/docker-compose.yaml down(without-v, data persists). Stop the corvus server withCtrl-C.
Why PDS_DEV_MODE=true #
PDS 0.4.x refuses to boot against http:// OAuth endpoints unless dev mode is on — the resource-URL validation enforces https otherwise. PDS_DEV_MODE=true relaxes that for localhost development and must stay off in production.