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```