···11+---
22+title: PDS on a pi
33+bio: installing a pds on my raspberry pi; Photo by Ritam Baishya on Unsplash
44+banner: blue-skies.png
55+pub: 2025-09-13
66+---
77+88+I want to self host my own pds for atproto, but I wanna host it on my own hardware in my room, since that feels fun and whimsical. I've got a pi 5 sat on my floor which I'm going to use for this, and then I'll route it out of my vps using tailscale and caddy. Currently the pi doesn't have ethernet or constant power (i attached a screen to it and forgot how to take it off so i turn it off at night lmfao) so I'm going to create a test account (at://test.vielle.dev) and use that for now. This'll also be a chance to create a did:web account to mess with.
99+1010+> _Note_: I'm writing this blog post while I do it so its going to be messy
1111+1212+The plan is to have all my services running on docker compose, which then can be accessed by using tailscale on the pi host. This then goes into tailscale on the vps, which goes into caddy, which goes out to the net. Little complex but should work. Heres a graph
1313+1414+<style>
1515+ /* select all images who's alt text starts with "Traffic from my docker containers" */
1616+ img[alt^="Traffic from my docker containers"] {
1717+ image-rendering: auto;
1818+ }
1919+</style>
2020+2121+
2222+2323+## 1. Get a dummy service on the pi
2424+2525+I'm gonna get a dummy ping/pong https thing on the pi which i can test forwarding. Presumably if this works, then the PDS will work too when I add it.
2626+2727+`ping-pong/main.ts`
2828+2929+```ts
3030+// using Deno
3131+// new http server on 0.0.0.0:8000 which just sends "Hello"
3232+Deno.serve({ port: 8000, hostname: "0.0.0.0" }, (req) => {
3333+ return new Response("Hello! " + req.url);
3434+});
3535+```
3636+3737+`ping-pong/Dockerfile`
3838+3939+```Dockerfile
4040+FROM denoland/deno:latest
4141+WORKDIR /app
4242+COPY . .
4343+CMD ["deno", "run", "--allow-net", "main.ts"]
4444+```
4545+4646+`compose.yml`
4747+4848+```yaml
4949+services:
5050+ pingpong:
5151+ build: ./ping-pong
5252+ restart: unless-stopped
5353+ ports:
5454+ - 8000:8000
5555+```
5656+5757+It works, so now I'm going to add a caddy rule to route port 8000 of my pi (`http://pi:8000`) to `https://pds.vielle.dev:443`.
5858+5959+## 2. Routing traffic from vps -> pi
6060+6161+All I need to do is add this code to my Caddyfile and run it locally to test.
6262+6363+```caddyfile
6464+pds.{$HOST:localhost} {
6565+ reverse_proxy pi:8000
6666+}
6767+```
6868+6969+Once I'm ready to deploy, this SHOULD just be a matter of pushing the commit to master and assuming my shitty cicd works it'll be deployed, along with this post!
7070+7171+## 3. Installing the pds in the container.
7272+7373+For this one, we're gonna look at the pds setup from <https://github.com/bluesky-social/pds/>. This has 3 parts:
7474+7575+- A caddy container
7676+- The pds
7777+- Watchtower
7878+7979+### 3.1. Caddyfile
8080+8181+Starting with the caddy container, let's just make sure our vps has anything in the caddy config its missing.
8282+The config is below, [found here](https://github.com/bluesky-social/pds/blob/00078b4658def4eb419efab717fa970d01fce045/installer.sh#L302)
8383+8484+```Caddyfile
8585+{
8686+ email ${PDS_ADMIN_EMAIL}
8787+ on_demand_tls {
8888+ ask http://localhost:3000/tls-check
8989+ }
9090+}
9191+9292+*.${PDS_HOSTNAME}, ${PDS_HOSTNAME} {
9393+ tls {
9494+ on_demand
9595+ }
9696+ reverse_proxy http://localhost:3000
9797+}
9898+```
9999+100100+This is using global config for an email address, which is set further up the script, but I'll configure with environment variables, and for on demand tls which is on the pds. This means the pds can decide which subdomains get tls, with any level of wildcarding. I don't plan on using on demand tls for anything else on my site right now, but since it can't be scoped I might need to make a custom handler myself.
101101+102102+Anyway heres the updated caddyfile config:
103103+104104+```caddyfile
105105+{
106106+ email {$PDS_ADMIN_EMAIL:404@vielle.dev}
107107+ on_demand_tls {
108108+ ask pi:8000/tls-check
109109+ }
110110+}
111111+112112+# ...
113113+114114+*.pds.{$HOST:localhost}, pds.{$HOST:localhost} {
115115+ tls {
116116+ on_demand
117117+ }
118118+ reverse_proxy pi:8000
119119+}
120120+```
121121+122122+This refuses to connect but I'm going to assume its because the on demand tls is failing so theres just no cert which I can tell firefox to accept.
123123+124124+### 3.2. Watchtower
125125+126126+Googling it, watchtower seems to be a way to automatically update containers?
127127+I'm just going to add it straight into the compose file lmao
128128+129129+```yaml
130130+services:
131131+ pingpong:
132132+ build: ./ping-pong
133133+ restart: unless-stopped
134134+ ports:
135135+ - 8000:8000
136136+137137+ watchtower:
138138+ container_name: watchtower
139139+ image: containrrr/watchtower:latest
140140+ network_mode: host
141141+ volumes:
142142+ - type: bind
143143+ source: /var/run/docker.sock
144144+ target: /var/run/docker.sock
145145+ restart: unless-stopped
146146+ environment:
147147+ WATCHTOWER_CLEANUP: true
148148+ WATCHTOWER_SCHEDULE: "@midnight"
149149+```
150150+151151+### 3.3. The PDS
152152+153153+The reference compose file configures the pds like this:
154154+155155+```yaml
156156+pds:
157157+ container_name: pds
158158+ image: ghcr.io/bluesky-social/pds:0.4
159159+ network_mode: host
160160+ restart: unless-stopped
161161+ volumes:
162162+ - type: bind
163163+ source: /pds
164164+ target: /pds
165165+ env_file:
166166+ - /pds/pds.env
167167+```
168168+169169+This is self explanatory; we make a pds directory, bind it to /pds, and make sure theres at least a pds.env file in there, which is the only file we need, based on installer.sh
170170+171171+In installer.sh, they generate the env file like so:
172172+173173+```bash
174174+cat <<PDS_CONFIG >"${PDS_DATADIR}/pds.env"
175175+PDS_HOSTNAME=${PDS_HOSTNAME}
176176+PDS_JWT_SECRET=$(eval "${GENERATE_SECURE_SECRET_CMD}")
177177+PDS_ADMIN_PASSWORD=${PDS_ADMIN_PASSWORD}
178178+PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "${GENERATE_K256_PRIVATE_KEY_CMD}")
179179+PDS_DATA_DIRECTORY=${PDS_DATADIR}
180180+PDS_BLOBSTORE_DISK_LOCATION=${PDS_DATADIR}/blocks
181181+PDS_BLOB_UPLOAD_LIMIT=52428800
182182+PDS_DID_PLC_URL=${PDS_DID_PLC_URL}
183183+PDS_BSKY_APP_VIEW_URL=${PDS_BSKY_APP_VIEW_URL}
184184+PDS_BSKY_APP_VIEW_DID=${PDS_BSKY_APP_VIEW_DID}
185185+PDS_REPORT_SERVICE_URL=${PDS_REPORT_SERVICE_URL}
186186+PDS_REPORT_SERVICE_DID=${PDS_REPORT_SERVICE_DID}
187187+PDS_CRAWLERS=${PDS_CRAWLERS}
188188+LOG_ENABLED=true
189189+PDS_CONFIG
190190+```
191191+192192+Lets write our own pds.env file first:
193193+194194+- PDS_HOSTNAME is just `pds.vielle.dev`
195195+- PDS_JWT_SECRET is just `openssl rand --hex 16`
196196+- PDS_ADMIN_PASSWORD is also just `openssl rand --hex 16`, but must be different
197197+- PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX is `openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32`
198198+- PDS_DATA_DIRECTORY is PDS_DATADIR, which is hardcoded to `/pds` in the shell. Since this is currrently hardcoded to `/pds`, we can hardcode it
199199+- PDS_BLOBSTORE_DISK_LOCATION is PDS_DATADIR/blocks, so `/pds/blocks`
200200+- PDS_BLOB_UPLOAD_LIMIT is set to `52428800`
201201+- PDS_DID_PLC_URL is `https://plc.directory`
202202+- PDS_BSKY_APP_VIEW_URL is `https://api.bsky.app`
203203+- PDS_BSKY_APP_VIEW_DID is `did:web:api.bsky.app`
204204+- PDS_REPORT_SERVICE_URL is `https://mod.bsky.app`
205205+- PDS_REPORT_SERVICE_DID is `did:plc:ar7c4by46qjdydhdevvrndac`
206206+- PDS_CRAWLERS is `https://bsky.network`, but I'll expand this to include things like <https://atproto.africa/> & bad example's EU relays
207207+- LOG_ENABLED is `true`
208208+209209+So to generate all this, I'm just going to run
210210+211211+```sh
212212+mkdir ./pds
213213+cat <<EOF > ./pds/pds.env
214214+ PDS_HOSTNAME=pds.vielle.dev
215215+ PDS_JWT_SECRET=$(eval "openssl rand --hex 16")
216216+ PDS_ADMIN_PASSWORD=$(eval "openssl rand --hex 16")
217217+ PDS_PLC_ROTATION_KEY_K256_PRIVATE_KEY_HEX=$(eval "openssl ecparam --name secp256k1 --genkey --noout --outform DER | tail --bytes=+8 | head --bytes=32 | xxd --plain --cols 32")
218218+ PDS_DATA_DIRECTORY=./pds # edit: change this to /pds (see below)
219219+ PDS_BLOBSTORE_DISK_LOCATION=./pds/blocks # edit: change this to /pds/blocks (see below)
220220+ PDS_BLOB_UPLOAD_LIMIT=52428800
221221+ PDS_DID_PLC_URL=https://plc.directory
222222+ PDS_BSKY_APP_VIEW_URL=https://api.bsky.app
223223+ PDS_BSKY_APP_VIEW_DID=did:web:api.bsky.app
224224+ PDS_REPORT_SERVICE_URL=https://mod.bsky.app
225225+ PDS_REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
226226+ PDS_CRAWLERS=https://bsky.network,https://atproto.africa,https://relay1.us-east.bsky.network,https://relay.fire.hose.cam,https://relay3.fr.hose.cam,https://relay.hayescmd.net,https://relay.xero.systems
227227+ LOG_ENABLED=true
228228+EOF
229229+```
230230+231231+Pretty much everythings set up for a pds now, last step is to configure pds in the compose file and it should be online !
232232+233233+Just got to add this to my compose file and it should all work!
234234+235235+```yaml
236236+pds:
237237+ container_name: pds
238238+ image: ghcr.io/bluesky-social/pds:0.4
239239+ restart: unless-stopped
240240+ # removed network_mode: host since it should still work without it
241241+ # and instead bound port 3000 of container to 8000 of host
242242+ ports:
243243+ - 8000:3000
244244+ volumes:
245245+ - type: bind
246246+ # source is relative
247247+ source: ./pds
248248+ target: /pds
249249+ # env is relative
250250+ env_file:
251251+ - ./pds/pds.env
252252+```
253253+254254+Ok. So. Seems i didn't configure it correctly: `Error: Must configure plc rotation key`. I was missing the `xxd` command and somehow missed that? A simple `sudo apt-get install xxd` should fix things. Running the env command again gives an error that the directory for the database doesnt exist !! How Fun !!
255255+256256+The issue was that i had `./pds` in the environment variables, (which were used inside the container), when the container used `/pds` as the datadir. Simple fix, and now it all works!!
257257+258258+## Next steps:
259259+260260+Deploy this post + caddy changes, create a test account or two, find a permanent setup for the pi, and setup regular backups of the pds; then migrate to it myself!!