forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
1# Tranquil PDS Containerized Production Deployment
2
3This guide covers deploying Tranquil PDS using containers with podman.
4
5- **Debian 13+**: Uses systemd quadlets (modern, declarative container management)
6- **Alpine 3.23+**: Uses OpenRC service script with podman-compose
7
8## Prerequisites
9
10- A VPS with at least 2GB RAM
11- Disk space for blobs (depends on usage; plan for ~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` (user handles are served as subdomains)
14- Root or sudo access
15
16## Quick Start (Docker/Podman Compose)
17
18If you just want to get running quickly:
19
20```sh
21cp .env.example .env
22```
23
24Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
25
26Build and start:
27```sh
28podman build -t tranquil-pds:latest .
29podman build -t tranquil-pds-frontend:latest ./frontend
30podman-compose -f docker-compose.prod.yaml up -d
31```
32
33Get initial certificate (after DNS is configured):
34```sh
35podman-compose -f docker-compose.prod.yaml run --rm certbot certonly \
36 --webroot -w /var/www/acme -d pds.example.com -d '*.pds.example.com'
37ln -sf live/pds.example.com/fullchain.pem certs/fullchain.pem
38ln -sf live/pds.example.com/privkey.pem certs/privkey.pem
39podman-compose -f docker-compose.prod.yaml restart nginx
40```
41
42For production setups with proper service management, continue to either the Debian or Alpine section below.
43
44## Standalone Containers (No Compose)
45
46If you already have postgres and valkey running on the host (eg., from the [Debian install guide](install-debian.md)), you can run just the app containers.
47
48Build the images:
49```sh
50podman build -t tranquil-pds:latest .
51podman build -t tranquil-pds-frontend:latest ./frontend
52```
53
54Run the backend with host networking (so it can access postgres/valkey on localhost) and mount the blob storage:
55```sh
56podman run -d --name tranquil-pds \
57 --network=host \
58 --env-file /etc/tranquil-pds/tranquil-pds.env \
59 -v /var/lib/tranquil:/var/lib/tranquil:Z \
60 tranquil-pds:latest
61```
62
63Run the frontend with port mapping (the container's nginx listens on port 80):
64```sh
65podman run -d --name tranquil-pds-frontend \
66 -p 8080:80 \
67 tranquil-pds-frontend:latest
68```
69
70Then configure your host nginx to proxy to both containers. Replace the static file `try_files` directives with proxy passes:
71
72```nginx
73# API routes to backend
74location /xrpc/ {
75 proxy_pass http://127.0.0.1:3000;
76 # ... (see Debian guide for full proxy headers)
77}
78
79# Static routes to frontend container
80location / {
81 proxy_pass http://127.0.0.1:8080;
82 proxy_http_version 1.1;
83 proxy_set_header Host $host;
84 proxy_set_header X-Real-IP $remote_addr;
85 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
86 proxy_set_header X-Forwarded-Proto $scheme;
87}
88```
89
90See the [Debian install guide](install-debian.md) for the full nginx config with all API routes.
91
92---
93
94# Debian 13+ with Systemd Quadlets
95
96Quadlets are the modern way to run podman containers under systemd.
97
98## Install Podman
99
100```bash
101apt update
102apt install -y podman
103```
104
105## Create Directory Structure
106
107```bash
108mkdir -p /etc/containers/systemd
109mkdir -p /srv/tranquil-pds/{postgres,valkey,blobs,backups,certs,acme,config}
110```
111
112## Create Environment File
113
114```bash
115cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
116chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
117```
118
119Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
120```bash
121openssl rand -base64 48
122```
123
124For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion).
125
126## Install Quadlet Definitions
127
128Copy the quadlet files from the repository:
129```bash
130cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/
131cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/
132```
133
134Note: Systemd doesn't support shell-style variable expansion in `Environment=` lines. The quadlet files expect DATABASE_URL to be set in the environment file.
135
136## Create nginx Configuration
137
138```bash
139cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf
140```
141
142## Clone and Build Images
143
144```bash
145cd /opt
146git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
147cd tranquil-pds
148podman build -t tranquil-pds:latest .
149podman build -t tranquil-pds-frontend:latest ./frontend
150```
151
152## Create Podman Secrets
153
154```bash
155source /srv/tranquil-pds/config/tranquil-pds.env
156echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
157```
158
159## Start Services and Initialize
160
161```bash
162systemctl daemon-reload
163systemctl start tranquil-pds-db tranquil-pds-valkey
164sleep 10
165```
166
167Run migrations:
168```bash
169cargo install sqlx-cli --no-default-features --features postgres
170DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations
171```
172
173## Obtain Wildcard SSL Certificate
174
175User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
176
177Create temporary self-signed cert to start services:
178```bash
179openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
180 -keyout /srv/tranquil-pds/certs/privkey.pem \
181 -out /srv/tranquil-pds/certs/fullchain.pem \
182 -subj "/CN=pds.example.com"
183systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
184```
185
186Get a wildcard certificate using DNS validation:
187```bash
188podman run --rm -it \
189 -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \
190 docker.io/certbot/certbot:v5.2.2 certonly \
191 --manual --preferred-challenges dns \
192 -d pds.example.com -d '*.pds.example.com' \
193 --agree-tos --email you@example.com
194```
195
196Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
197
198For automated renewal, use a DNS provider plugin (eg., cloudflare, route53).
199
200Link certificates and restart:
201```bash
202ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem
203ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem
204systemctl restart tranquil-pds-nginx
205```
206
207## Enable All Services
208
209```bash
210systemctl enable tranquil-pds-db tranquil-pds-valkey tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
211```
212
213## Configure Firewall
214
215```bash
216apt install -y ufw
217ufw allow ssh
218ufw allow 80/tcp
219ufw allow 443/tcp
220ufw enable
221```
222
223## Certificate Renewal
224
225Add to root's crontab (`crontab -e`):
226```
2270 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
228```
229
230---
231
232# Alpine 3.23+ with OpenRC
233
234Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper.
235
236## Install Podman
237
238```sh
239apk update
240apk add podman podman-compose fuse-overlayfs cni-plugins
241rc-update add cgroups
242rc-service cgroups start
243```
244
245Enable podman socket for compose:
246```sh
247rc-update add podman
248rc-service podman start
249```
250
251## Create Directory Structure
252
253```sh
254mkdir -p /srv/tranquil-pds/{data,config}
255mkdir -p /srv/tranquil-pds/data/{postgres,valkey,blobs,backups,certs,acme}
256```
257
258## Clone Repository and Build Images
259
260```sh
261cd /opt
262git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
263cd tranquil-pds
264podman build -t tranquil-pds:latest .
265podman build -t tranquil-pds-frontend:latest ./frontend
266```
267
268## Create Environment File
269
270```sh
271cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
272chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
273```
274
275Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
276```sh
277openssl rand -base64 48
278```
279
280## Set Up Compose and nginx
281
282Copy the production compose and nginx configs:
283```sh
284cp /opt/tranquil-pds/docker-compose.prod.yaml /srv/tranquil-pds/docker-compose.yml
285cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf
286```
287
288Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed:
289- Update volume mounts to use `/srv/tranquil-pds/data/` paths
290- Update nginx config path to `/srv/tranquil-pds/config/nginx.conf`
291
292Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths:
293- Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/`
294
295## Create OpenRC Service
296
297```sh
298cat > /etc/init.d/tranquil-pds << 'EOF'
299#!/sbin/openrc-run
300name="tranquil-pds"
301description="Tranquil PDS AT Protocol PDS (containerized)"
302command="/usr/bin/podman-compose"
303command_args="-f /srv/tranquil-pds/docker-compose.yml up"
304command_background=true
305pidfile="/run/${RC_SVCNAME}.pid"
306directory="/srv/tranquil-pds"
307depend() {
308 need net podman
309 after firewall
310}
311start_pre() {
312 set -a
313 . /srv/tranquil-pds/config/tranquil-pds.env
314 set +a
315}
316stop() {
317 ebegin "Stopping ${name}"
318 cd /srv/tranquil-pds
319 set -a
320 . /srv/tranquil-pds/config/tranquil-pds.env
321 set +a
322 podman-compose -f /srv/tranquil-pds/docker-compose.yml down
323 eend $?
324}
325EOF
326chmod +x /etc/init.d/tranquil-pds
327```
328
329## Initialize Services
330
331Start services:
332```sh
333rc-service tranquil-pds start
334sleep 15
335```
336
337Run migrations:
338```sh
339apk add rustup
340rustup-init -y
341source ~/.cargo/env
342cargo install sqlx-cli --no-default-features --features postgres
343DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}')
344DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations
345```
346
347## Obtain Wildcard SSL Certificate
348
349User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
350
351Create temporary self-signed cert to start services:
352```sh
353openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
354 -keyout /srv/tranquil-pds/data/certs/privkey.pem \
355 -out /srv/tranquil-pds/data/certs/fullchain.pem \
356 -subj "/CN=pds.example.com"
357rc-service tranquil-pds restart
358```
359
360Get a wildcard certificate using DNS validation:
361```sh
362podman run --rm -it \
363 -v /srv/tranquil-pds/data/certs:/etc/letsencrypt \
364 docker.io/certbot/certbot:v5.2.2 certonly \
365 --manual --preferred-challenges dns \
366 -d pds.example.com -d '*.pds.example.com' \
367 --agree-tos --email you@example.com
368```
369
370Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
371
372Link certificates and restart:
373```sh
374ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/data/certs/fullchain.pem
375ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem
376rc-service tranquil-pds restart
377```
378
379## Enable Service at Boot
380
381```sh
382rc-update add tranquil-pds
383```
384
385## Configure Firewall
386
387```sh
388apk add iptables ip6tables
389iptables -A INPUT -p tcp --dport 22 -j ACCEPT
390iptables -A INPUT -p tcp --dport 80 -j ACCEPT
391iptables -A INPUT -p tcp --dport 443 -j ACCEPT
392iptables -A INPUT -i lo -j ACCEPT
393iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
394iptables -P INPUT DROP
395ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT
396ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT
397ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT
398ip6tables -A INPUT -i lo -j ACCEPT
399ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
400ip6tables -P INPUT DROP
401rc-update add iptables
402rc-update add ip6tables
403/etc/init.d/iptables save
404/etc/init.d/ip6tables save
405```
406
407## Certificate Renewal
408
409Add to root's crontab (`crontab -e`):
410```
4110 0 * * * podman run --rm -v /srv/tranquil-pds/data/certs:/etc/letsencrypt -v /srv/tranquil-pds/data/acme:/var/www/acme docker.io/certbot/certbot:v5.2.2 renew --quiet && rc-service tranquil-pds restart
412```
413
414---
415
416# Verification and Maintenance
417
418## Verify Installation
419
420```sh
421curl -s https://pds.example.com/xrpc/_health | jq
422curl -s https://pds.example.com/.well-known/atproto-did
423```
424
425## View Logs
426
427**Debian:**
428```bash
429journalctl -u tranquil-pds-app -f
430podman logs -f tranquil-pds-app
431podman logs -f tranquil-pds-frontend
432```
433
434**Alpine:**
435```sh
436podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f
437podman logs -f tranquil-pds-tranquil-pds-1
438podman logs -f tranquil-pds-frontend-1
439```
440
441## Update Tranquil PDS
442
443```sh
444cd /opt/tranquil-pds
445git pull
446podman build -t tranquil-pds:latest .
447podman build -t tranquil-pds-frontend:latest ./frontend
448```
449
450Debian:
451```bash
452systemctl restart tranquil-pds-app tranquil-pds-frontend
453```
454
455Alpine:
456```sh
457rc-service tranquil-pds restart
458```
459
460## Backup Database
461
462**Debian:**
463```bash
464podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
465```
466
467**Alpine:**
468```sh
469podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
470```
471
472## Custom Homepage
473
474The frontend container serves `homepage.html` as the landing page. To customize it, either:
475
4761. Build a custom frontend image with your own `homepage.html`
4772. Mount a custom `homepage.html` into the frontend container
478
479Example custom homepage:
480```html
481<!DOCTYPE html>
482<html>
483<head>
484 <title>Welcome to my PDS</title>
485 <style>
486 body { font-family: system-ui; max-width: 600px; margin: 100px auto; padding: 20px; }
487 </style>
488</head>
489<body>
490 <h1>Welcome to my dark web popsocket store</h1>
491 <p>This is a <a href="https://atproto.com">AT Protocol</a> Personal Data Server.</p>
492 <p><a href="/app/">Sign in</a> or learn more at <a href="https://bsky.social">Bluesky</a>.</p>
493</body>
494</html>
495```