README.md

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 #

  1. Make sure Docker Desktop (or equivalent) is running.

  2. Generate docker/pds.env — the file is gitignored. Sample values (regenerate with openssl rand for 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
    
  3. 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-123
    • corvus-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.sh again — 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 with Ctrl-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.