1--- 2title: "Embracing ATProto, part 1: Setting up a PDS" 3description: In this series of posts, I'll explore my journey into setting up my workflows and services for atproto. The first step is setting up my PDS. Let's start with a bit of explanation for all this lingo. 4date: 2025-09-03 5updated: 2025-09-17 6authors: 7 - name: finxol 8tags: 9 - atproto 10 - self-hosting 11published: true 12bskyCid: 3lxwbhjz4l22i 13--- 14 15The [Atmosphere Protocol](https://atproto.com/), or atproto for short, is the protocol behind the Bluesky social network. 16It's "an open, decentralized network for building social applications." 17 18_If you are familiar with ATProto already, [skip ahead](#setting-up-a-pds)._ 19 20In atproto's decentralisation model, there are multiple parts that work together and can be hosted separately. 21This differs from other solutions like [ActivityPub](https://en.wikipedia.org/wiki/ActivityPub), which services like Mastodon implement. 22Dan Abramov, who previously worked at Bluesky, made a [good write-up explaining the difference](https://news.ycombinator.com/item?id=45077986). 23 24Let's go over some rough definitions for the important bits. You'll find better explanations in the [atproto docs](https://atproto.com/guides/glossary). 25 26#### PDS 27 28In atproto, a Personal Data Server (PDS) is a server that hosts a user. 29That's the place where the user's information lives, and only handles this. 30It's essentially a user database, but it's decentralised, so it feeds its data to Relays. 31 32#### Relay 33 34A relay is the part of the stack that ingests all the information sent from the PDSes, and exposes it for use in AppViews. 35 36It's an optimisation in the network to avoid many-to-many connections between PDSes and AppViews. 37 38#### AppView 39 40The AppView is basically the app you use to interact with the atproto service. 41[Bluesky](https://bsky.social/about) is an AppView, so are [tangled.sh](https://tangled.sh/) and [Smoke Signals](https://smokesignal.events/). 42 43They receive all the information from the Relays, and filter out only what they need in order to display it into a usable application. 44 45<img src="/posts/embracing-atproto-pt1/atproto.svg" width="100%" class="schema"> 46 47This means that you can also host and control only part of that stack if you want. 48The smallest—and most common—part to self-host is the PDS, enabling you to own your data, while still using Bluesky and the same atproto apps. 49 50That's exactly what I did. 51 52## Setting up a PDS 53 54To set up a PDS, you'll need: 55 56- A server connected to the internet running Debian or Ubuntu (a VPS, a Raspberry Pi, a laptop that's always on, whatever) 57- A domain name 58- Docker 59 60I chose to get a new [UpCloud](https://upcloud.com/) VPS, and use a `pds.finxol.io` subdomain of my usual domain. 61I could've used one of the other VPSes I have, but I've been meaning to migrate off of Digital Ocean for some time, mainly because it's a bit too expensive for me, but also because I prefer to use European services whenever possible. 62 63The setup process is super easy and very well explained [in their docs](https://github.com/bluesky-social/pds/blob/main/README.md#self-hosting-pds). 64The script sets everything up for you, it even installs docker if it's not there already. 65 66### Adapting to my setup 67 68I did have to make a couple tweaks to it to make it play well with the rest of my setup though. 69 70First, the script checks the OS version you're running. 71My VPS is running the latest Debian 13 Trixie, released less than a month ago. 72Since it's not one of the required Debian 11 or 12, the script won't let me continue the install. 73I just added a clause in the script to accept Debian Trixie. 74 75I also made some changes to the compose file. 76I didn't want [watchtower](https://github.com/containrrr/watchtower) to update all my containers constantly, and I was running caddy externally for all my other stuff, 77so I just removed those lines in the compose file and moved the Caddy directives to my root Caddyfile. 78 79Here is the final compose file I ended up with: 80 81```yaml 82services: 83 pds: 84 container_name: pds 85 image: ghcr.io/bluesky-social/pds:0.4 86 restart: unless-stopped 87 ports: 88 - "6010:3000" 89 volumes: 90 - type: bind 91 source: /pds 92 target: /pds 93 env_file: 94 - /pds/pds.env 95``` 96 97Once the script finished and everything was running, I simply pinged the pds with `curl https://pds.finxol.io/xrpc/_health`, 98tested the websocket connection as stated in the docs, only with [`websocat`](https://github.com/vi/websocat), 99and saw everything working as expected! 100 101*Edit:* 102Also, make sure the ***time*** is right on your server. 103An incorrect system time will lead to incorrect timestamps in OAuth tokens, getting them rejected by some clients. 104My server time was off by a few dozen seconds, enough to prevent me from logging into Tangled... 105 106## Account Migration 107 108And now we get to the trickier part. 109If you mess up your account migration, you might lose your existing data (which I'd prefer not to). 110 111I chose to go the easy way and follow [Tophhie's blog post](https://blog.tophhie.cloud/host-your-own-bluesky-pds-a-complete-azure-powered-guide/#%F0%9F%8F%83%E2%80%8D%E2%99%80%EF%B8%8F%E2%80%8D%E2%9E%A1%EF%B8%8F-4-migration-from-blueskys-pds). 112This makes use of Bluesky's very convenient [goat](https://github.com/bluesky-social/goat) atproto CLI tool, 113automating most of the migration process, with only 5 commands to run. 114 115Following these steps, I gathered the required info, ran the migration command, and boom, I now own my atproto identity! 116 117## Backups 118 119Since a PDS is basically your entire identity on atproto, it's rather important not to lose it. 120One way to ensure this is with backups! 121 122I chose a very simple path again: use [restic](https://restic.net/) to throw the data into an S3 bucket periodically with crontab. 123 124Restic is a simple CLI backup tool with some cool features: 125it works with "repositories", so you get encrypted versioned backups on a multitude of supported storage types. 126 127I once again went with UpCloud's S3 offering. 128It's dead simple, with a 250GB allowance for 5€/month. 129I know I won't be filling that up with backups any time soon, but I've got some other buckets in there taking up space. 130 131First off, I set up the backup repository with `restic init` and gave it a password. 132Then wrote the backup script and told `crontab` to run it every day. 133 134Here's my super complex backup script: 135 136```bash 137#!/bin/bash 138 139export AWS_ACCESS_KEY_ID=*** 140export AWS_SECRET_ACCESS_KEY=*** 141export RESTIC_PASSWORD=*** 142 143restic -r s3:https://top-secret.upcloudobjects.com/akhaten-bckp/pds backup /pds --skip-if-unchanged 144``` 145 146I know storing the keys as raw values in there isn't very safe, but I've restricted the S3 access key as much as I can. 147I'll set up a secrets manager at some point in the future. 148 149Now my atproto identity can be restored in case of problems with the VPS! 150 151I might also back it up to something else, maybe Hetzner Object Storage, Proton Drive ([through rclone](https://rclone.org/protondrive/)), Scaleway Storage, or some other server over SFTP. 152 153## Bonus 154 155As an added bonus, since your PDS stores and serves your identity on the atproto network, you can also have it [lie about your age verification status](https://bsky.app/profile/mary.my.id/post/3ltwlpjciecsq), 156and bypass the age check requirements recently put in place [in the UK](https://en.wikipedia.org/wiki/Online_Safety_Act_2023#Age_verification) and other places. 157This only requires a few extra lines in the Caddyfile: 158 159```Caddyfile 160*.pds.finxol.io, pds.finxol.io { 161 tls { 162 on_demand 163 } 164 165 @age_assurance path /xrpc/app.bsky.unspecced.getAgeAssuranceState 166 handle @age_assurance { 167 header Content-Type application/json 168 respond `{"lastInitiatedAt":"2025-08-02T15:22:45.829Z","status":"assured"}` 200 169 } 170 171 handle { 172 reverse_proxy localhost:6010 173 } 174} 175```