Our Personal Data Server from scratch!
at main 495 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 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```