Skip to content

Latest commit

 

History

History
297 lines (213 loc) · 8.89 KB

File metadata and controls

297 lines (213 loc) · 8.89 KB

Deployment

Production Deployment (Docker)

Prerequisites

  • Docker and Docker Compose
  • A domain with DNS control
  • An SMTP provider (e.g. Resend, SendGrid, AWS SES, Postmark)

DNS Setup

Point the following records at your server:

Record Target
pds.example.com your server IP
auth.pds.example.com your server IP
*.pds.example.com your server IP (for handle resolution)

Configuration

Run the setup script to generate .env and packages/demo/.env with all secrets auto-generated:

./scripts/setup.sh

See configuration.md for how env vars work across deployment contexts and a full variable reference.

Build and Start

# Build images (stamps the ePDS version automatically)
pnpm docker:build

# Start services
docker compose up -d

# View logs
docker compose logs -f

Caddy handles TLS automatically via ACME/Let's Encrypt.

Updating

pnpm docker:build
docker compose up -d

Note: docker compose restart does not pick up .env changes. Always use docker compose up -d to recreate containers after changing environment variables.

Railway Deployment

Railway is the primary cloud deployment target. Each package has a railway.toml that configures its build.

Prerequisites

  • A Railway account (Pro plan recommended for volume size)
  • The Railway CLI installed and logged in
  • DNS control for your domain

Project Setup

  1. Create a new Railway project and link it to your GitHub repo:

    railway init
    railway link
  2. Create three services in the Railway dashboard (or via CLI), one per package:

    • @certified-app/pds-core
    • @certified-app/auth-service
    • @certified-app/demo
  3. Link each package directory to its service:

    cd packages/pds-core && railway link
    cd packages/auth-service && railway link
    cd packages/demo && railway link

Volumes

Both pds-core and auth-service need persistent volumes for their SQLite databases. Create them from each package directory:

cd packages/pds-core
railway volume add --mount-path /data

cd packages/auth-service
railway volume add --mount-path /data

The demo app is stateless and does not need a volume.

Important: The mount path must match the env vars:

  • pds-core: PDS_DATA_DIRECTORY=/data (also stores blobs at /data/blobs)
  • auth-service: DB_LOCATION=/data/epds.sqlite

Environment Variables

Run the setup script to generate .env files, then paste the values into each service's raw environment editor in the Railway dashboard:

./scripts/setup.sh

# Strip comments and blank lines for pasting into Railway:
grep -v '^\s*#' packages/pds-core/.env | grep -v '^\s*$'
grep -v '^\s*#' packages/auth-service/.env | grep -v '^\s*$'
grep -v '^\s*#' packages/demo/.env | grep -v '^\s*$'

Important: The setup script sets PDS_INTERNAL_URL=http://core:3000 (the Docker Compose service name). For Railway, you must update this in the auth-service to the pds-core service's Railway internal URL. Find it via:

railway link -s '@certified-app/pds-core'
railway variables --json | python3 -c "import sys,json; print(json.load(sys.stdin)['RAILWAY_PRIVATE_DOMAIN'])"

Then update it on the auth-service:

railway link -s '@certified-app/auth-service'
railway variables set PDS_INTERNAL_URL=http://<private-domain>:3000

Without a correct PDS_INTERNAL_URL, the auth service will crash at startup if the value is missing, or log warnings and fail on auth→PDS calls at runtime if it points to the wrong host.

DNS Setup

Point the following records at Railway's DNS target (shown in the dashboard under each service's custom domain settings):

Record Service
pds.example.com pds-core
auth.pds.example.com auth-service
*.pds.example.com pds-core
demo.example.com demo

Railway handles TLS automatically.

Deploying

Railway deploys automatically on push to the linked branch. Each service's railway.toml defines root-anchored watchPatterns (leading /) so only relevant changes trigger a rebuild, even though the config files live under packages/*/.

To manually redeploy:

