Tranquil PDS production installation on debian#
This guide covers installing Tranquil PDS on Debian.
It is a "compile the thing on the server itself" -style guide. This cop-out is because Tranquil isn't built and released via CI as of yet.
Prerequisites#
- A server :p
- Disk space enough 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/sudo/doas access
System setup#
apt update && apt upgrade -y
apt install -y curl git build-essential pkg-config libssl-dev
Install rust#
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
source ~/.cargo/env
rustup default stable
This installs the latest stable Rust.
Install postgres#
apt install -y postgresql postgresql-contrib
systemctl enable postgresql
systemctl start postgresql
sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD 'your-secure-password';"
sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;"
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;"
Create blob storage directories#
mkdir -p /var/lib/tranquil/blobs /var/lib/tranquil/backups
We'll set ownership after creating the service user.
Install deno (for frontend build)#
curl -fsSL https://deno.land/install.sh | sh
export PATH="$HOME/.deno/bin:$PATH"
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
Clone and build Tranquil PDS#
cd /opt
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
cd tranquil-pds
cd frontend
deno task build
cd ..
cargo build --release
Configure Tranquil PDS#
mkdir -p /etc/tranquil-pds
cp /opt/tranquil-pds/example.toml /etc/tranquil-pds/config.toml
chmod 600 /etc/tranquil-pds/config.toml
Edit /etc/tranquil-pds/config.toml and fill in your values. Generate secrets with:
openssl rand -base64 48
Note: Every config option can also be set via environment variables (see comments in
example.toml). Environment variables always take precedence over the config file. You can also pass the config file path via theTRANQUIL_PDS_CONFIGenv var instead of--config.
You can validate your configuration before starting the service:
/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate
Install frontend files#
mkdir -p /var/www/tranquil-pds
cp -r /opt/tranquil-pds/frontend/dist/* /var/www/tranquil-pds/
chown -R www-data:www-data /var/www/tranquil-pds
Create systemd service#
useradd -r -s /sbin/nologin tranquil-pds
chown -R tranquil-pds:tranquil-pds /var/lib/tranquil
cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/
cat > /etc/systemd/system/tranquil-pds.service << 'EOF'
[Unit]
Description=Tranquil PDS - AT Protocol PDS
After=network.target postgresql.service
[Service]
Type=simple
User=tranquil-pds
Group=tranquil-pds
ExecStart=/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml
Restart=always
RestartSec=5
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/tranquil
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable tranquil-pds
systemctl start tranquil-pds
Install and configure nginx#
apt install -y nginx certbot python3-certbot-nginx
cat > /etc/nginx/sites-available/tranquil-pds << 'EOF'
server {
listen 80;
listen [::]:80;
server_name pds.example.com *.pds.example.com;
location /.well-known/acme-challenge/ {
root /var/www/acme;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name pds.example.com *.pds.example.com;
ssl_certificate /etc/letsencrypt/live/pds.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/pds.example.com/privkey.pem;
client_max_body_size 10G;
root /var/www/tranquil-pds;
location /xrpc/ {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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;
proxy_read_timeout 86400;
proxy_send_timeout 86400;
proxy_buffering off;
proxy_request_buffering off;
}
location = /oauth-client-metadata.json {
root /var/www/tranquil-pds;
default_type application/json;
sub_filter_once off;
sub_filter_types application/json;
sub_filter '__PDS_HOSTNAME__' $host;
}
location /oauth/ {
proxy_pass http://127.0.0.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;
proxy_read_timeout 300;
proxy_send_timeout 300;
}
location /.well-known/ {
proxy_pass http://127.0.0.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;
}
location /webhook/ {
proxy_pass http://127.0.0.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;
}
location = /metrics {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /health {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /robots.txt {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location = /logo {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
}
location ~ ^/u/[^/]+/did\.json$ {
proxy_pass http://127.0.0.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;
}
location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
try_files $uri =404;
}
location /app/ {
try_files $uri $uri/ /index.html;
}
location = / {
try_files /homepage.html /index.html;
}
location / {
try_files $uri $uri/ /index.html;
}
}
EOF
ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
mkdir -p /var/www/acme
nginx -t
systemctl reload nginx
Obtain a wildcard SSL cert#
User handles are served as subdomains (eg., alice.pds.example.com), so you need a wildcard certificate.
Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin:
apt install -y python3-certbot-dns-cloudflare
certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /etc/cloudflare.ini \
-d pds.example.com -d '*.pds.example.com'
For manual DNS validation (works with any provider):
certbot certonly --manual --preferred-challenges dns \
-d pds.example.com -d '*.pds.example.com'
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
After obtaining the cert, reload nginx:
systemctl reload nginx
Configure firewall if you're into that sort of thing#
apt install -y ufw
ufw allow ssh
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
Verify installation#
systemctl status tranquil-pds
curl -s https://pds.example.com/xrpc/_health | jq
curl -s https://pds.example.com/.well-known/atproto-did
Maintenance#
View logs:
journalctl -u tranquil-pds -f
Update Tranquil PDS:
cd /opt/tranquil-pds
git pull
cd frontend && deno task build && cd ..
cargo build --release
systemctl stop tranquil-pds
cp target/release/tranquil-pds /usr/local/bin/
cp -r frontend/dist/* /var/www/tranquil-pds/
systemctl start tranquil-pds
Tranquil should auto-migrate if there are any new migrations to be applied to the db, so you don't need to worry.
Backup database:
sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql
Custom homepage#
Drop a homepage.html in /var/www/tranquil-pds/ and it becomes your landing page. Account dashboard is at /app/ so you won't break anything.
cat > /var/www/tranquil-pds/homepage.html << 'EOF'
<!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 secret PDS</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>
EOF