# Tranquil PDS containerized production deployment This guide covers deploying Tranquil PDS using containers with podman. - **Debian 13+**: Uses systemd quadlets - **Alpine 3.23+**: Uses per-container OpenRC init scripts ## Prerequisites - A server :p - Disk space for blobs, around* 1GB per active user as a baseline - A domain name pointing to your server's IP - A **wildcard TLS certificate** for `*.pds.example.com`, since user handles are served as subdomains - Root/sudo/doas access > 🦪 Lewis > > * "around" here meaning "at absolute least!" ## Reverse proxy The 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`. Caddy 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. ### Terminating TLS in Tranquil You 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. Tranquil 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. ### Client IP and forwarded headers Rate 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. ## Quickstart (docker/podman compose) If you just want to get running quickly: ```sh cp example.toml config.toml ``` Edit `config.toml` with your values. Generate secrets with `openssl rand -base64 48`. `docker-compose.prod.yaml` pulls the prebuilt image from atcr.io. Sign in to the registry first with `podman login atcr.io`. nginx will not start without a certificate, so create a temporary self-signed one, bring the stack up, then swap in a real wildcard cert: ```sh mkdir -p certs openssl req -x509 -nodes -days 1 -newkey rsa:2048 \ -keyout certs/privkey.pem -out certs/fullchain.pem \ -subj "/CN=pds.example.com" podman-compose -f docker-compose.prod.yaml up -d ``` To build the image from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` before bringing the stack up. User 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: ```sh podman-compose -f docker-compose.prod.yaml restart nginx ``` ## Standalone container without compose If you already have postgres running on the host, you can run just the app container. Pull the image. atcr.io requires authentication, so sign in first: ```sh podman login atcr.io podman pull atcr.io/tranquil.farm/tranquil-pds:latest ``` Run with host networking so it can reach postgres on localhost, and mount config + storage: ```sh podman run -d --name tranquil-pds \ --network=host \ -v /etc/tranquil-pds/config.toml:/etc/tranquil-pds/config.toml:ro,Z \ -v /var/lib/tranquil-pds:/var/lib/tranquil-pds:Z \ atcr.io/tranquil.farm/tranquil-pds:latest ``` To build from source instead, run `podman build -t atcr.io/tranquil.farm/tranquil-pds:latest .` and use that tag. Then point your reverse proxy at the app on port 3000 for every route. With nginx that looks like: ```nginx location /xrpc/ { proxy_pass http://[::1]:3000; # full proxy headers are in deploy/nginx/nginx-pod.conf } location / { proxy_pass http://[::1]:3000; 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; } ``` With 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. The end!!! Or wait, you want more? Perhaps a deployment that comes back on server restart? --- # Common setup Both 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. ## Install podman **Debian:** ```bash apt update apt install -y podman ``` **Alpine:** ```sh apk update apk add podman fuse-overlayfs rc-update add cgroups rc-service cgroups start ``` ## Create the directory structure ```sh mkdir -p /srv/tranquil-pds/{postgres,blobs,store,certs,acme,config} ``` ## Clone the repo and pull the image The repo provides the quadlet, OpenRC, and nginx files. The image comes prebuilt from atcr.io. Sign in before pulling: ```sh cd /opt git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds podman login atcr.io podman pull atcr.io/tranquil.farm/tranquil-pds:latest ``` To build from source instead, tag it with the same name so the service uses it: ```sh cd tranquil-pds && podman build -t atcr.io/tranquil.farm/tranquil-pds:latest . ``` ## Create a configuration file ```sh cp /opt/tranquil-pds/example.toml /srv/tranquil-pds/config/config.toml chmod 600 /srv/tranquil-pds/config/config.toml ``` Edit `/srv/tranquil-pds/config/config.toml` and fill in your values. Generate secrets with: ```sh openssl rand -base64 48 ``` > 🦪 Lewis > > Every config option can also be set via an environment variable > named in the comments in `example.toml`. Environment variables always take > precedence over the config file. ## Create the nginx config and database secret The 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`: ```sh cp /opt/tranquil-pds/deploy/nginx/nginx-pod.conf /srv/tranquil-pds/config/nginx.conf ``` Create the podman secret for the database password. Use the same password in the `database.url` of your `config.toml`: ```sh echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - ``` ## Create a temporary TLS certificate The 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: ```sh 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" ``` If your proxy obtains its own certificates, like Caddy does, you can skip this step! The app runs migrations itself on first boot btw, so there is no separate migration step if you're looking for one. --- # Debian with systemd quadlets Quadlets are a nice way to run podman containers under systemd. ## Install quadlet definitions Copy the quadlet files from the repository: ```bash mkdir -p /etc/containers/systemd 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-nginx.container /etc/containers/systemd/ ``` Optional quadlets for valkey and minio are also available in `deploy/quadlets/` if you need them. ## Start services ```bash systemctl daemon-reload systemctl start tranquil-pds-db sleep 10 systemctl start tranquil-pds-app tranquil-pds-nginx ``` ## Enable all services ```bash systemctl enable tranquil-pds-db tranquil-pds-app tranquil-pds-nginx ``` ## Configure firewall if you're into that sort of thing ```bash apt install -y ufw ufw allow ssh ufw allow 80/tcp ufw allow 443/tcp ufw enable ``` Now finish in the Wildcard TLS certificate section below. --- # Alpine with OpenRC Alpine uses OpenRC, not systemd, bless its soul. So instead of quadlets we use a set of OpenRC init scripts, one per container. ## Install the OpenRC services Copy the init scripts. They run the pod, postgres, the app, and nginx as separate services ordered with `depend()`: ```sh cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-pod /etc/init.d/ cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-db /etc/init.d/ cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-app /etc/init.d/ cp /opt/tranquil-pds/deploy/openrc/tranquil-pds-nginx /etc/init.d/ chmod +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 ``` The 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. ## Start services Starting the nginx service pulls in the pod, postgres, and app through its dependencies: ```sh rc-service tranquil-pds-nginx start ``` ## Enable services at boot time ```sh rc-update add tranquil-pds-pod tranquil-pds-db tranquil-pds-app tranquil-pds-nginx ``` ## Configure firewall if you're into that sort of thing ```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 ``` Now finish in the Wildcard TLS certificate section below. --- # Wildcard TLS certificate This section sets up the real certificate for the bundled nginx. With 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. Get a wildcard certificate using DNS validation: ```sh 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 or something. Link the certificates into place: ```sh ln -sf live/pds.example.com/fullchain.pem /srv/tranquil-pds/certs/fullchain.pem ln -sf live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem ``` Restart nginx to load them: **Debian:** ```bash systemctl restart tranquil-pds-nginx ``` **Alpine:** ```sh rc-service tranquil-pds-nginx restart ``` ## Cert renewal Manual mode doesn't auto-renew, so add a renewal job to root's crontab with `crontab -e`. **Debian:** ``` 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:** ``` 0 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 ``` --- # 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 ``` **Alpine:** ```sh rc-service tranquil-pds-app status podman logs -f tranquil-pds-app ``` ## Update Tranquil PDS Pull the latest image: ```sh podman login atcr.io podman pull atcr.io/tranquil.farm/tranquil-pds:latest ``` Debian: ```bash systemctl restart tranquil-pds-app ``` Alpine: ```sh rc-service tranquil-pds-app restart ``` To 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. ## Backup database ```sh podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ``` ## Custom homepage If 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: ```sh -v /srv/tranquil-pds/homepage.html:/var/lib/tranquil-pds/frontend/homepage.html:ro,Z ``` For ex: ```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.

```