forked from
tranquil.farm/tranquil-pds
Our Personal Data Server from scratch!
1#!/bin/bash
2set -euo pipefail
3
4RED='\033[0;31m'
5GREEN='\033[0;32m'
6YELLOW='\033[1;33m'
7BLUE='\033[0;34m'
8NC='\033[0m'
9
10log_info() { echo -e "${BLUE}[INFO]${NC} $1"; }
11log_success() { echo -e "${GREEN}[OK]${NC} $1"; }
12log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
13log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
14
15if [[ $EUID -ne 0 ]]; then
16 log_error "This script must be run as root"
17 exit 1
18fi
19
20if ! grep -qi "debian" /etc/os-release 2>/dev/null; then
21 log_warn "This script is designed for Debian. Proceed with caution on other distros."
22fi
23
24nuke_installation() {
25 log_warn "NUKING EXISTING INSTALLATION"
26 log_info "Stopping services..."
27 systemctl stop tranquil-pds 2>/dev/null || true
28 systemctl disable tranquil-pds 2>/dev/null || true
29
30 log_info "Removing Tranquil PDS files..."
31 rm -rf /opt/tranquil-pds
32 rm -rf /var/lib/tranquil-pds
33 rm -f /usr/local/bin/tranquil-pds
34 rm -f /usr/local/bin/tranquil-pds-sendmail
35 rm -f /usr/local/bin/tranquil-pds-mailq
36 rm -rf /var/spool/tranquil-pds-mail
37 rm -f /etc/systemd/system/tranquil-pds.service
38 systemctl daemon-reload
39
40 log_info "Removing Tranquil PDS configuration..."
41 rm -rf /etc/tranquil-pds
42
43 log_info "Dropping postgres database and user..."
44 sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true
45 sudo -u postgres psql -c "DROP USER IF EXISTS tranquil_pds;" 2>/dev/null || true
46
47 log_info "Removing blob storage..."
48 rm -rf /var/lib/tranquil 2>/dev/null || true
49
50 log_info "Removing nginx config..."
51 rm -f /etc/nginx/sites-enabled/tranquil-pds
52 rm -f /etc/nginx/sites-available/tranquil-pds
53 systemctl reload nginx 2>/dev/null || true
54
55 log_success "Previous installation nuked"
56}
57
58if [[ -f /etc/tranquil-pds/tranquil-pds.env ]] || [[ -d /opt/tranquil-pds ]] || [[ -f /usr/local/bin/tranquil-pds ]]; then
59 log_warn "Existing installation detected"
60 echo ""
61 echo "Options:"
62 echo " 1) Nuke everything and start fresh (destroys database!)"
63 echo " 2) Continue with existing installation (idempotent update)"
64 echo " 3) Exit"
65 echo ""
66 read -p "Choose an option [1/2/3]: " INSTALL_CHOICE
67
68 case "$INSTALL_CHOICE" in
69 1)
70 echo ""
71 log_warn "This will DELETE:"
72 echo " - PostgreSQL database 'pds' and all data"
73 echo " - All Tranquil PDS configuration and credentials"
74 echo " - All source code in /opt/tranquil-pds"
75 echo " - All blobs and backups in /var/lib/tranquil/"
76 echo ""
77 read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE
78 if [[ "$CONFIRM_NUKE" == "NUKE" ]]; then
79 nuke_installation
80 else
81 log_error "Nuke cancelled"
82 exit 1
83 fi
84 ;;
85 2)
86 log_info "Continuing with existing installation..."
87 ;;
88 3)
89 exit 0
90 ;;
91 *)
92 log_error "Invalid option"
93 exit 1
94 ;;
95 esac
96fi
97
98echo ""
99log_info "Tranquil PDS Installation Script for Debian"
100echo ""
101
102get_public_ips() {
103 IPV4=$(curl -4 -s --max-time 5 ifconfig.me 2>/dev/null || curl -4 -s --max-time 5 icanhazip.com 2>/dev/null || echo "Could not detect")
104 IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "")
105}
106
107log_info "Detecting public IP addresses..."
108get_public_ips
109echo " IPv4: ${IPV4}"
110[[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}"
111echo ""
112
113read -p "Enter your PDS domain (eg., pds.example.com): " PDS_DOMAIN
114if [[ -z "$PDS_DOMAIN" ]]; then
115 log_error "Domain cannot be empty"
116 exit 1
117fi
118
119read -p "Enter your email for Let's Encrypt: " CERTBOT_EMAIL
120if [[ -z "$CERTBOT_EMAIL" ]]; then
121 log_error "Email cannot be empty"
122 exit 1
123fi
124
125echo ""
126log_info "DNS records required (create these now if you haven't):"
127echo ""
128echo " ${PDS_DOMAIN} A ${IPV4}"
129[[ -n "$IPV6" ]] && echo " ${PDS_DOMAIN} AAAA ${IPV6}"
130echo " *.${PDS_DOMAIN} A ${IPV4} (for user handles)"
131[[ -n "$IPV6" ]] && echo " *.${PDS_DOMAIN} AAAA ${IPV6} (for user handles)"
132echo ""
133read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED
134if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then
135 log_warn "Please create the DNS records and run this script again."
136 exit 0
137fi
138
139CREDENTIALS_FILE="/etc/tranquil-pds/.credentials"
140if [[ -f "$CREDENTIALS_FILE" ]]; then
141 log_info "Loading existing credentials..."
142 source "$CREDENTIALS_FILE"
143else
144 log_info "Generating secrets..."
145 JWT_SECRET=$(openssl rand -base64 48)
146 DPOP_SECRET=$(openssl rand -base64 48)
147 MASTER_KEY=$(openssl rand -base64 48)
148 DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32)
149
150 mkdir -p /etc/tranquil-pds
151 cat > "$CREDENTIALS_FILE" << EOF
152JWT_SECRET="$JWT_SECRET"
153DPOP_SECRET="$DPOP_SECRET"
154MASTER_KEY="$MASTER_KEY"
155DB_PASSWORD="$DB_PASSWORD"
156EOF
157 chmod 600 "$CREDENTIALS_FILE"
158 log_success "Secrets generated"
159fi
160
161log_info "Checking swap space..."
162TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}')
163TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}')
164
165if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then
166 if [[ ! -f /swapfile ]]; then
167 log_info "Adding swap space for compilation..."
168 SWAP_SIZE="4G"
169 [[ $TOTAL_MEM_KB -ge 4000000 ]] && SWAP_SIZE="2G"
170 fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096
171 chmod 600 /swapfile
172 mkswap /swapfile
173 swapon /swapfile
174 grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab
175 log_success "Swap added ($SWAP_SIZE)"
176 else
177 swapon /swapfile 2>/dev/null || true
178 fi
179fi
180
181log_info "Updating system packages..."
182apt update && apt upgrade -y
183
184log_info "Installing build dependencies..."
185apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd
186
187log_info "Installing postgres..."
188apt install -y postgresql postgresql-contrib
189systemctl enable postgresql
190systemctl start postgresql
191sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \
192 sudo -u postgres psql -c "ALTER USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';"
193sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 2>/dev/null || true
194sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;"
195log_success "postgres configured"
196
197log_info "Installing valkey..."
198apt install -y valkey 2>/dev/null || {
199 log_warn "valkey not in repos, installing redis..."
200 apt install -y redis-server
201 systemctl enable redis-server
202 systemctl start redis-server
203}
204systemctl enable valkey-server 2>/dev/null || true
205systemctl start valkey-server 2>/dev/null || true
206
207log_info "Creating blob storage directories..."
208mkdir -p /var/lib/tranquil/blobs /var/lib/tranquil/backups
209log_success "Blob storage directories created"
210
211log_info "Installing rust..."
212if [[ -f "$HOME/.cargo/env" ]]; then
213 source "$HOME/.cargo/env"
214fi
215if ! command -v rustc &>/dev/null; then
216 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
217 source "$HOME/.cargo/env"
218fi
219
220log_info "Installing deno..."
221export PATH="$HOME/.deno/bin:$PATH"
222if ! command -v deno &>/dev/null && [[ ! -f "$HOME/.deno/bin/deno" ]]; then
223 curl -fsSL https://deno.land/install.sh | sh
224 grep -q 'deno/bin' ~/.bashrc 2>/dev/null || echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
225fi
226
227log_info "Cloning Tranquil PDS..."
228if [[ ! -d /opt/tranquil-pds ]]; then
229 git clone https://tangled.org/tranquil.farm/tranquil-pds /opt/tranquil-pds
230else
231 cd /opt/tranquil-pds && git pull
232fi
233cd /opt/tranquil-pds
234
235log_info "Building frontend..."
236"$HOME/.deno/bin/deno" task build --filter=frontend
237log_success "Frontend built"
238
239log_info "Building Tranquil PDS (this takes a while)..."
240source "$HOME/.cargo/env"
241if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then
242 log_info "Low memory - limiting parallel jobs"
243 CARGO_BUILD_JOBS=1 cargo build --release
244else
245 cargo build --release
246fi
247log_success "Tranquil PDS built"
248
249log_info "Running migrations..."
250cargo install sqlx-cli --no-default-features --features postgres
251export DATABASE_URL="postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds"
252"$HOME/.cargo/bin/sqlx" migrate run
253log_success "Migrations complete"
254
255log_info "Setting up mail trap..."
256mkdir -p /var/spool/tranquil-pds-mail
257chmod 1777 /var/spool/tranquil-pds-mail
258
259cat > /usr/local/bin/tranquil-pds-sendmail << 'SENDMAIL_EOF'
260#!/bin/bash
261MAIL_DIR="/var/spool/tranquil-pds-mail"
262TIMESTAMP=$(date +%Y%m%d-%H%M%S)
263RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p)
264MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml"
265mkdir -p "$MAIL_DIR"
266{
267 echo "X-Tranquil-PDS-Received: $(date -Iseconds)"
268 echo "X-Tranquil-PDS-Args: $*"
269 echo ""
270 cat
271} > "$MAIL_FILE"
272chmod 644 "$MAIL_FILE"
273exit 0
274SENDMAIL_EOF
275chmod +x /usr/local/bin/tranquil-pds-sendmail
276
277cat > /usr/local/bin/tranquil-pds-mailq << 'MAILQ_EOF'
278#!/bin/bash
279MAIL_DIR="/var/spool/tranquil-pds-mail"
280case "${1:-list}" in
281 list)
282 ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails"
283 ;;
284 latest)
285 f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1)
286 [[ -f "$f" ]] && cat "$f" || echo "No emails"
287 ;;
288 clear)
289 rm -f "$MAIL_DIR"/*.eml
290 echo "Cleared"
291 ;;
292 count)
293 ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l
294 ;;
295 [0-9]*)
296 f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${1}p")
297 [[ -f "$f" ]] && cat "$f" || echo "Not found"
298 ;;
299 *)
300 [[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: tranquil-pds-mailq [list|latest|clear|count|N]"
301 ;;
302esac
303MAILQ_EOF
304chmod +x /usr/local/bin/tranquil-pds-mailq
305
306log_info "Creating Tranquil PDS configuration..."
307cat > /etc/tranquil-pds/tranquil-pds.env << EOF
308SERVER_HOST=127.0.0.1
309SERVER_PORT=3000
310PDS_HOSTNAME=${PDS_DOMAIN}
311DATABASE_URL=postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds
312DATABASE_MAX_CONNECTIONS=100
313DATABASE_MIN_CONNECTIONS=10
314BLOB_STORAGE_PATH=/var/lib/tranquil/blobs
315BACKUP_STORAGE_PATH=/var/lib/tranquil/backups
316VALKEY_URL=redis://localhost:6379
317JWT_SECRET=${JWT_SECRET}
318DPOP_SECRET=${DPOP_SECRET}
319MASTER_KEY=${MASTER_KEY}
320PLC_DIRECTORY_URL=https://plc.directory
321CRAWLERS=https://bsky.network
322AVAILABLE_USER_DOMAINS=${PDS_DOMAIN}
323MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN}
324MAIL_FROM_NAME=Tranquil PDS
325SENDMAIL_PATH=/usr/local/bin/tranquil-pds-sendmail
326EOF
327chmod 600 /etc/tranquil-pds/tranquil-pds.env
328
329log_info "Installing Tranquil PDS..."
330id -u tranquil-pds &>/dev/null || useradd -r -s /sbin/nologin tranquil-pds
331cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/
332mkdir -p /var/lib/tranquil-pds
333cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend
334chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds
335chown -R tranquil-pds:tranquil-pds /var/lib/tranquil
336
337cat > /etc/systemd/system/tranquil-pds.service << 'EOF'
338[Unit]
339Description=Tranquil PDS - AT Protocol PDS
340After=network.target postgresql.service
341
342[Service]
343Type=simple
344User=tranquil-pds
345Group=tranquil-pds
346EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env
347ExecStart=/usr/local/bin/tranquil-pds
348Restart=always
349RestartSec=5
350ProtectSystem=strict
351ProtectHome=true
352PrivateTmp=true
353ReadWritePaths=/var/lib/tranquil
354
355[Install]
356WantedBy=multi-user.target
357EOF
358
359systemctl daemon-reload
360systemctl enable tranquil-pds
361systemctl start tranquil-pds
362log_success "Tranquil PDS service started"
363
364log_info "Installing nginx..."
365apt install -y nginx
366cat > /etc/nginx/sites-available/tranquil-pds << EOF
367server {
368 listen 80;
369 listen [::]:80;
370 server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
371
372 location /.well-known/acme-challenge/ {
373 root /var/www/html;
374 }
375
376 location / {
377 proxy_pass http://127.0.0.1:3000;
378 proxy_http_version 1.1;
379 proxy_set_header Upgrade \$http_upgrade;
380 proxy_set_header Connection "upgrade";
381 proxy_set_header Host \$host;
382 proxy_set_header X-Real-IP \$remote_addr;
383 proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
384 proxy_set_header X-Forwarded-Proto \$scheme;
385 proxy_read_timeout 86400;
386 proxy_send_timeout 86400;
387 client_max_body_size 100M;
388 }
389}
390EOF
391
392ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/
393rm -f /etc/nginx/sites-enabled/default
394nginx -t
395systemctl reload nginx
396log_success "nginx configured"
397
398log_info "Configuring firewall..."
399apt install -y ufw
400ufw --force reset
401ufw default deny incoming
402ufw default allow outgoing
403ufw allow ssh
404ufw allow 80/tcp
405ufw allow 443/tcp
406ufw --force enable
407log_success "Firewall configured"
408
409echo ""
410log_info "Obtaining wildcard SSL certificate..."
411echo ""
412echo "User handles are served as subdomains (eg., alice.${PDS_DOMAIN}),"
413echo "so you need a wildcard certificate. This requires DNS validation."
414echo ""
415echo "You'll need to add a TXT record to your DNS when prompted."
416echo ""
417read -p "Ready to proceed? (y/N): " CERT_READY
418
419if [[ "$CERT_READY" =~ ^[Yy]$ ]]; then
420 apt install -y certbot python3-certbot-nginx
421
422 log_info "Running certbot with DNS challenge..."
423 echo ""
424 echo "When prompted, add the TXT record to your DNS, wait a minute"
425 echo "for propagation, then press Enter to continue."
426 echo ""
427
428 if certbot certonly --manual --preferred-challenges dns \
429 -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \
430 --email "${CERTBOT_EMAIL}" --agree-tos; then
431
432 cat > /etc/nginx/sites-available/tranquil-pds << EOF
433server {
434 listen 80;
435 listen [::]:80;
436 server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
437
438 location /.well-known/acme-challenge/ {
439 root /var/www/html;
440 }
441
442 location / {
443 return 301 https://\$host\$request_uri;
444 }
445}
446
447server {
448 listen 443 ssl http2;
449 listen [::]:443 ssl http2;
450 server_name ${PDS_DOMAIN} *.${PDS_DOMAIN};
451
452 ssl_certificate /etc/letsencrypt/live/${PDS_DOMAIN}/fullchain.pem;
453 ssl_certificate_key /etc/letsencrypt/live/${PDS_DOMAIN}/privkey.pem;
454 ssl_protocols TLSv1.2 TLSv1.3;
455 ssl_ciphers HIGH:!aNULL:!MD5;
456 ssl_prefer_server_ciphers on;
457 ssl_session_cache shared:SSL:10m;
458
459 location / {
460 proxy_pass http://127.0.0.1:3000;
461 proxy_http_version 1.1;
462 proxy_set_header Upgrade \$http_upgrade;
463 proxy_set_header Connection "upgrade";
464 proxy_set_header Host \$host;
465 proxy_set_header X-Real-IP \$remote_addr;
466 proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
467 proxy_set_header X-Forwarded-Proto \$scheme;
468 proxy_read_timeout 86400;
469 proxy_send_timeout 86400;
470 client_max_body_size 100M;
471 }
472}
473EOF
474 nginx -t && systemctl reload nginx
475 log_success "Wildcard SSL certificate installed"
476
477 echo ""
478 log_warn "Certificate renewal note:"
479 echo "Manual DNS challenges don't auto-renew. Before expiry, run:"
480 echo " certbot renew --manual"
481 echo ""
482 echo "For auto-renewal, consider using a DNS provider plugin:"
483 echo " apt install python3-certbot-dns-cloudflare # or your provider"
484 echo ""
485 else
486 log_warn "Wildcard cert failed. You can retry later with:"
487 echo " certbot certonly --manual --preferred-challenges dns \\"
488 echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
489 fi
490else
491 log_warn "Skipping SSL. Your PDS is running on HTTP only."
492 echo "To add SSL later, run:"
493 echo " certbot certonly --manual --preferred-challenges dns \\"
494 echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'"
495fi
496
497log_info "Verifying installation..."
498sleep 3
499if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then
500 log_success "Tranquil PDS is responding"
501else
502 log_warn "Tranquil PDS may still be starting. Check: journalctl -u tranquil-pds -f"
503fi
504
505echo ""
506log_success "Installation complete"
507echo ""
508echo "PDS: https://${PDS_DOMAIN}"
509echo ""
510echo "Credentials (also in /etc/tranquil-pds/.credentials):"
511echo " DB password: ${DB_PASSWORD}"
512echo ""
513echo "Data locations:"
514echo " Blobs: /var/lib/tranquil/blobs"
515echo " Backups: /var/lib/tranquil/backups"
516echo ""
517echo "Commands:"
518echo " journalctl -u tranquil-pds -f # logs"
519echo " systemctl restart tranquil-pds # restart"
520echo " tranquil-pds-mailq # view trapped emails"
521echo ""