
Host a PDS via a Cloudflare Tunnel
7/29/2025, 4:37:00 AM
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.
Table of Contents
Requirements
- A domain that is hosted on Cloudflare. I also recommend using a top level domain, Like
attoolbox.app
. Not something likepds.attoolbox.app
if 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 subdomain - Domain 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.
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
If you are wanting to install the PDS to a newer distro, like maybe Ubuntu 24.04 LTS, you can go to Bonus Install on newer distros and then come back here once you are done.
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.) [email protected]
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.env
and sometimes adocker compose down
anddocker compose up -d
helps 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 /pds
- Open open
compose.yaml
- Remove the service
caddy
lines4-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 down
to remove the caddy container - run
docker compose up -d
to bring everything back online
Bonus Install on newer distros
The installer.sh does a check to make sure you are using only a distro Bluesky has confirmed works. I have my personal account @baileytownsend.dev hosted on a VPS running Ubuntu 24.04 LTS. So I feel comfortable to say you can install and run a PDS from a newer LTS Ubuntu distro.
Once you download the installer.sh. You can open it in your text editor of your choice and comment out lines 89-114
. These are the lines to remove.
# Check for a supported architecture.
# If the platform is unknown (not uncommon) then we assume x86_64
if [[ "${PLATFORM}" == "unknown" ]]; then
PLATFORM="x86_64"
fi
if [[ "${PLATFORM}" != "x86_64" ]] && [[ "${PLATFORM}" != "aarch64" ]] && [[ "${PLATFORM}" != "arm64" ]]; then
usage "Sorry, only x86_64 and aarch64/arm64 are supported. Exiting..."
fi
# Check for a supported distribution.
SUPPORTED_OS="false"
if [[ "${DISTRIB_ID}" == "ubuntu" ]]; then
if [[ "${DISTRIB_CODENAME}" == "focal" ]]; then
SUPPORTED_OS="true"
echo "* Detected supported distribution Ubuntu 20.04 LTS"
elif [[ "${DISTRIB_CODENAME}" == "jammy" ]]; then
SUPPORTED_OS="true"
echo "* Detected supported distribution Ubuntu 22.04 LTS"
elif [[ "${DISTRIB_CODENAME}" == "mantic" ]]; then
SUPPORTED_OS="true"
echo "* Detected supported distribution Ubuntu 23.10 LTS"
fi
elif [[ "${DISTRIB_ID}" == "debian" ]]; then
if [[ "${DISTRIB_CODENAME}" == "bullseye" ]]; then
SUPPORTED_OS="true"
echo "* Detected supported distribution Debian 11"
elif [[ "${DISTRIB_CODENAME}" == "bookworm" ]]; then
SUPPORTED_OS="true"
echo "* Detected supported distribution Debian 12"
fi
fi
if [[ "${SUPPORTED_OS}" != "true" ]]; then
echo "Sorry, only Ubuntu 20.04, 22.04, Debian 11 and Debian 12 are supported by this installer. Exiting..."
exit 1
fi
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.
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.