To get started, I followed Casey's excellent post. I experienced a moment of wonder when things almost just worked. In total it took me about 30 minutes until my Personal Data Store was running on my own server. That's really a testament to the ATproto developers' dedication. Thank you!
There were a couple of things I needed to tweak further, but for completeness' sake, I'll start from the beginning.
The setup
I have a personal webpage running at https://j23n.com. These are just plain HTML files served by nginx
. I want nginx
to forward the relevant requests to my PDS but keep my static page as-is.
Installing the PDS
The full instructions are found here.
Installation is as easy as getting the install script and executing it:
curl https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh >installer.sh
sudo bash installer.sh
It's going to install fine but then eventually, when it asks you to create a user account, it's going to error out
curl: (22) The requested URL returned error: 404
That's because the installer creates a docker compose
file like this
version: '3.9'
services:
caddy:
container_name: caddy
image: caddy:2
network_mode: host
depends_on:
- pds
restart: unless-stopped
volumes:
[...]
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:0.4
network_mode: host
restart: unless-stopped
volumes:
[...]
env_file:
- /pds/pds.env
watchtower:
container_name: watchtower
image: containrrr/watchtower:latest
network_mode: host
volumes:
[...]
restart: unless-stopped
environment:
[...]
where caddy
is acting as a reverse-proxy that tries to bind to ports 80 and 443 of the host. But that's where nginx
is already bound. Our PDS is unreachable and the attempt to create a user account fails. Let's fix that.
First, stop the running docker images
cd /pds && docker compose down
Docker compose file
We'll use our existing nginx
to forward requests to our PDS container, so we don't need caddy
. Watchtower is a neat service that will auto-update our running docker images (at midnight) so we're always up to date.
version: '3.9'
services:
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:0.4
restart: unless-stopped
volumes:
- type: bind
source: /pds
target: /pds
ports:
- '6010:3000'
env_file:
- /pds/pds.env
watchtower:
container_name: watchtower
image: containrrr/watchtower:latest
network_mode: host
volumes:
- type: bind
source: /var/run/docker.sock
target: /var/run/docker.sock
restart: unless-stopped
environment:
WATCHTOWER_CLEANUP: true
WATCHTOWER_SCHEDULE: "@midnight"
Awesome, let's start it.
cd /pds && docker compose up -d
Double check that everything is as it should be by checking the logs
root@j23n.com:/pds# docker logs watchtower
time="2025-09-28T10:21:17Z" level=info msg="Watchtower 1.7.1"
time="2025-09-28T10:21:17Z" level=info msg="Using no notifications"
time="2025-09-28T10:21:17Z" level=info msg="Checking all containers (except explicitly disabled with label)"
time="2025-09-28T10:21:17Z" level=info msg="Scheduling first run: 2025-09-29 00:00:00 +0000 UTC"
time="2025-09-28T10:21:17Z" level=info msg="Note that the first check will be performed in 13 hours, 38 minutes, 42 seconds"
root@j23n.com:/pds# docker logs pds
{"level":30,"time":1759054870607,"pid":7,"hostname":"b9c1c219868b","name":"pds","msg":"pds has started"}
Everything seems to be working. Lovely. Next up, nginx
!
Nginx configuration
Now, we need to connect nginx
to our PDS container. There are a few routes that need forwarding
/.well-known/atproto-did
- this is where your DID lives/xrpc
- this is the main API appviews (websites) will use to interact with you PDS
There are also a bunch of oauth
routes that need forwarding if you want to e.g. sign into leaflet using your self-hosted PDS
/.well-known/oauth-protected-resource
/.well-known/oauth-authorization-server
/oauth
/@atproto
Putting all of that together, your nginx config should look something like this
server {
# You server configuration
[...]
location /xrpc {
proxy_pass http://localhost:6010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location ~ ^/(\.well-known/(atproto-did|oauth-protected-resource|oauth-authorization-server)|oauth|@atproto) {
proxy_pass http://localhost:6010;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
Let's make sure it works
nginx -c /etc/nginx/nginx.conf -t
And then restart nginx
systemctl restart nginx
Let's try creating a user account again
pdsadmin account create mail@j23n.com hello.j23n.com
Account created successfully!
-----------------------------
Handle : hello.j23n.com
DID : did:plc:enau5rzvrui4fx4dq5icgtle
Password : <password>
-----------------------------
Save this password, it will not be displayed again.
Progress!
However, when I tried to log into leaflet, I got a cryptic error about authentication (that I failed to screenshot). Digging into the it:
curl https://hello.j23n.com/oauth
curl: (60) SSL: no alternative certificate subject name matches target host name 'hello.j23n.com'
SSL certificate
I had create an SSL certificate with certbot
for j23n.com and www.j23n.com but not included a wildcard. Never fear, we'll update the certificate
certbot -d *.j23n.com -d j23n.com --preferred-challenge dns --manual certonly
I had to use the DNS challenge, which meant adding a TXT entry to my DNS records, the certbot
output explains that well.
Requesting a certificate for *.j23n.com and j23n.com
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name:
_acme-challenge.j23n.com.
with the following value:
DMYlroz8d6r_CNLG9iz9AhD-xTwfMImolbdKZkWA3Bg
Before continuing, verify the TXT record has been deployed. Depending on the DNS
provider, this may take some time, from a few seconds to multiple minutes. You can
check if it has finished deploying with aid of online tools, such as the Google
Admin Toolbox: https://toolbox.googleapps.com/apps/dig/#TXT/_acme-challenge.j23n.com.
Look for one or more bolded line(s) below the line ';ANSWER'. It should show the
value(s) you've just added.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
My DNS settings propagate super fast (thanks Porkbun?) so no hold up there.
And with all of that, I was finally up and running and writing this very blog post, which is about to be stored in my very own PDS.