Our Personal Data Server from scratch! tranquil.farm
oauth atproto pds rust postgresql objectstorage fun

feat: docs tweaks & standalone frontend

+792 -297
+2 -4
.env.example
··· 140 # ============================================================================= 141 # If configured, moderation reports will be proxied to this service 142 # instead of being stored locally. The service should implement the 143 - # com.atproto.moderation.createReport endpoint (e.g., Bluesky's Ozone). 144 # Both URL and DID must be set for proxying to be enabled. 145 # REPORT_SERVICE_URL=https://mod.bsky.app 146 # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac ··· 148 # Age Assurance Override 149 # ============================================================================= 150 # Enable this if you have separately assured the ages of your users 151 - # (e.g., through your own age verification process). When enabled, the PDS 152 # will return "assured" status for age assurance checks instead of proxying 153 # to the appview. This helps migrated users avoid the age assurance 154 # catch-22 on bsky.app. ··· 158 # ============================================================================= 159 # Allow HTTP for proxy requests (development only) 160 # ALLOW_HTTP_PROXY=1 161 - # Custom frontend directory (defaults to ./frontend/dist) 162 - # FRONTEND_DIR=/path/to/frontend/dist 163 # ============================================================================= 164 # SSO / Social Login 165 # =============================================================================
··· 140 # ============================================================================= 141 # If configured, moderation reports will be proxied to this service 142 # instead of being stored locally. The service should implement the 143 + # com.atproto.moderation.createReport endpoint (eg., Bluesky's Ozone). 144 # Both URL and DID must be set for proxying to be enabled. 145 # REPORT_SERVICE_URL=https://mod.bsky.app 146 # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac ··· 148 # Age Assurance Override 149 # ============================================================================= 150 # Enable this if you have separately assured the ages of your users 151 + # (eg., through your own age verification process). When enabled, the PDS 152 # will return "assured" status for age assurance checks instead of proxying 153 # to the appview. This helps migrated users avoid the age assurance 154 # catch-22 on bsky.app. ··· 158 # ============================================================================= 159 # Allow HTTP for proxy requests (development only) 160 # ALLOW_HTTP_PROXY=1 161 # ============================================================================= 162 # SSO / Social Login 163 # =============================================================================
-28
Cargo.lock
··· 2592 ] 2593 2594 [[package]] 2595 - name = "http-range-header" 2596 - version = "0.4.2" 2597 - source = "registry+https://github.com/rust-lang/crates.io-index" 2598 - checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" 2599 - 2600 - [[package]] 2601 name = "httparse" 2602 version = "1.10.1" 2603 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3476 version = "0.3.17" 3477 source = "registry+https://github.com/rust-lang/crates.io-index" 3478 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 3479 - 3480 - [[package]] 3481 - name = "mime_guess" 3482 - version = "2.0.5" 3483 - source = "registry+https://github.com/rust-lang/crates.io-index" 3484 - checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" 3485 - dependencies = [ 3486 - "mime", 3487 - "unicase", 3488 - ] 3489 3490 [[package]] 3491 name = "minimal-lexical" ··· 5850 "http 1.4.0", 5851 "http-body 1.0.1", 5852 "http-body-util", 5853 - "http-range-header", 5854 - "httpdate", 5855 "iri-string", 5856 - "mime", 5857 - "mime_guess", 5858 - "percent-encoding", 5859 "pin-project-lite", 5860 "tokio", 5861 "tokio-util", 5862 "tower", 5863 "tower-layer", 5864 "tower-service", 5865 - "tracing", 5866 ] 5867 5868 [[package]] ··· 6240 version = "1.19.0" 6241 source = "registry+https://github.com/rust-lang/crates.io-index" 6242 checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 6243 - 6244 - [[package]] 6245 - name = "unicase" 6246 - version = "2.8.1" 6247 - source = "registry+https://github.com/rust-lang/crates.io-index" 6248 - checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" 6249 6250 [[package]] 6251 name = "unicode-bidi"
··· 2592 ] 2593 2594 [[package]] 2595 name = "httparse" 2596 version = "1.10.1" 2597 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3470 version = "0.3.17" 3471 source = "registry+https://github.com/rust-lang/crates.io-index" 3472 checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 3473 3474 [[package]] 3475 name = "minimal-lexical" ··· 5834 "http 1.4.0", 5835 "http-body 1.0.1", 5836 "http-body-util", 5837 "iri-string", 5838 "pin-project-lite", 5839 "tokio", 5840 "tokio-util", 5841 "tower", 5842 "tower-layer", 5843 "tower-service", 5844 ] 5845 5846 [[package]] ··· 6218 version = "1.19.0" 6219 source = "registry+https://github.com/rust-lang/crates.io-index" 6220 checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" 6221 6222 [[package]] 6223 name = "unicode-bidi"
+1 -1
Cargo.toml
··· 90 tokio-tungstenite = { version = "0.28", features = ["native-tls"] } 91 totp-rs = { version = "5", features = ["qr"] } 92 tower = "0.5" 93 - tower-http = { version = "0.6", features = ["fs", "cors"] } 94 tower-layer = "0.3" 95 tracing = "0.1" 96 tracing-subscriber = "0.3"
··· 90 tokio-tungstenite = { version = "0.28", features = ["native-tls"] } 91 totp-rs = { version = "5", features = ["qr"] } 92 tower = "0.5" 93 + tower-http = { version = "0.6", features = ["cors"] } 94 tower-layer = "0.3" 95 tracing = "0.1" 96 tracing-subscriber = "0.3"
+5 -13
Dockerfile
··· 1 - FROM denoland/deno:alpine AS frontend-builder 2 - WORKDIR /frontend 3 - COPY frontend/ ./ 4 - RUN deno task build 5 - 6 FROM rust:1.92-alpine AS builder 7 - RUN apk add ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev 8 WORKDIR /app 9 COPY Cargo.toml Cargo.lock ./ 10 - COPY src ./src 11 - COPY tests ./tests 12 - COPY migrations ./migrations 13 COPY .sqlx ./.sqlx 14 RUN --mount=type=cache,target=/usr/local/cargo/registry \ 15 --mount=type=cache,target=/app/target \ 16 - cargo build --release && \ 17 cp target/release/tranquil-pds /tmp/tranquil-pds 18 19 FROM alpine:3.23 20 RUN apk add --no-cache msmtp ca-certificates && ln -sf /usr/bin/msmtp /usr/sbin/sendmail 21 COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds 22 - COPY --from=builder /app/migrations /app/migrations 23 - COPY --from=frontend-builder /frontend/dist /app/frontend/dist 24 WORKDIR /app 25 ENV SERVER_HOST=0.0.0.0 26 ENV SERVER_PORT=3000 27 - ENV FRONTEND_DIR=/app/frontend/dist 28 EXPOSE 3000 29 CMD ["tranquil-pds"]
··· 1 FROM rust:1.92-alpine AS builder 2 + RUN apk add --no-cache ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev 3 WORKDIR /app 4 COPY Cargo.toml Cargo.lock ./ 5 + COPY crates ./crates 6 COPY .sqlx ./.sqlx 7 + COPY migrations ./crates/tranquil-pds/migrations 8 RUN --mount=type=cache,target=/usr/local/cargo/registry \ 9 --mount=type=cache,target=/app/target \ 10 + SQLX_OFFLINE=true cargo build --release -p tranquil-pds && \ 11 cp target/release/tranquil-pds /tmp/tranquil-pds 12 13 FROM alpine:3.23 14 RUN apk add --no-cache msmtp ca-certificates && ln -sf /usr/bin/msmtp /usr/sbin/sendmail 15 COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds 16 + COPY migrations /app/migrations 17 WORKDIR /app 18 ENV SERVER_HOST=0.0.0.0 19 ENV SERVER_PORT=3000 20 EXPOSE 3000 21 CMD ["tranquil-pds"]
+2 -2
README.md
··· 12 13 ## What's different about Tranquil PDS 14 15 - It is a superset of the reference PDS, including: passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices), did:web support (PDS-hosted subdomains or bring-your-own), multi-channel communication (email, discord, telegram, signal) for verification and alerts, granular OAuth scopes with a consent UI showing human-readable descriptions, app passwords with granular permissions (read-only, post-only, or custom scopes), account delegation (letting others manage an account with configurable permission levels), automatic backups to s3-compatible object storage (configurable retention and frequency, one-click restore), and a built-in web UI for account management, OAuth consent, repo browsing, and admin. 16 17 The PDS itself is a single small binary with no node/npm runtime. It does require postgres, valkey, and s3-compatible storage, which makes setup heavier than the reference PDS's sqlite. The tradeoff is that these are battle-tested pieces of infra that we already know how to scale, back up, and monitor. 18 ··· 45 46 ```bash 47 cp .env.prod.example .env.prod 48 - podman-compose -f docker-compose.prod.yml up -d 49 ``` 50 51 ### Installation Guides
··· 12 13 ## What's different about Tranquil PDS 14 15 + It is a superset of the reference PDS, including: passkeys and 2FA (WebAuthn/FIDO2, TOTP, backup codes, trusted devices), SSO login and signup, did:web support (PDS-hosted subdomains or bring-your-own), multi-channel communication (email, discord, telegram, signal) for verification and alerts, granular OAuth scopes with a consent UI showing human-readable descriptions, app passwords with granular permissions (read-only, post-only, or custom scopes), account delegation (letting others manage an account with configurable permission levels), automatic backups to s3-compatible object storage (configurable retention and frequency, one-click restore), and a built-in web UI for account management, OAuth consent, repo browsing, and admin. 16 17 The PDS itself is a single small binary with no node/npm runtime. It does require postgres, valkey, and s3-compatible storage, which makes setup heavier than the reference PDS's sqlite. The tradeoff is that these are battle-tested pieces of infra that we already know how to scale, back up, and monitor. 18 ··· 45 46 ```bash 47 cp .env.prod.example .env.prod 48 + podman-compose -f docker-compose.prod.yaml up -d 49 ``` 50 51 ### Installation Guides
+5 -3
crates/tranquil-oauth/src/client.rs
··· 83 .connect_timeout(std::time::Duration::from_secs(10)) 84 .pool_max_idle_per_host(10) 85 .pool_idle_timeout(std::time::Duration::from_secs(90)) 86 - .user_agent( 87 - "Tranquil-PDS/1.0 (ATProto; +https://tangled.org/lewis.moe/bspds-sandbox)", 88 - ) 89 .build() 90 .unwrap_or_else(|_| Client::new()), 91 cache_ttl_secs,
··· 83 .connect_timeout(std::time::Duration::from_secs(10)) 84 .pool_max_idle_per_host(10) 85 .pool_idle_timeout(std::time::Duration::from_secs(90)) 86 + .user_agent(concat!( 87 + "Tranquil-PDS/", 88 + env!("CARGO_PKG_VERSION"), 89 + " (ATProto; +https://tangled.org/tranquil.farm/tranquil-pds)" 90 + )) 91 .build() 92 .unwrap_or_else(|_| Client::new()), 93 cache_ttl_secs,
+2 -31
crates/tranquil-pds/src/lib.rs
··· 38 pub use sync::util::AccountStatus; 39 use tower::ServiceBuilder; 40 use tower_http::cors::{Any, CorsLayer}; 41 - use tower_http::services::{ServeDir, ServeFile}; 42 pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey}; 43 44 pub fn app(state: AppState) -> Router { ··· 612 get(oauth::endpoints::oauth_authorization_server), 613 ); 614 615 - let router = Router::new() 616 .nest_service("/xrpc", xrpc_service) 617 .nest("/oauth", oauth_router) 618 .nest("/.well-known", well_known_router) ··· 644 "atproto-content-labelers".parse().unwrap(), 645 ]), 646 ) 647 - .with_state(state); 648 - 649 - let frontend_dir = 650 - std::env::var("FRONTEND_DIR").unwrap_or_else(|_| "./frontend/dist".to_string()); 651 - if std::path::Path::new(&frontend_dir) 652 - .join("index.html") 653 - .exists() 654 - { 655 - let index_path = format!("{}/index.html", frontend_dir); 656 - let homepage_path = format!("{}/homepage.html", frontend_dir); 657 - 658 - let homepage_exists = std::path::Path::new(&homepage_path).exists(); 659 - let homepage_file = if homepage_exists { 660 - homepage_path 661 - } else { 662 - index_path.clone() 663 - }; 664 - 665 - let spa_router = Router::new().fallback_service(ServeFile::new(&index_path)); 666 - 667 - let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path)); 668 - 669 - return router 670 - .route_service("/", ServeFile::new(&homepage_file)) 671 - .nest("/app", spa_router) 672 - .fallback_service(serve_dir); 673 - } 674 - 675 - router 676 }
··· 38 pub use sync::util::AccountStatus; 39 use tower::ServiceBuilder; 40 use tower_http::cors::{Any, CorsLayer}; 41 pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey}; 42 43 pub fn app(state: AppState) -> Router { ··· 611 get(oauth::endpoints::oauth_authorization_server), 612 ); 613 614 + Router::new() 615 .nest_service("/xrpc", xrpc_service) 616 .nest("/oauth", oauth_router) 617 .nest("/.well-known", well_known_router) ··· 643 "atproto-content-labelers".parse().unwrap(), 644 ]), 645 ) 646 + .with_state(state) 647 }
+1 -1
crates/tranquil-pds/tests/common/mod.rs
··· 162 163 #[cfg(not(feature = "external-infra"))] 164 async fn setup_with_testcontainers() -> String { 165 - let s3_container = GenericImage::new("minio/minio", "latest") 166 .with_exposed_port(ContainerPort::Tcp(9000)) 167 .with_env_var("MINIO_ROOT_USER", "minioadmin") 168 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
··· 162 163 #[cfg(not(feature = "external-infra"))] 164 async fn setup_with_testcontainers() -> String { 165 + let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest") 166 .with_exposed_port(ContainerPort::Tcp(9000)) 167 .with_env_var("MINIO_ROOT_USER", "minioadmin") 168 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
+82 -3
deploy/nginx/nginx-quadlet.conf
··· 1 worker_processes auto; 2 error_log /var/log/nginx/error.log warn; 3 events { 4 worker_connections 4096; 5 } 6 http { 7 include /etc/nginx/mime.types; 8 default_type application/octet-stream; 9 access_log /var/log/nginx/access.log; 10 sendfile on; 11 keepalive_timeout 65; 12 gzip on; 13 gzip_types text/plain text/css application/json application/javascript text/xml application/xml; 14 ssl_protocols TLSv1.2 TLSv1.3; 15 ssl_prefer_server_ciphers off; 16 ssl_session_cache shared:SSL:10m; 17 ssl_stapling on; 18 ssl_stapling_verify on; 19 server { 20 listen 80; 21 listen [::]:80; 22 server_name _; 23 location /.well-known/acme-challenge/ { 24 root /var/www/acme; 25 } 26 location / { 27 return 301 https://$host$request_uri; 28 } 29 } 30 server { 31 - listen 443 ssl http2; 32 - listen [::]:443 ssl http2; 33 server_name _; 34 ssl_certificate /etc/nginx/certs/fullchain.pem; 35 ssl_certificate_key /etc/nginx/certs/privkey.pem; 36 client_max_body_size 10G; 37 - location / { 38 proxy_pass http://127.0.0.1:3000; 39 proxy_http_version 1.1; 40 proxy_set_header Upgrade $http_upgrade; ··· 46 proxy_read_timeout 86400; 47 proxy_send_timeout 86400; 48 proxy_buffering off; 49 } 50 } 51 }
··· 1 worker_processes auto; 2 error_log /var/log/nginx/error.log warn; 3 + 4 events { 5 worker_connections 4096; 6 } 7 + 8 http { 9 include /etc/nginx/mime.types; 10 default_type application/octet-stream; 11 access_log /var/log/nginx/access.log; 12 + 13 sendfile on; 14 keepalive_timeout 65; 15 + 16 gzip on; 17 gzip_types text/plain text/css application/json application/javascript text/xml application/xml; 18 + 19 ssl_protocols TLSv1.2 TLSv1.3; 20 ssl_prefer_server_ciphers off; 21 ssl_session_cache shared:SSL:10m; 22 ssl_stapling on; 23 ssl_stapling_verify on; 24 + 25 server { 26 listen 80; 27 listen [::]:80; 28 server_name _; 29 + 30 location /.well-known/acme-challenge/ { 31 root /var/www/acme; 32 } 33 + 34 location / { 35 return 301 https://$host$request_uri; 36 } 37 } 38 + 39 server { 40 + listen 443 ssl; 41 + listen [::]:443 ssl; 42 + http2 on; 43 server_name _; 44 + 45 ssl_certificate /etc/nginx/certs/fullchain.pem; 46 ssl_certificate_key /etc/nginx/certs/privkey.pem; 47 + 48 client_max_body_size 10G; 49 + 50 + location /xrpc/ { 51 proxy_pass http://127.0.0.1:3000; 52 proxy_http_version 1.1; 53 proxy_set_header Upgrade $http_upgrade; ··· 59 proxy_read_timeout 86400; 60 proxy_send_timeout 86400; 61 proxy_buffering off; 62 + proxy_request_buffering off; 63 + } 64 + 65 + location /oauth/ { 66 + proxy_pass http://127.0.0.1:3000; 67 + proxy_http_version 1.1; 68 + proxy_set_header Host $host; 69 + proxy_set_header X-Real-IP $remote_addr; 70 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 71 + proxy_set_header X-Forwarded-Proto $scheme; 72 + proxy_read_timeout 300; 73 + proxy_send_timeout 300; 74 + } 75 + 76 + location /.well-known/ { 77 + proxy_pass http://127.0.0.1:3000; 78 + proxy_http_version 1.1; 79 + proxy_set_header Host $host; 80 + proxy_set_header X-Real-IP $remote_addr; 81 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 82 + proxy_set_header X-Forwarded-Proto $scheme; 83 + } 84 + 85 + location = /metrics { 86 + proxy_pass http://127.0.0.1:3000; 87 + proxy_http_version 1.1; 88 + proxy_set_header Host $host; 89 + proxy_set_header X-Real-IP $remote_addr; 90 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 91 + proxy_set_header X-Forwarded-Proto $scheme; 92 + } 93 + 94 + location = /health { 95 + proxy_pass http://127.0.0.1:3000; 96 + proxy_http_version 1.1; 97 + proxy_set_header Host $host; 98 + } 99 + 100 + location = /robots.txt { 101 + proxy_pass http://127.0.0.1:3000; 102 + proxy_http_version 1.1; 103 + proxy_set_header Host $host; 104 + } 105 + 106 + location = /logo { 107 + proxy_pass http://127.0.0.1:3000; 108 + proxy_http_version 1.1; 109 + proxy_set_header Host $host; 110 + } 111 + 112 + location ~ ^/u/[^/]+/did\.json$ { 113 + proxy_pass http://127.0.0.1:3000; 114 + proxy_http_version 1.1; 115 + proxy_set_header Host $host; 116 + proxy_set_header X-Real-IP $remote_addr; 117 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 118 + proxy_set_header X-Forwarded-Proto $scheme; 119 + } 120 + 121 + location / { 122 + proxy_pass http://127.0.0.1:8080; 123 + proxy_http_version 1.1; 124 + proxy_set_header Host $host; 125 + proxy_set_header X-Real-IP $remote_addr; 126 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 127 + proxy_set_header X-Forwarded-Proto $scheme; 128 } 129 } 130 }
-1
deploy/quadlets/tranquil-pds-app.container
··· 12 Environment=AWS_REGION=us-east-1 13 Environment=S3_BUCKET=pds-blobs 14 Environment=VALKEY_URL=redis://localhost:6379 15 - Environment=FRONTEND_DIR=/app/frontend/dist 16 HealthCmd=wget -q --spider http://localhost:3000/xrpc/_health 17 HealthInterval=30s 18 HealthTimeout=10s
··· 12 Environment=AWS_REGION=us-east-1 13 Environment=S3_BUCKET=pds-blobs 14 Environment=VALKEY_URL=redis://localhost:6379 15 HealthCmd=wget -q --spider http://localhost:3000/xrpc/_health 16 HealthInterval=30s 17 HealthTimeout=10s
+21
deploy/quadlets/tranquil-pds-frontend.container
···
··· 1 + [Unit] 2 + Description=Tranquil PDS frontend 3 + After=tranquil-pds-app.service 4 + 5 + [Container] 6 + ContainerName=tranquil-pds-frontend 7 + Image=localhost/tranquil-pds-frontend:latest 8 + Pod=tranquil-pds.pod 9 + Volume=/opt/tranquil-pds/frontend/nginx-quadlet.conf:/etc/nginx/conf.d/default.conf:ro,Z 10 + HealthCmd=wget -q --spider http://localhost:8080/ 11 + HealthInterval=30s 12 + HealthTimeout=10s 13 + HealthRetries=3 14 + HealthStartPeriod=5s 15 + 16 + [Service] 17 + Restart=always 18 + RestartSec=10 19 + 20 + [Install] 21 + WantedBy=default.target
+1 -6
deploy/quadlets/tranquil-pds-minio.container
··· 2 Description=Tranquil PDS minio object storage 3 [Container] 4 ContainerName=tranquil-pds-minio 5 - Image=docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z 6 Pod=tranquil-pds.pod 7 Environment=MINIO_ROOT_USER=minioadmin 8 Secret=tranquil-pds-minio-password,type=env,target=MINIO_ROOT_PASSWORD 9 Volume=/srv/tranquil-pds/minio:/data:Z 10 Exec=server /data --console-address :9001 11 - HealthCmd=curl -f http://localhost:9000/minio/health/live || exit 1 12 - HealthInterval=30s 13 - HealthTimeout=10s 14 - HealthRetries=3 15 - HealthStartPeriod=10s 16 [Service] 17 Restart=always 18 RestartSec=10
··· 2 Description=Tranquil PDS minio object storage 3 [Container] 4 ContainerName=tranquil-pds-minio 5 + Image=cgr.dev/chainguard/minio:latest 6 Pod=tranquil-pds.pod 7 Environment=MINIO_ROOT_USER=minioadmin 8 Secret=tranquil-pds-minio-password,type=env,target=MINIO_ROOT_PASSWORD 9 Volume=/srv/tranquil-pds/minio:/data:Z 10 Exec=server /data --console-address :9001 11 [Service] 12 Restart=always 13 RestartSec=10
+1 -1
deploy/quadlets/tranquil-pds-nginx.container
··· 1 [Unit] 2 Description=Tranquil PDS nginx reverse proxy 3 - After=tranquil-pds-app.service 4 [Container] 5 ContainerName=tranquil-pds-nginx 6 Image=docker.io/library/nginx:1.28-alpine
··· 1 [Unit] 2 Description=Tranquil PDS nginx reverse proxy 3 + After=tranquil-pds-app.service tranquil-pds-frontend.service 4 [Container] 5 ContainerName=tranquil-pds-nginx 6 Image=docker.io/library/nginx:1.28-alpine
+40 -18
docker-compose.prod.yml docker-compose.prod.yaml
··· 5 dockerfile: Dockerfile 6 image: tranquil-pds:latest 7 restart: unless-stopped 8 - ports: 9 - - "127.0.0.1:3000:3000" 10 environment: 11 SERVER_HOST: "0.0.0.0" 12 SERVER_PORT: "3000" ··· 22 DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" 23 MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" 24 CRAWLERS: "${CRAWLERS:-https://bsky.network}" 25 - FRONTEND_DIR: "/app/frontend/dist" 26 depends_on: 27 db: 28 condition: service_healthy ··· 42 memory: 1G 43 reservations: 44 memory: 256M 45 db: 46 image: postgres:18-alpine 47 restart: unless-stopped ··· 63 memory: 512M 64 reservations: 65 memory: 128M 66 minio: 67 - image: minio/minio:RELEASE.2025-10-15T17-29-55Z 68 restart: unless-stopped 69 command: server /data --console-address ":9001" 70 environment: ··· 72 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 73 volumes: 74 - minio_data:/data 75 - healthcheck: 76 - test: ["CMD", "mc", "ready", "local"] 77 - interval: 30s 78 - timeout: 10s 79 - retries: 3 80 - start_period: 10s 81 deploy: 82 resources: 83 limits: 84 memory: 512M 85 reservations: 86 memory: 128M 87 minio-init: 88 - image: minio/mc:RELEASE.2025-07-16T15-35-03Z 89 depends_on: 90 - minio: 91 - condition: service_healthy 92 entrypoint: > 93 /bin/sh -c " 94 - mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; 95 mc mb --ignore-existing local/pds-blobs; 96 mc anonymous set none local/pds-blobs; 97 exit 0; 98 " 99 environment: 100 MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}" 101 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 102 valkey: 103 image: valkey/valkey:9-alpine 104 restart: unless-stopped ··· 117 memory: 300M 118 reservations: 119 memory: 64M 120 nginx: 121 - image: nginx:1.28-alpine 122 restart: unless-stopped 123 ports: 124 - "80:80" 125 - "443:443" 126 volumes: 127 - - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro 128 - ./certs:/etc/nginx/certs:ro 129 - acme_challenge:/var/www/acme:ro 130 depends_on: 131 - tranquil-pds 132 healthcheck: 133 test: ["CMD", "nginx", "-t"] 134 interval: 30s 135 timeout: 10s 136 retries: 3 137 certbot: 138 image: certbot/certbot:v5.2.2 139 volumes: 140 - ./certs:/etc/letsencrypt 141 - acme_challenge:/var/www/acme 142 entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/acme; sleep 12h & wait $${!}; done'" 143 prometheus: 144 image: prom/prometheus:v3.8.0 145 restart: unless-stopped 146 ports: 147 - "127.0.0.1:9090:9090" 148 volumes: 149 - - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro 150 - prometheus_data:/prometheus 151 command: 152 - - '--config.file=/etc/prometheus/prometheus.yml' 153 - '--storage.tsdb.path=/prometheus' 154 - '--storage.tsdb.retention.time=30d' 155 deploy: 156 resources: 157 limits: 158 memory: 256M 159 volumes: 160 postgres_data: 161 minio_data:
··· 5 dockerfile: Dockerfile 6 image: tranquil-pds:latest 7 restart: unless-stopped 8 environment: 9 SERVER_HOST: "0.0.0.0" 10 SERVER_PORT: "3000" ··· 20 DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" 21 MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" 22 CRAWLERS: "${CRAWLERS:-https://bsky.network}" 23 depends_on: 24 db: 25 condition: service_healthy ··· 39 memory: 1G 40 reservations: 41 memory: 256M 42 + 43 + frontend: 44 + build: 45 + context: ./frontend 46 + dockerfile: Dockerfile 47 + image: tranquil-pds-frontend:latest 48 + restart: unless-stopped 49 + healthcheck: 50 + test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"] 51 + interval: 30s 52 + timeout: 10s 53 + retries: 3 54 + start_period: 5s 55 + deploy: 56 + resources: 57 + limits: 58 + memory: 128M 59 + reservations: 60 + memory: 32M 61 + 62 db: 63 image: postgres:18-alpine 64 restart: unless-stopped ··· 80 memory: 512M 81 reservations: 82 memory: 128M 83 + 84 minio: 85 + image: cgr.dev/chainguard/minio:latest 86 restart: unless-stopped 87 command: server /data --console-address ":9001" 88 environment: ··· 90 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 91 volumes: 92 - minio_data:/data 93 deploy: 94 resources: 95 limits: 96 memory: 512M 97 reservations: 98 memory: 128M 99 + 100 minio-init: 101 + image: cgr.dev/chainguard/minio-client:latest-dev 102 depends_on: 103 + - minio 104 entrypoint: > 105 /bin/sh -c " 106 + for i in 1 2 3 4 5 6 7 8 9 10; do 107 + mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} && break; 108 + echo 'Waiting for minio...'; sleep 2; 109 + done; 110 mc mb --ignore-existing local/pds-blobs; 111 + mc mb --ignore-existing local/pds-backups; 112 mc anonymous set none local/pds-blobs; 113 exit 0; 114 " 115 environment: 116 MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}" 117 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 118 + 119 valkey: 120 image: valkey/valkey:9-alpine 121 restart: unless-stopped ··· 134 memory: 300M 135 reservations: 136 memory: 64M 137 + 138 nginx: 139 + image: nginx:1.29-alpine 140 restart: unless-stopped 141 ports: 142 - "80:80" 143 - "443:443" 144 volumes: 145 + - ./nginx.frontend.conf:/etc/nginx/nginx.conf:ro 146 - ./certs:/etc/nginx/certs:ro 147 - acme_challenge:/var/www/acme:ro 148 depends_on: 149 - tranquil-pds 150 + - frontend 151 healthcheck: 152 test: ["CMD", "nginx", "-t"] 153 interval: 30s 154 timeout: 10s 155 retries: 3 156 + 157 certbot: 158 image: certbot/certbot:v5.2.2 159 volumes: 160 - ./certs:/etc/letsencrypt 161 - acme_challenge:/var/www/acme 162 entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/acme; sleep 12h & wait $${!}; done'" 163 + 164 prometheus: 165 image: prom/prometheus:v3.8.0 166 restart: unless-stopped 167 ports: 168 - "127.0.0.1:9090:9090" 169 volumes: 170 + - ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro 171 - prometheus_data:/prometheus 172 command: 173 + - '--config.file=/etc/prometheus/prometheus.yaml' 174 - '--storage.tsdb.path=/prometheus' 175 - '--storage.tsdb.retention.time=30d' 176 deploy: 177 resources: 178 limits: 179 memory: 256M 180 + 181 volumes: 182 postgres_data: 183 minio_data:
+19 -4
docker-compose.yaml
··· 16 - db 17 - objsto 18 - cache 19 db: 20 image: postgres:18-alpine 21 environment: ··· 26 - "5432:5432" 27 volumes: 28 - postgres_data:/var/lib/postgresql 29 objsto: 30 - image: minio/minio 31 ports: 32 - "9000:9000" 33 - "9001:9001" ··· 37 volumes: 38 - minio_data:/data 39 command: server /data --console-address ":9001" 40 cache: 41 - image: valkey/valkey:8-alpine 42 ports: 43 - "6379:6379" 44 volumes: 45 - valkey_data:/data 46 prometheus: 47 image: prom/prometheus:v3.8.0 48 ports: 49 - "9090:9090" 50 volumes: 51 - - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro 52 - prometheus_data:/prometheus 53 command: 54 - - '--config.file=/etc/prometheus/prometheus.yml' 55 - '--storage.tsdb.path=/prometheus' 56 depends_on: 57 - app 58 volumes: 59 postgres_data: 60 minio_data:
··· 16 - db 17 - objsto 18 - cache 19 + 20 + frontend: 21 + build: 22 + context: ./frontend 23 + dockerfile: Dockerfile 24 + image: tranquil-pds-frontend 25 + ports: 26 + - "8080:80" 27 + depends_on: 28 + - app 29 + 30 db: 31 image: postgres:18-alpine 32 environment: ··· 37 - "5432:5432" 38 volumes: 39 - postgres_data:/var/lib/postgresql 40 + 41 objsto: 42 + image: cgr.dev/chainguard/minio:latest 43 ports: 44 - "9000:9000" 45 - "9001:9001" ··· 49 volumes: 50 - minio_data:/data 51 command: server /data --console-address ":9001" 52 + 53 cache: 54 + image: valkey/valkey:9-alpine 55 ports: 56 - "6379:6379" 57 volumes: 58 - valkey_data:/data 59 + 60 prometheus: 61 image: prom/prometheus:v3.8.0 62 ports: 63 - "9090:9090" 64 volumes: 65 + - ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro 66 - prometheus_data:/prometheus 67 command: 68 + - '--config.file=/etc/prometheus/prometheus.yaml' 69 - '--storage.tsdb.path=/prometheus' 70 depends_on: 71 - app 72 + 73 volumes: 74 postgres_data: 75 minio_data:
+183 -43
docs/install-containers.md
··· 1 # Tranquil PDS Containerized Production Deployment 2 - > **Warning**: These instructions are untested and theoretical, written from the top of Lewis' head. They may contain errors or omissions. This warning will be removed once the guide has been verified. 3 This guide covers deploying Tranquil PDS using containers with podman. 4 - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) 5 - **Alpine 3.23+**: Uses OpenRC service script with podman-compose 6 ## Prerequisites 7 - A VPS with at least 2GB RAM and 20GB disk 8 - A domain name pointing to your server's IP 9 - A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains) 10 - Root or sudo access 11 ## Quick Start (Docker/Podman Compose) 12 If you just want to get running quickly: 13 ```sh 14 cp .env.example .env 15 ``` ··· 18 19 Build and start: 20 ```sh 21 - podman-compose -f docker-compose.prod.yml up -d 22 ``` 23 24 Get initial certificate (after DNS is configured): 25 ```sh 26 - podman-compose -f docker-compose.prod.yml run --rm certbot certonly \ 27 - --webroot -w /var/www/acme -d pds.example.com 28 - podman-compose -f docker-compose.prod.yml restart nginx 29 ``` 30 For production setups with proper service management, continue to either the Debian or Alpine section below. 31 --- 32 # Debian 13+ with Systemd Quadlets 33 Quadlets are the modern way to run podman containers under systemd. 34 - ## 1. Install Podman 35 ```bash 36 apt update 37 apt install -y podman 38 ``` 39 - ## 2. Create Directory Structure 40 ```bash 41 mkdir -p /etc/containers/systemd 42 mkdir -p /srv/tranquil-pds/{postgres,minio,valkey,certs,acme,config} 43 ``` 44 - ## 3. Create Environment File 45 ```bash 46 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 47 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 48 ``` 49 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 50 ```bash 51 openssl rand -base64 48 52 ``` 53 For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion). 54 - ## 4. Install Quadlet Definitions 55 Copy the quadlet files from the repository: 56 ```bash 57 cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/ 58 cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/ 59 ``` 60 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. 61 - ## 5. Create nginx Configuration 62 ```bash 63 - cp /opt/tranquil-pds/deploy/nginx/nginx-quadlet.conf /srv/tranquil-pds/config/nginx.conf 64 ``` 65 - ## 6. Build Tranquil PDS Image 66 ```bash 67 cd /opt 68 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 69 cd tranquil-pds 70 podman build -t tranquil-pds:latest . 71 ``` 72 - ## 7. Create Podman Secrets 73 ```bash 74 source /srv/tranquil-pds/config/tranquil-pds.env 75 echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - 76 echo "$MINIO_ROOT_PASSWORD" | podman secret create tranquil-pds-minio-password - 77 ``` 78 - ## 8. Start Services and Initialize 79 ```bash 80 systemctl daemon-reload 81 systemctl start tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey ··· 87 podman run --rm --pod tranquil-pds \ 88 -e MINIO_ROOT_USER=minioadmin \ 89 -e MINIO_ROOT_PASSWORD=your-minio-password \ 90 - docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ 91 sh -c "mc alias set local http://localhost:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs && mc mb --ignore-existing local/pds-backups" 92 ``` 93 ··· 96 cargo install sqlx-cli --no-default-features --features postgres 97 DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 98 ``` 99 - ## 9. Obtain Wildcard SSL Certificate 100 - User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. 101 102 Create temporary self-signed cert to start services: 103 ```bash ··· 105 -keyout /srv/tranquil-pds/certs/privkey.pem \ 106 -out /srv/tranquil-pds/certs/fullchain.pem \ 107 -subj "/CN=pds.example.com" 108 - systemctl start tranquil-pds-app tranquil-pds-nginx 109 ``` 110 111 Get a wildcard certificate using DNS validation: ··· 117 -d pds.example.com -d '*.pds.example.com' \ 118 --agree-tos --email you@example.com 119 ``` 120 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 121 122 - For automated renewal, use a DNS provider plugin (e.g., cloudflare, route53). 123 124 Link certificates and restart: 125 ```bash ··· 127 ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem 128 systemctl restart tranquil-pds-nginx 129 ``` 130 - ## 10. Enable All Services 131 ```bash 132 - systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-nginx 133 ``` 134 - ## 11. Configure Firewall 135 ```bash 136 apt install -y ufw 137 ufw allow ssh ··· 139 ufw allow 443/tcp 140 ufw enable 141 ``` 142 - ## 12. Certificate Renewal 143 Add to root's crontab (`crontab -e`): 144 ``` 145 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 146 ``` 147 --- 148 # Alpine 3.23+ with OpenRC 149 Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper. 150 - ## 1. Install Podman 151 ```sh 152 apk update 153 apk add podman podman-compose fuse-overlayfs cni-plugins 154 rc-update add cgroups 155 rc-service cgroups start 156 ``` 157 Enable podman socket for compose: 158 ```sh 159 rc-update add podman 160 rc-service podman start 161 ``` 162 - ## 2. Create Directory Structure 163 ```sh 164 mkdir -p /srv/tranquil-pds/{data,config} 165 mkdir -p /srv/tranquil-pds/data/{postgres,minio,valkey,certs,acme} 166 ``` 167 - ## 3. Clone Repository and Build 168 ```sh 169 cd /opt 170 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 171 cd tranquil-pds 172 podman build -t tranquil-pds:latest . 173 ``` 174 - ## 4. Create Environment File 175 ```sh 176 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 177 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 178 ``` 179 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 180 ```sh 181 openssl rand -base64 48 182 ``` 183 - ## 5. Set Up Compose and nginx 184 Copy the production compose and nginx configs: 185 ```sh 186 - cp /opt/tranquil-pds/docker-compose.prod.yml /srv/tranquil-pds/docker-compose.yml 187 - cp /opt/tranquil-pds/nginx.prod.conf /srv/tranquil-pds/config/nginx.conf 188 ``` 189 Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed: 190 - Update volume mounts to use `/srv/tranquil-pds/data/` paths 191 - - Update nginx cert paths to match `/srv/tranquil-pds/data/certs/` 192 Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths: 193 - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` 194 - ## 6. Create OpenRC Service 195 ```sh 196 cat > /etc/init.d/tranquil-pds << 'EOF' 197 #!/sbin/openrc-run ··· 223 EOF 224 chmod +x /etc/init.d/tranquil-pds 225 ``` 226 - ## 7. Initialize Services 227 Start services: 228 ```sh 229 rc-service tranquil-pds start ··· 236 podman run --rm --network tranquil-pds_default \ 237 -e MINIO_ROOT_USER="$MINIO_ROOT_USER" \ 238 -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ 239 - docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ 240 sh -c 'mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs && mc mb --ignore-existing local/pds-backups' 241 ``` 242 ··· 249 DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}') 250 DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 251 ``` 252 - ## 8. Obtain Wildcard SSL Certificate 253 - User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. 254 255 Create temporary self-signed cert to start services: 256 ```sh ··· 270 -d pds.example.com -d '*.pds.example.com' \ 271 --agree-tos --email you@example.com 272 ``` 273 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 274 275 Link certificates and restart: ··· 278 ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem 279 rc-service tranquil-pds restart 280 ``` 281 - ## 9. Enable Service at Boot 282 ```sh 283 rc-update add tranquil-pds 284 ``` 285 - ## 10. Configure Firewall 286 ```sh 287 apk add iptables ip6tables 288 iptables -A INPUT -p tcp --dport 22 -j ACCEPT ··· 302 /etc/init.d/iptables save 303 /etc/init.d/ip6tables save 304 ``` 305 - ## 11. Certificate Renewal 306 Add to root's crontab (`crontab -e`): 307 ``` 308 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 309 ``` 310 --- 311 # Verification and Maintenance 312 ## Verify Installation 313 ```sh 314 curl -s https://pds.example.com/xrpc/_health | jq 315 curl -s https://pds.example.com/.well-known/atproto-did 316 ``` 317 ## View Logs 318 **Debian:** 319 ```bash 320 journalctl -u tranquil-pds-app -f 321 podman logs -f tranquil-pds-app 322 ``` 323 **Alpine:** 324 ```sh 325 podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f 326 podman logs -f tranquil-pds-tranquil-pds-1 327 ``` 328 ## Update Tranquil PDS 329 ```sh 330 cd /opt/tranquil-pds 331 git pull 332 podman build -t tranquil-pds:latest . 333 ``` 334 335 Debian: 336 ```bash 337 - systemctl restart tranquil-pds-app 338 ``` 339 340 Alpine: 341 ```sh 342 rc-service tranquil-pds restart 343 ``` 344 ## Backup Database 345 **Debian:** 346 ```bash 347 podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql 348 ``` 349 **Alpine:** 350 ```sh 351 podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 353 354 ## Custom Homepage 355 356 - Mount a `homepage.html` into the container's frontend directory and it becomes your landing page. Go nuts with it. Account dashboard is at `/app/` so you won't break anything. 357 358 ```html 359 <!DOCTYPE html> 360 <html>
··· 1 # Tranquil PDS Containerized Production Deployment 2 + 3 This guide covers deploying Tranquil PDS using containers with podman. 4 + 5 - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) 6 - **Alpine 3.23+**: Uses OpenRC service script with podman-compose 7 + 8 ## Prerequisites 9 + 10 - A VPS with at least 2GB RAM and 20GB disk 11 - A domain name pointing to your server's IP 12 - A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains) 13 - Root or sudo access 14 + 15 ## Quick Start (Docker/Podman Compose) 16 + 17 If you just want to get running quickly: 18 + 19 ```sh 20 cp .env.example .env 21 ``` ··· 24 25 Build and start: 26 ```sh 27 + podman build -t tranquil-pds:latest . 28 + podman build -t tranquil-pds-frontend:latest ./frontend 29 + podman-compose -f docker-compose.prod.yaml up -d 30 ``` 31 32 Get initial certificate (after DNS is configured): 33 ```sh 34 + podman-compose -f docker-compose.prod.yaml run --rm certbot certonly \ 35 + --webroot -w /var/www/acme -d pds.example.com -d '*.pds.example.com' 36 + ln -sf live/pds.example.com/fullchain.pem certs/fullchain.pem 37 + ln -sf live/pds.example.com/privkey.pem certs/privkey.pem 38 + podman-compose -f docker-compose.prod.yaml restart nginx 39 ``` 40 + 41 For production setups with proper service management, continue to either the Debian or Alpine section below. 42 + 43 + ## Standalone Containers (No Compose) 44 + 45 + If you already have postgres, valkey, and minio running on the host (eg., from the [Debian install guide](install-debian.md)), you can run just the app containers. 46 + 47 + Build the images: 48 + ```sh 49 + podman build -t tranquil-pds:latest . 50 + podman build -t tranquil-pds-frontend:latest ./frontend 51 + ``` 52 + 53 + Run the backend with host networking (so it can access postgres/valkey/minio on localhost): 54 + ```sh 55 + podman run -d --name tranquil-pds \ 56 + --network=host \ 57 + --env-file /etc/tranquil-pds/tranquil-pds.env \ 58 + tranquil-pds:latest 59 + ``` 60 + 61 + Run the frontend with port mapping (the container's nginx listens on port 80): 62 + ```sh 63 + podman run -d --name tranquil-pds-frontend \ 64 + -p 8080:80 \ 65 + tranquil-pds-frontend:latest 66 + ``` 67 + 68 + Then configure your host nginx to proxy to both containers. Replace the static file `try_files` directives with proxy passes: 69 + 70 + ```nginx 71 + # API routes to backend 72 + location /xrpc/ { 73 + proxy_pass http://127.0.0.1:3000; 74 + # ... (see Debian guide for full proxy headers) 75 + } 76 + 77 + # Static routes to frontend container 78 + location / { 79 + proxy_pass http://127.0.0.1:8080; 80 + proxy_http_version 1.1; 81 + proxy_set_header Host $host; 82 + proxy_set_header X-Real-IP $remote_addr; 83 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 84 + proxy_set_header X-Forwarded-Proto $scheme; 85 + } 86 + ``` 87 + 88 + See the [Debian install guide](install-debian.md) for the full nginx config with all API routes. 89 + 90 --- 91 + 92 # Debian 13+ with Systemd Quadlets 93 + 94 Quadlets are the modern way to run podman containers under systemd. 95 + 96 + ## Install Podman 97 + 98 ```bash 99 apt update 100 apt install -y podman 101 ``` 102 + 103 + ## Create Directory Structure 104 + 105 ```bash 106 mkdir -p /etc/containers/systemd 107 mkdir -p /srv/tranquil-pds/{postgres,minio,valkey,certs,acme,config} 108 ``` 109 + 110 + ## Create Environment File 111 + 112 ```bash 113 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 114 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 115 ``` 116 + 117 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 118 ```bash 119 openssl rand -base64 48 120 ``` 121 + 122 For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion). 123 + 124 + ## Install Quadlet Definitions 125 + 126 Copy the quadlet files from the repository: 127 ```bash 128 cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/ 129 cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/ 130 ``` 131 + 132 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. 133 + 134 + ## Create nginx Configuration 135 + 136 ```bash 137 + cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf 138 ``` 139 + 140 + ## Clone and Build Images 141 + 142 ```bash 143 cd /opt 144 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 145 cd tranquil-pds 146 podman build -t tranquil-pds:latest . 147 + podman build -t tranquil-pds-frontend:latest ./frontend 148 ``` 149 + 150 + ## Create Podman Secrets 151 + 152 ```bash 153 source /srv/tranquil-pds/config/tranquil-pds.env 154 echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - 155 echo "$MINIO_ROOT_PASSWORD" | podman secret create tranquil-pds-minio-password - 156 ``` 157 + 158 + ## Start Services and Initialize 159 + 160 ```bash 161 systemctl daemon-reload 162 systemctl start tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey ··· 168 podman run --rm --pod tranquil-pds \ 169 -e MINIO_ROOT_USER=minioadmin \ 170 -e MINIO_ROOT_PASSWORD=your-minio-password \ 171 + cgr.dev/chainguard/minio-client:latest-dev \ 172 sh -c "mc alias set local http://localhost:9000 \$MINIO_ROOT_USER \$MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs && mc mb --ignore-existing local/pds-backups" 173 ``` 174 ··· 177 cargo install sqlx-cli --no-default-features --features postgres 178 DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 179 ``` 180 + 181 + ## Obtain Wildcard SSL Certificate 182 + 183 + User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. 184 185 Create temporary self-signed cert to start services: 186 ```bash ··· 188 -keyout /srv/tranquil-pds/certs/privkey.pem \ 189 -out /srv/tranquil-pds/certs/fullchain.pem \ 190 -subj "/CN=pds.example.com" 191 + systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx 192 ``` 193 194 Get a wildcard certificate using DNS validation: ··· 200 -d pds.example.com -d '*.pds.example.com' \ 201 --agree-tos --email you@example.com 202 ``` 203 + 204 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 205 206 + For automated renewal, use a DNS provider plugin (eg., cloudflare, route53). 207 208 Link certificates and restart: 209 ```bash ··· 211 ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem 212 systemctl restart tranquil-pds-nginx 213 ``` 214 + 215 + ## Enable All Services 216 + 217 ```bash 218 + systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx 219 ``` 220 + 221 + ## Configure Firewall 222 + 223 ```bash 224 apt install -y ufw 225 ufw allow ssh ··· 227 ufw allow 443/tcp 228 ufw enable 229 ``` 230 + 231 + ## Certificate Renewal 232 + 233 Add to root's crontab (`crontab -e`): 234 ``` 235 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 236 ``` 237 + 238 --- 239 + 240 # Alpine 3.23+ with OpenRC 241 + 242 Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper. 243 + 244 + ## Install Podman 245 + 246 ```sh 247 apk update 248 apk add podman podman-compose fuse-overlayfs cni-plugins 249 rc-update add cgroups 250 rc-service cgroups start 251 ``` 252 + 253 Enable podman socket for compose: 254 ```sh 255 rc-update add podman 256 rc-service podman start 257 ``` 258 + 259 + ## Create Directory Structure 260 + 261 ```sh 262 mkdir -p /srv/tranquil-pds/{data,config} 263 mkdir -p /srv/tranquil-pds/data/{postgres,minio,valkey,certs,acme} 264 ``` 265 + 266 + ## Clone Repository and Build Images 267 + 268 ```sh 269 cd /opt 270 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 271 cd tranquil-pds 272 podman build -t tranquil-pds:latest . 273 + podman build -t tranquil-pds-frontend:latest ./frontend 274 ``` 275 + 276 + ## Create Environment File 277 + 278 ```sh 279 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 280 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 281 ``` 282 + 283 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 284 ```sh 285 openssl rand -base64 48 286 ``` 287 + 288 + ## Set Up Compose and nginx 289 + 290 Copy the production compose and nginx configs: 291 ```sh 292 + cp /opt/tranquil-pds/docker-compose.prod.yaml /srv/tranquil-pds/docker-compose.yml 293 + cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf 294 ``` 295 + 296 Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed: 297 - Update volume mounts to use `/srv/tranquil-pds/data/` paths 298 + - Update nginx config path to `/srv/tranquil-pds/config/nginx.conf` 299 + 300 Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths: 301 - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` 302 + 303 + ## Create OpenRC Service 304 + 305 ```sh 306 cat > /etc/init.d/tranquil-pds << 'EOF' 307 #!/sbin/openrc-run ··· 333 EOF 334 chmod +x /etc/init.d/tranquil-pds 335 ``` 336 + 337 + ## Initialize Services 338 + 339 Start services: 340 ```sh 341 rc-service tranquil-pds start ··· 348 podman run --rm --network tranquil-pds_default \ 349 -e MINIO_ROOT_USER="$MINIO_ROOT_USER" \ 350 -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ 351 + cgr.dev/chainguard/minio-client:latest-dev \ 352 sh -c 'mc alias set local http://minio:9000 $MINIO_ROOT_USER $MINIO_ROOT_PASSWORD && mc mb --ignore-existing local/pds-blobs && mc mb --ignore-existing local/pds-backups' 353 ``` 354 ··· 361 DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}') 362 DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 363 ``` 364 + 365 + ## Obtain Wildcard SSL Certificate 366 + 367 + User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation. 368 369 Create temporary self-signed cert to start services: 370 ```sh ··· 384 -d pds.example.com -d '*.pds.example.com' \ 385 --agree-tos --email you@example.com 386 ``` 387 + 388 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 389 390 Link certificates and restart: ··· 393 ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem 394 rc-service tranquil-pds restart 395 ``` 396 + 397 + ## Enable Service at Boot 398 + 399 ```sh 400 rc-update add tranquil-pds 401 ``` 402 + 403 + ## Configure Firewall 404 + 405 ```sh 406 apk add iptables ip6tables 407 iptables -A INPUT -p tcp --dport 22 -j ACCEPT ··· 421 /etc/init.d/iptables save 422 /etc/init.d/ip6tables save 423 ``` 424 + 425 + ## Certificate Renewal 426 + 427 Add to root's crontab (`crontab -e`): 428 ``` 429 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 430 ``` 431 + 432 --- 433 + 434 # Verification and Maintenance 435 + 436 ## Verify Installation 437 + 438 ```sh 439 curl -s https://pds.example.com/xrpc/_health | jq 440 curl -s https://pds.example.com/.well-known/atproto-did 441 ``` 442 + 443 ## View Logs 444 + 445 **Debian:** 446 ```bash 447 journalctl -u tranquil-pds-app -f 448 podman logs -f tranquil-pds-app 449 + podman logs -f tranquil-pds-frontend 450 ``` 451 + 452 **Alpine:** 453 ```sh 454 podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f 455 podman logs -f tranquil-pds-tranquil-pds-1 456 + podman logs -f tranquil-pds-frontend-1 457 ``` 458 + 459 ## Update Tranquil PDS 460 + 461 ```sh 462 cd /opt/tranquil-pds 463 git pull 464 podman build -t tranquil-pds:latest . 465 + podman build -t tranquil-pds-frontend:latest ./frontend 466 ``` 467 468 Debian: 469 ```bash 470 + systemctl restart tranquil-pds-app tranquil-pds-frontend 471 ``` 472 473 Alpine: 474 ```sh 475 rc-service tranquil-pds restart 476 ``` 477 + 478 ## Backup Database 479 + 480 **Debian:** 481 ```bash 482 podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql 483 ``` 484 + 485 **Alpine:** 486 ```sh 487 podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 489 490 ## Custom Homepage 491 492 + The frontend container serves `homepage.html` as the landing page. To customize it, either: 493 494 + 1. Build a custom frontend image with your own `homepage.html` 495 + 2. Mount a custom `homepage.html` into the frontend container 496 + 497 + Example custom homepage: 498 ```html 499 <!DOCTYPE html> 500 <html>
+173 -26
docs/install-debian.md
··· 1 # Tranquil PDS Production Installation on Debian 2 This guide covers installing Tranquil PDS on Debian 13. 3 4 ## Prerequisites 5 - A VPS with at least 2GB RAM and 20GB disk 6 - A domain name pointing to your server's IP 7 - A wildcard TLS certificate for `*.pds.example.com` (user handles are served as subdomains) 8 - Root or sudo access 9 - ## 1. System Setup 10 ```bash 11 apt update && apt upgrade -y 12 apt install -y curl git build-essential pkg-config libssl-dev 13 ``` 14 - ## 2. Install Rust 15 ```bash 16 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 17 source ~/.cargo/env 18 rustup default stable 19 ``` 20 This installs the latest stable Rust. 21 - ## 3. Install postgres 22 ```bash 23 apt install -y postgresql postgresql-contrib 24 systemctl enable postgresql ··· 27 sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 28 sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 29 ``` 30 - ## 4. Install minio 31 ```bash 32 curl -O https://dl.min.io/server/minio/release/linux-amd64/minio 33 chmod +x minio ··· 59 systemctl enable minio 60 systemctl start minio 61 ``` 62 Create the buckets (wait a few seconds for minio to start): 63 ```bash 64 curl -O https://dl.min.io/client/mc/release/linux-amd64/mc ··· 68 mc mb local/pds-blobs 69 mc mb local/pds-backups 70 ``` 71 - ## 5. Install valkey 72 ```bash 73 apt install -y valkey 74 systemctl enable valkey-server 75 systemctl start valkey-server 76 ``` 77 - ## 6. Install deno (for frontend build) 78 ```bash 79 curl -fsSL https://deno.land/install.sh | sh 80 export PATH="$HOME/.deno/bin:$PATH" 81 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 82 ``` 83 - ## 7. Clone and Build Tranquil PDS 84 ```bash 85 cd /opt 86 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 87 cd tranquil-pds 88 cd frontend 89 deno task build 90 cd .. 91 cargo build --release 92 ``` 93 - ## 8. Install sqlx-cli and Run Migrations 94 ```bash 95 cargo install sqlx-cli --no-default-features --features postgres 96 export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 97 sqlx migrate run 98 ``` 99 - ## 9. Configure Tranquil PDS 100 ```bash 101 mkdir -p /etc/tranquil-pds 102 cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env 103 chmod 600 /etc/tranquil-pds/tranquil-pds.env 104 ``` 105 Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with: 106 ```bash 107 openssl rand -base64 48 108 ``` 109 - ## 10. Create Systemd Service 110 ```bash 111 useradd -r -s /sbin/nologin tranquil-pds 112 cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 113 - mkdir -p /var/lib/tranquil-pds 114 - cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend 115 - chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds 116 cat > /etc/systemd/system/tranquil-pds.service << 'EOF' 117 [Unit] 118 Description=Tranquil PDS - AT Protocol PDS ··· 122 User=tranquil-pds 123 Group=tranquil-pds 124 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 125 - Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 126 ExecStart=/usr/local/bin/tranquil-pds 127 Restart=always 128 RestartSec=5 129 [Install] 130 WantedBy=multi-user.target 131 EOF 132 systemctl daemon-reload 133 systemctl enable tranquil-pds 134 systemctl start tranquil-pds 135 ``` 136 - ## 11. Install and Configure nginx 137 ```bash 138 apt install -y nginx certbot python3-certbot-nginx 139 cat > /etc/nginx/sites-available/tranquil-pds << 'EOF' 140 server { 141 listen 80; 142 listen [::]:80; 143 - server_name pds.example.com; 144 location / { 145 proxy_pass http://127.0.0.1:3000; 146 proxy_http_version 1.1; 147 proxy_set_header Upgrade $http_upgrade; ··· 151 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 152 proxy_set_header X-Forwarded-Proto $scheme; 153 proxy_read_timeout 86400; 154 } 155 } 156 EOF 157 - ln -s /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 158 rm -f /etc/nginx/sites-enabled/default 159 nginx -t 160 systemctl reload nginx 161 ``` 162 - ## 12. Obtain Wildcard SSL Certificate 163 - User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. 164 165 Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin: 166 ```bash ··· 175 certbot certonly --manual --preferred-challenges dns \ 176 -d pds.example.com -d '*.pds.example.com' 177 ``` 178 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 179 180 - After obtaining the cert, update nginx to use it and reload. 181 - ## 13. Configure Firewall 182 ```bash 183 apt install -y ufw 184 ufw allow ssh ··· 186 ufw allow 443/tcp 187 ufw enable 188 ``` 189 - ## 14. Verify Installation 190 ```bash 191 systemctl status tranquil-pds 192 curl -s https://pds.example.com/xrpc/_health | jq 193 curl -s https://pds.example.com/.well-known/atproto-did 194 ``` 195 ## Maintenance 196 View logs: 197 ```bash 198 journalctl -u tranquil-pds -f 199 ``` 200 Update Tranquil PDS: 201 ```bash 202 cd /opt/tranquil-pds ··· 205 cargo build --release 206 systemctl stop tranquil-pds 207 cp target/release/tranquil-pds /usr/local/bin/ 208 - cp -r frontend/dist /var/lib/tranquil-pds/frontend 209 DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 210 systemctl start tranquil-pds 211 ``` 212 Backup database: 213 ```bash 214 sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 216 217 ## Custom Homepage 218 219 - Drop a `homepage.html` in `/var/lib/tranquil-pds/frontend/` and it becomes your landing page. Go nuts with it. Account dashboard is at `/app/` so you won't break anything. 220 221 ```bash 222 - cat > /var/lib/tranquil-pds/frontend/homepage.html << 'EOF' 223 <!DOCTYPE html> 224 <html> 225 <head>
··· 1 # Tranquil PDS Production Installation on Debian 2 + 3 This guide covers installing Tranquil PDS on Debian 13. 4 5 ## Prerequisites 6 + 7 - A VPS with at least 2GB RAM and 20GB disk 8 - A domain name pointing to your server's IP 9 - A wildcard TLS certificate for `*.pds.example.com` (user handles are served as subdomains) 10 - Root or sudo access 11 + 12 + ## System Setup 13 + 14 ```bash 15 apt update && apt upgrade -y 16 apt install -y curl git build-essential pkg-config libssl-dev 17 ``` 18 + 19 + ## Install Rust 20 + 21 ```bash 22 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 23 source ~/.cargo/env 24 rustup default stable 25 ``` 26 + 27 This installs the latest stable Rust. 28 + 29 + ## Install postgres 30 + 31 ```bash 32 apt install -y postgresql postgresql-contrib 33 systemctl enable postgresql ··· 36 sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 37 sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 38 ``` 39 + 40 + ## Install minio 41 + 42 ```bash 43 curl -O https://dl.min.io/server/minio/release/linux-amd64/minio 44 chmod +x minio ··· 70 systemctl enable minio 71 systemctl start minio 72 ``` 73 + 74 Create the buckets (wait a few seconds for minio to start): 75 ```bash 76 curl -O https://dl.min.io/client/mc/release/linux-amd64/mc ··· 80 mc mb local/pds-blobs 81 mc mb local/pds-backups 82 ``` 83 + 84 + ## Install valkey 85 + 86 ```bash 87 apt install -y valkey 88 systemctl enable valkey-server 89 systemctl start valkey-server 90 ``` 91 + 92 + ## Install deno (for frontend build) 93 + 94 ```bash 95 curl -fsSL https://deno.land/install.sh | sh 96 export PATH="$HOME/.deno/bin:$PATH" 97 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 98 ``` 99 + 100 + ## Clone and Build Tranquil PDS 101 + 102 ```bash 103 cd /opt 104 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 105 cd tranquil-pds 106 cd frontend 107 deno task build 108 cd .. 109 cargo build --release 110 ``` 111 + 112 + ## Install sqlx-cli and Run Migrations 113 + 114 ```bash 115 cargo install sqlx-cli --no-default-features --features postgres 116 export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 117 sqlx migrate run 118 ``` 119 + 120 + ## Configure Tranquil PDS 121 + 122 ```bash 123 mkdir -p /etc/tranquil-pds 124 cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env 125 chmod 600 /etc/tranquil-pds/tranquil-pds.env 126 ``` 127 + 128 Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with: 129 ```bash 130 openssl rand -base64 48 131 ``` 132 + 133 + ## Install Frontend Files 134 + 135 + ```bash 136 + mkdir -p /var/www/tranquil-pds 137 + cp -r /opt/tranquil-pds/frontend/dist/* /var/www/tranquil-pds/ 138 + chown -R www-data:www-data /var/www/tranquil-pds 139 + ``` 140 + 141 + ## Create Systemd Service 142 + 143 ```bash 144 useradd -r -s /sbin/nologin tranquil-pds 145 cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/ 146 + 147 cat > /etc/systemd/system/tranquil-pds.service << 'EOF' 148 [Unit] 149 Description=Tranquil PDS - AT Protocol PDS ··· 153 User=tranquil-pds 154 Group=tranquil-pds 155 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 156 ExecStart=/usr/local/bin/tranquil-pds 157 Restart=always 158 RestartSec=5 159 [Install] 160 WantedBy=multi-user.target 161 EOF 162 + 163 systemctl daemon-reload 164 systemctl enable tranquil-pds 165 systemctl start tranquil-pds 166 ``` 167 + 168 + ## Install and Configure nginx 169 + 170 ```bash 171 apt install -y nginx certbot python3-certbot-nginx 172 + 173 cat > /etc/nginx/sites-available/tranquil-pds << 'EOF' 174 server { 175 listen 80; 176 listen [::]:80; 177 + server_name pds.example.com *.pds.example.com; 178 + 179 + location /.well-known/acme-challenge/ { 180 + root /var/www/acme; 181 + } 182 + 183 location / { 184 + return 301 https://$host$request_uri; 185 + } 186 + } 187 + 188 + server { 189 + listen 443 ssl; 190 + listen [::]:443 ssl; 191 + http2 on; 192 + server_name pds.example.com *.pds.example.com; 193 + 194 + ssl_certificate /etc/letsencrypt/live/pds.example.com/fullchain.pem; 195 + ssl_certificate_key /etc/letsencrypt/live/pds.example.com/privkey.pem; 196 + 197 + client_max_body_size 10G; 198 + 199 + root /var/www/tranquil-pds; 200 + 201 + location /xrpc/ { 202 proxy_pass http://127.0.0.1:3000; 203 proxy_http_version 1.1; 204 proxy_set_header Upgrade $http_upgrade; ··· 208 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 209 proxy_set_header X-Forwarded-Proto $scheme; 210 proxy_read_timeout 86400; 211 + proxy_send_timeout 86400; 212 + proxy_buffering off; 213 + proxy_request_buffering off; 214 + } 215 + 216 + location /oauth/ { 217 + proxy_pass http://127.0.0.1:3000; 218 + proxy_http_version 1.1; 219 + proxy_set_header Host $host; 220 + proxy_set_header X-Real-IP $remote_addr; 221 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 222 + proxy_set_header X-Forwarded-Proto $scheme; 223 + proxy_read_timeout 300; 224 + proxy_send_timeout 300; 225 + } 226 + 227 + location /.well-known/ { 228 + proxy_pass http://127.0.0.1:3000; 229 + proxy_http_version 1.1; 230 + proxy_set_header Host $host; 231 + proxy_set_header X-Real-IP $remote_addr; 232 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 233 + proxy_set_header X-Forwarded-Proto $scheme; 234 + } 235 + 236 + location = /metrics { 237 + proxy_pass http://127.0.0.1:3000; 238 + proxy_http_version 1.1; 239 + proxy_set_header Host $host; 240 + } 241 + 242 + location = /health { 243 + proxy_pass http://127.0.0.1:3000; 244 + proxy_http_version 1.1; 245 + proxy_set_header Host $host; 246 + } 247 + 248 + location = /robots.txt { 249 + proxy_pass http://127.0.0.1:3000; 250 + proxy_http_version 1.1; 251 + proxy_set_header Host $host; 252 + } 253 + 254 + location = /logo { 255 + proxy_pass http://127.0.0.1:3000; 256 + proxy_http_version 1.1; 257 + proxy_set_header Host $host; 258 + } 259 + 260 + location ~ ^/u/[^/]+/did\.json$ { 261 + proxy_pass http://127.0.0.1:3000; 262 + proxy_http_version 1.1; 263 + proxy_set_header Host $host; 264 + proxy_set_header X-Real-IP $remote_addr; 265 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 266 + proxy_set_header X-Forwarded-Proto $scheme; 267 + } 268 + 269 + location /assets/ { 270 + expires 1y; 271 + add_header Cache-Control "public, immutable"; 272 + try_files $uri =404; 273 + } 274 + 275 + location /app/ { 276 + try_files $uri $uri/ /index.html; 277 + } 278 + 279 + location = / { 280 + try_files /homepage.html /index.html; 281 + } 282 + 283 + location / { 284 + try_files $uri $uri/ /index.html; 285 } 286 } 287 EOF 288 + 289 + ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 290 rm -f /etc/nginx/sites-enabled/default 291 + mkdir -p /var/www/acme 292 nginx -t 293 systemctl reload nginx 294 ``` 295 + 296 + ## Obtain Wildcard SSL Certificate 297 + 298 + User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. 299 300 Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin: 301 ```bash ··· 310 certbot certonly --manual --preferred-challenges dns \ 311 -d pds.example.com -d '*.pds.example.com' 312 ``` 313 + 314 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 315 316 + After obtaining the cert, reload nginx: 317 + ```bash 318 + systemctl reload nginx 319 + ``` 320 + 321 + ## Configure Firewall 322 + 323 ```bash 324 apt install -y ufw 325 ufw allow ssh ··· 327 ufw allow 443/tcp 328 ufw enable 329 ``` 330 + 331 + ## Verify Installation 332 + 333 ```bash 334 systemctl status tranquil-pds 335 curl -s https://pds.example.com/xrpc/_health | jq 336 curl -s https://pds.example.com/.well-known/atproto-did 337 ``` 338 + 339 ## Maintenance 340 + 341 View logs: 342 ```bash 343 journalctl -u tranquil-pds -f 344 ``` 345 + 346 Update Tranquil PDS: 347 ```bash 348 cd /opt/tranquil-pds ··· 351 cargo build --release 352 systemctl stop tranquil-pds 353 cp target/release/tranquil-pds /usr/local/bin/ 354 + cp -r frontend/dist/* /var/www/tranquil-pds/ 355 DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 356 systemctl start tranquil-pds 357 ``` 358 + 359 Backup database: 360 ```bash 361 sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 363 364 ## Custom Homepage 365 366 + 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. 367 368 ```bash 369 + cat > /var/www/tranquil-pds/homepage.html << 'EOF' 370 <!DOCTYPE html> 371 <html> 372 <head>
+9
frontend/Dockerfile
···
··· 1 + FROM denoland/deno:alpine AS builder 2 + WORKDIR /app 3 + COPY . ./ 4 + RUN deno task build 5 + 6 + FROM nginx:1.29-alpine 7 + COPY --from=builder /app/dist /usr/share/nginx/html 8 + COPY nginx.conf /etc/nginx/conf.d/default.conf 9 + EXPOSE 80
+30
frontend/nginx-quadlet.conf
···
··· 1 + server { 2 + listen 8080; 3 + listen [::]:8080; 4 + server_name _; 5 + 6 + root /usr/share/nginx/html; 7 + index index.html; 8 + 9 + gzip on; 10 + gzip_vary on; 11 + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; 12 + 13 + location /assets/ { 14 + expires 1y; 15 + add_header Cache-Control "public, immutable"; 16 + try_files $uri =404; 17 + } 18 + 19 + location = / { 20 + try_files /homepage.html /index.html; 21 + } 22 + 23 + location /app/ { 24 + try_files $uri $uri/ /index.html; 25 + } 26 + 27 + location / { 28 + try_files $uri $uri/ /index.html; 29 + } 30 + }
+30
frontend/nginx.conf
···
··· 1 + server { 2 + listen 80; 3 + listen [::]:80; 4 + server_name _; 5 + 6 + root /usr/share/nginx/html; 7 + index index.html; 8 + 9 + gzip on; 10 + gzip_vary on; 11 + gzip_types text/plain text/css application/json application/javascript text/xml application/xml; 12 + 13 + location /assets/ { 14 + expires 1y; 15 + add_header Cache-Control "public, immutable"; 16 + try_files $uri =404; 17 + } 18 + 19 + location = / { 20 + try_files /homepage.html /index.html; 21 + } 22 + 23 + location /app/ { 24 + try_files $uri $uri/ /index.html; 25 + } 26 + 27 + location / { 28 + try_files $uri $uri/ /index.html; 29 + } 30 + }
+5 -5
frontend/public/homepage.html
··· 440 <a href="/app/register" class="btn primary" id="heroPrimary" 441 >Join This Server</a> 442 <a 443 - href="https://tangled.org/lewis.moe/bspds-sandbox" 444 class="btn secondary" 445 id="heroSecondary" 446 target="_blank" ··· 461 <div class="feature"> 462 <h3>Real security</h3> 463 <p> 464 - Sign in with passkeys, add two-factor authentication, set up 465 - backup codes, and mark devices you trust. Your account stays 466 - yours. 467 </p> 468 </div> 469 ··· 546 <a href="/app/register" class="btn primary" id="footerPrimary" 547 >Join This Server</a> 548 <a 549 - href="https://tangled.org/lewis.moe/bspds-sandbox" 550 class="btn secondary" 551 target="_blank" 552 rel="noopener"
··· 440 <a href="/app/register" class="btn primary" id="heroPrimary" 441 >Join This Server</a> 442 <a 443 + href="https://tangled.org/tranquil.farm/tranquil-pds" 444 class="btn secondary" 445 id="heroSecondary" 446 target="_blank" ··· 461 <div class="feature"> 462 <h3>Real security</h3> 463 <p> 464 + Sign in with passkeys or SSO, add two-factor authentication, 465 + set up backup codes, and mark devices you trust. Your account 466 + stays yours. 467 </p> 468 </div> 469 ··· 546 <a href="/app/register" class="btn primary" id="footerPrimary" 547 >Join This Server</a> 548 <a 549 + href="https://tangled.org/tranquil.farm/tranquil-pds" 550 class="btn secondary" 551 target="_blank" 552 rel="noopener"
+8 -8
frontend/src/locales/en.json
··· 160 "signal": "Signal", 161 "signalNumber": "Signal Phone Number", 162 "signalNumberPlaceholder": "+1234567890", 163 - "signalNumberHint": "Include country code (e.g., +1 for US)", 164 "notConfigured": "not configured", 165 "inviteCode": "Invite Code", 166 "inviteCodePlaceholder": "Enter your invite code", ··· 263 "saveFailed": "Failed to save DID document", 264 "loadFailed": "Failed to load DID document", 265 "invalidMultibase": "Public key must be a valid multibase string starting with 'z'", 266 - "invalidHandle": "Handle must be an at:// URI (e.g., at://handle.example.com)", 267 "helpTitle": "What is this?", 268 "helpText": "When you migrate to another PDS, that PDS generates new signing keys. Update your DID document here so it points to your new keys and location. This enables multi-hop migrations (PDS 1 → PDS 2 → PDS 3)." 269 }, ··· 385 "title": "App Passwords", 386 "description": "App passwords let you sign in to third-party apps without giving them your main password. Each app password can be revoked individually.", 387 "createNew": "Create New App Password", 388 - "appNamePlaceholder": "App name (e.g., Graysky, Skeets)", 389 "created": "App Password Created", 390 "createdMessage": "Copy this password now. You won't be able to see it again.", 391 "yourPasswords": "Your App Passwords", ··· 452 "adding": "Adding...", 453 "noPasskeys": "No passkeys registered", 454 "passkeyName": "Passkey name", 455 - "passkeyNamePlaceholder": "e.g., MacBook Pro, iPhone", 456 "register": "Register", 457 "registering": "Registering...", 458 "rename": "Rename", ··· 1007 "infoAppAccess": "Using third-party apps", 1008 "infoAppAccessDesc": "After creating your account, you will receive an app password. Use this to sign in to Bluesky apps and other AT Protocol clients.", 1009 "passkeyNameLabel": "Passkey Name (optional)", 1010 - "passkeyNamePlaceholder": "e.g., MacBook Touch ID", 1011 "passkeyNameHint": "A friendly name to identify this passkey", 1012 "passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:", 1013 "passkeyPromptBullet1": "Touch ID or Face ID", ··· 1279 "checkingAvailability": "Checking availability...", 1280 "handleAvailable": "Handle is available!", 1281 "handleTaken": "Handle is already taken", 1282 - "handleHint": "You can also use your own domain by entering the full handle (e.g., alice.mydomain.com)", 1283 "email": "Email Address", 1284 "authMethod": "Authentication Method", 1285 "authPassword": "Password", ··· 1320 "title": "Set Up Your Passkey", 1321 "desc": "Your email has been verified. Now set up your passkey for secure, passwordless login.", 1322 "nameLabel": "Passkey Name (optional)", 1323 - "namePlaceholder": "e.g., MacBook Pro, iPhone", 1324 "nameHint": "A friendly name to identify this passkey", 1325 "instructions": "Click the button below to register your passkey. Your device will prompt you to use biometrics (fingerprint, Face ID) or a security key.", 1326 "register": "Register Passkey", ··· 1418 "title": "Enter Your DID", 1419 "desc": "Enter the DID of the account you want to restore.", 1420 "label": "Your DID", 1421 - "hint": "Your decentralized identifier (e.g., did:plc:abc123...)" 1422 }, 1423 "uploadCar": { 1424 "title": "Upload Repository Backup",
··· 160 "signal": "Signal", 161 "signalNumber": "Signal Phone Number", 162 "signalNumberPlaceholder": "+1234567890", 163 + "signalNumberHint": "Include country code (eg., +1 for US)", 164 "notConfigured": "not configured", 165 "inviteCode": "Invite Code", 166 "inviteCodePlaceholder": "Enter your invite code", ··· 263 "saveFailed": "Failed to save DID document", 264 "loadFailed": "Failed to load DID document", 265 "invalidMultibase": "Public key must be a valid multibase string starting with 'z'", 266 + "invalidHandle": "Handle must be an at:// URI (eg., at://handle.example.com)", 267 "helpTitle": "What is this?", 268 "helpText": "When you migrate to another PDS, that PDS generates new signing keys. Update your DID document here so it points to your new keys and location. This enables multi-hop migrations (PDS 1 → PDS 2 → PDS 3)." 269 }, ··· 385 "title": "App Passwords", 386 "description": "App passwords let you sign in to third-party apps without giving them your main password. Each app password can be revoked individually.", 387 "createNew": "Create New App Password", 388 + "appNamePlaceholder": "App name (eg., Graysky, Skeets)", 389 "created": "App Password Created", 390 "createdMessage": "Copy this password now. You won't be able to see it again.", 391 "yourPasswords": "Your App Passwords", ··· 452 "adding": "Adding...", 453 "noPasskeys": "No passkeys registered", 454 "passkeyName": "Passkey name", 455 + "passkeyNamePlaceholder": "eg., MacBook Pro, iPhone", 456 "register": "Register", 457 "registering": "Registering...", 458 "rename": "Rename", ··· 1007 "infoAppAccess": "Using third-party apps", 1008 "infoAppAccessDesc": "After creating your account, you will receive an app password. Use this to sign in to Bluesky apps and other AT Protocol clients.", 1009 "passkeyNameLabel": "Passkey Name (optional)", 1010 + "passkeyNamePlaceholder": "eg., MacBook Touch ID", 1011 "passkeyNameHint": "A friendly name to identify this passkey", 1012 "passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:", 1013 "passkeyPromptBullet1": "Touch ID or Face ID", ··· 1279 "checkingAvailability": "Checking availability...", 1280 "handleAvailable": "Handle is available!", 1281 "handleTaken": "Handle is already taken", 1282 + "handleHint": "You can also use your own domain by entering the full handle (eg., alice.mydomain.com)", 1283 "email": "Email Address", 1284 "authMethod": "Authentication Method", 1285 "authPassword": "Password", ··· 1320 "title": "Set Up Your Passkey", 1321 "desc": "Your email has been verified. Now set up your passkey for secure, passwordless login.", 1322 "nameLabel": "Passkey Name (optional)", 1323 + "namePlaceholder": "eg., MacBook Pro, iPhone", 1324 "nameHint": "A friendly name to identify this passkey", 1325 "instructions": "Click the button below to register your passkey. Your device will prompt you to use biometrics (fingerprint, Face ID) or a security key.", 1326 "register": "Register Passkey", ··· 1418 "title": "Enter Your DID", 1419 "desc": "Enter the DID of the account you want to restore.", 1420 "label": "Your DID", 1421 + "hint": "Your decentralized identifier (eg., did:plc:abc123...)" 1422 }, 1423 "uploadCar": { 1424 "title": "Upload Repository Backup",
+3 -2
justfile
··· 81 podman compose down 82 podman-logs: 83 podman compose logs -f 84 - podman-build: 85 - podman compose build 86 87 frontend-dev: 88 . ~/.deno/env && cd frontend && deno task dev
··· 81 podman compose down 82 podman-logs: 83 podman compose logs -f 84 + container-build: 85 + podman build -t tranquil-pds:latest . 86 + podman build -t tranquil-pds-frontend:latest ./frontend 87 88 frontend-dev: 89 . ~/.deno/env && cd frontend && deno task dev
+160
nginx.frontend.conf
···
··· 1 + worker_processes auto; 2 + error_log /var/log/nginx/error.log warn; 3 + pid /var/run/nginx.pid; 4 + 5 + events { 6 + worker_connections 4096; 7 + use epoll; 8 + multi_accept on; 9 + } 10 + 11 + http { 12 + include /etc/nginx/mime.types; 13 + default_type application/octet-stream; 14 + 15 + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 16 + '$status $body_bytes_sent "$http_referer" ' 17 + '"$http_user_agent" "$http_x_forwarded_for" ' 18 + 'rt=$request_time uct="$upstream_connect_time" ' 19 + 'uht="$upstream_header_time" urt="$upstream_response_time"'; 20 + 21 + access_log /var/log/nginx/access.log main; 22 + 23 + sendfile on; 24 + tcp_nopush on; 25 + tcp_nodelay on; 26 + keepalive_timeout 65; 27 + types_hash_max_size 2048; 28 + 29 + gzip on; 30 + gzip_vary on; 31 + gzip_proxied any; 32 + gzip_comp_level 6; 33 + gzip_types text/plain text/css text/xml application/json application/javascript 34 + application/xml application/xml+rss text/javascript application/activity+json; 35 + 36 + ssl_protocols TLSv1.2 TLSv1.3; 37 + ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; 38 + ssl_prefer_server_ciphers off; 39 + ssl_session_cache shared:SSL:10m; 40 + ssl_session_timeout 1d; 41 + ssl_session_tickets off; 42 + ssl_stapling on; 43 + ssl_stapling_verify on; 44 + 45 + upstream backend { 46 + server tranquil-pds:3000; 47 + keepalive 32; 48 + } 49 + 50 + upstream frontend { 51 + server frontend:80; 52 + keepalive 16; 53 + } 54 + 55 + server { 56 + listen 80; 57 + listen [::]:80; 58 + server_name _; 59 + 60 + location /.well-known/acme-challenge/ { 61 + root /var/www/acme; 62 + } 63 + 64 + location / { 65 + return 301 https://$host$request_uri; 66 + } 67 + } 68 + 69 + server { 70 + listen 443 ssl; 71 + listen [::]:443 ssl; 72 + http2 on; 73 + server_name _; 74 + 75 + ssl_certificate /etc/nginx/certs/fullchain.pem; 76 + ssl_certificate_key /etc/nginx/certs/privkey.pem; 77 + 78 + client_max_body_size 10G; 79 + 80 + location /xrpc/ { 81 + proxy_pass http://backend; 82 + proxy_http_version 1.1; 83 + proxy_set_header Upgrade $http_upgrade; 84 + proxy_set_header Connection "upgrade"; 85 + proxy_set_header Host $host; 86 + proxy_set_header X-Real-IP $remote_addr; 87 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 88 + proxy_set_header X-Forwarded-Proto $scheme; 89 + proxy_read_timeout 86400; 90 + proxy_send_timeout 86400; 91 + proxy_buffering off; 92 + proxy_request_buffering off; 93 + } 94 + 95 + location /oauth/ { 96 + proxy_pass http://backend; 97 + proxy_http_version 1.1; 98 + proxy_set_header Host $host; 99 + proxy_set_header X-Real-IP $remote_addr; 100 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 101 + proxy_set_header X-Forwarded-Proto $scheme; 102 + proxy_read_timeout 300; 103 + proxy_send_timeout 300; 104 + } 105 + 106 + location /.well-known/ { 107 + proxy_pass http://backend; 108 + proxy_http_version 1.1; 109 + proxy_set_header Host $host; 110 + proxy_set_header X-Real-IP $remote_addr; 111 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 112 + proxy_set_header X-Forwarded-Proto $scheme; 113 + } 114 + 115 + location = /metrics { 116 + proxy_pass http://backend; 117 + proxy_http_version 1.1; 118 + proxy_set_header Host $host; 119 + proxy_set_header X-Real-IP $remote_addr; 120 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 121 + proxy_set_header X-Forwarded-Proto $scheme; 122 + } 123 + 124 + location = /health { 125 + proxy_pass http://backend; 126 + proxy_http_version 1.1; 127 + proxy_set_header Host $host; 128 + } 129 + 130 + location = /robots.txt { 131 + proxy_pass http://backend; 132 + proxy_http_version 1.1; 133 + proxy_set_header Host $host; 134 + } 135 + 136 + location = /logo { 137 + proxy_pass http://backend; 138 + proxy_http_version 1.1; 139 + proxy_set_header Host $host; 140 + } 141 + 142 + location ~ ^/u/[^/]+/did\.json$ { 143 + proxy_pass http://backend; 144 + proxy_http_version 1.1; 145 + proxy_set_header Host $host; 146 + proxy_set_header X-Real-IP $remote_addr; 147 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 148 + proxy_set_header X-Forwarded-Proto $scheme; 149 + } 150 + 151 + location / { 152 + proxy_pass http://frontend; 153 + proxy_http_version 1.1; 154 + proxy_set_header Host $host; 155 + proxy_set_header X-Real-IP $remote_addr; 156 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 157 + proxy_set_header X-Forwarded-Proto $scheme; 158 + } 159 + } 160 + }
-87
nginx.prod.conf
··· 1 - worker_processes auto; 2 - error_log /var/log/nginx/error.log warn; 3 - pid /var/run/nginx.pid; 4 - events { 5 - worker_connections 4096; 6 - use epoll; 7 - multi_accept on; 8 - } 9 - http { 10 - include /etc/nginx/mime.types; 11 - default_type application/octet-stream; 12 - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 13 - '$status $body_bytes_sent "$http_referer" ' 14 - '"$http_user_agent" "$http_x_forwarded_for" ' 15 - 'rt=$request_time uct="$upstream_connect_time" ' 16 - 'uht="$upstream_header_time" urt="$upstream_response_time"'; 17 - access_log /var/log/nginx/access.log main; 18 - sendfile on; 19 - tcp_nopush on; 20 - tcp_nodelay on; 21 - keepalive_timeout 65; 22 - types_hash_max_size 2048; 23 - gzip on; 24 - gzip_vary on; 25 - gzip_proxied any; 26 - gzip_comp_level 6; 27 - gzip_types text/plain text/css text/xml application/json application/javascript 28 - application/xml application/xml+rss text/javascript application/activity+json; 29 - ssl_protocols TLSv1.2 TLSv1.3; 30 - ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384; 31 - ssl_prefer_server_ciphers off; 32 - ssl_session_cache shared:SSL:10m; 33 - ssl_session_timeout 1d; 34 - ssl_session_tickets off; 35 - ssl_stapling on; 36 - ssl_stapling_verify on; 37 - upstream tranquil-pds { 38 - server tranquil-pds:3000; 39 - keepalive 32; 40 - } 41 - server { 42 - listen 80; 43 - listen [::]:80; 44 - server_name _; 45 - location /.well-known/acme-challenge/ { 46 - root /var/www/acme; 47 - } 48 - location / { 49 - return 301 https://$host$request_uri; 50 - } 51 - } 52 - server { 53 - listen 443 ssl http2; 54 - listen [::]:443 ssl http2; 55 - server_name _; 56 - ssl_certificate /etc/nginx/certs/live/${PDS_HOSTNAME}/fullchain.pem; 57 - ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem; 58 - client_max_body_size 10G; 59 - location / { 60 - proxy_pass http://tranquil-pds; 61 - proxy_http_version 1.1; 62 - proxy_set_header Upgrade $http_upgrade; 63 - proxy_set_header Connection "upgrade"; 64 - proxy_set_header Host $host; 65 - proxy_set_header X-Real-IP $remote_addr; 66 - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 67 - proxy_set_header X-Forwarded-Proto $scheme; 68 - proxy_read_timeout 86400; 69 - proxy_send_timeout 86400; 70 - proxy_buffering off; 71 - proxy_request_buffering off; 72 - } 73 - location /xrpc/com.atproto.sync.subscribeRepos { 74 - proxy_pass http://tranquil-pds; 75 - proxy_http_version 1.1; 76 - proxy_set_header Upgrade $http_upgrade; 77 - proxy_set_header Connection "upgrade"; 78 - proxy_set_header Host $host; 79 - proxy_set_header X-Real-IP $remote_addr; 80 - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 81 - proxy_set_header X-Forwarded-Proto $scheme; 82 - proxy_read_timeout 86400; 83 - proxy_send_timeout 86400; 84 - proxy_buffering off; 85 - } 86 - } 87 - }
···
+1 -1
observability/prometheus.yml observability/prometheus.yaml
··· 9 10 - job_name: 'tranquil-pds' 11 static_configs: 12 - - targets: ['app:3000'] 13 metrics_path: /metrics
··· 9 10 - job_name: 'tranquil-pds' 11 static_configs: 12 + - targets: ['tranquil-pds:3000'] 13 metrics_path: /metrics
+3 -4
scripts/install-debian.sh
··· 117 [[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}" 118 echo "" 119 120 - read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN 121 if [[ -z "$PDS_DOMAIN" ]]; then 122 log_error "Domain cannot be empty" 123 exit 1 ··· 296 297 log_info "Cloning Tranquil PDS..." 298 if [[ ! -d /opt/tranquil-pds ]]; then 299 - git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/tranquil-pds 300 else 301 cd /opt/tranquil-pds && git pull 302 fi ··· 417 User=tranquil-pds 418 Group=tranquil-pds 419 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 420 - Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 421 ExecStart=/usr/local/bin/tranquil-pds 422 Restart=always 423 RestartSec=5 ··· 479 echo "" 480 log_info "Obtaining wildcard SSL certificate..." 481 echo "" 482 - echo "User handles are served as subdomains (e.g., alice.${PDS_DOMAIN})," 483 echo "so you need a wildcard certificate. This requires DNS validation." 484 echo "" 485 echo "You'll need to add a TXT record to your DNS when prompted."
··· 117 [[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}" 118 echo "" 119 120 + read -p "Enter your PDS domain (eg., pds.example.com): " PDS_DOMAIN 121 if [[ -z "$PDS_DOMAIN" ]]; then 122 log_error "Domain cannot be empty" 123 exit 1 ··· 296 297 log_info "Cloning Tranquil PDS..." 298 if [[ ! -d /opt/tranquil-pds ]]; then 299 + git clone https://tangled.org/tranquil.farm/tranquil-pds /opt/tranquil-pds 300 else 301 cd /opt/tranquil-pds && git pull 302 fi ··· 417 User=tranquil-pds 418 Group=tranquil-pds 419 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 420 ExecStart=/usr/local/bin/tranquil-pds 421 Restart=always 422 RestartSec=5 ··· 478 echo "" 479 log_info "Obtaining wildcard SSL certificate..." 480 echo "" 481 + echo "User handles are served as subdomains (eg., alice.${PDS_DOMAIN})," 482 echo "so you need a wildcard certificate. This requires DNS validation." 483 echo "" 484 echo "You'll need to add a TXT record to your DNS when prompted."
+5 -5
scripts/test-infra.sh
··· 48 --name "${CONTAINER_PREFIX}-minio" \ 49 -e MINIO_ROOT_USER=minioadmin \ 50 -e MINIO_ROOT_PASSWORD=minioadmin \ 51 - -P \ 52 --label tranquil_pds_test=true \ 53 - minio/minio:latest server /data >/dev/null 54 echo "Starting Valkey..." 55 $CONTAINER_CMD run -d \ 56 --name "${CONTAINER_PREFIX}-valkey" \ 57 -P \ 58 --label tranquil_pds_test=true \ 59 - valkey/valkey:8-alpine >/dev/null 60 echo "Waiting for services to be ready..." 61 sleep 2 62 PG_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-postgres" 5432 | head -1 | cut -d: -f2) ··· 86 echo "Creating MinIO buckets..." 87 $CONTAINER_CMD run --rm --network host \ 88 -e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \ 89 - minio/mc:latest mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true 90 $CONTAINER_CMD run --rm --network host \ 91 -e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \ 92 - minio/mc:latest mb minio/test-backups --ignore-existing >/dev/null 2>&1 || true 93 cat > "$INFRA_FILE" << EOF 94 export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:${PG_PORT}/postgres" 95 export TEST_DB_PORT="${PG_PORT}"
··· 48 --name "${CONTAINER_PREFIX}-minio" \ 49 -e MINIO_ROOT_USER=minioadmin \ 50 -e MINIO_ROOT_PASSWORD=minioadmin \ 51 + -p 9000 \ 52 --label tranquil_pds_test=true \ 53 + cgr.dev/chainguard/minio:latest server /data >/dev/null 54 echo "Starting Valkey..." 55 $CONTAINER_CMD run -d \ 56 --name "${CONTAINER_PREFIX}-valkey" \ 57 -P \ 58 --label tranquil_pds_test=true \ 59 + valkey/valkey:9-alpine >/dev/null 60 echo "Waiting for services to be ready..." 61 sleep 2 62 PG_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-postgres" 5432 | head -1 | cut -d: -f2) ··· 86 echo "Creating MinIO buckets..." 87 $CONTAINER_CMD run --rm --network host \ 88 -e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \ 89 + cgr.dev/chainguard/minio-client:latest-dev mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true 90 $CONTAINER_CMD run --rm --network host \ 91 -e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \ 92 + cgr.dev/chainguard/minio-client:latest-dev mb minio/test-backups --ignore-existing >/dev/null 2>&1 || true 93 cat > "$INFRA_FILE" << EOF 94 export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:${PG_PORT}/postgres" 95 export TEST_DB_PORT="${PG_PORT}"