mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-02-08 21:30:08 +00:00
501 lines
14 KiB
Markdown
501 lines
14 KiB
Markdown
# 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
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>Welcome to my PDS</title>
|
|
<style>
|
|
body { font-family: system-ui; max-width: 600px; margin: 100px auto; padding: 20px; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Welcome to my dark web popsocket store</h1>
|
|
<p>This is a <a href="https://atproto.com">AT Protocol</a> Personal Data Server.</p>
|
|
<p><a href="/app/">Sign in</a> or learn more at <a href="https://bsky.social">Bluesky</a>.</p>
|
|
</body>
|
|
</html>
|
|
```
|