cd packages/pds-core && railway redeploy --yes
cd packages/auth-service && railway redeploy --yes
cd packages/demo && railway redeploy --yes

Viewing Logs

cd packages/pds-core && railway logs --deployment
cd packages/auth-service && railway logs --deployment
cd packages/demo && railway logs --deployment

Railway-Specific Notes

  • Each service reads only its own per-package .env — the top-level .env is not used on Railway.
  • The demo app runs Next.js with output: "standalone" in production. If you see a warning about this, ensure the start command uses node .next/standalone/server.js instead of next start.
  • Volumes cannot be configured via railway.toml — they must be created through the dashboard or CLI.
  • Services with volumes have brief downtime during redeployment (Railway cannot mount the same volume to two containers simultaneously).

Service Ports

Service Internal port Public
PDS Core 3000 via Caddy (443)
Auth Service 3001 via Caddy (443)
Caddy 80, 443 yes

Generating Secrets

# PLC rotation key (secp256k1)
openssl ecparam -name secp256k1 -genkey -noout | \
  openssl ec -text -noout 2>/dev/null | \
  grep priv -A 3 | tail -n +2 | tr -d '[:space:]:'

# Generic secrets (JWT, DPOP, HMAC keys, etc.)
openssl rand -hex 32

Invite Codes

The AT Protocol PDS requires invite codes for account creation by default (PDS_INVITE_REQUIRED=true). You have two options:

Option 1: Create an invite code (recommended for production)

Once the PDS is running, generate a high-useCount invite code via the admin API and set it as EPDS_INVITE_CODE on the pds-core service:

curl -X POST https://$PDS_HOSTNAME/xrpc/com.atproto.server.createInviteCode \
  -H "Content-Type: application/json" \
  -u "admin:$PDS_ADMIN_PASSWORD" \
  -d '{"useCount": 9999999}'

The response contains the invite code:

{ "code": "your-pds-hostname-xxxxx-xxxxx" }

Set it as EPDS_INVITE_CODE on pds-core (Railway example):

railway variable set EPDS_INVITE_CODE=<code> -s '@certified-app/pds-core'

Option 2: Disable invite codes (simpler, less secure)

For test or development environments, you can disable the invite code requirement entirely by setting PDS_INVITE_REQUIRED=false on the pds-core service. This allows anyone who can reach the PDS to create accounts, so it is not recommended for production.

Backfilling Email Confirmation

Accounts created before email confirmation was recorded at sign-up report email_verified: false to relying parties. New accounts are recorded as confirmed automatically, and existing accounts are repaired the next time their owner signs in through the emailed-code flow — repair requires a sign-in that proves control of the address, so an authentication method that does not (a passkey, say) leaves the account unconfirmed. This backfill is therefore needed for accounts whose owners have not signed in since the upgrade, and for any that sign in only by other means.

Preview first:

pnpm --filter @certified-app/pds-core backfill:email-confirmed --dry-run

Then run it for real:

pnpm --filter @certified-app/pds-core backfill:email-confirmed

Run it with the same environment as the PDS service, so it targets that deployment's account database. It is idempotent — already-confirmed accounts are skipped, so re-running is safe.

A trailing argument scopes the run to addresses containing it (case-insensitive), which lets you work through a large deployment in batches or repair a single account:

pnpm --filter @certified-app/pds-core backfill:email-confirmed @gmail.com
pnpm --filter @certified-app/pds-core backfill:email-confirmed my.account@yahoo.com

With no argument, every account is considered.

Accounts that cannot be confirmed are listed by DID at the end and the command exits non-zero, so a scripted run does not report success after a partial failure.

This is deliberately not automatic. It marks every account that has an email address but no confirmation timestamp. ePDS does not block upstream's com.atproto.server.createAccount XRPC route, so if you provisioned any accounts outside the normal sign-in flow, their addresses would be marked confirmed without having been verified. Check the dry-run count before committing.