Originally posted on 7/28/2025 on my old blog
So, you want to host a PDS on your network, but you may not want to open a port on your router(or able to). Or port 80/443 is already taken up by an application, and you don't want to deal with Caddy/nginx. Well, if that's the case then this is the blog post for you! Today we are going to set up a PDS and use a Cloudflare Tunnel to proxy requests from the web to your locally hosted PDS.
Hint from the future. I found in my testing in later months that the free tier of Cloudflare tunnels has a 100mb upload limit for files. So if you have big blobs or repos and see an error about the upload being too large good chance it's that you're hitting instead of the PDS limit since they return similar errors.
Table of Contents
Requirements
Cloudflare Tunnel Setup
Installing your PDS
Installing The actual PDS
After Install Surgery
Invalid Handle
Wrap up
Requirements
A domain that is hosted on Cloudflare. I also recommend using a top level domain, Like
attoolbox.app. Not something likepds.attoolbox.appif you are planing on using handles on it likebailey.attoolbox.app. If you don't have one, can dopds.yourdomain.name. Just may expect to have to manually set a_atproto.DNS TXT record so they resolve.A Linux Distro. Raspberry Pi OS works great and what I used when writing this guide. Or I use Ubuntu 24.04 LTS for my main.
About 30 minutes of free time
Cloudflare Tunnel Setup
I'm not going to get too much into how to create a tunnel since their documentation does it well. Once you are done with step 1 and have cloudflared installed and connected you can come back to this guide.
With cloudflared installed and your tunnel connecting you should now be on a page to add a public hostname. We want to create 2 of these.
The first one that handles all your XRPC requests
Leave subdomain blank
Domain is the domain you are using
Service Type is HTTP
URL is
localhost:3000
Then can click complete setup. We do need to setup a second one so can click on the tunnel name -> edit -> public hostnames -> add a public hostname
The second one for handles like bailey.attoolbox.app
Set
*for the subdomainDomain is the domain you are using
Service Type is HTTP
URL is
localhost:3000
Next we are going to set up a CNAME record with the name * for the domain following this. (Can copy the Target from the other record the tunnel created for the first public hostname).
And that's it! Cloudflare will handle your SSL and routing via the Tunnel.
Installing Your PDS
We're going to mostly follow the guide from Bluesky found here for PDS self-hosting.
We are going to skip on down to Installer on Ubuntu20.04/22.04 and Debian 11/12. Since we set up all the DNS stuff with the Cloudflare Tunnel setup.
Update note: The installer has been updated to support newer Ubuntu versions since this post was first written. So you are good to use Ubuntu 24.04 now thanks to this PR from @distraction.engineer!
Installing the actual PDS
Get the installer.sh script.
Can use wget
wget https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh
or curl
curl https://raw.githubusercontent.com/bluesky-social/pds/main/installer.sh >installer.sh
Setup screens
Can run the script with this
# Gives the script execute permission
chmod +x ./installer.sh
# Runs the script. Needs root
sudo ./installer.sh
After you get the script and run it, you should see this screen.
---------------------------------------
Add DNS Record for Public IP
---------------------------------------
From your DNS provider's control panel, create the required
DNS record with the value of your server's public IP address.
+ Any DNS name that can be resolved on the public internet will work.
+ Replace example.com below with any valid domain name you control.
+ A TTL of 600 seconds (10 minutes) is recommended.
Example DNS record:
NAME TYPE VALUE
---- ---- -----
example.com A 104.236.54.66
*.example.com A 104.236.54.66
**IMPORTANT**
It's recommended to wait 3-5 minutes after creating a new DNS record
before attempting to use it. This will allow time for the DNS record
to be fully updated.
Enter your public DNS address (e.g. example.com):
You can ignore the DNS setup, since it's already done with the tunnel setup. Go ahead and enter your domain name you want to use for the PDS,
For Enter an admin email address (e.g.) you@example.com I put an email I set up using resend from their Setting up SMTP section.
If you're not getting emails may check that you have the value set in/pds/pds.envand sometimes adocker compose downanddocker compose up -dhelps to refresh the containers env variables.
After that you wait for the installer to do its thing. Once that is done it asks you if you Create a PDS user account? (y/N): I recommend setting one up so you can use it as a test that *.yourdomain.com handles are resolving fine.
After Install Surgery
This is optional, but I recommend it. Since we're not using Caddy you want to remove it from the docker compose
Login as root
cd /pdsOpen open
compose.yamlRemove the service
caddylines4-16. Should look like this after
version: '3.9'
services:
pds:
container_name: pds
image: ghcr.io/bluesky-social/pds:0.4
network_mode: host
restart: unless-stopped
volumes:
- type: bind
source: /pds
target: /pds
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"
run
docker compose downto remove the caddy containerrun
docker compose up -dto bring everything back online
Invalid Handle
IF you see the dreaded Invalid Handle like below, don't sweat it. I'm going to give you a few tips and can always @ me on Bluesky, and we can figure it out. I got it twice today setting up PDSes, it's easy to mess up.
First use https://bsky-debug.app/handle
IF your handle is like bailey.yourpdsdomain.com, and when you check on the debug page and see that HTTP Verification method is failing. Then double check Cloudflare Tunnel Setup. Will most likely have to do with one of the * settings. Either the DNS record was missed or the tunnel public hostname. If you do resolve it and still see Invalid Handle on bsky, but the debug page says you're good. You may have to wait about 2-4 hours. The app view caches it for a while, that's how long it took for mine to resolve itself.
IF you went with something like bailey.pds.yourpdsdomain.com, you are probably going to be better off setting a _atproto TXT record for it. The record would be _atproto.bailey.pds, more info on that here.
Worse comes to worst can always just do _atproto.bailey so you end up with bailey.yourpdsdomain.com via the setup in settings found here. Just remember it will be cached for a bit and may not show up right away on Bluesky even tho the debug tool says it's fine.
Update note: I've since learned this is pretty common for new PDSs or migrations. Make sure to request a crawl via pdsadmin and as long as it checks out on the bsky-debug.app/handle tool it usually resolves it self within 30ish minutes
Wrap up
I ended up writing this guide while setting up a PDS on a Raspberry Pi Zero 2 W. So that may be a cheap fun way to try out self hosting. I'm not sure if I would host your main account on it though...
And that's about it! Thanks for reading, and I hope this helps, feel free to @ me if you hit any problems or have questions.