forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
14 kB
426 lines
1# Tranquil PDS containerized production deployment
2
3This guide covers deploying Tranquil PDS using containers with podman.
4
5- **Debian 13+**: Uses systemd quadlets
6- **Alpine 3.23+**: Uses per-container OpenRC init scripts
7
8## Prerequisites
9
10- A server :p
11- Disk space for blobs, around* 1GB per active user as a baseline
12- A domain name pointing to your server's IP
13- A **wildcard TLS certificate** for `*.pds.example.com`, since user handles are served as subdomains
14- Root/sudo/doas access
15
16> 🦪 Lewis
17>
18> * "around" here meaning "at absolute least!"
19
20## Reverse proxy
21
22The bundled pod ships nginx as its reverse proxy, and this guide uses it throughout. nginx is just the default. Swap in whatever you prefer. Tranquil serves its API and web UI on a single port, so any reverse proxy works once it forwards to the app on `[::1]:3000`.
23
24Caddy is one good option. It grabs & renews TLS certificates automatically, including the wildcard this setup needs (if you're lucky, which Lewis is not), so the manual certbot steps later become unnecessary.
25
26### Terminating TLS in Tranquil
27
28You can also skip the reverse proxy and let Tranquil terminate TLS itself. Set `TLS_CERT_PATH` and `TLS_KEY_PATH` (or the `[server.tls]` block in the config file) to your cert chain and private key.
29
30Tranquil does not request or renew certs. Keep using certbot, acme.sh, lego, step, or whatever you're already using. After each renewal, send the process `SIGHUP` to reload the certificates and key. Live requests are unaffected and new connections pick up the new cert.
31
32### Client IP and forwarded headers
33
34Rate limiting and device records are based on the client IP ofc. Behind a reverse proxy, Tranquil reads it from `X-Forwarded-For`, counting hops from the right. You can set `TRUSTED_PROXY_COUNT` for how many proxies to trust. Leave it unset to let Tranquil assume the count. We assumes 1 proxy when something else terminates TLS, and 0 when Tranquil terminates TLS itself. At 0 it uses the direct conn address and ignores forwarded headers that a direct client could maliciously invent.
35
36
37## Quickstart (docker/podman compose)
38
39If you just want to get running quickly:
40
41```sh
42cp example.toml config.toml
43```
44
45Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`.
46
47`docker-compose.prod.yaml` pulls the prebuilt image from atcr.io. Sign in to the registry first with `podman login atcr.io`.
48
49nginx will not start without a certificate, so create a temporary self-signed one, bring the stack up, then swap in a real wildcard cert:
50```sh
51mkdir -p certs
52openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
53 -keyout certs/privkey.pem -out certs/fullchain.pem \
54 -subj "/CN=pds.example.com"
55podman-compose -f docker-compose.prod.yaml up -d
56```
57
58To build the image from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` before bringing the stack up.
59
60User handles are subdomains, so the real certificate must be a wildcard for `*.pds.example.com`, which requires DNS-01 validation. Follow the DNS cert steps in the Wildcard TLS certificate section below, then:
61```sh
62podman-compose -f docker-compose.prod.yaml restart nginx
63```
64
65## Standalone container without compose
66
67If you already have postgres running on the host, you can run just the app container.
68
69Pull the image. atcr.io requires authentication, so sign in first:
70```sh
71podman login atcr.io
72podman pull atcr.io/tranquil.farm/tranquil-pds:latest
73```
74
75Run with host networking so it can reach postgres on localhost, and mount config + storage:
76```sh
77podman run -d --name tranquil-pds \
78 --network=host \
79 -v /etc/tranquil-pds/config.toml:/etc/tranquil-pds/config.toml:ro,Z \
80 -v /var/lib/tranquil-pds:/var/lib/tranquil-pds:Z \
81 atcr.io/tranquil.farm/tranquil-pds:latest
82```
83
84To build from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` and use that tag.
85
86Then point your reverse proxy at the app on port 3000 for every route. With nginx that looks like:
87
88```nginx
89location /xrpc/ {
90 proxy_pass http://[::1]:3000;
91 # full proxy headers are in deploy/nginx/nginx-pod.conf
92}
93
94location / {
95 proxy_pass http://[::1]:3000;
96 proxy_http_version 1.1;
97 proxy_set_header Host $host;
98 proxy_set_header X-Real-IP $remote_addr;
99 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
100 proxy_set_header X-Forwarded-Proto $scheme;
101}
102```
103
104With Caddy the equivalent is a one-line `reverse_proxy [::1]:3000`, and it handles TLS for you. See `deploy/nginx/nginx-pod.conf` in the repo for the full nginx config with all routes.
105
106The end!!!
107
108Or wait, you want more? Perhaps a deployment that comes back on server restart?
109
110---
111
112# Common setup
113
114Both service-managed deployments share the steps below. Do these first, then jump to the Debian or Alpine section for the init-specific stuff, and finish in the Wildcard TLS certificate section.
115
116## Install podman
117
118**Debian:**
119```bash
120apt update
121apt install -y podman
122```
123
124**Alpine:**
125```sh
126apk update
127apk add podman fuse-overlayfs
128rc-update add cgroups
129rc-service cgroups start
130```
131
132## Create the directory structure
133
134```sh
135mkdir -p /srv/tranquil-pds/{postgres,blobs,store,certs,acme,config}
136```
137
138## Clone the repo and pull the image
139
140The repo provides the quadlet, OpenRC, and nginx files. The image comes prebuilt from atcr.io. Sign in before pulling:
141```sh
142cd /opt
143git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
144podman login atcr.io
145podman pull atcr.io/tranquil.farm/tranquil-pds:latest
146```
147
148To build from source instead, tag it with the same name so the service uses it:
149```sh
150cd tranquil-pds && podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .
151```
152
153## Create a configuration file
154
155```sh
156cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml
157chmod 600 /srv/tranquil-pds/config/config.toml
158```
159
160Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with:
161```sh
162openssl rand -base64 48
163```
164
165> 🦪 Lewis
166>
167> Every config option can also be set via an environment variable
168> named in the comments in `example.toml`. Environment variables always take
169> precedence over the config file.
170
171## Create the nginx config and database secret
172
173The bundled pod runs nginx as its proxy. The app and nginx share the pod's network namespace, so the proxy reaches the app on `[::1]:3000`:
174```sh
175cp /opt/tranquil-pds/deploy/nginx/nginx-pod.conf /srv/tranquil-pds/config/nginx.conf
176```
177
178Create the podman secret for the database password. Use the same password in the `database.url` of your `config.toml`:
179```sh
180echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
181```
182
183## Create a temporary TLS certificate
184
185The nginx that comes out of the box will not start without a certificate. The real wildcard cert needs DNS-01 validation and is set up once the stack is running, so for now let's add a self-signed placeholder so the services can start:
186```sh
187openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
188 -keyout /srv/tranquil-pds/certs/privkey.pem \
189 -out /srv/tranquil-pds/certs/fullchain.pem \
190 -subj "/CN=pds.example.com"
191```
192
193If your proxy obtains its own certificates, like Caddy does, you can skip this step!
194
195The app runs migrations itself on first boot btw, so there is no separate migration step if you're looking for one.
196
197---
198
199# Debian with systemd quadlets
200
201Quadlets are a nice way to run podman containers under systemd.
202
203## Install quadlet definitions
204
205Copy the quadlet files from the repository:
206```bash
207mkdir -p /etc/containers/systemd
208cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds.pod /etc/containers/systemd/
209cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-db.container /etc/containers/systemd/
210cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-app.container /etc/containers/systemd/
211cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-nginx.container /etc/containers/systemd/
212```
213
214Optional quadlets for valkey and minio are also available in `deploy/quadlets/` if you need them.
215
216## Start services
217
218```bash
219systemctl daemon-reload
220systemctl start tranquil-pds-db
221sleep 10
222systemctl start tranquil-pds-app tranquil-pds-nginx
223```
224
225## Enable all services
226
227```bash
228systemctl enable tranquil-pds-db tranquil-pds-app tranquil-pds-nginx
229```
230
231## Configure firewall if you're into that sort of thing
232
233```bash
234apt install -y ufw
235ufw allow ssh
236ufw allow 80/tcp
237ufw allow 443/tcp
238ufw enable
239```
240
241Now finish in the Wildcard TLS certificate section below.
242
243---
244
245# Alpine with OpenRC
246
247Alpine uses OpenRC, not systemd, bless its soul. So instead of quadlets we use a set of OpenRC init scripts, one per container.
248
249## Install the OpenRC services
250
251Copy the init scripts. They run the pod, postgres, the app, and nginx as separate services ordered with `depend()`:
252```sh
253cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-pod /etc/init.d/
254cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-db /etc/init.d/
255cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-app /etc/init.d/
256cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-nginx /etc/init.d/
257chmod +x /etc/init.d/tranquil-pds-pod /etc/init.d/tranquil-pds-db /etc/init.d/tranquil-pds-app /etc/init.d/tranquil-pds-nginx
258```
259
260The scripts default to `/srv/tranquil-pds` for data and `/srv/tranquil-pds/config/config.toml` for config. Override via `/etc/conf.d/tranquil-pds-app` and friends if your paths differ.
261
262## Start services
263
264Starting the nginx service pulls in the pod, postgres, and app through its dependencies:
265```sh
266rc-service tranquil-pds-nginx start
267```
268
269## Enable services at boot time
270
271```sh
272rc-update add tranquil-pds-pod tranquil-pds-db tranquil-pds-app tranquil-pds-nginx
273```
274
275## Configure firewall if you're into that sort of thing
276
277```sh
278apk add iptables ip6tables
279iptables -A INPUT -p tcp --dport 22 -j ACCEPT
280iptables -A INPUT -p tcp --dport 80 -j ACCEPT
281iptables -A INPUT -p tcp --dport 443 -j ACCEPT
282iptables -A INPUT -i lo -j ACCEPT
283iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
284iptables -P INPUT DROP
285ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
286ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
287ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
288ip6tables -A INPUT -i lo -j ACCEPT
289ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
290ip6tables -P INPUT DROP
291rc-update add iptables
292rc-update add ip6tables
293/etc/init.d/iptables save
294/etc/init.d/ip6tables save
295```
296
297Now finish in the Wildcard TLS certificate section below.
298
299---
300
301# Wildcard TLS certificate
302
303This section sets up the real certificate for the bundled nginx.
304
305With the stack running behind the temporary self-signed certificate, swap in a real wildcard cert. User handles are served as subdomains like `nel.pds.example.com`, so the certificate must cover `*.pds.example.com`, which requires DNS-01 validation.
306
307Get a wildcard certificate using DNS validation:
308```sh
309podman run --rm -it \
310 -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \
311 docker.io/certbot/certbot:v5.2.2 certonly \
312 --manual --preferred-challenges dns \
313 -d pds.example.com -d '*.pds.example.com' \
314 --agree-tos --email you@example.com
315```
316
317Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. For automated renewal, use a DNS provider plugin or something.
318
319Link the certificates into place:
320```sh
321ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem
322ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem
323```
324
325Restart nginx to load them:
326
327**Debian:**
328```bash
329systemctl restart tranquil-pds-nginx
330```
331
332**Alpine:**
333```sh
334rc-service tranquil-pds-nginx restart
335```
336
337## Cert renewal
338
339Manual mode doesn't auto-renew, so add a renewal job to root's crontab with `crontab -e`.
340
341**Debian:**
342```
3430 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z -v /srv/tranquil-pds/acme:/var/www/acme:Z docker.io/certbot/certbot:v5.2.2 renew --quiet && systemctl reload tranquil-pds-nginx
344```
345
346**Alpine:**
347```
3480 0 * * * podman run --rm -v /srv/tranquil-pds/certs:/etc/letsencrypt -v /srv/tranquil-pds/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service tranquil-pds-nginx restart
349```
350
351---
352
353# Verification and maintenance
354
355## Verify installation
356
357```sh
358curl -s https://pds.example.com/xrpc/_health | jq
359curl -s https://pds.example.com/.well-known/atproto-did
360```
361
362## View logs
363
364**Debian:**
365```bash
366journalctl -u tranquil-pds-app -f
367podman logs -f tranquil-pds-app
368```
369
370**Alpine:**
371```sh
372rc-service tranquil-pds-app status
373podman logs -f tranquil-pds-app
374```
375
376## Update Tranquil PDS
377
378Pull the latest image:
379```sh
380podman login atcr.io
381podman pull atcr.io/tranquil.farm/tranquil-pds:latest
382```
383
384Debian:
385```bash
386systemctl restart tranquil-pds-app
387```
388
389Alpine:
390```sh
391rc-service tranquil-pds-app restart
392```
393
394To update a source-built deployment, `git pull` in the repo and rebuild with `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` before restarting.
395
396## Backup database
397
398```sh
399podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
400```
401
402## Custom homepage
403
404If a `homepage.html` exists in the app's frontend directory it is served at `/`. The account dashboard stays at `/app/`. The directory defaults to `/var/lib/tranquil-pds/frontend` and is set by `FRONTEND_DIR`. Mount your own into the app container:
405
406```sh
407-v /srv/tranquil-pds/homepage.html:/var/lib/tranquil-pds/frontend/homepage.html:ro,Z
408```
409
410For ex:
411```html
412<!DOCTYPE html>
413<html>
414<head>
415 <title>Welcome to my PDS</title>
416 <style>
417 body { font-family: system-ui; max-width: 600px; margin: 100px auto; padding: 20px; }
418 </style>
419</head>
420<body>
421 <h1>Welcome to my dark web popsocket store</h1>
422 <p>This is a <a href="https://atproto.com">AT Protocol</a> Personal Data Server.</p>
423 <p><a href="/app/">Sign in</a> or learn more at <a href="https://bsky.social">Bluesky</a>.</p>
424</body>
425</html>
426```