Self-hosting your ATProto PDS

shintoist1 pts0 comments

Self-hosting your PDS | jola.dev

Home

About

Blog

Newsletter

Projects

Talks

I take a lot of pleasure in running my own services. Self-hosting is a lot of fun and I keep accumulating more and more things on my server, from a feed reader to uptime monitoring (as backup to larm.dev), not to mention all of my toy projects.

I’ve been writing a lot about atproto recently and it’s frankly a very fertile area for self-hosting. Not only are there lots of open source components you can run, but the nature of atproto decentralization means that you can run your own services while seamlessly participating in shared systems. For example, you can run your own PDS (Personal Data Server) and create an account on it, and then log into Bluesky and post and participate with everyone else. Your account will work across the ATmosphere.

So, let’s talk about what it takes to self-host a PDS. Follow along and by the end of this you're posting on Bluesky from your own self-hosted account.

tranquil-pds

There are multiple PDS implementations out there. The original and probably most common one is the Bluesky reference PDS, but when I was researching options I came across one that matches the reference feature set but adds useful features on top: tranquil-pds. It’s implemented in Rust, comes with a nice admin panel, invite code management, passkeys, granular OAuth scopes, and a lot more.

There are plenty of instructions in the repo, but they’re a bit spread out, and some parts can get a bit tricky around wildcard certs, so I thought I’d write a bit of a getting started guide. This will be very similar to the setup I have for cove.town, with some simplifications.

There are a few things going on under the hood here that are worth highlighting:

The PDS holds the account repo, or in other words, all of your on-protocol data. This can be things like blog posts, Bluesky posts, standard reader reading history, book reviews, and much more. It also holds the blobs you upload, like images and videos.

Handles need a verification method, either through DNS or a web server that responds on .well-known/atproto-did. The PDS is built to handle this, but it requires getting the right DNS records and TLS certificates in place.

As long as you make backups and set up a rotation key, self-hosting your atproto account is fairly safe, you can recover from things like the PDS going down or accidentally deleting the database. So make backups.

Getting started

Before we get to the code, we need a few things.

A domain that you can add records to. You’re going to need to set up an A or CNAME wildcard record pointing at your server. For example, if your domain is example.com, the record would be *.example.com. This is because the accounts will get handles like alice.example.com. Alternatively, if you’re happy to add each subdomain manually, you can skip the wildcard record and just create pds.example.com.

A server with Docker or something similar installed. We’re going to use it to set up a compose. You can use alternatives like podman, but this guide will focus on Docker.

An account on atcr.io, the atproto container registry. Log in using docker login atcr.io or docker login atcr.io -u --password-stdin, using an app password.

Armed with those things, we’re ready to go.

TLS termination

A PDS needs to serve HTTPS traffic, and it needs to be able to serve that traffic on subdomains. That means the subdomains need TLS certificates issued. Caddy (unlike Traefik) comes with the ability to provision TLS certificates for domains on demand, so we’ll start by setting this up.

If you’ve opted not to use a DNS wildcard, and instead manually create each record for each account, you can skip the on_demand_tls and hardcode the subdomains instead.

Create a file called Caddyfile where you want the project.

on_demand_tls {

interval 2m

burst 5

*.example.com {

reverse_proxy pds:3000

tls {

on_demand

The PDS itself

This is mostly an adapted version of the example compose from the Tangled repo, with the configuration moved into the environment section. Create compose.yml with this content.

services:

pds:

image: atcr.io/tranquil.farm/tranquil-pds:latest

restart: always

environment:

SERVER_HOST: "[::]"

PDS_HOSTNAME: pds.example.com

PDS_USER_HANDLE_DOMAINS: example.com

INVITE_CODE_REQUIRED: "true"

DATABASE_URL: postgres://tranquil_pds:${POSTGRES_PASSWORD}@db:5432/pds

JWT_SECRET: ${JWT_SECRET}

MASTER_KEY: ${MASTER_KEY}

DPOP_SECRET: ${DPOP_SECRET}

DISABLE_ACCOUNT_VERIFICATION_GATE: "true"

volumes:

- blob_data:/var/lib/tranquil-pds/blobs

depends_on: [db]

db:

image: postgres:18-alpine

restart: always

environment:

POSTGRES_USER: tranquil_pds

POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}

POSTGRES_DB: pds

volumes:

- pg_data:/var/lib/postgresql

caddy:

image: caddy:2

restart: always

ports: ["80:80", "443:443"]

volumes:

- ./Caddyfile:/etc/caddy/Caddyfile

- caddy_data:/data

volumes:

blob_data:

pg_data:

caddy_data:

You’ll want to...

example self account atproto things hosting

Related Articles