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

feat: docs tweaks & standalone frontend

authored by lewis.moe and committed by tangled.org 158628f1 b3ec7feb

+844 -468
+2 -4
.env.example
··· 140 140 # ============================================================================= 141 141 # If configured, moderation reports will be proxied to this service 142 142 # instead of being stored locally. The service should implement the 143 - # com.atproto.moderation.createReport endpoint (e.g., Bluesky's Ozone). 143 + # com.atproto.moderation.createReport endpoint (eg., Bluesky's Ozone). 144 144 # Both URL and DID must be set for proxying to be enabled. 145 145 # REPORT_SERVICE_URL=https://mod.bsky.app 146 146 # REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac ··· 148 148 # Age Assurance Override 149 149 # ============================================================================= 150 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 151 + # (eg., through your own age verification process). When enabled, the PDS 152 152 # will return "assured" status for age assurance checks instead of proxying 153 153 # to the appview. This helps migrated users avoid the age assurance 154 154 # catch-22 on bsky.app. ··· 158 158 # ============================================================================= 159 159 # Allow HTTP for proxy requests (development only) 160 160 # ALLOW_HTTP_PROXY=1 161 - # Custom frontend directory (defaults to ./frontend/dist) 162 - # FRONTEND_DIR=/path/to/frontend/dist 163 161 # ============================================================================= 164 162 # SSO / Social Login 165 163 # =============================================================================
-28
Cargo.lock
··· 2592 2592 ] 2593 2593 2594 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 2595 name = "httparse" 2602 2596 version = "1.10.1" 2603 2597 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 3476 3470 version = "0.3.17" 3477 3471 source = "registry+https://github.com/rust-lang/crates.io-index" 3478 3472 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 3473 3490 3474 [[package]] 3491 3475 name = "minimal-lexical" ··· 5850 5834 "http 1.4.0", 5851 5835 "http-body 1.0.1", 5852 5836 "http-body-util", 5853 - "http-range-header", 5854 - "httpdate", 5855 5837 "iri-string", 5856 - "mime", 5857 - "mime_guess", 5858 - "percent-encoding", 5859 5838 "pin-project-lite", 5860 5839 "tokio", 5861 5840 "tokio-util", 5862 5841 "tower", 5863 5842 "tower-layer", 5864 5843 "tower-service", 5865 - "tracing", 5866 5844 ] 5867 5845 5868 5846 [[package]] ··· 6240 6218 version = "1.19.0" 6241 6219 source = "registry+https://github.com/rust-lang/crates.io-index" 6242 6220 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 6221 6250 6222 [[package]] 6251 6223 name = "unicode-bidi"
+1 -1
Cargo.toml
··· 90 90 tokio-tungstenite = { version = "0.28", features = ["native-tls"] } 91 91 totp-rs = { version = "5", features = ["qr"] } 92 92 tower = "0.5" 93 - tower-http = { version = "0.6", features = ["fs", "cors"] } 93 + tower-http = { version = "0.6", features = ["cors"] } 94 94 tower-layer = "0.3" 95 95 tracing = "0.1" 96 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 1 FROM rust:1.92-alpine AS builder 7 - RUN apk add ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev 2 + RUN apk add --no-cache ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev 8 3 WORKDIR /app 9 4 COPY Cargo.toml Cargo.lock ./ 10 - COPY src ./src 11 - COPY tests ./tests 12 - COPY migrations ./migrations 5 + COPY crates ./crates 13 6 COPY .sqlx ./.sqlx 7 + COPY migrations ./crates/tranquil-pds/migrations 14 8 RUN --mount=type=cache,target=/usr/local/cargo/registry \ 15 9 --mount=type=cache,target=/app/target \ 16 - cargo build --release && \ 10 + SQLX_OFFLINE=true cargo build --release -p tranquil-pds && \ 17 11 cp target/release/tranquil-pds /tmp/tranquil-pds 18 12 19 13 FROM alpine:3.23 20 14 RUN apk add --no-cache msmtp ca-certificates && ln -sf /usr/bin/msmtp /usr/sbin/sendmail 21 15 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 16 + COPY migrations /app/migrations 24 17 WORKDIR /app 25 18 ENV SERVER_HOST=0.0.0.0 26 19 ENV SERVER_PORT=3000 27 - ENV FRONTEND_DIR=/app/frontend/dist 28 20 EXPOSE 3000 29 21 CMD ["tranquil-pds"]
+2 -2
README.md
··· 12 12 13 13 ## What's different about Tranquil PDS 14 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. 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 16 17 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 18 ··· 45 45 46 46 ```bash 47 47 cp .env.prod.example .env.prod 48 - podman-compose -f docker-compose.prod.yml up -d 48 + podman-compose -f docker-compose.prod.yaml up -d 49 49 ``` 50 50 51 51 ### Installation Guides
+5 -3
crates/tranquil-oauth/src/client.rs
··· 83 83 .connect_timeout(std::time::Duration::from_secs(10)) 84 84 .pool_max_idle_per_host(10) 85 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 - ) 86 + .user_agent(concat!( 87 + "Tranquil-PDS/", 88 + env!("CARGO_PKG_VERSION"), 89 + " (ATProto; +https://tangled.org/tranquil.farm/tranquil-pds)" 90 + )) 89 91 .build() 90 92 .unwrap_or_else(|_| Client::new()), 91 93 cache_ttl_secs,
+2 -35
crates/tranquil-pds/src/lib.rs
··· 38 38 pub use sync::util::AccountStatus; 39 39 use tower::ServiceBuilder; 40 40 use tower_http::cors::{Any, CorsLayer}; 41 - use tower_http::services::{ServeDir, ServeFile}; 42 41 pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey}; 43 42 44 43 pub fn app(state: AppState) -> Router { ··· 525 524 526 525 let oauth_router = Router::new() 527 526 .route("/jwks", get(oauth::endpoints::oauth_jwks)) 528 - .route( 529 - "/client-metadata.json", 530 - get(oauth::endpoints::frontend_client_metadata), 531 - ) 532 527 .route("/par", post(oauth::endpoints::pushed_authorization_request)) 533 528 .route("/authorize", get(oauth::endpoints::authorize_get)) 534 529 .route("/authorize", post(oauth::endpoints::authorize_post)) ··· 612 607 get(oauth::endpoints::oauth_authorization_server), 613 608 ); 614 609 615 - let router = Router::new() 610 + Router::new() 616 611 .nest_service("/xrpc", xrpc_service) 617 612 .nest("/oauth", oauth_router) 618 613 .nest("/.well-known", well_known_router) ··· 644 639 "atproto-content-labelers".parse().unwrap(), 645 640 ]), 646 641 ) 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 642 + .with_state(state) 676 643 }
-52
crates/tranquil-pds/src/oauth/endpoints/metadata.rs
··· 82 82 "transition:generic".to_string(), 83 83 "transition:chat.bsky".to_string(), 84 84 "transition:email".to_string(), 85 - "repo:*".to_string(), 86 - "repo:*?action=create".to_string(), 87 - "repo:*?action=read".to_string(), 88 - "repo:*?action=update".to_string(), 89 - "repo:*?action=delete".to_string(), 90 - "blob:*/*".to_string(), 91 - "rpc:*".to_string(), 92 - "account:*".to_string(), 93 - "account:*?action=read".to_string(), 94 - "account:*?action=write".to_string(), 95 - "identity:*".to_string(), 96 85 ]), 97 86 response_types_supported: vec!["code".to_string()], 98 87 response_modes_supported: Some(vec!["query".to_string(), "fragment".to_string()]), ··· 142 131 }; 143 132 Json(create_jwk_set(vec![server_key])) 144 133 } 145 - 146 - #[derive(Debug, Serialize, Deserialize)] 147 - pub struct FrontendClientMetadata { 148 - pub client_id: String, 149 - pub client_name: String, 150 - pub client_uri: String, 151 - pub redirect_uris: Vec<String>, 152 - pub grant_types: Vec<String>, 153 - pub response_types: Vec<String>, 154 - pub scope: String, 155 - pub token_endpoint_auth_method: String, 156 - pub application_type: String, 157 - pub dpop_bound_access_tokens: bool, 158 - } 159 - 160 - pub async fn frontend_client_metadata( 161 - State(_state): State<AppState>, 162 - ) -> Json<FrontendClientMetadata> { 163 - let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 164 - let base_url = format!("https://{}", pds_hostname); 165 - let client_id = format!("{}/oauth/client-metadata.json", base_url); 166 - Json(FrontendClientMetadata { 167 - client_id, 168 - client_name: "PDS Account Manager".to_string(), 169 - client_uri: base_url.clone(), 170 - redirect_uris: vec![ 171 - format!("{}/app/", base_url), 172 - format!("{}/app/migrate", base_url), 173 - ], 174 - grant_types: vec![ 175 - "authorization_code".to_string(), 176 - "refresh_token".to_string(), 177 - ], 178 - response_types: vec!["code".to_string()], 179 - scope: "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*" 180 - .to_string(), 181 - token_endpoint_auth_method: "none".to_string(), 182 - application_type: "web".to_string(), 183 - dpop_bound_access_tokens: true, 184 - }) 185 - }
+1 -1
crates/tranquil-pds/tests/common/mod.rs
··· 162 162 163 163 #[cfg(not(feature = "external-infra"))] 164 164 async fn setup_with_testcontainers() -> String { 165 - let s3_container = GenericImage::new("minio/minio", "latest") 165 + let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest") 166 166 .with_exposed_port(ContainerPort::Tcp(9000)) 167 167 .with_env_var("MINIO_ROOT_USER", "minioadmin") 168 168 .with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
-114
crates/tranquil-pds/tests/oauth_client_metadata.rs
··· 1 - mod common; 2 - use common::*; 3 - use reqwest::StatusCode; 4 - use serde_json::Value; 5 - 6 - #[tokio::test] 7 - async fn test_frontend_client_metadata_returns_valid_json() { 8 - let client = client(); 9 - let res = client 10 - .get(format!("{}/oauth/client-metadata.json", base_url().await)) 11 - .send() 12 - .await 13 - .expect("Failed to send request"); 14 - assert_eq!(res.status(), StatusCode::OK); 15 - let body: Value = res.json().await.expect("Should return valid JSON"); 16 - assert!( 17 - body["client_id"].as_str().is_some(), 18 - "Should have client_id" 19 - ); 20 - assert!( 21 - body["client_name"].as_str().is_some(), 22 - "Should have client_name" 23 - ); 24 - assert!( 25 - body["redirect_uris"].as_array().is_some(), 26 - "Should have redirect_uris" 27 - ); 28 - assert!( 29 - body["grant_types"].as_array().is_some(), 30 - "Should have grant_types" 31 - ); 32 - assert!( 33 - body["response_types"].as_array().is_some(), 34 - "Should have response_types" 35 - ); 36 - assert!(body["scope"].as_str().is_some(), "Should have scope"); 37 - assert!( 38 - body["token_endpoint_auth_method"].as_str().is_some(), 39 - "Should have token_endpoint_auth_method" 40 - ); 41 - } 42 - 43 - #[tokio::test] 44 - async fn test_frontend_client_metadata_correct_values() { 45 - let client = client(); 46 - let res = client 47 - .get(format!("{}/oauth/client-metadata.json", base_url().await)) 48 - .send() 49 - .await 50 - .expect("Failed to send request"); 51 - assert_eq!(res.status(), StatusCode::OK); 52 - let body: Value = res.json().await.unwrap(); 53 - let client_id = body["client_id"].as_str().unwrap(); 54 - assert!( 55 - client_id.ends_with("/oauth/client-metadata.json"), 56 - "client_id should end with /oauth/client-metadata.json" 57 - ); 58 - let grant_types = body["grant_types"].as_array().unwrap(); 59 - let grant_strs: Vec<&str> = grant_types.iter().filter_map(|v| v.as_str()).collect(); 60 - assert!( 61 - grant_strs.contains(&"authorization_code"), 62 - "Should support authorization_code grant" 63 - ); 64 - assert!( 65 - grant_strs.contains(&"refresh_token"), 66 - "Should support refresh_token grant" 67 - ); 68 - let response_types = body["response_types"].as_array().unwrap(); 69 - let response_strs: Vec<&str> = response_types.iter().filter_map(|v| v.as_str()).collect(); 70 - assert!( 71 - response_strs.contains(&"code"), 72 - "Should support code response type" 73 - ); 74 - assert_eq!( 75 - body["token_endpoint_auth_method"].as_str(), 76 - Some("none"), 77 - "Should be public client (none auth)" 78 - ); 79 - assert_eq!( 80 - body["application_type"].as_str(), 81 - Some("web"), 82 - "Should be web application" 83 - ); 84 - assert_eq!( 85 - body["dpop_bound_access_tokens"].as_bool(), 86 - Some(true), 87 - "AT Protocol requires DPoP-bound access tokens" 88 - ); 89 - let scope = body["scope"].as_str().unwrap(); 90 - assert!(scope.contains("atproto"), "Scope should include atproto"); 91 - } 92 - 93 - #[tokio::test] 94 - async fn test_frontend_client_metadata_redirect_uri_matches_client_uri() { 95 - let client = client(); 96 - let res = client 97 - .get(format!("{}/oauth/client-metadata.json", base_url().await)) 98 - .send() 99 - .await 100 - .expect("Failed to send request"); 101 - assert_eq!(res.status(), StatusCode::OK); 102 - let body: Value = res.json().await.unwrap(); 103 - let client_uri = body["client_uri"].as_str().unwrap(); 104 - let redirect_uris = body["redirect_uris"].as_array().unwrap(); 105 - assert!( 106 - !redirect_uris.is_empty(), 107 - "Should have at least one redirect URI" 108 - ); 109 - let redirect_uri = redirect_uris[0].as_str().unwrap(); 110 - assert!( 111 - redirect_uri.starts_with(client_uri), 112 - "Redirect URI should be on same origin as client_uri" 113 - ); 114 - }
+92 -3
deploy/nginx/nginx-quadlet.conf
··· 1 1 worker_processes auto; 2 2 error_log /var/log/nginx/error.log warn; 3 + 3 4 events { 4 5 worker_connections 4096; 5 6 } 7 + 6 8 http { 7 9 include /etc/nginx/mime.types; 8 10 default_type application/octet-stream; 9 11 access_log /var/log/nginx/access.log; 12 + 10 13 sendfile on; 11 14 keepalive_timeout 65; 15 + 12 16 gzip on; 13 17 gzip_types text/plain text/css application/json application/javascript text/xml application/xml; 18 + 14 19 ssl_protocols TLSv1.2 TLSv1.3; 15 20 ssl_prefer_server_ciphers off; 16 21 ssl_session_cache shared:SSL:10m; 17 22 ssl_stapling on; 18 23 ssl_stapling_verify on; 24 + 19 25 server { 20 26 listen 80; 21 27 listen [::]:80; 22 28 server_name _; 29 + 23 30 location /.well-known/acme-challenge/ { 24 31 root /var/www/acme; 25 32 } 33 + 26 34 location / { 27 35 return 301 https://$host$request_uri; 28 36 } 29 37 } 38 + 30 39 server { 31 - listen 443 ssl http2; 32 - listen [::]:443 ssl http2; 40 + listen 443 ssl; 41 + listen [::]:443 ssl; 42 + http2 on; 33 43 server_name _; 44 + 34 45 ssl_certificate /etc/nginx/certs/fullchain.pem; 35 46 ssl_certificate_key /etc/nginx/certs/privkey.pem; 47 + 36 48 client_max_body_size 10G; 37 - location / { 49 + 50 + location /xrpc/ { 38 51 proxy_pass http://127.0.0.1:3000; 39 52 proxy_http_version 1.1; 40 53 proxy_set_header Upgrade $http_upgrade; ··· 46 59 proxy_read_timeout 86400; 47 60 proxy_send_timeout 86400; 48 61 proxy_buffering off; 62 + proxy_request_buffering off; 63 + } 64 + 65 + location = /oauth/client-metadata.json { 66 + proxy_pass http://127.0.0.1:8080; 67 + proxy_http_version 1.1; 68 + proxy_set_header Host $host; 69 + proxy_set_header Accept-Encoding ""; 70 + sub_filter_once off; 71 + sub_filter_types application/json; 72 + sub_filter '__PDS_HOSTNAME__' $host; 73 + } 74 + 75 + location /oauth/ { 76 + proxy_pass http://127.0.0.1:3000; 77 + proxy_http_version 1.1; 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 300; 83 + proxy_send_timeout 300; 84 + } 85 + 86 + location /.well-known/ { 87 + proxy_pass http://127.0.0.1:3000; 88 + proxy_http_version 1.1; 89 + proxy_set_header Host $host; 90 + proxy_set_header X-Real-IP $remote_addr; 91 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 92 + proxy_set_header X-Forwarded-Proto $scheme; 93 + } 94 + 95 + location = /metrics { 96 + proxy_pass http://127.0.0.1:3000; 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 + } 103 + 104 + location = /health { 105 + proxy_pass http://127.0.0.1:3000; 106 + proxy_http_version 1.1; 107 + proxy_set_header Host $host; 108 + } 109 + 110 + location = /robots.txt { 111 + proxy_pass http://127.0.0.1:3000; 112 + proxy_http_version 1.1; 113 + proxy_set_header Host $host; 114 + } 115 + 116 + location = /logo { 117 + proxy_pass http://127.0.0.1:3000; 118 + proxy_http_version 1.1; 119 + proxy_set_header Host $host; 120 + } 121 + 122 + location ~ ^/u/[^/]+/did\.json$ { 123 + proxy_pass http://127.0.0.1:3000; 124 + proxy_http_version 1.1; 125 + proxy_set_header Host $host; 126 + proxy_set_header X-Real-IP $remote_addr; 127 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 128 + proxy_set_header X-Forwarded-Proto $scheme; 129 + } 130 + 131 + location / { 132 + proxy_pass http://127.0.0.1:8080; 133 + proxy_http_version 1.1; 134 + proxy_set_header Host $host; 135 + proxy_set_header X-Real-IP $remote_addr; 136 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 137 + proxy_set_header X-Forwarded-Proto $scheme; 49 138 } 50 139 } 51 140 }
-1
deploy/quadlets/tranquil-pds-app.container
··· 12 12 Environment=AWS_REGION=us-east-1 13 13 Environment=S3_BUCKET=pds-blobs 14 14 Environment=VALKEY_URL=redis://localhost:6379 15 - Environment=FRONTEND_DIR=/app/frontend/dist 16 15 HealthCmd=wget -q --spider http://localhost:3000/xrpc/_health 17 16 HealthInterval=30s 18 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 2 Description=Tranquil PDS minio object storage 3 3 [Container] 4 4 ContainerName=tranquil-pds-minio 5 - Image=docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z 5 + Image=cgr.dev/chainguard/minio:latest 6 6 Pod=tranquil-pds.pod 7 7 Environment=MINIO_ROOT_USER=minioadmin 8 8 Secret=tranquil-pds-minio-password,type=env,target=MINIO_ROOT_PASSWORD 9 9 Volume=/srv/tranquil-pds/minio:/data:Z 10 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 11 [Service] 17 12 Restart=always 18 13 RestartSec=10
+1 -1
deploy/quadlets/tranquil-pds-nginx.container
··· 1 1 [Unit] 2 2 Description=Tranquil PDS nginx reverse proxy 3 - After=tranquil-pds-app.service 3 + After=tranquil-pds-app.service tranquil-pds-frontend.service 4 4 [Container] 5 5 ContainerName=tranquil-pds-nginx 6 6 Image=docker.io/library/nginx:1.28-alpine
+40 -18
docker-compose.prod.yml docker-compose.prod.yaml
··· 5 5 dockerfile: Dockerfile 6 6 image: tranquil-pds:latest 7 7 restart: unless-stopped 8 - ports: 9 - - "127.0.0.1:3000:3000" 10 8 environment: 11 9 SERVER_HOST: "0.0.0.0" 12 10 SERVER_PORT: "3000" ··· 22 20 DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}" 23 21 MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}" 24 22 CRAWLERS: "${CRAWLERS:-https://bsky.network}" 25 - FRONTEND_DIR: "/app/frontend/dist" 26 23 depends_on: 27 24 db: 28 25 condition: service_healthy ··· 42 39 memory: 1G 43 40 reservations: 44 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 + 45 62 db: 46 63 image: postgres:18-alpine 47 64 restart: unless-stopped ··· 63 80 memory: 512M 64 81 reservations: 65 82 memory: 128M 83 + 66 84 minio: 67 - image: minio/minio:RELEASE.2025-10-15T17-29-55Z 85 + image: cgr.dev/chainguard/minio:latest 68 86 restart: unless-stopped 69 87 command: server /data --console-address ":9001" 70 88 environment: ··· 72 90 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 73 91 volumes: 74 92 - 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 93 deploy: 82 94 resources: 83 95 limits: 84 96 memory: 512M 85 97 reservations: 86 98 memory: 128M 99 + 87 100 minio-init: 88 - image: minio/mc:RELEASE.2025-07-16T15-35-03Z 101 + image: cgr.dev/chainguard/minio-client:latest-dev 89 102 depends_on: 90 - minio: 91 - condition: service_healthy 103 + - minio 92 104 entrypoint: > 93 105 /bin/sh -c " 94 - mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD}; 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; 95 110 mc mb --ignore-existing local/pds-blobs; 111 + mc mb --ignore-existing local/pds-backups; 96 112 mc anonymous set none local/pds-blobs; 97 113 exit 0; 98 114 " 99 115 environment: 100 116 MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}" 101 117 MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}" 118 + 102 119 valkey: 103 120 image: valkey/valkey:9-alpine 104 121 restart: unless-stopped ··· 117 134 memory: 300M 118 135 reservations: 119 136 memory: 64M 137 + 120 138 nginx: 121 - image: nginx:1.28-alpine 139 + image: nginx:1.29-alpine 122 140 restart: unless-stopped 123 141 ports: 124 142 - "80:80" 125 143 - "443:443" 126 144 volumes: 127 - - ./nginx.prod.conf:/etc/nginx/nginx.conf:ro 145 + - ./nginx.frontend.conf:/etc/nginx/nginx.conf:ro 128 146 - ./certs:/etc/nginx/certs:ro 129 147 - acme_challenge:/var/www/acme:ro 130 148 depends_on: 131 149 - tranquil-pds 150 + - frontend 132 151 healthcheck: 133 152 test: ["CMD", "nginx", "-t"] 134 153 interval: 30s 135 154 timeout: 10s 136 155 retries: 3 156 + 137 157 certbot: 138 158 image: certbot/certbot:v5.2.2 139 159 volumes: 140 160 - ./certs:/etc/letsencrypt 141 161 - acme_challenge:/var/www/acme 142 162 entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/acme; sleep 12h & wait $${!}; done'" 163 + 143 164 prometheus: 144 165 image: prom/prometheus:v3.8.0 145 166 restart: unless-stopped 146 167 ports: 147 168 - "127.0.0.1:9090:9090" 148 169 volumes: 149 - - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro 170 + - ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro 150 171 - prometheus_data:/prometheus 151 172 command: 152 - - '--config.file=/etc/prometheus/prometheus.yml' 173 + - '--config.file=/etc/prometheus/prometheus.yaml' 153 174 - '--storage.tsdb.path=/prometheus' 154 175 - '--storage.tsdb.retention.time=30d' 155 176 deploy: 156 177 resources: 157 178 limits: 158 179 memory: 256M 180 + 159 181 volumes: 160 182 postgres_data: 161 183 minio_data:
+19 -4
docker-compose.yaml
··· 16 16 - db 17 17 - objsto 18 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 + 19 30 db: 20 31 image: postgres:18-alpine 21 32 environment: ··· 26 37 - "5432:5432" 27 38 volumes: 28 39 - postgres_data:/var/lib/postgresql 40 + 29 41 objsto: 30 - image: minio/minio 42 + image: cgr.dev/chainguard/minio:latest 31 43 ports: 32 44 - "9000:9000" 33 45 - "9001:9001" ··· 37 49 volumes: 38 50 - minio_data:/data 39 51 command: server /data --console-address ":9001" 52 + 40 53 cache: 41 - image: valkey/valkey:8-alpine 54 + image: valkey/valkey:9-alpine 42 55 ports: 43 56 - "6379:6379" 44 57 volumes: 45 58 - valkey_data:/data 59 + 46 60 prometheus: 47 61 image: prom/prometheus:v3.8.0 48 62 ports: 49 63 - "9090:9090" 50 64 volumes: 51 - - ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro 65 + - ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro 52 66 - prometheus_data:/prometheus 53 67 command: 54 - - '--config.file=/etc/prometheus/prometheus.yml' 68 + - '--config.file=/etc/prometheus/prometheus.yaml' 55 69 - '--storage.tsdb.path=/prometheus' 56 70 depends_on: 57 71 - app 72 + 58 73 volumes: 59 74 postgres_data: 60 75 minio_data:
+183 -43
docs/install-containers.md
··· 1 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. 2 + 3 3 This guide covers deploying Tranquil PDS using containers with podman. 4 + 4 5 - **Debian 13+**: Uses systemd quadlets (modern, declarative container management) 5 6 - **Alpine 3.23+**: Uses OpenRC service script with podman-compose 7 + 6 8 ## Prerequisites 9 + 7 10 - A VPS with at least 2GB RAM and 20GB disk 8 11 - A domain name pointing to your server's IP 9 12 - A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains) 10 13 - Root or sudo access 14 + 11 15 ## Quick Start (Docker/Podman Compose) 16 + 12 17 If you just want to get running quickly: 18 + 13 19 ```sh 14 20 cp .env.example .env 15 21 ``` ··· 18 24 19 25 Build and start: 20 26 ```sh 21 - podman-compose -f docker-compose.prod.yml up -d 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 22 30 ``` 23 31 24 32 Get initial certificate (after DNS is configured): 25 33 ```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 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 29 39 ``` 40 + 30 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 + 31 90 --- 91 + 32 92 # Debian 13+ with Systemd Quadlets 93 + 33 94 Quadlets are the modern way to run podman containers under systemd. 34 - ## 1. Install Podman 95 + 96 + ## Install Podman 97 + 35 98 ```bash 36 99 apt update 37 100 apt install -y podman 38 101 ``` 39 - ## 2. Create Directory Structure 102 + 103 + ## Create Directory Structure 104 + 40 105 ```bash 41 106 mkdir -p /etc/containers/systemd 42 107 mkdir -p /srv/tranquil-pds/{postgres,minio,valkey,certs,acme,config} 43 108 ``` 44 - ## 3. Create Environment File 109 + 110 + ## Create Environment File 111 + 45 112 ```bash 46 113 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 47 114 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 48 115 ``` 116 + 49 117 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 50 118 ```bash 51 119 openssl rand -base64 48 52 120 ``` 121 + 53 122 For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion). 54 - ## 4. Install Quadlet Definitions 123 + 124 + ## Install Quadlet Definitions 125 + 55 126 Copy the quadlet files from the repository: 56 127 ```bash 57 128 cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/ 58 129 cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/ 59 130 ``` 131 + 60 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. 61 - ## 5. Create nginx Configuration 133 + 134 + ## Create nginx Configuration 135 + 62 136 ```bash 63 - cp /opt/tranquil-pds/deploy/nginx/nginx-quadlet.conf /srv/tranquil-pds/config/nginx.conf 137 + cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf 64 138 ``` 65 - ## 6. Build Tranquil PDS Image 139 + 140 + ## Clone and Build Images 141 + 66 142 ```bash 67 143 cd /opt 68 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 144 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 69 145 cd tranquil-pds 70 146 podman build -t tranquil-pds:latest . 147 + podman build -t tranquil-pds-frontend:latest ./frontend 71 148 ``` 72 - ## 7. Create Podman Secrets 149 + 150 + ## Create Podman Secrets 151 + 73 152 ```bash 74 153 source /srv/tranquil-pds/config/tranquil-pds.env 75 154 echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password - 76 155 echo "$MINIO_ROOT_PASSWORD" | podman secret create tranquil-pds-minio-password - 77 156 ``` 78 - ## 8. Start Services and Initialize 157 + 158 + ## Start Services and Initialize 159 + 79 160 ```bash 80 161 systemctl daemon-reload 81 162 systemctl start tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey ··· 87 168 podman run --rm --pod tranquil-pds \ 88 169 -e MINIO_ROOT_USER=minioadmin \ 89 170 -e MINIO_ROOT_PASSWORD=your-minio-password \ 90 - docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ 171 + cgr.dev/chainguard/minio-client:latest-dev \ 91 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" 92 173 ``` 93 174 ··· 96 177 cargo install sqlx-cli --no-default-features --features postgres 97 178 DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 98 179 ``` 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. 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. 101 184 102 185 Create temporary self-signed cert to start services: 103 186 ```bash ··· 105 188 -keyout /srv/tranquil-pds/certs/privkey.pem \ 106 189 -out /srv/tranquil-pds/certs/fullchain.pem \ 107 190 -subj "/CN=pds.example.com" 108 - systemctl start tranquil-pds-app tranquil-pds-nginx 191 + systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx 109 192 ``` 110 193 111 194 Get a wildcard certificate using DNS validation: ··· 117 200 -d pds.example.com -d '*.pds.example.com' \ 118 201 --agree-tos --email you@example.com 119 202 ``` 203 + 120 204 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 121 205 122 - For automated renewal, use a DNS provider plugin (e.g., cloudflare, route53). 206 + For automated renewal, use a DNS provider plugin (eg., cloudflare, route53). 123 207 124 208 Link certificates and restart: 125 209 ```bash ··· 127 211 ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem 128 212 systemctl restart tranquil-pds-nginx 129 213 ``` 130 - ## 10. Enable All Services 214 + 215 + ## Enable All Services 216 + 131 217 ```bash 132 - systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-nginx 218 + systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx 133 219 ``` 134 - ## 11. Configure Firewall 220 + 221 + ## Configure Firewall 222 + 135 223 ```bash 136 224 apt install -y ufw 137 225 ufw allow ssh ··· 139 227 ufw allow 443/tcp 140 228 ufw enable 141 229 ``` 142 - ## 12. Certificate Renewal 230 + 231 + ## Certificate Renewal 232 + 143 233 Add to root's crontab (`crontab -e`): 144 234 ``` 145 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 146 236 ``` 237 + 147 238 --- 239 + 148 240 # Alpine 3.23+ with OpenRC 241 + 149 242 Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper. 150 - ## 1. Install Podman 243 + 244 + ## Install Podman 245 + 151 246 ```sh 152 247 apk update 153 248 apk add podman podman-compose fuse-overlayfs cni-plugins 154 249 rc-update add cgroups 155 250 rc-service cgroups start 156 251 ``` 252 + 157 253 Enable podman socket for compose: 158 254 ```sh 159 255 rc-update add podman 160 256 rc-service podman start 161 257 ``` 162 - ## 2. Create Directory Structure 258 + 259 + ## Create Directory Structure 260 + 163 261 ```sh 164 262 mkdir -p /srv/tranquil-pds/{data,config} 165 263 mkdir -p /srv/tranquil-pds/data/{postgres,minio,valkey,certs,acme} 166 264 ``` 167 - ## 3. Clone Repository and Build 265 + 266 + ## Clone Repository and Build Images 267 + 168 268 ```sh 169 269 cd /opt 170 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 270 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 171 271 cd tranquil-pds 172 272 podman build -t tranquil-pds:latest . 273 + podman build -t tranquil-pds-frontend:latest ./frontend 173 274 ``` 174 - ## 4. Create Environment File 275 + 276 + ## Create Environment File 277 + 175 278 ```sh 176 279 cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env 177 280 chmod 600 /srv/tranquil-pds/config/tranquil-pds.env 178 281 ``` 282 + 179 283 Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with: 180 284 ```sh 181 285 openssl rand -base64 48 182 286 ``` 183 - ## 5. Set Up Compose and nginx 287 + 288 + ## Set Up Compose and nginx 289 + 184 290 Copy the production compose and nginx configs: 185 291 ```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 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 188 294 ``` 295 + 189 296 Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed: 190 297 - Update volume mounts to use `/srv/tranquil-pds/data/` paths 191 - - Update nginx cert paths to match `/srv/tranquil-pds/data/certs/` 298 + - Update nginx config path to `/srv/tranquil-pds/config/nginx.conf` 299 + 192 300 Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths: 193 301 - Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/` 194 - ## 6. Create OpenRC Service 302 + 303 + ## Create OpenRC Service 304 + 195 305 ```sh 196 306 cat > /etc/init.d/tranquil-pds << 'EOF' 197 307 #!/sbin/openrc-run ··· 223 333 EOF 224 334 chmod +x /etc/init.d/tranquil-pds 225 335 ``` 226 - ## 7. Initialize Services 336 + 337 + ## Initialize Services 338 + 227 339 Start services: 228 340 ```sh 229 341 rc-service tranquil-pds start ··· 236 348 podman run --rm --network tranquil-pds_default \ 237 349 -e MINIO_ROOT_USER="$MINIO_ROOT_USER" \ 238 350 -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ 239 - docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \ 351 + cgr.dev/chainguard/minio-client:latest-dev \ 240 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' 241 353 ``` 242 354 ··· 249 361 DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}') 250 362 DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations 251 363 ``` 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. 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. 254 368 255 369 Create temporary self-signed cert to start services: 256 370 ```sh ··· 270 384 -d pds.example.com -d '*.pds.example.com' \ 271 385 --agree-tos --email you@example.com 272 386 ``` 387 + 273 388 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 274 389 275 390 Link certificates and restart: ··· 278 393 ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem 279 394 rc-service tranquil-pds restart 280 395 ``` 281 - ## 9. Enable Service at Boot 396 + 397 + ## Enable Service at Boot 398 + 282 399 ```sh 283 400 rc-update add tranquil-pds 284 401 ``` 285 - ## 10. Configure Firewall 402 + 403 + ## Configure Firewall 404 + 286 405 ```sh 287 406 apk add iptables ip6tables 288 407 iptables -A INPUT -p tcp --dport 22 -j ACCEPT ··· 302 421 /etc/init.d/iptables save 303 422 /etc/init.d/ip6tables save 304 423 ``` 305 - ## 11. Certificate Renewal 424 + 425 + ## Certificate Renewal 426 + 306 427 Add to root's crontab (`crontab -e`): 307 428 ``` 308 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 309 430 ``` 431 + 310 432 --- 433 + 311 434 # Verification and Maintenance 435 + 312 436 ## Verify Installation 437 + 313 438 ```sh 314 439 curl -s https://pds.example.com/xrpc/_health | jq 315 440 curl -s https://pds.example.com/.well-known/atproto-did 316 441 ``` 442 + 317 443 ## View Logs 444 + 318 445 **Debian:** 319 446 ```bash 320 447 journalctl -u tranquil-pds-app -f 321 448 podman logs -f tranquil-pds-app 449 + podman logs -f tranquil-pds-frontend 322 450 ``` 451 + 323 452 **Alpine:** 324 453 ```sh 325 454 podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f 326 455 podman logs -f tranquil-pds-tranquil-pds-1 456 + podman logs -f tranquil-pds-frontend-1 327 457 ``` 458 + 328 459 ## Update Tranquil PDS 460 + 329 461 ```sh 330 462 cd /opt/tranquil-pds 331 463 git pull 332 464 podman build -t tranquil-pds:latest . 465 + podman build -t tranquil-pds-frontend:latest ./frontend 333 466 ``` 334 467 335 468 Debian: 336 469 ```bash 337 - systemctl restart tranquil-pds-app 470 + systemctl restart tranquil-pds-app tranquil-pds-frontend 338 471 ``` 339 472 340 473 Alpine: 341 474 ```sh 342 475 rc-service tranquil-pds restart 343 476 ``` 477 + 344 478 ## Backup Database 479 + 345 480 **Debian:** 346 481 ```bash 347 482 podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql 348 483 ``` 484 + 349 485 **Alpine:** 350 486 ```sh 351 487 podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 353 489 354 490 ## Custom Homepage 355 491 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. 492 + The frontend container serves `homepage.html` as the landing page. To customize it, either: 357 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: 358 498 ```html 359 499 <!DOCTYPE html> 360 500 <html>
+173 -26
docs/install-debian.md
··· 1 1 # Tranquil PDS Production Installation on Debian 2 + 2 3 This guide covers installing Tranquil PDS on Debian 13. 3 4 4 5 ## Prerequisites 6 + 5 7 - A VPS with at least 2GB RAM and 20GB disk 6 8 - A domain name pointing to your server's IP 7 9 - A wildcard TLS certificate for `*.pds.example.com` (user handles are served as subdomains) 8 10 - Root or sudo access 9 - ## 1. System Setup 11 + 12 + ## System Setup 13 + 10 14 ```bash 11 15 apt update && apt upgrade -y 12 16 apt install -y curl git build-essential pkg-config libssl-dev 13 17 ``` 14 - ## 2. Install Rust 18 + 19 + ## Install Rust 20 + 15 21 ```bash 16 22 curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 17 23 source ~/.cargo/env 18 24 rustup default stable 19 25 ``` 26 + 20 27 This installs the latest stable Rust. 21 - ## 3. Install postgres 28 + 29 + ## Install postgres 30 + 22 31 ```bash 23 32 apt install -y postgresql postgresql-contrib 24 33 systemctl enable postgresql ··· 27 36 sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;" 28 37 sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;" 29 38 ``` 30 - ## 4. Install minio 39 + 40 + ## Install minio 41 + 31 42 ```bash 32 43 curl -O https://dl.min.io/server/minio/release/linux-amd64/minio 33 44 chmod +x minio ··· 59 70 systemctl enable minio 60 71 systemctl start minio 61 72 ``` 73 + 62 74 Create the buckets (wait a few seconds for minio to start): 63 75 ```bash 64 76 curl -O https://dl.min.io/client/mc/release/linux-amd64/mc ··· 68 80 mc mb local/pds-blobs 69 81 mc mb local/pds-backups 70 82 ``` 71 - ## 5. Install valkey 83 + 84 + ## Install valkey 85 + 72 86 ```bash 73 87 apt install -y valkey 74 88 systemctl enable valkey-server 75 89 systemctl start valkey-server 76 90 ``` 77 - ## 6. Install deno (for frontend build) 91 + 92 + ## Install deno (for frontend build) 93 + 78 94 ```bash 79 95 curl -fsSL https://deno.land/install.sh | sh 80 96 export PATH="$HOME/.deno/bin:$PATH" 81 97 echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc 82 98 ``` 83 - ## 7. Clone and Build Tranquil PDS 99 + 100 + ## Clone and Build Tranquil PDS 101 + 84 102 ```bash 85 103 cd /opt 86 - git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds 104 + git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds 87 105 cd tranquil-pds 88 106 cd frontend 89 107 deno task build 90 108 cd .. 91 109 cargo build --release 92 110 ``` 93 - ## 8. Install sqlx-cli and Run Migrations 111 + 112 + ## Install sqlx-cli and Run Migrations 113 + 94 114 ```bash 95 115 cargo install sqlx-cli --no-default-features --features postgres 96 116 export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" 97 117 sqlx migrate run 98 118 ``` 99 - ## 9. Configure Tranquil PDS 119 + 120 + ## Configure Tranquil PDS 121 + 100 122 ```bash 101 123 mkdir -p /etc/tranquil-pds 102 124 cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env 103 125 chmod 600 /etc/tranquil-pds/tranquil-pds.env 104 126 ``` 127 + 105 128 Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with: 106 129 ```bash 107 130 openssl rand -base64 48 108 131 ``` 109 - ## 10. Create Systemd Service 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 + 110 143 ```bash 111 144 useradd -r -s /sbin/nologin tranquil-pds 112 145 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 146 + 116 147 cat > /etc/systemd/system/tranquil-pds.service << 'EOF' 117 148 [Unit] 118 149 Description=Tranquil PDS - AT Protocol PDS ··· 122 153 User=tranquil-pds 123 154 Group=tranquil-pds 124 155 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 125 - Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 126 156 ExecStart=/usr/local/bin/tranquil-pds 127 157 Restart=always 128 158 RestartSec=5 129 159 [Install] 130 160 WantedBy=multi-user.target 131 161 EOF 162 + 132 163 systemctl daemon-reload 133 164 systemctl enable tranquil-pds 134 165 systemctl start tranquil-pds 135 166 ``` 136 - ## 11. Install and Configure nginx 167 + 168 + ## Install and Configure nginx 169 + 137 170 ```bash 138 171 apt install -y nginx certbot python3-certbot-nginx 172 + 139 173 cat > /etc/nginx/sites-available/tranquil-pds << 'EOF' 140 174 server { 141 175 listen 80; 142 176 listen [::]:80; 143 - server_name pds.example.com; 177 + server_name pds.example.com *.pds.example.com; 178 + 179 + location /.well-known/acme-challenge/ { 180 + root /var/www/acme; 181 + } 182 + 144 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/ { 145 202 proxy_pass http://127.0.0.1:3000; 146 203 proxy_http_version 1.1; 147 204 proxy_set_header Upgrade $http_upgrade; ··· 151 208 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 152 209 proxy_set_header X-Forwarded-Proto $scheme; 153 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; 154 285 } 155 286 } 156 287 EOF 157 - ln -s /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 288 + 289 + ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/ 158 290 rm -f /etc/nginx/sites-enabled/default 291 + mkdir -p /var/www/acme 159 292 nginx -t 160 293 systemctl reload nginx 161 294 ``` 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. 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. 164 299 165 300 Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin: 166 301 ```bash ··· 175 310 certbot certonly --manual --preferred-challenges dns \ 176 311 -d pds.example.com -d '*.pds.example.com' 177 312 ``` 313 + 178 314 Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew. 179 315 180 - After obtaining the cert, update nginx to use it and reload. 181 - ## 13. Configure Firewall 316 + After obtaining the cert, reload nginx: 317 + ```bash 318 + systemctl reload nginx 319 + ``` 320 + 321 + ## Configure Firewall 322 + 182 323 ```bash 183 324 apt install -y ufw 184 325 ufw allow ssh ··· 186 327 ufw allow 443/tcp 187 328 ufw enable 188 329 ``` 189 - ## 14. Verify Installation 330 + 331 + ## Verify Installation 332 + 190 333 ```bash 191 334 systemctl status tranquil-pds 192 335 curl -s https://pds.example.com/xrpc/_health | jq 193 336 curl -s https://pds.example.com/.well-known/atproto-did 194 337 ``` 338 + 195 339 ## Maintenance 340 + 196 341 View logs: 197 342 ```bash 198 343 journalctl -u tranquil-pds -f 199 344 ``` 345 + 200 346 Update Tranquil PDS: 201 347 ```bash 202 348 cd /opt/tranquil-pds ··· 205 351 cargo build --release 206 352 systemctl stop tranquil-pds 207 353 cp target/release/tranquil-pds /usr/local/bin/ 208 - cp -r frontend/dist /var/lib/tranquil-pds/frontend 354 + cp -r frontend/dist/* /var/www/tranquil-pds/ 209 355 DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run 210 356 systemctl start tranquil-pds 211 357 ``` 358 + 212 359 Backup database: 213 360 ```bash 214 361 sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql ··· 216 363 217 364 ## Custom Homepage 218 365 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. 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. 220 367 221 368 ```bash 222 - cat > /var/lib/tranquil-pds/frontend/homepage.html << 'EOF' 369 + cat > /var/www/tranquil-pds/homepage.html << 'EOF' 223 370 <!DOCTYPE html> 224 371 <html> 225 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
+38
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 = /oauth/client-metadata.json { 14 + default_type application/json; 15 + sub_filter_once off; 16 + sub_filter_types application/json; 17 + sub_filter '__PDS_HOSTNAME__' $host; 18 + try_files /oauth/client-metadata.json =404; 19 + } 20 + 21 + location /assets/ { 22 + expires 1y; 23 + add_header Cache-Control "public, immutable"; 24 + try_files $uri =404; 25 + } 26 + 27 + location = / { 28 + try_files /homepage.html /index.html; 29 + } 30 + 31 + location /app/ { 32 + try_files $uri $uri/ /index.html; 33 + } 34 + 35 + location / { 36 + try_files $uri $uri/ /index.html; 37 + } 38 + }
+38
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 = /oauth/client-metadata.json { 14 + default_type application/json; 15 + sub_filter_once off; 16 + sub_filter_types application/json; 17 + sub_filter '__PDS_HOSTNAME__' $host; 18 + try_files /oauth/client-metadata.json =404; 19 + } 20 + 21 + location /assets/ { 22 + expires 1y; 23 + add_header Cache-Control "public, immutable"; 24 + try_files $uri =404; 25 + } 26 + 27 + location = / { 28 + try_files /homepage.html /index.html; 29 + } 30 + 31 + location /app/ { 32 + try_files $uri $uri/ /index.html; 33 + } 34 + 35 + location / { 36 + try_files $uri $uri/ /index.html; 37 + } 38 + }
+5 -5
frontend/public/homepage.html
··· 440 440 <a href="/app/register" class="btn primary" id="heroPrimary" 441 441 >Join This Server</a> 442 442 <a 443 - href="https://tangled.org/lewis.moe/bspds-sandbox" 443 + href="https://tangled.org/tranquil.farm/tranquil-pds" 444 444 class="btn secondary" 445 445 id="heroSecondary" 446 446 target="_blank" ··· 461 461 <div class="feature"> 462 462 <h3>Real security</h3> 463 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. 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 467 </p> 468 468 </div> 469 469 ··· 546 546 <a href="/app/register" class="btn primary" id="footerPrimary" 547 547 >Join This Server</a> 548 548 <a 549 - href="https://tangled.org/lewis.moe/bspds-sandbox" 549 + href="https://tangled.org/tranquil.farm/tranquil-pds" 550 550 class="btn secondary" 551 551 target="_blank" 552 552 rel="noopener"
+15
frontend/public/oauth/client-metadata.json
··· 1 + { 2 + "client_id": "https://__PDS_HOSTNAME__/oauth/client-metadata.json", 3 + "client_name": "PDS Account Manager", 4 + "client_uri": "https://__PDS_HOSTNAME__", 5 + "redirect_uris": [ 6 + "https://__PDS_HOSTNAME__/app/", 7 + "https://__PDS_HOSTNAME__/app/migrate" 8 + ], 9 + "grant_types": ["authorization_code", "refresh_token"], 10 + "response_types": ["code"], 11 + "scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*", 12 + "token_endpoint_auth_method": "none", 13 + "application_type": "web", 14 + "dpop_bound_access_tokens": true 15 + }
+8 -8
frontend/src/locales/en.json
··· 160 160 "signal": "Signal", 161 161 "signalNumber": "Signal Phone Number", 162 162 "signalNumberPlaceholder": "+1234567890", 163 - "signalNumberHint": "Include country code (e.g., +1 for US)", 163 + "signalNumberHint": "Include country code (eg., +1 for US)", 164 164 "notConfigured": "not configured", 165 165 "inviteCode": "Invite Code", 166 166 "inviteCodePlaceholder": "Enter your invite code", ··· 263 263 "saveFailed": "Failed to save DID document", 264 264 "loadFailed": "Failed to load DID document", 265 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)", 266 + "invalidHandle": "Handle must be an at:// URI (eg., at://handle.example.com)", 267 267 "helpTitle": "What is this?", 268 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 269 }, ··· 385 385 "title": "App Passwords", 386 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 387 "createNew": "Create New App Password", 388 - "appNamePlaceholder": "App name (e.g., Graysky, Skeets)", 388 + "appNamePlaceholder": "App name (eg., Graysky, Skeets)", 389 389 "created": "App Password Created", 390 390 "createdMessage": "Copy this password now. You won't be able to see it again.", 391 391 "yourPasswords": "Your App Passwords", ··· 452 452 "adding": "Adding...", 453 453 "noPasskeys": "No passkeys registered", 454 454 "passkeyName": "Passkey name", 455 - "passkeyNamePlaceholder": "e.g., MacBook Pro, iPhone", 455 + "passkeyNamePlaceholder": "eg., MacBook Pro, iPhone", 456 456 "register": "Register", 457 457 "registering": "Registering...", 458 458 "rename": "Rename", ··· 1007 1007 "infoAppAccess": "Using third-party apps", 1008 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 1009 "passkeyNameLabel": "Passkey Name (optional)", 1010 - "passkeyNamePlaceholder": "e.g., MacBook Touch ID", 1010 + "passkeyNamePlaceholder": "eg., MacBook Touch ID", 1011 1011 "passkeyNameHint": "A friendly name to identify this passkey", 1012 1012 "passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:", 1013 1013 "passkeyPromptBullet1": "Touch ID or Face ID", ··· 1279 1279 "checkingAvailability": "Checking availability...", 1280 1280 "handleAvailable": "Handle is available!", 1281 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)", 1282 + "handleHint": "You can also use your own domain by entering the full handle (eg., alice.mydomain.com)", 1283 1283 "email": "Email Address", 1284 1284 "authMethod": "Authentication Method", 1285 1285 "authPassword": "Password", ··· 1320 1320 "title": "Set Up Your Passkey", 1321 1321 "desc": "Your email has been verified. Now set up your passkey for secure, passwordless login.", 1322 1322 "nameLabel": "Passkey Name (optional)", 1323 - "namePlaceholder": "e.g., MacBook Pro, iPhone", 1323 + "namePlaceholder": "eg., MacBook Pro, iPhone", 1324 1324 "nameHint": "A friendly name to identify this passkey", 1325 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 1326 "register": "Register Passkey", ··· 1418 1418 "title": "Enter Your DID", 1419 1419 "desc": "Enter the DID of the account you want to restore.", 1420 1420 "label": "Your DID", 1421 - "hint": "Your decentralized identifier (e.g., did:plc:abc123...)" 1421 + "hint": "Your decentralized identifier (eg., did:plc:abc123...)" 1422 1422 }, 1423 1423 "uploadCar": { 1424 1424 "title": "Upload Repository Backup",
+4 -3
justfile
··· 23 23 SQLX_OFFLINE=true cargo test --test dpop_unit --test validation_edge_cases --test scope_edge_cases 24 24 25 25 test-auth: 26 - ./scripts/run-tests.sh --test oauth --test oauth_lifecycle --test oauth_scopes --test oauth_security --test oauth_client_metadata --test jwt_security --test session_management --test change_password --test password_reset 26 + ./scripts/run-tests.sh --test oauth --test oauth_lifecycle --test oauth_scopes --test oauth_security --test jwt_security --test session_management --test change_password --test password_reset 27 27 28 28 test-admin: 29 29 ./scripts/run-tests.sh --test admin_email --test admin_invite --test admin_moderation --test admin_search --test admin_stats ··· 81 81 podman compose down 82 82 podman-logs: 83 83 podman compose logs -f 84 - podman-build: 85 - podman compose build 84 + container-build: 85 + podman build -t tranquil-pds:latest . 86 + podman build -t tranquil-pds-frontend:latest ./frontend 86 87 87 88 frontend-dev: 88 89 . ~/.deno/env && cd frontend && deno task dev
+170
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/client-metadata.json { 96 + proxy_pass http://frontend; 97 + proxy_http_version 1.1; 98 + proxy_set_header Host $host; 99 + proxy_set_header Accept-Encoding ""; 100 + sub_filter_once off; 101 + sub_filter_types application/json; 102 + sub_filter '__PDS_HOSTNAME__' $host; 103 + } 104 + 105 + location /oauth/ { 106 + proxy_pass http://backend; 107 + proxy_http_version 1.1; 108 + proxy_set_header Host $host; 109 + proxy_set_header X-Real-IP $remote_addr; 110 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 111 + proxy_set_header X-Forwarded-Proto $scheme; 112 + proxy_read_timeout 300; 113 + proxy_send_timeout 300; 114 + } 115 + 116 + location /.well-known/ { 117 + proxy_pass http://backend; 118 + proxy_http_version 1.1; 119 + proxy_set_header Host $host; 120 + proxy_set_header X-Real-IP $remote_addr; 121 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 122 + proxy_set_header X-Forwarded-Proto $scheme; 123 + } 124 + 125 + location = /metrics { 126 + proxy_pass http://backend; 127 + proxy_http_version 1.1; 128 + proxy_set_header Host $host; 129 + proxy_set_header X-Real-IP $remote_addr; 130 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 131 + proxy_set_header X-Forwarded-Proto $scheme; 132 + } 133 + 134 + location = /health { 135 + proxy_pass http://backend; 136 + proxy_http_version 1.1; 137 + proxy_set_header Host $host; 138 + } 139 + 140 + location = /robots.txt { 141 + proxy_pass http://backend; 142 + proxy_http_version 1.1; 143 + proxy_set_header Host $host; 144 + } 145 + 146 + location = /logo { 147 + proxy_pass http://backend; 148 + proxy_http_version 1.1; 149 + proxy_set_header Host $host; 150 + } 151 + 152 + location ~ ^/u/[^/]+/did\.json$ { 153 + proxy_pass http://backend; 154 + proxy_http_version 1.1; 155 + proxy_set_header Host $host; 156 + proxy_set_header X-Real-IP $remote_addr; 157 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 158 + proxy_set_header X-Forwarded-Proto $scheme; 159 + } 160 + 161 + location / { 162 + proxy_pass http://frontend; 163 + proxy_http_version 1.1; 164 + proxy_set_header Host $host; 165 + proxy_set_header X-Real-IP $remote_addr; 166 + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 167 + proxy_set_header X-Forwarded-Proto $scheme; 168 + } 169 + } 170 + }
-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 9 10 10 - job_name: 'tranquil-pds' 11 11 static_configs: 12 - - targets: ['app:3000'] 12 + - targets: ['tranquil-pds:3000'] 13 13 metrics_path: /metrics
+3 -4
scripts/install-debian.sh
··· 117 117 [[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}" 118 118 echo "" 119 119 120 - read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN 120 + read -p "Enter your PDS domain (eg., pds.example.com): " PDS_DOMAIN 121 121 if [[ -z "$PDS_DOMAIN" ]]; then 122 122 log_error "Domain cannot be empty" 123 123 exit 1 ··· 296 296 297 297 log_info "Cloning Tranquil PDS..." 298 298 if [[ ! -d /opt/tranquil-pds ]]; then 299 - git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/tranquil-pds 299 + git clone https://tangled.org/tranquil.farm/tranquil-pds /opt/tranquil-pds 300 300 else 301 301 cd /opt/tranquil-pds && git pull 302 302 fi ··· 417 417 User=tranquil-pds 418 418 Group=tranquil-pds 419 419 EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env 420 - Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend 421 420 ExecStart=/usr/local/bin/tranquil-pds 422 421 Restart=always 423 422 RestartSec=5 ··· 479 478 echo "" 480 479 log_info "Obtaining wildcard SSL certificate..." 481 480 echo "" 482 - echo "User handles are served as subdomains (e.g., alice.${PDS_DOMAIN})," 481 + echo "User handles are served as subdomains (eg., alice.${PDS_DOMAIN})," 483 482 echo "so you need a wildcard certificate. This requires DNS validation." 484 483 echo "" 485 484 echo "You'll need to add a TXT record to your DNS when prompted."
+5 -5
scripts/test-infra.sh
··· 48 48 --name "${CONTAINER_PREFIX}-minio" \ 49 49 -e MINIO_ROOT_USER=minioadmin \ 50 50 -e MINIO_ROOT_PASSWORD=minioadmin \ 51 - -P \ 51 + -p 9000 \ 52 52 --label tranquil_pds_test=true \ 53 - minio/minio:latest server /data >/dev/null 53 + cgr.dev/chainguard/minio:latest server /data >/dev/null 54 54 echo "Starting Valkey..." 55 55 $CONTAINER_CMD run -d \ 56 56 --name "${CONTAINER_PREFIX}-valkey" \ 57 57 -P \ 58 58 --label tranquil_pds_test=true \ 59 - valkey/valkey:8-alpine >/dev/null 59 + valkey/valkey:9-alpine >/dev/null 60 60 echo "Waiting for services to be ready..." 61 61 sleep 2 62 62 PG_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-postgres" 5432 | head -1 | cut -d: -f2) ··· 86 86 echo "Creating MinIO buckets..." 87 87 $CONTAINER_CMD run --rm --network host \ 88 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 89 + cgr.dev/chainguard/minio-client:latest-dev mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true 90 90 $CONTAINER_CMD run --rm --network host \ 91 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 92 + cgr.dev/chainguard/minio-client:latest-dev mb minio/test-backups --ignore-existing >/dev/null 2>&1 || true 93 93 cat > "$INFRA_FILE" << EOF 94 94 export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:${PG_PORT}/postgres" 95 95 export TEST_DB_PORT="${PG_PORT}"