Installing an AT Protocol PDS on a Synology NAS

By Alexandre Plennevaux 🇧🇪🇪🇺 (@pixeline.be)
Published:

My (completely biased) opinion is that most people who have a NAS at home own a Synology NAS. I'm one of them, I have a DS716+II

My good old Synology NAS

If you're anything like me, your Synology NAS is already the quiet workhorse humming in a corner somewhere, storing backups, media, half-finished side projects, and who knows what else. When I realized I could run my own AT Protocol Personal Data Server (PDS) on it, I knew I had to try.

Running a PDS on a Synology NAS is great because:

The standard installer.sh script really doesn’t like Synology DSM (or maybe it's the other way around, I'm not very good with understanding relationships!)

Anyways, here is how I got it working.

Why the Standard installer.sh Doesn't Work

The official PDS installer expects a “normal” Linux environment. Synology DSM is not that.

Here are a few issues I ran into:

This guide avoids those problems by doing a manual setup that plays nicely with DSM.

Prerequisites

Before jumping in, make sure you have:

Note: If your ISP gives you a dynamic IP (most do), Synology's built-in DDNS feature is perfect for this. You do not need to buy a domain name.

Part 1: DSM Configuration

Step 1: Enable SSH Access

You will use this to run the commands in the rest of the guide.

Step 2: Install Docker

Step 3: Create Required Directories

SSH into your NAS:

ssh your-username@your-nas-ip
Then create the folder structure:
sudo mkdir -p /volume1/docker/bluesky-pds
sudo mkdir -p /volume1/docker/bluesky-pds/pds-data
sudo mkdir -p /volume1/docker/bluesky-pds/caddy/data
sudo mkdir -p /volume1/docker/bluesky-pds/caddy/etc/caddy
Give yourself permission to work in there (replace your-username with your real DSM username):
sudo chown -R your-username:users /volume1/docker/bluesky-pds

Step 4: Generate Caddy Instance UUID

Caddy needs a UUID. Generate it like this:

cd /volume1/docker/bluesky-pds
uuidgen > caddy/data/caddy/instance.uuid
If uuidgen is missing, use Python:

python3 -c "import uuid; print(uuid.uuid4())" > caddy/data/caddy/instance.uuid

Part 2: Router Configuration

DSM uses ports 80 and 443 for its own web interface, so the idea is:

Step 1: Find Your NAS IP

In DSM, go to Control Panel → Network → Network Interface and note your NAS IP address, for example 192.168.1.100.

Step 2: Port Forwarding on Your Router

In your router web interface, create two port forwarding rules that point to your NAS IP.

Rule 1:

Rule 2:

Fritz!Box users: Go to Internet → Port Sharing → Port Forwarding.

Step 3: DNS Setup

Most home internet connections have a dynamic IP address that changes periodically. If that is your situation (and it probably is), Synology's built-in Dynamic DNS (DDNS) feature is perfect for this.

Option A: Dynamic DNS (Recommended for Most Users)

If your ISP gives you a dynamic IP, use Synology's DDNS service:

If you use Synology's DDNS, you get a hostname like yourname.synology.me. This works perfectly for your PDS. You can also use other DDNS providers (DuckDNS, No-IP, etc.) if you prefer. Synology will automatically update the DNS record whenever your public IP changes. The hostname you create here (e.g., yourname.synology.me) will be your PDS hostname.

Option B: Static IP with Manual DNS

If you have a static IP address (or a domain you manage yourself):

If you want multiple user handles later, add a wildcard:

Part 3: PDS Configuration Files

Step 1: docker-compose.yml

Create the file /volume1/docker/bluesky-pds/docker-compose.yml and put this in it:

services:
  caddy:
    container_name: caddy
    image: caddy:2
    depends_on:
      - pds
    restart: unless-stopped
    extra_hosts:
      # Allow Caddy to reach services exposed on the NAS host (e.g. Kuma on 3001)
      - "host.docker.internal:host-gateway"
    volumes:
      - type: bind
        source: /volume1/docker/bluesky-pds/caddy/data
        target: /data
      - type: bind
        source: /volume1/docker/bluesky-pds/caddy/etc/caddy
        target: /etc/caddy
      - type: bind
        source: /volume1/docker/bluesky-pds/site
        target: /srv/site
    ports:
      - "8080:80"
      - "8443:443"
  pds:
    container_name: pds
    image: ghcr.io/bluesky-social/pds:latest
    restart: unless-stopped
    volumes:
      - type: bind
        source: /volume1/docker/bluesky-pds
        target: /pds
      - type: bind
        source: /volume1/docker/bluesky-pds/pds-data
        target: /pds-data
    env_file:
      - ./pds.env
  pdsdash:
    container_name: pdsdash
    image: nginx:alpine
    restart: unless-stopped
    volumes:
      - type: bind
        source: /volume1/docker/bluesky-pds/pds-dash/dist
        target: /usr/share/nginx/html
      - type: bind
        source: /volume1/docker/bluesky-pds/pds-dash/nginx.conf
        target: /etc/nginx/conf.d/default.conf
  watchtower:
    container_name: watchtower
    image: ghcr.io/nicholas-fedor/watchtower:latest
    volumes:
      - type: bind
        source: /var/run/docker.sock
        target: /var/run/docker.sock
    restart: unless-stopped
    environment:
      WATCHTOWER_CLEANUP: true
      WATCHTOWER_SCHEDULE: "@midnight"

