Skip to content

Instantly share code, notes, and snippets.

@urcades
Created December 11, 2025 15:10
Show Gist options
  • Select an option

  • Save urcades/1f72895f7d707952801f8a88b5125aa3 to your computer and use it in GitHub Desktop.

Select an option

Save urcades/1f72895f7d707952801f8a88b5125aa3 to your computer and use it in GitHub Desktop.

PDS on macOS with Cloudflare Tunnel

This guide covers running a Bluesky PDS (Personal Data Server) on macOS using Docker Desktop and Cloudflare Tunnel for public access.

Overview

The standard PDS distribution targets Linux VPS deployments. This setup adapts it for macOS with:

  • Docker Desktop instead of native Docker
  • Cloudflare Tunnel instead of Caddy (handles TLS, no port forwarding needed)
  • ~/pds data directory instead of /pds
  • launchd instead of systemd for service management

Architecture

Internet → Cloudflare Edge → cloudflared tunnel → localhost:3000 → PDS container

Prerequisites

Quick Start

# Install cloudflared
brew install cloudflared

# Run the setup script
./setup.macos.sh

Then configure Cloudflare Tunnel (see below).

Files

File Purpose
compose.macos.yaml Docker Compose for macOS (PDS + Watchtower, no Caddy)
setup.macos.sh Interactive setup script
pdsadmin.macos.sh macOS-compatible admin CLI

Manual Setup

1. Create Data Directory

mkdir -p ~/pds/blocks
chmod 700 ~/pds

2. Generate Secrets and Create Config

cat <<EOF > ~/pds/pds.env
PDS_HOSTNAME=pds.yourdomain.com
PDS_JWT_SECRET=$(openssl rand -hex 16)
PDS_ADMIN_PASSWORD=$(openssl rand -hex 16)
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(openssl ecparam -name secp256k1 -genkey -noout -outform DER 2>/dev/null | tail -c +8 | head -c 32 | xxd -p -c 32)
PDS_DATA_DIRECTORY=/pds
PDS_BLOBSTORE_DISK_LOCATION=/pds/blocks
PDS_BLOB_UPLOAD_LIMIT=104857600
PDS_DID_PLC_URL=https://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
EOF

Save the PDS_ADMIN_PASSWORD value - you'll need it for admin operations.

3. Start PDS Containers

PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml up -d

4. Configure Cloudflare Tunnel

# Login to Cloudflare
cloudflared tunnel login

# Create tunnel
cloudflared tunnel create pds

# Note the tunnel ID (e.g., e435494d-e8f0-4ce4-b107-628f2201ac1f)

Create ~/.cloudflared/config.yml:

tunnel: <YOUR_TUNNEL_ID>
credentials-file: /Users/<username>/.cloudflared/<YOUR_TUNNEL_ID>.json

ingress:
  - hostname: pds.yourdomain.com
    service: http://localhost:3000
  - hostname: "*.pds.yourdomain.com"
    service: http://localhost:3000
  - service: http_status:404

Add DNS routes:

cloudflared tunnel route dns pds pds.yourdomain.com
cloudflared tunnel route dns pds "*.pds.yourdomain.com"

5. Run Tunnel as Background Service

Create ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.cloudflare.cloudflared-pds</string>
    <key>ProgramArguments</key>
    <array>
        <string>/opt/homebrew/bin/cloudflared</string>
        <string>tunnel</string>
        <string>--config</string>
        <string>/Users/<username>/.cloudflared/config.yml</string>
        <string>run</string>
        <string>pds</string>
    </array>
    <key>RunAtLoad</key>
    <true/>
    <key>KeepAlive</key>
    <true/>
    <key>StandardOutPath</key>
    <string>/Users/<username>/Library/Logs/cloudflared-pds.log</string>
    <key>StandardErrorPath</key>
    <string>/Users/<username>/Library/Logs/cloudflared-pds.log</string>
</dict>
</plist>

Load the service:

launchctl load ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist

6. Verify Setup

curl https://pds.yourdomain.com/xrpc/_health
# Should return: {"version":"0.4.xxx"}

7. Create Your Account

./pdsadmin.macos.sh account create [email protected] yourhandle.pds.yourdomain.com

Save the generated password - you'll use it to log in at bsky.app.

Using Shorter Handles (Optional)

