A developer's journey through setting up a custom Personal Data Server for BlueSky, complete with all the wrong turns, learnings, and eventual success.
The `why?` & first attempt
We wanted a custom domain handle from the moment we knew it was a possibility.
In true dev fashion, we found the official documentation at atproto.com, located some commands, and proceeded to execute them.
First Issue: Stuck at 99%
Hmm, why? Well, we started a fresh Ubuntu 22.04 Docker container, and the documentation indicated installer support. What happened? Dockerception - or, a conflict within containers.
Reality Check: Understanding the Installer
Back to the documentation. What does the installer *actually* does? Ah, it attempts to launch *its own* Docker container. This approach wasn't compatible with our existing container environment.
Our first challenge: we're using a RHEL-based OS, so the installer commands aren't directly supported. We had to understand each step, realizing we could:
- Clone the repository
- Configure the
pds.env
file - Run Docker manually
... and celebrate!
Back to 99% - Nginx Configuration
Still stuck at 99%? What's the issue? Ah, it's also trying to use Caddy for SSL and proxy management. But we already utilize Nginx. An easy fix: examine the compose.yaml
file, make the necessary adjustments - leaving only the main PDS service, and run docker up
! We finally had a working container!
All was well. We created a pds.insights.blue
subdomain, configured SSL, updated Nginx to route traffic to the PDS container, and enabled the necessary WebSocket connections. This part was crucial!
# PDS proxy configuration
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# Standard proxy headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts for long-running connections
proxy_read_timeout 86400;
proxy_send_timeout 86400;
# Buffer settings
proxy_buffers 8 32k;
proxy_buffer_size 64k;
}
Step 1 complete.
Step 2: DNS and AI Mishaps
Step 2: configure the _atproto
DNS entries. Should we follow the official documentation? Nope, we're in the age of AI. We asked Claude and received JSON values for .well-known/atproto-did
.
We even consulted Google Gemini 2 for a second opinion. It provided the *exact* same instructions. Two respected AIs in agreement? Surely that must be correct?
Of course not! Back to the documentation. We discovered the invaluable debug tool at bsky-debug.app/handle, and read through the resources. It turns out, the file needs to be a plain text file containing *only* the DID value. Bad AI, bad. And a bad developer for taking shortcuts!
Pro tip: Use bsky-debug.app/handle to verify your setup!
Successful PDS Setup
We utilized the debug tool to confirm our DNS and verification files were working correctly. Our PDS container's health check at https://pds.insights.blue/xrpc/_health returned a positive response, and we verified our WebSocket connection with https://pds.insights.blue/xrpc/com.atproto.sync.subscribeRepos?cursor=0.
Next, we ran the pdsadmin
commands to generate our DID, update our DNS records, configure the verification files, and generate our invitation code. This process went smoothly.
Relief washed over us—the setup was working!
Handle Registration
By the way, why did we choose the subdomain route? Our main domain already runs in a container with Deno, making it theoretically possible to install the PDS there. However, we wanted to keep services isolated and there was no guarantee it would have worked anyway.
We returned to bsky.app to register…and failed. We couldn't register the handle we wanted, handle.insights.blue
, despite having valid DNS and verification records.
It turned out, after consulting the documentation *again*… bad devs! :) - we needed to register initially using the PDS subdomain. Only after completing the handle registration can the handle be changed to the main domain!
Final Hurdle: Email Validation
One last hurdle: handle validation through email. This time, we consulted the documentation *first*, configured the necessary pds.env
variables, restarted, validated the email, and achieved success!
PDS_EMAIL_SMTP_URL=smtp://user:password@smtp.domain.com:587
PDS_EMAIL_FROM_ADDRESS=user@domain.com
Great Success!
What did we learn?
- This setup can be used in any environment as long as Docker can be run, and SSL/proxy management is handled.
- The official documentation is valuable, but its tutorial focuses on supported OSs for the installer only.
- Fortunately, we found inspiration in their main repository at github.com/bluesky-social/pds, as well as some excellent resources at cprimozic.net.
We are now using it for our basic domain handle, but the AT Protocol offers much more, and we are eager to start exploring and experimenting!
Ready to Track Your Growth?
Join our waitlist to get early access to comprehensive BlueSky analytics
Join Waitlist