Step 2: pds.env

Create /volume1/docker/bluesky-pds/pds.env with your configuration:

PDS_HOSTNAME=yourname.synology.me
[email protected]
PDS_ADMIN_PASSWORD=YourSecurePassword123!
PDS_JWT_SECRET=your-64-character-hex-secret-here
PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=your-64-character-hex-key-here
PDS_EMAIL_SMTP_URL=smtp://user:[email protected]:587
[email protected]
PDS_DATA_DIRECTORY=/pds-data
PDS_BLOBSTORE_DISK_LOCATION=/pds-data/blobs
PDS_BLOBSTORE_DISK_TMP=/pds-data/tmp
PDS_SQLITE_LOCATION=/pds-data/pds.sqlite
PDS_BLOB_UPLOAD_LIMIT=104857600
PDS_ENABLE_ACCOUNT_REGISTRATION=true
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_LEVEL=info
LOG_ENABLED=true

Replace these values:

Step 3: Caddyfile

Create /volume1/docker/bluesky-pds/caddy/etc/caddy/Caddyfile:

{
    email [email protected]
    on_demand_tls {
        ask http://pds:3000/tls-check
    }
}

yourname.synology.me, *.yourname.synology.me {
    tls {
        on_demand
    }
    encode gzip zstd
    reverse_proxy http://pds:3000
}

hello.yourname.synology.me {
    tls [email protected]
    encode gzip zstd
    reverse_proxy http://pdsdash:80
}

Replace yourname.synology.me with your domain (or your Synology DDNS hostname if you went that route) and [email protected] with your email address.

Part 4: Deployment

Step 1: Start the Services

From your SSH session on the NAS:

cd /volume1/docker/bluesky-pds
sudo docker compose pull
sudo docker compose up -d

Step 2: Check Services

sudo docker compose ps

You should see all containers with status Up.

Step 3: Watch Logs

Check the PDS logs:

sudo docker compose logs -f pds
Press Ctrl+C to exit.

Check Caddy logs:

sudo docker compose logs -f caddy

Step 4: Test the Health Endpoint

From another machine (replace with your actual hostname):

curl -vk --resolve yourname.synology.me:443:YOUR_PUBLIC_IP \
https://yourname.synology.me/xrpc/_health

Or if you want to test without specifying the IP:

curl https://yourname.synology.me/xrpc/_health
You should get a JSON response like:

{"version":"0.4.x"}
You can also open https://yourname.synology.me/ (or your DDNS hostname) in a browser to see the PDS banner.

Part 5: Creating Invites and First User

The goat CLI tool is preinstalled inside the PDS container. All commands here run on the NAS.

Step 1: Create an Invite Code

cd /volume1/docker/bluesky-pds
sudo docker compose exec pds goat pds admin create-invites \
    --count 1 \
    --uses 1

This will output something like:

code: yourname-synology-me-xxxxxx-xxxx
uses: 1
Keep that code somewhere safe.

To create more invite codes in one go:

sudo docker compose exec pds goat pds admin create-invites \
    --count 5 \
    --uses 3

Step 2: Create Your First User

Use your invite code to create your account:

sudo docker compose exec pds goat account create \
    --pds-host https://yourname.synology.me \
    --handle alice.yourname.synology.me \
    --email [email protected] \
    --password 'YourSecurePassword123!' \
    --invite-code yourname-synology-me-xxxxxx-xxxx

Of course, you'll need to replace:

On success, goat will print your DID, which looks like did:plc:something.

Step 3: List Accounts

sudo docker compose exec pds goat account list \
    --pds-host https://yourname.synology.me \
    --admin-password 'YourSecurePassword123!'
Use the admin password you set in pds.env.

Step 4: List Invite Codes

sudo docker compose exec pds goat pds admin list-invites

Troubleshooting

Port Conflicts

If Docker complains about ports, check what is using them:

sudo netstat -tuln | grep -E '8080|8443'

DNS and Certificates

If TLS certificates fail:

sudo docker compose logs caddy

Permission Issues

If you see permission errors, reset ownership and permissions:

sudo chown -R $(whoami):users /volume1/docker/bluesky-pds
sudo chmod -R 755 /volume1/docker/bluesky-pds

Containers Do Not Start

Check logs:

sudo docker compose logs pds
sudo docker compose logs caddy
Restart everything:
sudo docker compose down
sudo docker compose up -d

Maintenance

View Logs

# PDS logs
sudo docker compose logs -f pds

# Caddy logs
sudo docker compose logs -f caddy

# All services
sudo docker compose logs -f

Update PDS

Watchtower will update images regularly, but you can also do it manually:

cd /volume1/docker/bluesky-pds
sudo docker compose pull
sudo docker compose up -d

Backups

Back up your PDS data regularly:

sudo tar -czf /volume1/backups/pds-backup-$(date +%Y%m%d).tar.gz \
/volume1/docker/bluesky-pds/pds-data

Security Tips

Next Steps

Conclusion

Wow! that was quite a mouthful, and honestly, from the length of this, it might look difficult but it really is not. If you got this far, you now have a working Bluesky PDS running on your Synology NAS. No rented VPS, no mystery cloud, just your hardware, your data, and your identity.

It feels good to run your own corner of the network.

Additional Resources