# Tranquil PDS Containerized Production Deployment This guide covers deploying Tranquil PDS using containers with podman. - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) - **Alpine 3.23+**: Uses OpenRC service script with podman-compose ## Prerequisites - A VPS with at least 2GB RAM - Disk space for blobs (depends on usage; plan for ~1GB per active user as a baseline) - A domain name pointing to your server's IP - A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains) - Root or sudo access ## Quick Start (Docker/Podman Compose) If you just want to get running quickly: ```sh cp .env.example .env ``` Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`. Build and start: ```sh podman build -t tranquil-pds:latest . podman build -t tranquil-pds-frontend:latest ./frontend podman-compose -f docker-compose.prod.yaml up -d ``` Get initial certificate (after DNS is configured): ```sh podman-compose -f docker-compose.prod.yaml run --rm certbot certonly \ --webroot -w /var/www/acme -d pds.example.com -d '*.pds.example.com' ln -sf live/pds.example.com/fullchain.pem certs/fullchain.pem ln -sf live/pds.example.com/privkey.pem certs/privkey.pem podman-compose -f docker-compose.prod.yaml restart nginx ``` For production setups with proper service management, continue to either the Debian or Alpine section below. ## Standalone Containers (No Compose) If you already have postgres running on the host (eg. from the [Debian install guide](install-debian.md)), you can run just the app containers. Build the images: ```sh podman build -t tranquil-pds:latest . podman build -t tranquil-pds-frontend:latest ./frontend ``` Run the backend with host networking (so it can access postgres on localhost) and mount the blob storage: ```sh podman run -d --name tranquil-pds \ --network=host \ --env-file /etc/tranquil-pds/tranquil-pds.env \ -v /var/lib/tranquil:/var/lib/tranquil:Z \ tranquil-pds:latest ``` Run the frontend with port mapping (the container's nginx listens on port 80): ```sh podman run -d --name tranquil-pds-frontend \ -p 8080:80 \ tranquil-pds-frontend:latest ``` Then configure your host nginx to proxy to both containers. Replace the static file `try_files` directives with proxy passes: ```nginx # API routes to backend location /xrpc/ { proxy_pass http://127.0.0.1:3000; # ... (see Debian guide for full proxy headers) } # Static routes to frontend container location / { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } ``` See the [Debian install guide](install-debian.md) for the full nginx config with all API routes. --- # Debian 13+ with Systemd Quadlets Quadlets are the modern way to run podman containers under systemd. ## Install Podman ```bash apt update apt install -y podman ``` ## Create Directory Structure ```bash mkdir -p /etc/containers/systemd mkdir -p /srv/tranquil-pds/{postgres,blobs,backups,certs,acme,config} ``` ## Create Environment File ```bash cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env chmod 600 /srv/tranquil-pds/config/tranquil-pds.env ``` Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: ```bash openssl rand -base64 48 ``` For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion). ## Install Quadlet Definitions Copy the quadlet files from the repository: ```bash cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds.pod /etc/containers/systemd/ cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-db.container /etc/containers/systemd/ cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-app.container /etc/containers/systemd/ cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-frontend.container /etc/containers/systemd/ cp /opt/tranquil-pds/deploy/quadlets/tranquil-pds-nginx.container /etc/containers/systemd/ ``` Optional quadlets for valkey and minio are also available in `deploy/quadlets/` if you need them. Note: Systemd doesn't support shell-style variable expansion in `Environment=` lines. The quadlet files expect DATABASE_URL to be set in the environment file. ## Create nginx Configuration ```bash cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf ``` ## Clone and Build Images ```bash cd /opt git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds cd tranquil-pds podman build -t tranquil-pds:latest . podman build -t tranquil-pds-frontend:latest ./frontend ``` ## Create Podman Secrets ```bash source /srv/tranquil-pds/config/tranquil-pds.env echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - ``` ## Start Services and Initialize ```bash systemctl daemon-reload systemctl start tranquil-pds-db sleep 10 ``` Run migrations: ```bash cargo install sqlx-cli --no-default-features --features postgres DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations ``` ## Obtain Wildcard SSL Certificate User handles are served as subdomains (eg. `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. Create temporary self-signed cert to start services: ```bash openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ -keyout /srv/tranquil-pds/certs/privkey.pem \ -out /srv/tranquil-pds/certs/fullchain.pem \ -subj "/CN=pds.example.com" systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx ``` Get a wildcard certificate using DNS validation: ```bash podman run --rm -it \ -v /srv/tranquil-pds/certs:/etc/letsencrypt:Z \ docker.io/certbot/certbot:v5.2.2 certonly \ --manual --preferred-challenges dns \ -d pds.example.com -d '*.pds.example.com' \ --agree-tos --email you@example.com ``` Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. For automated renewal, use a DNS provider plugin (eg. cloudflare, route53). Link certificates and restart: ```bash ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem systemctl restart tranquil-pds-nginx ``` ## Enable All Services ```bash systemctl enable tranquil-pds-db tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx ``` ## Configure Firewall ```bash apt install -y ufw ufw allow ssh ufw allow 80/tcp ufw allow 443/tcp ufw enable ``` ## Certificate Renewal Add to root's crontab (`crontab -e`): ``` 0 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 ``` --- # Alpine 3.23+ with OpenRC Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper. ## Install Podman ```sh apk update apk add podman podman-compose fuse-overlayfs cni-plugins rc-update add cgroups rc-service cgroups start ``` Enable podman socket for compose: ```sh rc-update add podman rc-service podman start ``` ## Create Directory Structure ```sh mkdir -p /srv/tranquil-pds/{data,config} mkdir -p /srv/tranquil-pds/data/{postgres,blobs,backups,certs,acme} ``` ## Clone Repository and Build Images ```sh cd /opt git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds cd tranquil-pds podman build -t tranquil-pds:latest . podman build -t tranquil-pds-frontend:latest ./frontend ``` ## Create Environment File ```sh cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env chmod 600 /srv/tranquil-pds/config/tranquil-pds.env ``` Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: ```sh openssl rand -base64 48 ``` ## Set Up Compose and nginx Copy the production compose and nginx configs: ```sh cp /opt/tranquil-pds/docker-compose.prod.yaml /srv/tranquil-pds/docker-compose.yml cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf ``` Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed: - Update volume mounts to use `/srv/tranquil-pds/data/` paths - Update nginx config path to `/srv/tranquil-pds/config/nginx.conf` Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths: - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` ## Create OpenRC Service ```sh cat > /etc/init.d/tranquil-pds << 'EOF' #!/sbin/openrc-run name="tranquil-pds" description="Tranquil PDS AT Protocol PDS (containerized)" command="/usr/bin/podman-compose" command_args="-f /srv/tranquil-pds/docker-compose.yml up" command_background=true pidfile="/run/${RC_SVCNAME}.pid" directory="/srv/tranquil-pds" depend() { need net podman after firewall } start_pre() { set -a . /srv/tranquil-pds/config/tranquil-pds.env set +a } stop() { ebegin "Stopping ${name}" cd /srv/tranquil-pds set -a . /srv/tranquil-pds/config/tranquil-pds.env set +a podman-compose -f /srv/tranquil-pds/docker-compose.yml down eend $? } EOF chmod +x /etc/init.d/tranquil-pds ``` ## Initialize Services Start services: ```sh rc-service tranquil-pds start sleep 15 ``` Run migrations: ```sh apk add rustup rustup-init -y source ~/.cargo/env cargo install sqlx-cli --no-default-features --features postgres DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}') DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations ``` ## Obtain Wildcard SSL Certificate User handles are served as subdomains (eg. `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. Create temporary self-signed cert to start services: ```sh openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ -keyout /srv/tranquil-pds/data/certs/privkey.pem \ -out /srv/tranquil-pds/data/certs/fullchain.pem \ -subj "/CN=pds.example.com" rc-service tranquil-pds restart ``` Get a wildcard certificate using DNS validation: ```sh podman run --rm -it \ -v /srv/tranquil-pds/data/certs:/etc/letsencrypt \ docker.io/certbot/certbot:v5.2.2 certonly \ --manual --preferred-challenges dns \ -d pds.example.com -d '*.pds.example.com' \ --agree-tos --email you@example.com ``` Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. Link certificates and restart: ```sh ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/fullchain.pem /srv/tranquil-pds/data/certs/fullchain.pem ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem rc-service tranquil-pds restart ``` ## Enable Service at Boot ```sh rc-update add tranquil-pds ``` ## Configure Firewall ```sh apk add iptables ip6tables iptables -A INPUT -p tcp --dport 22 -j ACCEPT iptables -A INPUT -p tcp --dport 80 -j ACCEPT iptables -A INPUT -p tcp --dport 443 -j ACCEPT iptables -A INPUT -i lo -j ACCEPT iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT iptables -P INPUT DROP ip6tables -A INPUT -p tcp --dport 22 -j ACCEPT ip6tables -A INPUT -p tcp --dport 80 -j ACCEPT ip6tables -A INPUT -p tcp --dport 443 -j ACCEPT ip6tables -A INPUT -i lo -j ACCEPT ip6tables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT ip6tables -P INPUT DROP rc-update add iptables rc-update add ip6tables /etc/init.d/iptables save /etc/init.d/ip6tables save ``` ## Certificate Renewal Add to root's crontab (`crontab -e`): ``` 0 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 ``` --- # Verification and Maintenance ## Verify Installation ```sh curl -s https://pds.example.com/xrpc/_health | jq curl -s https://pds.example.com/.well-known/atproto-did ``` ## View Logs **Debian:** ```bash journalctl -u tranquil-pds-app -f podman logs -f tranquil-pds-app podman logs -f tranquil-pds-frontend ``` **Alpine:** ```sh podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f podman logs -f tranquil-pds-tranquil-pds-1 podman logs -f tranquil-pds-frontend-1 ``` ## Update Tranquil PDS ```sh cd /opt/tranquil-pds git pull podman build -t tranquil-pds:latest . podman build -t tranquil-pds-frontend:latest ./frontend ``` Debian: ```bash systemctl restart tranquil-pds-app tranquil-pds-frontend ``` Alpine: ```sh rc-service tranquil-pds restart ``` ## Backup Database **Debian:** ```bash podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ``` **Alpine:** ```sh podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ``` ## Custom Homepage The frontend container serves `homepage.html` as the landing page. To customize it, either: 1. Build a custom frontend image with your own `homepage.html` 2. Mount a custom `homepage.html` into the frontend container Example custom homepage: ```html Welcome to my PDS

Welcome to my dark web popsocket store

This is a AT Protocol Personal Data Server.

Sign in or learn more at Bluesky.

```