diff --git a/Dockerfile b/Dockerfile index d9d218d..2b68fa6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -50,7 +50,6 @@ RUN apk add --no-cache msmtp ca-certificates \ && ln -sf /usr/bin/msmtp /usr/sbin/sendmail COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds COPY --from=frontend /app/dist /var/lib/tranquil-pds/frontend -COPY migrations /app/migrations WORKDIR /app ENV SERVER_HOST=0.0.0.0 ENV SERVER_PORT=3000 diff --git a/README.md b/README.md index c303989..287bd28 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,6 @@ podman-compose -f docker-compose.prod.yaml up -d ### Installation Guides - [Nix](docs/install-nix.md) -- [Debian](docs/install-debian.md) - [Containers](docs/install-containers.md) - [Kubernetes](docs/install-kubernetes.md) diff --git a/crates/tranquil-config/src/lib.rs b/crates/tranquil-config/src/lib.rs index 93e7a34..983de85 100644 --- a/crates/tranquil-config/src/lib.rs +++ b/crates/tranquil-config/src/lib.rs @@ -83,8 +83,9 @@ pub fn ensure_test_defaults() { /// /// Precedence (highest to lowest): /// 1. Environment variables -/// 2. TOML config file (if provided) -/// 3. Built-in defaults +/// 2. Toml config file passed as `config_path`, if provided +/// 3. `/etc/tranquil-pds/config.toml` - hardcoded fallback, silently skipped if absent +/// 4. Built-in defaults pub fn load(config_path: Option<&PathBuf>) -> Result { let mut builder = TranquilConfig::builder().env(); if let Some(path) = config_path { diff --git a/crates/tranquil-pds/tests/common/mod.rs b/crates/tranquil-pds/tests/common/mod.rs index 598319b..60bc039 100644 --- a/crates/tranquil-pds/tests/common/mod.rs +++ b/crates/tranquil-pds/tests/common/mod.rs @@ -191,16 +191,12 @@ async fn setup_with_external_infra() -> String { async fn setup_with_testcontainers() -> String { let temp_dir = std::env::temp_dir().join(format!("tranquil-pds-test-{}", uuid::Uuid::new_v4())); let blob_path = temp_dir.join("blobs"); - let backup_path = temp_dir.join("backups"); std::fs::create_dir_all(&blob_path).expect("Failed to create blob temp directory"); - std::fs::create_dir_all(&backup_path).expect("Failed to create backup temp directory"); TEST_TEMP_DIR.set(temp_dir).ok(); let plc_url = setup_mock_plc_directory().await; unsafe { std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); - std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); - std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); std::env::set_var("MAX_IMPORT_SIZE", "100000000"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); std::env::set_var("PLC_DIRECTORY_URL", &plc_url); @@ -242,8 +238,6 @@ async fn setup_with_testcontainers() -> String { let plc_url = setup_mock_plc_directory().await; unsafe { std::env::set_var("BLOB_STORAGE_BACKEND", "s3"); - std::env::set_var("BACKUP_STORAGE_BACKEND", "s3"); - std::env::set_var("BACKUP_S3_BUCKET", "test-backups"); std::env::set_var("S3_BUCKET", "test-bucket"); std::env::set_var("AWS_ACCESS_KEY_ID", "minioadmin"); std::env::set_var("AWS_SECRET_ACCESS_KEY", "minioadmin"); @@ -333,8 +327,6 @@ unsafe fn configure_external_storage_env() { if std::env::var("S3_ENDPOINT").is_ok() { let s3_endpoint = std::env::var("S3_ENDPOINT").unwrap(); std::env::set_var("BLOB_STORAGE_BACKEND", "s3"); - std::env::set_var("BACKUP_STORAGE_BACKEND", "s3"); - std::env::set_var("BACKUP_S3_BUCKET", "test-backups"); std::env::set_var( "S3_BUCKET", std::env::var("S3_BUCKET").unwrap_or_else(|_| "test-bucket".to_string()), @@ -356,14 +348,10 @@ unsafe fn configure_external_storage_env() { let process_dir = std::env::temp_dir().join(format!("tranquil-pds-test-{}", std::process::id())); let blob_path = process_dir.join("blobs"); - let backup_path = process_dir.join("backups"); std::fs::create_dir_all(&blob_path).expect("Failed to create blob directory"); - std::fs::create_dir_all(&backup_path).expect("Failed to create backup directory"); TEST_TEMP_DIR.set(process_dir).ok(); std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); - std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); - std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); } std::env::set_var("MAX_IMPORT_SIZE", "100000000"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); @@ -622,18 +610,14 @@ async fn setup_store_backend() -> String { let temp_dir = std::env::temp_dir().join(format!("tranquil-pds-store-{}", uuid::Uuid::new_v4())); let blob_path = temp_dir.join("blobs"); - let backup_path = temp_dir.join("backups"); let store_path = temp_dir.join("store"); std::fs::create_dir_all(&blob_path).expect("failed to create blob temp directory"); - std::fs::create_dir_all(&backup_path).expect("failed to create backup temp directory"); std::fs::create_dir_all(&store_path).expect("failed to create store temp directory"); TEST_TEMP_DIR.set(temp_dir).ok(); let plc_url = setup_mock_plc_directory().await; unsafe { std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); - std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); - std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); std::env::set_var("MAX_IMPORT_SIZE", "100000000"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); std::env::set_var("PLC_DIRECTORY_URL", &plc_url); @@ -790,18 +774,14 @@ async fn setup_cluster_store_backend() -> Option { uuid::Uuid::new_v4() )); let blob_path = temp_dir.join("blobs"); - let backup_path = temp_dir.join("backups"); let store_path = temp_dir.join("store"); std::fs::create_dir_all(&blob_path).expect("failed to create blob temp directory"); - std::fs::create_dir_all(&backup_path).expect("failed to create backup temp directory"); std::fs::create_dir_all(&store_path).expect("failed to create store temp directory"); TEST_TEMP_DIR.set(temp_dir).ok(); let plc_url = setup_mock_plc_directory().await; unsafe { std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); - std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); - std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); std::env::set_var("MAX_IMPORT_SIZE", "100000000"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); std::env::set_var("PLC_DIRECTORY_URL", &plc_url); @@ -847,16 +827,12 @@ async fn setup_cluster_testcontainers() -> Option { let temp_dir = std::env::temp_dir().join(format!("tranquil-pds-cluster-{}", uuid::Uuid::new_v4())); let blob_path = temp_dir.join("blobs"); - let backup_path = temp_dir.join("backups"); std::fs::create_dir_all(&blob_path).expect("Failed to create blob temp directory"); - std::fs::create_dir_all(&backup_path).expect("Failed to create backup temp directory"); TEST_TEMP_DIR.set(temp_dir).ok(); let plc_url = setup_mock_plc_directory().await; unsafe { std::env::set_var("BLOB_STORAGE_BACKEND", "filesystem"); std::env::set_var("BLOB_STORAGE_PATH", blob_path.to_str().unwrap()); - std::env::set_var("BACKUP_STORAGE_BACKEND", "filesystem"); - std::env::set_var("BACKUP_STORAGE_PATH", backup_path.to_str().unwrap()); std::env::set_var("MAX_IMPORT_SIZE", "100000000"); std::env::set_var("SKIP_IMPORT_VERIFICATION", "true"); std::env::set_var("PLC_DIRECTORY_URL", &plc_url); diff --git a/crates/tranquil-pds/tests/handle_domains.rs b/crates/tranquil-pds/tests/handle_domains.rs index 17ab727..a912d03 100644 --- a/crates/tranquil-pds/tests/handle_domains.rs +++ b/crates/tranquil-pds/tests/handle_domains.rs @@ -8,7 +8,6 @@ const HANDLE_DOMAIN: &str = "handles.test"; fn set_handle_domain() { unsafe { - std::env::set_var("AVAILABLE_USER_DOMAINS", HANDLE_DOMAIN); std::env::set_var("PDS_USER_HANDLE_DOMAINS", HANDLE_DOMAIN); } } diff --git a/deploy/quadlets/tranquil-pds-app.container b/deploy/quadlets/tranquil-pds-app.container index 0b9c534..c8e7113 100644 --- a/deploy/quadlets/tranquil-pds-app.container +++ b/deploy/quadlets/tranquil-pds-app.container @@ -5,11 +5,11 @@ After=tranquil-pds-db.service ContainerName=tranquil-pds-app Image=localhost/tranquil-pds:latest Pod=tranquil-pds.pod -EnvironmentFile=/srv/tranquil-pds/config/tranquil-pds.env Environment=SERVER_HOST=0.0.0.0 Environment=SERVER_PORT=3000 -Volume=/srv/tranquil-pds/blobs:/var/lib/tranquil/blobs:Z -Volume=/srv/tranquil-pds/backups:/var/lib/tranquil/backups:Z +Volume=/srv/tranquil-pds/config/config.toml:/etc/tranquil-pds/config.toml:ro,Z +Volume=/srv/tranquil-pds/blobs:/var/lib/tranquil-pds/blobs:Z +Volume=/srv/tranquil-pds/store:/var/lib/tranquil-pds/store:Z HealthCmd=wget -q --spider http://localhost:3000/xrpc/_health HealthInterval=30s HealthTimeout=10s diff --git a/docker-compose.prod.yaml b/docker-compose.prod.yaml index f530fe2..1c89438 100644 --- a/docker-compose.prod.yaml +++ b/docker-compose.prod.yaml @@ -9,7 +9,9 @@ services: SERVER_HOST: "0.0.0.0" volumes: - ./config.toml:/etc/tranquil-pds/config.toml:ro - - blob_data:/var/lib/tranquil/blobs + # In memory of @olaren.dev's blobs when lewis forgot to update /tranquil to /tranquil-pds :( + - blob_data:/var/lib/tranquil-pds/blobs + - store_data:/var/lib/tranquil-pds/store depends_on: db: condition: service_healthy @@ -94,5 +96,6 @@ services: volumes: postgres_data: blob_data: + store_data: prometheus_data: acme_challenge: diff --git a/docker-compose.yaml b/docker-compose.yaml index 9b6e039..caaed47 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,8 @@ services: DATABASE_URL: postgres://postgres:postgres@db:5432/pds volumes: - ./config.toml:/etc/tranquil-pds/config.toml:ro - - blob_data:/var/lib/tranquil/blobs + - blob_data:/var/lib/tranquil-pds/blobs + - store_data:/var/lib/tranquil-pds/store depends_on: - db @@ -51,4 +52,5 @@ services: volumes: postgres_data: blob_data: + store_data: prometheus_data: diff --git a/docs/install-containers.md b/docs/install-containers.md index 82ca517..fd9a893 100644 --- a/docs/install-containers.md +++ b/docs/install-containers.md @@ -47,7 +47,7 @@ For production setups with proper service management, continue to either the Deb ## 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. +If you already have postgres running on the host, you can run just the app containers. Build the images: ```sh @@ -60,7 +60,7 @@ Run the backend with host networking (so it can access postgres on localhost) an 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:/var/lib/tranquil:Z \ + -v /var/lib/tranquil-pds:/var/lib/tranquil-pds:Z \ tranquil-pds:latest ``` @@ -91,7 +91,7 @@ location / { } ``` -See the [Debian install guide](install-debian.md) for the full nginx config with all API routes. +See the Debian with systemd quadlets section below for the full nginx config with all API routes. --- @@ -110,7 +110,7 @@ apt install -y podman ```bash mkdir -p /etc/containers/systemd -mkdir -p /srv/tranquil-pds/{postgres,blobs,certs,acme,config} +mkdir -p /srv/tranquil-pds/{postgres,blobs,store,certs,acme,config} ``` ## Create a configuration file diff --git a/docs/install-debian.md b/docs/install-debian.md deleted file mode 100644 index 87b54a5..0000000 --- a/docs/install-debian.md +++ /dev/null @@ -1,370 +0,0 @@ -# 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 - -```bash -apt update && apt upgrade -y -apt install -y curl git build-essential pkg-config libssl-dev -``` - -## Install rust - -```bash -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 - -```bash -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 - -```bash -mkdir -p /var/lib/tranquil/blobs -``` - -We'll set ownership after creating the service user. - -## Install Node.js and pnpm (for frontend build) - -```bash -curl -fsSL https://deb.nodesource.com/setup_24.x | bash - -apt install -y nodejs -npm install -g pnpm -``` - -## Clone and build Tranquil PDS - -```bash -cd /opt -git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds -cd tranquil-pds -cd frontend -pnpm install --frozen-lockfile -pnpm build -cd .. -cargo build --release -``` - -## Configure Tranquil PDS - -```bash -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: -```bash -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 the `TRANQUIL_PDS_CONFIG` env var instead of `--config`. - -You can validate your configuration before starting the service: -```bash -/usr/local/bin/tranquil-pds --config /etc/tranquil-pds/config.toml validate -``` - -## Install frontend files - -```bash -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 - -```bash -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 - -```bash -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: -```bash -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): -```bash -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: -```bash -systemctl reload 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 -``` - -## Verify installation - -```bash -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: -```bash -journalctl -u tranquil-pds -f -``` - -Update Tranquil PDS: -```bash -cd /opt/tranquil-pds -git pull -cd frontend && pnpm install --frozen-lockfile && pnpm 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: -```bash -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. - -```bash -cat > /var/www/tranquil-pds/homepage.html << 'EOF' - - - - Welcome to my PDS - - - -