To allow handles like username.yourdomain.com instead of username.pds.yourdomain.com:

  1. Add to ~/.cloudflared/config.yml:

    ingress:
      - hostname: pds.yourdomain.com
        service: http://localhost:3000
      - hostname: "*.pds.yourdomain.com"
        service: http://localhost:3000
      - hostname: "*.yourdomain.com"
        service: http://localhost:3000
      - service: http_status:404
  2. Add DNS route:

    cloudflared tunnel route dns pds "*.yourdomain.com"
  3. Add to ~/pds/pds.env:

    PDS_SERVICE_HANDLE_DOMAINS=.yourdomain.com,.pds.yourdomain.com
    
  4. Restart services:

    PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml down
    PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml up -d
    launchctl unload ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist
    launchctl load ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist

Note: Wildcard subdomains (*.yourdomain.com) require either:

  • Cloudflare's free Universal SSL (covers one level: *.yourdomain.com)
  • Cloudflare Advanced Certificate (covers two levels: *.pds.yourdomain.com)

SSL Certificates

Cloudflare's free Universal SSL covers:

  • yourdomain.com
  • *.yourdomain.com (one level of subdomain)

For two-level subdomains like *.pds.yourdomain.com, you need an Advanced Certificate:

  1. Cloudflare Dashboard → SSL/TLS → Edge Certificates
  2. Order Advanced Certificate
  3. Add hostnames: pds.yourdomain.com and *.pds.yourdomain.com

Admin Commands

# List accounts
./pdsadmin.macos.sh account list

# Create account
./pdsadmin.macos.sh account create [email protected] handle.yourdomain.com

# Create invite code (for others to sign up)
./pdsadmin.macos.sh create-invite-code

# Reset password
./pdsadmin.macos.sh account reset-password <did>

# Delete account
./pdsadmin.macos.sh account delete <did>

# Request crawl from relay
./pdsadmin.macos.sh request-crawl

Container Management

# Start
PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml up -d

# Stop
PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml down

# View logs
docker logs -f pds

# Restart PDS only
PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml restart pds

Tunnel Management

# Start service
launchctl load ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist

# Stop service
launchctl unload ~/Library/LaunchAgents/com.cloudflare.cloudflared-pds.plist

# View logs
tail -f ~/Library/Logs/cloudflared-pds.log

# Run manually (for debugging)
cloudflared tunnel run pds

Maintenance

Backups

Periodically backup your data directory:

cp -r ~/pds ~/pds-backup-$(date +%Y%m%d)

Updates

Watchtower automatically updates the PDS container nightly. To manually update:

PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml pull
PDS_DATA_DIR=~/pds docker compose -f compose.macos.yaml up -d

To pin a specific version, edit compose.macos.yaml:

image: ghcr.io/bluesky-social/pds:0.4.136  # instead of :0.4

Monitoring

# Health check
curl -s https://pds.yourdomain.com/xrpc/_health

# Container status
docker ps

# Disk usage
du -sh ~/pds

macOS-Specific Considerations

Prevent Sleep

System Settings → Energy → Prevent automatic sleeping when the display is off

Auto-Start Docker Desktop

Docker Desktop → Settings → General → Start Docker Desktop when you sign in

What Happens When Offline

  • Your profile/posts become temporarily unavailable
  • You can't post or interact
  • When back online, everything resumes automatically
  • No data is lost (stored locally in ~/pds)

Optional: SMTP for Email

To enable email verification and password reset emails, add to ~/pds/pds.env:

PDS_EMAIL_SMTP_URL=smtps://resend:<api-key>@smtp.resend.com:465/
[email protected]

Works with Resend, SendGrid, Mailgun, or any SMTP server. Restart PDS after changes.

Troubleshooting

PDS not accessible

# Check container is running
docker ps | grep pds

# Check tunnel is connected
tail ~/Library/Logs/cloudflared-pds.log

# Test local connection
curl http://localhost:3000/xrpc/_health

Handle verification failing

  • Ensure DNS CNAME records point to tunnel
  • Check SSL certificate covers the subdomain
  • Verify tunnel config includes the hostname

"Not a supported handle domain" error

  • Add domain to PDS_SERVICE_HANDLE_DOMAINS in ~/pds/pds.env
  • Restart PDS with docker compose down && up -d

Security Notes

  • Keep ~/pds/pds.env private (contains admin password and secrets)
  • Invite codes are required to create accounts (not open registration)
  • Cloudflare provides DDoS protection
  • All traffic is encrypted via Cloudflare's TLS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment