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