Welcome to my secret PDS

-

This is a AT Protocol Personal Data Server.

-

Sign in or learn more at Bluesky.

- - -EOF -``` diff --git a/scripts/install-debian.sh b/scripts/install-debian.sh deleted file mode 100755 index d1ff84a..0000000 --- a/scripts/install-debian.sh +++ /dev/null @@ -1,512 +0,0 @@ -#!/bin/bash -set -euo pipefail - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -log_info() { echo -e "${BLUE}[INFO]${NC} $1"; } -log_success() { echo -e "${GREEN}[OK]${NC} $1"; } -log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } -log_error() { echo -e "${RED}[ERROR]${NC} $1"; } - -if [[ $EUID -ne 0 ]]; then - log_error "This script must be run as root" - exit 1 -fi - -if ! grep -qi "debian" /etc/os-release 2>/dev/null; then - log_warn "This script is designed for Debian. Proceed with caution on other distros." -fi - -nuke_installation() { - log_warn "NUKING EXISTING INSTALLATION" - log_info "Stopping services..." - systemctl stop tranquil-pds 2>/dev/null || true - systemctl disable tranquil-pds 2>/dev/null || true - - log_info "Removing Tranquil PDS files..." - rm -rf /opt/tranquil-pds - rm -rf /var/lib/tranquil-pds - rm -f /usr/local/bin/tranquil-pds - rm -f /usr/local/bin/tranquil-pds-sendmail - rm -f /usr/local/bin/tranquil-pds-mailq - rm -rf /var/spool/tranquil-pds-mail - rm -f /etc/systemd/system/tranquil-pds.service - systemctl daemon-reload - - log_info "Removing Tranquil PDS configuration..." - rm -rf /etc/tranquil-pds - - log_info "Dropping postgres database and user..." - sudo -u postgres psql -c "DROP DATABASE IF EXISTS pds;" 2>/dev/null || true - sudo -u postgres psql -c "DROP USER IF EXISTS tranquil_pds;" 2>/dev/null || true - - log_info "Removing blob storage..." - rm -rf /var/lib/tranquil 2>/dev/null || true - - log_info "Removing nginx config..." - rm -f /etc/nginx/sites-enabled/tranquil-pds - rm -f /etc/nginx/sites-available/tranquil-pds - systemctl reload nginx 2>/dev/null || true - - log_success "Previous installation nuked" -} - -if [[ -f /etc/tranquil-pds/tranquil-pds.env ]] || [[ -d /opt/tranquil-pds ]] || [[ -f /usr/local/bin/tranquil-pds ]]; then - log_warn "Existing installation detected" - echo "" - echo "Options:" - echo " 1) Nuke everything and start fresh (destroys database!)" - echo " 2) Continue with existing installation (idempotent update)" - echo " 3) Exit" - echo "" - read -p "Choose an option [1/2/3]: " INSTALL_CHOICE - - case "$INSTALL_CHOICE" in - 1) - echo "" - log_warn "This will DELETE:" - echo " - PostgreSQL database 'pds' and all data" - echo " - All Tranquil PDS configuration and credentials" - echo " - All source code in /opt/tranquil-pds" - echo " - All blobs in /var/lib/tranquil/" - echo "" - read -p "Type 'NUKE' to confirm: " CONFIRM_NUKE - if [[ "$CONFIRM_NUKE" == "NUKE" ]]; then - nuke_installation - else - log_error "Nuke cancelled" - exit 1 - fi - ;; - 2) - log_info "Continuing with existing installation..." - ;; - 3) - exit 0 - ;; - *) - log_error "Invalid option" - exit 1 - ;; - esac -fi - -echo "" -log_info "Tranquil PDS Installation Script for Debian" -echo "" - -get_public_ips() { - 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") - IPV6=$(curl -6 -s --max-time 5 ifconfig.me 2>/dev/null || curl -6 -s --max-time 5 icanhazip.com 2>/dev/null || echo "") -} - -log_info "Detecting public IP addresses..." -get_public_ips -echo " IPv4: ${IPV4}" -[[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}" -echo "" - -read -p "Enter your PDS domain (eg., pds.example.com): " PDS_DOMAIN -if [[ -z "$PDS_DOMAIN" ]]; then - log_error "Domain cannot be empty" - exit 1 -fi - -read -p "Enter your email for Let's Encrypt: " CERTBOT_EMAIL -if [[ -z "$CERTBOT_EMAIL" ]]; then - log_error "Email cannot be empty" - exit 1 -fi - -echo "" -log_info "DNS records required (create these now if you haven't):" -echo "" -echo " ${PDS_DOMAIN} A ${IPV4}" -[[ -n "$IPV6" ]] && echo " ${PDS_DOMAIN} AAAA ${IPV6}" -echo " *.${PDS_DOMAIN} A ${IPV4} (for user handles)" -[[ -n "$IPV6" ]] && echo " *.${PDS_DOMAIN} AAAA ${IPV6} (for user handles)" -echo "" -read -p "Have you created these DNS records? (y/N): " DNS_CONFIRMED -if [[ ! "$DNS_CONFIRMED" =~ ^[Yy]$ ]]; then - log_warn "Please create the DNS records and run this script again." - exit 0 -fi - -CREDENTIALS_FILE="/etc/tranquil-pds/.credentials" -if [[ -f "$CREDENTIALS_FILE" ]]; then - log_info "Loading existing credentials..." - source "$CREDENTIALS_FILE" -else - log_info "Generating secrets..." - JWT_SECRET=$(openssl rand -base64 48) - DPOP_SECRET=$(openssl rand -base64 48) - MASTER_KEY=$(openssl rand -base64 48) - DB_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 32) - - mkdir -p /etc/tranquil-pds - cat > "$CREDENTIALS_FILE" << EOF -JWT_SECRET="$JWT_SECRET" -DPOP_SECRET="$DPOP_SECRET" -MASTER_KEY="$MASTER_KEY" -DB_PASSWORD="$DB_PASSWORD" -EOF - chmod 600 "$CREDENTIALS_FILE" - log_success "Secrets generated" -fi - -log_info "Checking swap space..." -TOTAL_MEM_KB=$(grep MemTotal /proc/meminfo | awk '{print $2}') -TOTAL_SWAP_KB=$(grep SwapTotal /proc/meminfo | awk '{print $2}') - -if [[ $TOTAL_SWAP_KB -lt 2000000 ]]; then - if [[ ! -f /swapfile ]]; then - log_info "Adding swap space for compilation..." - SWAP_SIZE="4G" - [[ $TOTAL_MEM_KB -ge 4000000 ]] && SWAP_SIZE="2G" - fallocate -l $SWAP_SIZE /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=4096 - chmod 600 /swapfile - mkswap /swapfile - swapon /swapfile - grep -q '/swapfile' /etc/fstab || echo '/swapfile none swap sw 0 0' >> /etc/fstab - log_success "Swap added ($SWAP_SIZE)" - else - swapon /swapfile 2>/dev/null || true - fi -fi - -log_info "Updating system packages..." -apt update && apt upgrade -y - -log_info "Installing build dependencies..." -apt install -y curl git build-essential pkg-config libssl-dev ca-certificates gnupg lsb-release unzip xxd - -log_info "Installing postgres..." -apt install -y postgresql postgresql-contrib -systemctl enable postgresql -systemctl start postgresql -sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';" 2>/dev/null || \ - sudo -u postgres psql -c "ALTER USER tranquil_pds WITH PASSWORD '${DB_PASSWORD}';" -sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 2>/dev/null || true -sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" -log_success "postgres configured" - -log_info "Creating blob storage directories..." -mkdir -p /var/lib/tranquil/blobs -log_success "Blob storage directories created" - -log_info "Installing rust..." -if [[ -f "$HOME/.cargo/env" ]]; then - source "$HOME/.cargo/env" -fi -if ! command -v rustc &>/dev/null; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y - source "$HOME/.cargo/env" -fi - -log_info "Installing Node.js..." -if ! command -v node &>/dev/null; then - curl -fsSL https://deb.nodesource.com/setup_24.x | bash - - apt install -y nodejs -fi - -log_info "Installing pnpm..." -if ! command -v pnpm &>/dev/null; then - npm install -g pnpm -fi - -log_info "Cloning Tranquil PDS..." -if [[ ! -d /opt/tranquil-pds ]]; then - git clone https://tangled.org/tranquil.farm/tranquil-pds /opt/tranquil-pds -else - cd /opt/tranquil-pds && git pull -fi -cd /opt/tranquil-pds - -log_info "Building frontend..." -cd frontend && pnpm install --frozen-lockfile && pnpm build && cd .. -log_success "Frontend built" - -log_info "Building Tranquil PDS (this takes a while)..." -source "$HOME/.cargo/env" -if [[ $TOTAL_MEM_KB -lt 4000000 ]]; then - log_info "Low memory - limiting parallel jobs" - CARGO_BUILD_JOBS=1 cargo build --release -else - cargo build --release -fi -log_success "Tranquil PDS built" - -log_info "Running migrations..." -cargo install sqlx-cli --no-default-features --features postgres -export DATABASE_URL="postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds" -"$HOME/.cargo/bin/sqlx" migrate run -log_success "Migrations complete" - -log_info "Setting up mail trap..." -mkdir -p /var/spool/tranquil-pds-mail -chmod 1777 /var/spool/tranquil-pds-mail - -cat > /usr/local/bin/tranquil-pds-sendmail << 'SENDMAIL_EOF' -#!/bin/bash -MAIL_DIR="/var/spool/tranquil-pds-mail" -TIMESTAMP=$(date +%Y%m%d-%H%M%S) -RANDOM_ID=$(head -c 4 /dev/urandom | xxd -p) -MAIL_FILE="${MAIL_DIR}/${TIMESTAMP}-${RANDOM_ID}.eml" -mkdir -p "$MAIL_DIR" -{ - echo "X-Tranquil-PDS-Received: $(date -Iseconds)" - echo "X-Tranquil-PDS-Args: $*" - echo "" - cat -} > "$MAIL_FILE" -chmod 644 "$MAIL_FILE" -exit 0 -SENDMAIL_EOF -chmod +x /usr/local/bin/tranquil-pds-sendmail - -cat > /usr/local/bin/tranquil-pds-mailq << 'MAILQ_EOF' -#!/bin/bash -MAIL_DIR="/var/spool/tranquil-pds-mail" -case "${1:-list}" in - list) - ls -lt "$MAIL_DIR"/*.eml 2>/dev/null | head -20 || echo "No emails" - ;; - latest) - f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | head -1) - [[ -f "$f" ]] && cat "$f" || echo "No emails" - ;; - clear) - rm -f "$MAIL_DIR"/*.eml - echo "Cleared" - ;; - count) - ls -1 "$MAIL_DIR"/*.eml 2>/dev/null | wc -l - ;; - [0-9]*) - f=$(ls -t "$MAIL_DIR"/*.eml 2>/dev/null | sed -n "${1}p") - [[ -f "$f" ]] && cat "$f" || echo "Not found" - ;; - *) - [[ -f "$MAIL_DIR/$1" ]] && cat "$MAIL_DIR/$1" || echo "Usage: tranquil-pds-mailq [list|latest|clear|count|N]" - ;; -esac -MAILQ_EOF -chmod +x /usr/local/bin/tranquil-pds-mailq - -log_info "Creating Tranquil PDS configuration..." -cat > /etc/tranquil-pds/tranquil-pds.env << EOF -SERVER_HOST=127.0.0.1 -SERVER_PORT=3000 -PDS_HOSTNAME=${PDS_DOMAIN} -DATABASE_URL=postgres://tranquil_pds:${DB_PASSWORD}@localhost:5432/pds -DATABASE_MAX_CONNECTIONS=100 -DATABASE_MIN_CONNECTIONS=10 -BLOB_STORAGE_PATH=/var/lib/tranquil/blobs -JWT_SECRET=${JWT_SECRET} -DPOP_SECRET=${DPOP_SECRET} -MASTER_KEY=${MASTER_KEY} -PLC_DIRECTORY_URL=https://plc.directory -CRAWLERS=https://bsky.network -AVAILABLE_USER_DOMAINS=${PDS_DOMAIN} -MAIL_FROM_ADDRESS=noreply@${PDS_DOMAIN} -MAIL_FROM_NAME=Tranquil PDS -SENDMAIL_PATH=/usr/local/bin/tranquil-pds-sendmail -EOF -chmod 600 /etc/tranquil-pds/tranquil-pds.env - -log_info "Installing Tranquil PDS..." -id -u tranquil-pds &>/dev/null || useradd -r -s /sbin/nologin tranquil-pds -cp /opt/tranquil-pds/target/release/tranquil-server /usr/local/bin/tranquil-pds -mkdir -p /var/lib/tranquil-pds -cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend -chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds -chown -R tranquil-pds:tranquil-pds /var/lib/tranquil - -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 -EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env -ExecStart=/usr/local/bin/tranquil-pds -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 -log_success "Tranquil PDS service started" - -log_info "Installing nginx..." -apt install -y nginx -cat > /etc/nginx/sites-available/tranquil-pds << EOF -server { - listen 80; - listen [::]:80; - server_name ${PDS_DOMAIN} *.${PDS_DOMAIN}; - - location /.well-known/acme-challenge/ { - root /var/www/html; - } - - location / { - 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; - client_max_body_size 100M; - } -} -EOF - -ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ -rm -f /etc/nginx/sites-enabled/default -nginx -t -systemctl reload nginx -log_success "nginx configured" - -log_info "Configuring firewall..." -apt install -y ufw -ufw --force reset -ufw default deny incoming -ufw default allow outgoing -ufw allow ssh -ufw allow 80/tcp -ufw allow 443/tcp -ufw --force enable -log_success "Firewall configured" - -echo "" -log_info "Obtaining wildcard SSL certificate..." -echo "" -echo "User handles are served as subdomains (eg., alice.${PDS_DOMAIN})," -echo "so you need a wildcard certificate. This requires DNS validation." -echo "" -echo "You'll need to add a TXT record to your DNS when prompted." -echo "" -read -p "Ready to proceed? (y/N): " CERT_READY - -if [[ "$CERT_READY" =~ ^[Yy]$ ]]; then - apt install -y certbot python3-certbot-nginx - - log_info "Running certbot with DNS challenge..." - echo "" - echo "When prompted, add the TXT record to your DNS, wait a minute" - echo "for propagation, then press Enter to continue." - echo "" - - if certbot certonly --manual --preferred-challenges dns \ - -d "${PDS_DOMAIN}" -d "*.${PDS_DOMAIN}" \ - --email "${CERTBOT_EMAIL}" --agree-tos; then - - cat > /etc/nginx/sites-available/tranquil-pds << EOF -server { - listen 80; - listen [::]:80; - server_name ${PDS_DOMAIN} *.${PDS_DOMAIN}; - - location /.well-known/acme-challenge/ { - root /var/www/html; - } - - location / { - return 301 https://\$host\$request_uri; - } -} - -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name ${PDS_DOMAIN} *.${PDS_DOMAIN}; - - ssl_certificate /etc/letsencrypt/live/${PDS_DOMAIN}/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/${PDS_DOMAIN}/privkey.pem; - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - ssl_prefer_server_ciphers on; - ssl_session_cache shared:SSL:10m; - - location / { - 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; - client_max_body_size 100M; - } -} -EOF - nginx -t && systemctl reload nginx - log_success "Wildcard SSL certificate installed" - - echo "" - log_warn "Certificate renewal note:" - echo "Manual DNS challenges don't auto-renew. Before expiry, run:" - echo " certbot renew --manual" - echo "" - echo "For auto-renewal, consider using a DNS provider plugin:" - echo " apt install python3-certbot-dns-cloudflare # or your provider" - echo "" - else - log_warn "Wildcard cert failed. You can retry later with:" - echo " certbot certonly --manual --preferred-challenges dns \\" - echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'" - fi -else - log_warn "Skipping SSL. Your PDS is running on HTTP only." - echo "To add SSL later, run:" - echo " certbot certonly --manual --preferred-challenges dns \\" - echo " -d ${PDS_DOMAIN} -d '*.${PDS_DOMAIN}'" -fi - -log_info "Verifying installation..." -sleep 3 -if curl -s "http://localhost:3000/xrpc/_health" | grep -q "version"; then - log_success "Tranquil PDS is responding" -else - log_warn "Tranquil PDS may still be starting. Check: journalctl -u tranquil-pds -f" -fi - -echo "" -log_success "Installation complete" -echo "" -echo "PDS: https://${PDS_DOMAIN}" -echo "" -echo "Credentials (also in /etc/tranquil-pds/.credentials):" -echo " DB password: ${DB_PASSWORD}" -echo "" -echo "Data locations:" -echo " Blobs: /var/lib/tranquil/blobs" -echo "" -echo "Commands:" -echo " journalctl -u tranquil-pds -f # logs" -echo " systemctl restart tranquil-pds # restart" -echo " tranquil-pds-mailq # view trapped emails" -echo ""