Host a PDS via a Cloudflare Tunnel

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 like pds.attoolbox.app if you are planing on using handles on it like bailey.attoolbox.app. If you don't have one, can do pds.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.

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.env and sometimes a docker compose down and docker 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 lines 4-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

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.