mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-20 01:04:14 +00:00
feat: docs tweaks & standalone frontend
This commit is contained in:
+2
-4
@@ -140,7 +140,7 @@ AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
# =============================================================================
|
||||
# If configured, moderation reports will be proxied to this service
|
||||
# instead of being stored locally. The service should implement the
|
||||
# com.atproto.moderation.createReport endpoint (e.g., Bluesky's Ozone).
|
||||
# com.atproto.moderation.createReport endpoint (eg., Bluesky's Ozone).
|
||||
# Both URL and DID must be set for proxying to be enabled.
|
||||
# REPORT_SERVICE_URL=https://mod.bsky.app
|
||||
# REPORT_SERVICE_DID=did:plc:ar7c4by46qjdydhdevvrndac
|
||||
@@ -148,7 +148,7 @@ AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
# Age Assurance Override
|
||||
# =============================================================================
|
||||
# Enable this if you have separately assured the ages of your users
|
||||
# (e.g., through your own age verification process). When enabled, the PDS
|
||||
# (eg., through your own age verification process). When enabled, the PDS
|
||||
# will return "assured" status for age assurance checks instead of proxying
|
||||
# to the appview. This helps migrated users avoid the age assurance
|
||||
# catch-22 on bsky.app.
|
||||
@@ -158,8 +158,6 @@ AWS_SECRET_ACCESS_KEY=minioadmin
|
||||
# =============================================================================
|
||||
# Allow HTTP for proxy requests (development only)
|
||||
# ALLOW_HTTP_PROXY=1
|
||||
# Custom frontend directory (defaults to ./frontend/dist)
|
||||
# FRONTEND_DIR=/path/to/frontend/dist
|
||||
# =============================================================================
|
||||
# SSO / Social Login
|
||||
# =============================================================================
|
||||
|
||||
Generated
-28
@@ -2591,12 +2591,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range-header"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.10.1"
|
||||
@@ -3477,16 +3471,6 @@ version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
@@ -5850,19 +5834,13 @@ dependencies = [
|
||||
"http 1.4.0",
|
||||
"http-body 1.0.1",
|
||||
"http-body-util",
|
||||
"http-range-header",
|
||||
"httpdate",
|
||||
"iri-string",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6241,12 +6219,6 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.18"
|
||||
|
||||
+1
-1
@@ -90,7 +90,7 @@ tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "time", "si
|
||||
tokio-tungstenite = { version = "0.28", features = ["native-tls"] }
|
||||
totp-rs = { version = "5", features = ["qr"] }
|
||||
tower = "0.5"
|
||||
tower-http = { version = "0.6", features = ["fs", "cors"] }
|
||||
tower-http = { version = "0.6", features = ["cors"] }
|
||||
tower-layer = "0.3"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = "0.3"
|
||||
|
||||
+5
-13
@@ -1,29 +1,21 @@
|
||||
FROM denoland/deno:alpine AS frontend-builder
|
||||
WORKDIR /frontend
|
||||
COPY frontend/ ./
|
||||
RUN deno task build
|
||||
|
||||
FROM rust:1.92-alpine AS builder
|
||||
RUN apk add ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev
|
||||
RUN apk add --no-cache ca-certificates openssl openssl-dev openssl-libs-static pkgconfig musl-dev
|
||||
WORKDIR /app
|
||||
COPY Cargo.toml Cargo.lock ./
|
||||
COPY src ./src
|
||||
COPY tests ./tests
|
||||
COPY migrations ./migrations
|
||||
COPY crates ./crates
|
||||
COPY .sqlx ./.sqlx
|
||||
COPY migrations ./crates/tranquil-pds/migrations
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
--mount=type=cache,target=/app/target \
|
||||
cargo build --release && \
|
||||
SQLX_OFFLINE=true cargo build --release -p tranquil-pds && \
|
||||
cp target/release/tranquil-pds /tmp/tranquil-pds
|
||||
|
||||
FROM alpine:3.23
|
||||
RUN apk add --no-cache msmtp ca-certificates && ln -sf /usr/bin/msmtp /usr/sbin/sendmail
|
||||
COPY --from=builder /tmp/tranquil-pds /usr/local/bin/tranquil-pds
|
||||
COPY --from=builder /app/migrations /app/migrations
|
||||
COPY --from=frontend-builder /frontend/dist /app/frontend/dist
|
||||
COPY migrations /app/migrations
|
||||
WORKDIR /app
|
||||
ENV SERVER_HOST=0.0.0.0
|
||||
ENV SERVER_PORT=3000
|
||||
ENV FRONTEND_DIR=/app/frontend/dist
|
||||
EXPOSE 3000
|
||||
CMD ["tranquil-pds"]
|
||||
|
||||
@@ -12,7 +12,7 @@ Another excellent PDS is [Cocoon](https://tangled.org/hailey.at/cocoon), written
|
||||
|
||||
## What's different about Tranquil PDS
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
@@ -45,7 +45,7 @@ Edit `.env.prod` with your values. Generate secrets with `openssl rand -base64 4
|
||||
|
||||
```bash
|
||||
cp .env.prod.example .env.prod
|
||||
podman-compose -f docker-compose.prod.yml up -d
|
||||
podman-compose -f docker-compose.prod.yaml up -d
|
||||
```
|
||||
|
||||
### Installation Guides
|
||||
|
||||
@@ -83,9 +83,11 @@ impl ClientMetadataCache {
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.pool_max_idle_per_host(10)
|
||||
.pool_idle_timeout(std::time::Duration::from_secs(90))
|
||||
.user_agent(
|
||||
"Tranquil-PDS/1.0 (ATProto; +https://tangled.org/lewis.moe/bspds-sandbox)",
|
||||
)
|
||||
.user_agent(concat!(
|
||||
"Tranquil-PDS/",
|
||||
env!("CARGO_PKG_VERSION"),
|
||||
" (ATProto; +https://tangled.org/tranquil.farm/tranquil-pds)"
|
||||
))
|
||||
.build()
|
||||
.unwrap_or_else(|_| Client::new()),
|
||||
cache_ttl_secs,
|
||||
|
||||
@@ -38,7 +38,6 @@ use state::AppState;
|
||||
pub use sync::util::AccountStatus;
|
||||
use tower::ServiceBuilder;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
use tower_http::services::{ServeDir, ServeFile};
|
||||
pub use types::{AccountState, AtIdentifier, AtUri, Did, Handle, Nsid, Rkey};
|
||||
|
||||
pub fn app(state: AppState) -> Router {
|
||||
@@ -525,10 +524,6 @@ pub fn app(state: AppState) -> Router {
|
||||
|
||||
let oauth_router = Router::new()
|
||||
.route("/jwks", get(oauth::endpoints::oauth_jwks))
|
||||
.route(
|
||||
"/client-metadata.json",
|
||||
get(oauth::endpoints::frontend_client_metadata),
|
||||
)
|
||||
.route("/par", post(oauth::endpoints::pushed_authorization_request))
|
||||
.route("/authorize", get(oauth::endpoints::authorize_get))
|
||||
.route("/authorize", post(oauth::endpoints::authorize_post))
|
||||
@@ -612,7 +607,7 @@ pub fn app(state: AppState) -> Router {
|
||||
get(oauth::endpoints::oauth_authorization_server),
|
||||
);
|
||||
|
||||
let router = Router::new()
|
||||
Router::new()
|
||||
.nest_service("/xrpc", xrpc_service)
|
||||
.nest("/oauth", oauth_router)
|
||||
.nest("/.well-known", well_known_router)
|
||||
@@ -644,33 +639,5 @@ pub fn app(state: AppState) -> Router {
|
||||
"atproto-content-labelers".parse().unwrap(),
|
||||
]),
|
||||
)
|
||||
.with_state(state);
|
||||
|
||||
let frontend_dir =
|
||||
std::env::var("FRONTEND_DIR").unwrap_or_else(|_| "./frontend/dist".to_string());
|
||||
if std::path::Path::new(&frontend_dir)
|
||||
.join("index.html")
|
||||
.exists()
|
||||
{
|
||||
let index_path = format!("{}/index.html", frontend_dir);
|
||||
let homepage_path = format!("{}/homepage.html", frontend_dir);
|
||||
|
||||
let homepage_exists = std::path::Path::new(&homepage_path).exists();
|
||||
let homepage_file = if homepage_exists {
|
||||
homepage_path
|
||||
} else {
|
||||
index_path.clone()
|
||||
};
|
||||
|
||||
let spa_router = Router::new().fallback_service(ServeFile::new(&index_path));
|
||||
|
||||
let serve_dir = ServeDir::new(&frontend_dir).not_found_service(ServeFile::new(&index_path));
|
||||
|
||||
return router
|
||||
.route_service("/", ServeFile::new(&homepage_file))
|
||||
.nest("/app", spa_router)
|
||||
.fallback_service(serve_dir);
|
||||
}
|
||||
|
||||
router
|
||||
.with_state(state)
|
||||
}
|
||||
|
||||
@@ -82,17 +82,6 @@ pub async fn oauth_authorization_server(
|
||||
"transition:generic".to_string(),
|
||||
"transition:chat.bsky".to_string(),
|
||||
"transition:email".to_string(),
|
||||
"repo:*".to_string(),
|
||||
"repo:*?action=create".to_string(),
|
||||
"repo:*?action=read".to_string(),
|
||||
"repo:*?action=update".to_string(),
|
||||
"repo:*?action=delete".to_string(),
|
||||
"blob:*/*".to_string(),
|
||||
"rpc:*".to_string(),
|
||||
"account:*".to_string(),
|
||||
"account:*?action=read".to_string(),
|
||||
"account:*?action=write".to_string(),
|
||||
"identity:*".to_string(),
|
||||
]),
|
||||
response_types_supported: vec!["code".to_string()],
|
||||
response_modes_supported: Some(vec!["query".to_string(), "fragment".to_string()]),
|
||||
@@ -142,44 +131,3 @@ pub async fn oauth_jwks(State(_state): State<AppState>) -> Json<JwkSet> {
|
||||
};
|
||||
Json(create_jwk_set(vec![server_key]))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct FrontendClientMetadata {
|
||||
pub client_id: String,
|
||||
pub client_name: String,
|
||||
pub client_uri: String,
|
||||
pub redirect_uris: Vec<String>,
|
||||
pub grant_types: Vec<String>,
|
||||
pub response_types: Vec<String>,
|
||||
pub scope: String,
|
||||
pub token_endpoint_auth_method: String,
|
||||
pub application_type: String,
|
||||
pub dpop_bound_access_tokens: bool,
|
||||
}
|
||||
|
||||
pub async fn frontend_client_metadata(
|
||||
State(_state): State<AppState>,
|
||||
) -> Json<FrontendClientMetadata> {
|
||||
let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
||||
let base_url = format!("https://{}", pds_hostname);
|
||||
let client_id = format!("{}/oauth/client-metadata.json", base_url);
|
||||
Json(FrontendClientMetadata {
|
||||
client_id,
|
||||
client_name: "PDS Account Manager".to_string(),
|
||||
client_uri: base_url.clone(),
|
||||
redirect_uris: vec![
|
||||
format!("{}/app/", base_url),
|
||||
format!("{}/app/migrate", base_url),
|
||||
],
|
||||
grant_types: vec![
|
||||
"authorization_code".to_string(),
|
||||
"refresh_token".to_string(),
|
||||
],
|
||||
response_types: vec!["code".to_string()],
|
||||
scope: "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*"
|
||||
.to_string(),
|
||||
token_endpoint_auth_method: "none".to_string(),
|
||||
application_type: "web".to_string(),
|
||||
dpop_bound_access_tokens: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -162,7 +162,7 @@ async fn setup_with_external_infra() -> String {
|
||||
|
||||
#[cfg(not(feature = "external-infra"))]
|
||||
async fn setup_with_testcontainers() -> String {
|
||||
let s3_container = GenericImage::new("minio/minio", "latest")
|
||||
let s3_container = GenericImage::new("cgr.dev/chainguard/minio", "latest")
|
||||
.with_exposed_port(ContainerPort::Tcp(9000))
|
||||
.with_env_var("MINIO_ROOT_USER", "minioadmin")
|
||||
.with_env_var("MINIO_ROOT_PASSWORD", "minioadmin")
|
||||
|
||||
@@ -1,114 +0,0 @@
|
||||
mod common;
|
||||
use common::*;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::Value;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_returns_valid_json() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!("{}/oauth/client-metadata.json", base_url().await))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.expect("Should return valid JSON");
|
||||
assert!(
|
||||
body["client_id"].as_str().is_some(),
|
||||
"Should have client_id"
|
||||
);
|
||||
assert!(
|
||||
body["client_name"].as_str().is_some(),
|
||||
"Should have client_name"
|
||||
);
|
||||
assert!(
|
||||
body["redirect_uris"].as_array().is_some(),
|
||||
"Should have redirect_uris"
|
||||
);
|
||||
assert!(
|
||||
body["grant_types"].as_array().is_some(),
|
||||
"Should have grant_types"
|
||||
);
|
||||
assert!(
|
||||
body["response_types"].as_array().is_some(),
|
||||
"Should have response_types"
|
||||
);
|
||||
assert!(body["scope"].as_str().is_some(), "Should have scope");
|
||||
assert!(
|
||||
body["token_endpoint_auth_method"].as_str().is_some(),
|
||||
"Should have token_endpoint_auth_method"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_correct_values() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!("{}/oauth/client-metadata.json", base_url().await))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let client_id = body["client_id"].as_str().unwrap();
|
||||
assert!(
|
||||
client_id.ends_with("/oauth/client-metadata.json"),
|
||||
"client_id should end with /oauth/client-metadata.json"
|
||||
);
|
||||
let grant_types = body["grant_types"].as_array().unwrap();
|
||||
let grant_strs: Vec<&str> = grant_types.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(
|
||||
grant_strs.contains(&"authorization_code"),
|
||||
"Should support authorization_code grant"
|
||||
);
|
||||
assert!(
|
||||
grant_strs.contains(&"refresh_token"),
|
||||
"Should support refresh_token grant"
|
||||
);
|
||||
let response_types = body["response_types"].as_array().unwrap();
|
||||
let response_strs: Vec<&str> = response_types.iter().filter_map(|v| v.as_str()).collect();
|
||||
assert!(
|
||||
response_strs.contains(&"code"),
|
||||
"Should support code response type"
|
||||
);
|
||||
assert_eq!(
|
||||
body["token_endpoint_auth_method"].as_str(),
|
||||
Some("none"),
|
||||
"Should be public client (none auth)"
|
||||
);
|
||||
assert_eq!(
|
||||
body["application_type"].as_str(),
|
||||
Some("web"),
|
||||
"Should be web application"
|
||||
);
|
||||
assert_eq!(
|
||||
body["dpop_bound_access_tokens"].as_bool(),
|
||||
Some(true),
|
||||
"AT Protocol requires DPoP-bound access tokens"
|
||||
);
|
||||
let scope = body["scope"].as_str().unwrap();
|
||||
assert!(scope.contains("atproto"), "Scope should include atproto");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_frontend_client_metadata_redirect_uri_matches_client_uri() {
|
||||
let client = client();
|
||||
let res = client
|
||||
.get(format!("{}/oauth/client-metadata.json", base_url().await))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
let body: Value = res.json().await.unwrap();
|
||||
let client_uri = body["client_uri"].as_str().unwrap();
|
||||
let redirect_uris = body["redirect_uris"].as_array().unwrap();
|
||||
assert!(
|
||||
!redirect_uris.is_empty(),
|
||||
"Should have at least one redirect URI"
|
||||
);
|
||||
let redirect_uri = redirect_uris[0].as_str().unwrap();
|
||||
assert!(
|
||||
redirect_uri.starts_with(client_uri),
|
||||
"Redirect URI should be on same origin as client_uri"
|
||||
);
|
||||
}
|
||||
@@ -1,40 +1,53 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
access_log /var/log/nginx/access.log;
|
||||
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
client_max_body_size 10G;
|
||||
location / {
|
||||
|
||||
location /xrpc/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@@ -46,6 +59,82 @@ http {
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
}
|
||||
|
||||
location /oauth/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300;
|
||||
proxy_send_timeout 300;
|
||||
}
|
||||
|
||||
location /.well-known/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /metrics {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /logo {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ~ ^/u/[^/]+/did\.json$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ Environment=S3_ENDPOINT=http://localhost:9000
|
||||
Environment=AWS_REGION=us-east-1
|
||||
Environment=S3_BUCKET=pds-blobs
|
||||
Environment=VALKEY_URL=redis://localhost:6379
|
||||
Environment=FRONTEND_DIR=/app/frontend/dist
|
||||
HealthCmd=wget -q --spider http://localhost:3000/xrpc/_health
|
||||
HealthInterval=30s
|
||||
HealthTimeout=10s
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[Unit]
|
||||
Description=Tranquil PDS frontend
|
||||
After=tranquil-pds-app.service
|
||||
|
||||
[Container]
|
||||
ContainerName=tranquil-pds-frontend
|
||||
Image=localhost/tranquil-pds-frontend:latest
|
||||
Pod=tranquil-pds.pod
|
||||
Volume=/opt/tranquil-pds/frontend/nginx-quadlet.conf:/etc/nginx/conf.d/default.conf:ro,Z
|
||||
HealthCmd=wget -q --spider http://localhost:8080/
|
||||
HealthInterval=30s
|
||||
HealthTimeout=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=5s
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -2,17 +2,12 @@
|
||||
Description=Tranquil PDS minio object storage
|
||||
[Container]
|
||||
ContainerName=tranquil-pds-minio
|
||||
Image=docker.io/minio/minio:RELEASE.2025-10-15T17-29-55Z
|
||||
Image=cgr.dev/chainguard/minio:latest
|
||||
Pod=tranquil-pds.pod
|
||||
Environment=MINIO_ROOT_USER=minioadmin
|
||||
Secret=tranquil-pds-minio-password,type=env,target=MINIO_ROOT_PASSWORD
|
||||
Volume=/srv/tranquil-pds/minio:/data:Z
|
||||
Exec=server /data --console-address :9001
|
||||
HealthCmd=curl -f http://localhost:9000/minio/health/live || exit 1
|
||||
HealthInterval=30s
|
||||
HealthTimeout=10s
|
||||
HealthRetries=3
|
||||
HealthStartPeriod=10s
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=Tranquil PDS nginx reverse proxy
|
||||
After=tranquil-pds-app.service
|
||||
After=tranquil-pds-app.service tranquil-pds-frontend.service
|
||||
[Container]
|
||||
ContainerName=tranquil-pds-nginx
|
||||
Image=docker.io/library/nginx:1.28-alpine
|
||||
|
||||
@@ -5,8 +5,6 @@ services:
|
||||
dockerfile: Dockerfile
|
||||
image: tranquil-pds:latest
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:3000:3000"
|
||||
environment:
|
||||
SERVER_HOST: "0.0.0.0"
|
||||
SERVER_PORT: "3000"
|
||||
@@ -22,7 +20,6 @@ services:
|
||||
DPOP_SECRET: "${DPOP_SECRET:?DPOP_SECRET is required (min 32 chars)}"
|
||||
MASTER_KEY: "${MASTER_KEY:?MASTER_KEY is required (min 32 chars)}"
|
||||
CRAWLERS: "${CRAWLERS:-https://bsky.network}"
|
||||
FRONTEND_DIR: "/app/frontend/dist"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
@@ -42,6 +39,26 @@ services:
|
||||
memory: 1G
|
||||
reservations:
|
||||
memory: 256M
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
image: tranquil-pds-frontend:latest
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "--spider", "http://localhost:80/"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 5s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 128M
|
||||
reservations:
|
||||
memory: 32M
|
||||
|
||||
db:
|
||||
image: postgres:18-alpine
|
||||
restart: unless-stopped
|
||||
@@ -63,8 +80,9 @@ services:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 128M
|
||||
|
||||
minio:
|
||||
image: minio/minio:RELEASE.2025-10-15T17-29-55Z
|
||||
image: cgr.dev/chainguard/minio:latest
|
||||
restart: unless-stopped
|
||||
command: server /data --console-address ":9001"
|
||||
environment:
|
||||
@@ -72,33 +90,32 @@ services:
|
||||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "mc", "ready", "local"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 512M
|
||||
reservations:
|
||||
memory: 128M
|
||||
|
||||
minio-init:
|
||||
image: minio/mc:RELEASE.2025-07-16T15-35-03Z
|
||||
image: cgr.dev/chainguard/minio-client:latest-dev
|
||||
depends_on:
|
||||
minio:
|
||||
condition: service_healthy
|
||||
- minio
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD};
|
||||
for i in 1 2 3 4 5 6 7 8 9 10; do
|
||||
mc alias set local http://minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} && break;
|
||||
echo 'Waiting for minio...'; sleep 2;
|
||||
done;
|
||||
mc mb --ignore-existing local/pds-blobs;
|
||||
mc mb --ignore-existing local/pds-backups;
|
||||
mc anonymous set none local/pds-blobs;
|
||||
exit 0;
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}"
|
||||
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}"
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:9-alpine
|
||||
restart: unless-stopped
|
||||
@@ -117,45 +134,50 @@ services:
|
||||
memory: 300M
|
||||
reservations:
|
||||
memory: 64M
|
||||
|
||||
nginx:
|
||||
image: nginx:1.28-alpine
|
||||
image: nginx:1.29-alpine
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- ./nginx.prod.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./nginx.frontend.conf:/etc/nginx/nginx.conf:ro
|
||||
- ./certs:/etc/nginx/certs:ro
|
||||
- acme_challenge:/var/www/acme:ro
|
||||
depends_on:
|
||||
- tranquil-pds
|
||||
- frontend
|
||||
healthcheck:
|
||||
test: ["CMD", "nginx", "-t"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot:v5.2.2
|
||||
volumes:
|
||||
- ./certs:/etc/letsencrypt
|
||||
- acme_challenge:/var/www/acme
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/acme; sleep 12h & wait $${!}; done'"
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.8.0
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "127.0.0.1:9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--config.file=/etc/prometheus/prometheus.yaml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--storage.tsdb.retention.time=30d'
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
memory: 256M
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
+19
-4
@@ -16,6 +16,17 @@ services:
|
||||
- db
|
||||
- objsto
|
||||
- cache
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: ./frontend
|
||||
dockerfile: Dockerfile
|
||||
image: tranquil-pds-frontend
|
||||
ports:
|
||||
- "8080:80"
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
db:
|
||||
image: postgres:18-alpine
|
||||
environment:
|
||||
@@ -26,8 +37,9 @@ services:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
|
||||
objsto:
|
||||
image: minio/minio
|
||||
image: cgr.dev/chainguard/minio:latest
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
@@ -37,24 +49,27 @@ services:
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
cache:
|
||||
image: valkey/valkey:8-alpine
|
||||
image: valkey/valkey:9-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- valkey_data:/data
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:v3.8.0
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./observability/prometheus.yml:/etc/prometheus/prometheus.yml:ro
|
||||
- ./observability/prometheus.yaml:/etc/prometheus/prometheus.yaml:ro
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--config.file=/etc/prometheus/prometheus.yaml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
depends_on:
|
||||
- app
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
|
||||
+183
-43
@@ -1,15 +1,21 @@
|
||||
# Tranquil PDS Containerized Production Deployment
|
||||
> **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.
|
||||
|
||||
This guide covers deploying Tranquil PDS using containers with podman.
|
||||
|
||||
- **Debian 13+**: Uses systemd quadlets (modern, declarative container management)
|
||||
- **Alpine 3.23+**: Uses OpenRC service script with podman-compose
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A **wildcard TLS certificate** for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root or sudo access
|
||||
|
||||
## Quick Start (Docker/Podman Compose)
|
||||
|
||||
If you just want to get running quickly:
|
||||
|
||||
```sh
|
||||
cp .env.example .env
|
||||
```
|
||||
@@ -18,64 +24,139 @@ Edit `.env` with your values. Generate secrets with `openssl rand -base64 48`.
|
||||
|
||||
Build and start:
|
||||
```sh
|
||||
podman-compose -f docker-compose.prod.yml up -d
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
podman-compose -f docker-compose.prod.yaml up -d
|
||||
```
|
||||
|
||||
Get initial certificate (after DNS is configured):
|
||||
```sh
|
||||
podman-compose -f docker-compose.prod.yml run --rm certbot certonly \
|
||||
--webroot -w /var/www/acme -d pds.example.com
|
||||
podman-compose -f docker-compose.prod.yml restart nginx
|
||||
podman-compose -f docker-compose.prod.yaml run --rm certbot certonly \
|
||||
--webroot -w /var/www/acme -d pds.example.com -d '*.pds.example.com'
|
||||
ln -sf live/pds.example.com/fullchain.pem certs/fullchain.pem
|
||||
ln -sf live/pds.example.com/privkey.pem certs/privkey.pem
|
||||
podman-compose -f docker-compose.prod.yaml restart nginx
|
||||
```
|
||||
|
||||
For production setups with proper service management, continue to either the Debian or Alpine section below.
|
||||
|
||||
## Standalone Containers (No Compose)
|
||||
|
||||
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.
|
||||
|
||||
Build the images:
|
||||
```sh
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
```
|
||||
|
||||
Run the backend with host networking (so it can access postgres/valkey/minio on localhost):
|
||||
```sh
|
||||
podman run -d --name tranquil-pds \
|
||||
--network=host \
|
||||
--env-file /etc/tranquil-pds/tranquil-pds.env \
|
||||
tranquil-pds:latest
|
||||
```
|
||||
|
||||
Run the frontend with port mapping (the container's nginx listens on port 80):
|
||||
```sh
|
||||
podman run -d --name tranquil-pds-frontend \
|
||||
-p 8080:80 \
|
||||
tranquil-pds-frontend:latest
|
||||
```
|
||||
|
||||
Then configure your host nginx to proxy to both containers. Replace the static file `try_files` directives with proxy passes:
|
||||
|
||||
```nginx
|
||||
# API routes to backend
|
||||
location /xrpc/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
# ... (see Debian guide for full proxy headers)
|
||||
}
|
||||
|
||||
# Static routes to frontend container
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
```
|
||||
|
||||
See the [Debian install guide](install-debian.md) for the full nginx config with all API routes.
|
||||
|
||||
---
|
||||
|
||||
# Debian 13+ with Systemd Quadlets
|
||||
|
||||
Quadlets are the modern way to run podman containers under systemd.
|
||||
## 1. Install Podman
|
||||
|
||||
## Install Podman
|
||||
|
||||
```bash
|
||||
apt update
|
||||
apt install -y podman
|
||||
```
|
||||
## 2. Create Directory Structure
|
||||
|
||||
## Create Directory Structure
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/containers/systemd
|
||||
mkdir -p /srv/tranquil-pds/{postgres,minio,valkey,certs,acme,config}
|
||||
```
|
||||
## 3. Create Environment File
|
||||
|
||||
## Create Environment File
|
||||
|
||||
```bash
|
||||
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
|
||||
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
|
||||
```
|
||||
|
||||
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
```
|
||||
|
||||
For quadlets, also add `DATABASE_URL` with the full connection string (systemd doesn't support variable expansion).
|
||||
## 4. Install Quadlet Definitions
|
||||
|
||||
## Install Quadlet Definitions
|
||||
|
||||
Copy the quadlet files from the repository:
|
||||
```bash
|
||||
cp /opt/tranquil-pds/deploy/quadlets/*.pod /etc/containers/systemd/
|
||||
cp /opt/tranquil-pds/deploy/quadlets/*.container /etc/containers/systemd/
|
||||
```
|
||||
|
||||
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.
|
||||
## 5. Create nginx Configuration
|
||||
|
||||
## Create nginx Configuration
|
||||
|
||||
```bash
|
||||
cp /opt/tranquil-pds/deploy/nginx/nginx-quadlet.conf /srv/tranquil-pds/config/nginx.conf
|
||||
cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf
|
||||
```
|
||||
## 6. Build Tranquil PDS Image
|
||||
|
||||
## Clone and Build Images
|
||||
|
||||
```bash
|
||||
cd /opt
|
||||
git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds
|
||||
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
|
||||
cd tranquil-pds
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
```
|
||||
## 7. Create Podman Secrets
|
||||
|
||||
## Create Podman Secrets
|
||||
|
||||
```bash
|
||||
source /srv/tranquil-pds/config/tranquil-pds.env
|
||||
echo "$DB_PASSWORD" | podman secret create tranquil-pds-db-password -
|
||||
echo "$MINIO_ROOT_PASSWORD" | podman secret create tranquil-pds-minio-password -
|
||||
```
|
||||
## 8. Start Services and Initialize
|
||||
|
||||
## Start Services and Initialize
|
||||
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl start tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey
|
||||
@@ -87,7 +168,7 @@ Create the minio buckets:
|
||||
podman run --rm --pod tranquil-pds \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=your-minio-password \
|
||||
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
|
||||
cgr.dev/chainguard/minio-client:latest-dev \
|
||||
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"
|
||||
```
|
||||
|
||||
@@ -96,8 +177,10 @@ Run migrations:
|
||||
cargo install sqlx-cli --no-default-features --features postgres
|
||||
DATABASE_URL="postgres://tranquil_pds:your-db-password@localhost:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations
|
||||
```
|
||||
## 9. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
## Obtain Wildcard SSL Certificate
|
||||
|
||||
User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
Create temporary self-signed cert to start services:
|
||||
```bash
|
||||
@@ -105,7 +188,7 @@ openssl req -x509 -nodes -days 1 -newkey rsa:2048 \
|
||||
-keyout /srv/tranquil-pds/certs/privkey.pem \
|
||||
-out /srv/tranquil-pds/certs/fullchain.pem \
|
||||
-subj "/CN=pds.example.com"
|
||||
systemctl start tranquil-pds-app tranquil-pds-nginx
|
||||
systemctl start tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
|
||||
```
|
||||
|
||||
Get a wildcard certificate using DNS validation:
|
||||
@@ -117,9 +200,10 @@ podman run --rm -it \
|
||||
-d pds.example.com -d '*.pds.example.com' \
|
||||
--agree-tos --email you@example.com
|
||||
```
|
||||
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
For automated renewal, use a DNS provider plugin (e.g., cloudflare, route53).
|
||||
For automated renewal, use a DNS provider plugin (eg., cloudflare, route53).
|
||||
|
||||
Link certificates and restart:
|
||||
```bash
|
||||
@@ -127,11 +211,15 @@ ln -sf /srv/tranquil-pds/certs/live/pds.example.com/fullchain.pem /srv/tranquil-
|
||||
ln -sf /srv/tranquil-pds/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/certs/privkey.pem
|
||||
systemctl restart tranquil-pds-nginx
|
||||
```
|
||||
## 10. Enable All Services
|
||||
|
||||
## Enable All Services
|
||||
|
||||
```bash
|
||||
systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-nginx
|
||||
systemctl enable tranquil-pds-db tranquil-pds-minio tranquil-pds-valkey tranquil-pds-app tranquil-pds-frontend tranquil-pds-nginx
|
||||
```
|
||||
## 11. Configure Firewall
|
||||
|
||||
## Configure Firewall
|
||||
|
||||
```bash
|
||||
apt install -y ufw
|
||||
ufw allow ssh
|
||||
@@ -139,59 +227,81 @@ ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw enable
|
||||
```
|
||||
## 12. Certificate Renewal
|
||||
|
||||
## Certificate Renewal
|
||||
|
||||
Add to root's crontab (`crontab -e`):
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Alpine 3.23+ with OpenRC
|
||||
|
||||
Alpine uses OpenRC, not systemd. We'll use podman-compose with an OpenRC service wrapper.
|
||||
## 1. Install Podman
|
||||
|
||||
## Install Podman
|
||||
|
||||
```sh
|
||||
apk update
|
||||
apk add podman podman-compose fuse-overlayfs cni-plugins
|
||||
rc-update add cgroups
|
||||
rc-service cgroups start
|
||||
```
|
||||
|
||||
Enable podman socket for compose:
|
||||
```sh
|
||||
rc-update add podman
|
||||
rc-service podman start
|
||||
```
|
||||
## 2. Create Directory Structure
|
||||
|
||||
## Create Directory Structure
|
||||
|
||||
```sh
|
||||
mkdir -p /srv/tranquil-pds/{data,config}
|
||||
mkdir -p /srv/tranquil-pds/data/{postgres,minio,valkey,certs,acme}
|
||||
```
|
||||
## 3. Clone Repository and Build
|
||||
|
||||
## Clone Repository and Build Images
|
||||
|
||||
```sh
|
||||
cd /opt
|
||||
git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds
|
||||
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
|
||||
cd tranquil-pds
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
```
|
||||
## 4. Create Environment File
|
||||
|
||||
## Create Environment File
|
||||
|
||||
```sh
|
||||
cp /opt/tranquil-pds/.env.example /srv/tranquil-pds/config/tranquil-pds.env
|
||||
chmod 600 /srv/tranquil-pds/config/tranquil-pds.env
|
||||
```
|
||||
|
||||
Edit `/srv/tranquil-pds/config/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
```sh
|
||||
openssl rand -base64 48
|
||||
```
|
||||
## 5. Set Up Compose and nginx
|
||||
|
||||
## Set Up Compose and nginx
|
||||
|
||||
Copy the production compose and nginx configs:
|
||||
```sh
|
||||
cp /opt/tranquil-pds/docker-compose.prod.yml /srv/tranquil-pds/docker-compose.yml
|
||||
cp /opt/tranquil-pds/nginx.prod.conf /srv/tranquil-pds/config/nginx.conf
|
||||
cp /opt/tranquil-pds/docker-compose.prod.yaml /srv/tranquil-pds/docker-compose.yml
|
||||
cp /opt/tranquil-pds/nginx.frontend.conf /srv/tranquil-pds/config/nginx.conf
|
||||
```
|
||||
|
||||
Edit `/srv/tranquil-pds/docker-compose.yml` to adjust paths if needed:
|
||||
- Update volume mounts to use `/srv/tranquil-pds/data/` paths
|
||||
- Update nginx cert paths to match `/srv/tranquil-pds/data/certs/`
|
||||
- Update nginx config path to `/srv/tranquil-pds/config/nginx.conf`
|
||||
|
||||
Edit `/srv/tranquil-pds/config/nginx.conf` to update cert paths:
|
||||
- Change `/etc/nginx/certs/live/${PDS_HOSTNAME}/` to `/etc/nginx/certs/`
|
||||
## 6. Create OpenRC Service
|
||||
|
||||
## Create OpenRC Service
|
||||
|
||||
```sh
|
||||
cat > /etc/init.d/tranquil-pds << 'EOF'
|
||||
#!/sbin/openrc-run
|
||||
@@ -223,7 +333,9 @@ stop() {
|
||||
EOF
|
||||
chmod +x /etc/init.d/tranquil-pds
|
||||
```
|
||||
## 7. Initialize Services
|
||||
|
||||
## Initialize Services
|
||||
|
||||
Start services:
|
||||
```sh
|
||||
rc-service tranquil-pds start
|
||||
@@ -236,7 +348,7 @@ source /srv/tranquil-pds/config/tranquil-pds.env
|
||||
podman run --rm --network tranquil-pds_default \
|
||||
-e MINIO_ROOT_USER="$MINIO_ROOT_USER" \
|
||||
-e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \
|
||||
docker.io/minio/mc:RELEASE.2025-07-16T15-35-03Z \
|
||||
cgr.dev/chainguard/minio-client:latest-dev \
|
||||
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'
|
||||
```
|
||||
|
||||
@@ -249,8 +361,10 @@ cargo install sqlx-cli --no-default-features --features postgres
|
||||
DB_IP=$(podman inspect tranquil-pds-db-1 --format '{{.NetworkSettings.Networks.tranquil-pds_default.IPAddress}}')
|
||||
DATABASE_URL="postgres://tranquil_pds:$DB_PASSWORD@$DB_IP:5432/pds" sqlx migrate run --source /opt/tranquil-pds/migrations
|
||||
```
|
||||
## 8. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
## Obtain Wildcard SSL Certificate
|
||||
|
||||
User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate. Wildcard certs require DNS-01 validation.
|
||||
|
||||
Create temporary self-signed cert to start services:
|
||||
```sh
|
||||
@@ -270,6 +384,7 @@ podman run --rm -it \
|
||||
-d pds.example.com -d '*.pds.example.com' \
|
||||
--agree-tos --email you@example.com
|
||||
```
|
||||
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
Link certificates and restart:
|
||||
@@ -278,11 +393,15 @@ ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/fullchain.pem /srv/tran
|
||||
ln -sf /srv/tranquil-pds/data/certs/live/pds.example.com/privkey.pem /srv/tranquil-pds/data/certs/privkey.pem
|
||||
rc-service tranquil-pds restart
|
||||
```
|
||||
## 9. Enable Service at Boot
|
||||
|
||||
## Enable Service at Boot
|
||||
|
||||
```sh
|
||||
rc-update add tranquil-pds
|
||||
```
|
||||
## 10. Configure Firewall
|
||||
|
||||
## Configure Firewall
|
||||
|
||||
```sh
|
||||
apk add iptables ip6tables
|
||||
iptables -A INPUT -p tcp --dport 22 -j ACCEPT
|
||||
@@ -302,50 +421,67 @@ rc-update add ip6tables
|
||||
/etc/init.d/iptables save
|
||||
/etc/init.d/ip6tables save
|
||||
```
|
||||
## 11. Certificate Renewal
|
||||
|
||||
## Certificate Renewal
|
||||
|
||||
Add to root's crontab (`crontab -e`):
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Verification and Maintenance
|
||||
|
||||
## Verify Installation
|
||||
|
||||
```sh
|
||||
curl -s https://pds.example.com/xrpc/_health | jq
|
||||
curl -s https://pds.example.com/.well-known/atproto-did
|
||||
```
|
||||
|
||||
## View Logs
|
||||
|
||||
**Debian:**
|
||||
```bash
|
||||
journalctl -u tranquil-pds-app -f
|
||||
podman logs -f tranquil-pds-app
|
||||
podman logs -f tranquil-pds-frontend
|
||||
```
|
||||
|
||||
**Alpine:**
|
||||
```sh
|
||||
podman-compose -f /srv/tranquil-pds/docker-compose.yml logs -f
|
||||
podman logs -f tranquil-pds-tranquil-pds-1
|
||||
podman logs -f tranquil-pds-frontend-1
|
||||
```
|
||||
|
||||
## Update Tranquil PDS
|
||||
|
||||
```sh
|
||||
cd /opt/tranquil-pds
|
||||
git pull
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
```
|
||||
|
||||
Debian:
|
||||
```bash
|
||||
systemctl restart tranquil-pds-app
|
||||
systemctl restart tranquil-pds-app tranquil-pds-frontend
|
||||
```
|
||||
|
||||
Alpine:
|
||||
```sh
|
||||
rc-service tranquil-pds restart
|
||||
```
|
||||
|
||||
## Backup Database
|
||||
|
||||
**Debian:**
|
||||
```bash
|
||||
podman exec tranquil-pds-db pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
**Alpine:**
|
||||
```sh
|
||||
podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(date +%Y%m%d).sql
|
||||
@@ -353,8 +489,12 @@ podman exec tranquil-pds-db-1 pg_dump -U tranquil_pds pds > /var/backups/pds-$(d
|
||||
|
||||
## Custom Homepage
|
||||
|
||||
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.
|
||||
The frontend container serves `homepage.html` as the landing page. To customize it, either:
|
||||
|
||||
1. Build a custom frontend image with your own `homepage.html`
|
||||
2. Mount a custom `homepage.html` into the frontend container
|
||||
|
||||
Example custom homepage:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
+173
-26
@@ -1,24 +1,33 @@
|
||||
# Tranquil PDS Production Installation on Debian
|
||||
|
||||
This guide covers installing Tranquil PDS on Debian 13.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A VPS with at least 2GB RAM and 20GB disk
|
||||
- A domain name pointing to your server's IP
|
||||
- A wildcard TLS certificate for `*.pds.example.com` (user handles are served as subdomains)
|
||||
- Root or sudo access
|
||||
## 1. System Setup
|
||||
|
||||
## System Setup
|
||||
|
||||
```bash
|
||||
apt update && apt upgrade -y
|
||||
apt install -y curl git build-essential pkg-config libssl-dev
|
||||
```
|
||||
## 2. Install Rust
|
||||
|
||||
## Install Rust
|
||||
|
||||
```bash
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
source ~/.cargo/env
|
||||
rustup default stable
|
||||
```
|
||||
|
||||
This installs the latest stable Rust.
|
||||
## 3. Install postgres
|
||||
|
||||
## Install postgres
|
||||
|
||||
```bash
|
||||
apt install -y postgresql postgresql-contrib
|
||||
systemctl enable postgresql
|
||||
@@ -27,7 +36,9 @@ sudo -u postgres psql -c "CREATE USER tranquil_pds WITH PASSWORD 'your-secure-pa
|
||||
sudo -u postgres psql -c "CREATE DATABASE pds OWNER tranquil_pds;"
|
||||
sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE pds TO tranquil_pds;"
|
||||
```
|
||||
## 4. Install minio
|
||||
|
||||
## Install minio
|
||||
|
||||
```bash
|
||||
curl -O https://dl.min.io/server/minio/release/linux-amd64/minio
|
||||
chmod +x minio
|
||||
@@ -59,6 +70,7 @@ systemctl daemon-reload
|
||||
systemctl enable minio
|
||||
systemctl start minio
|
||||
```
|
||||
|
||||
Create the buckets (wait a few seconds for minio to start):
|
||||
```bash
|
||||
curl -O https://dl.min.io/client/mc/release/linux-amd64/mc
|
||||
@@ -68,51 +80,70 @@ mc alias set local http://localhost:9000 minioadmin your-minio-password
|
||||
mc mb local/pds-blobs
|
||||
mc mb local/pds-backups
|
||||
```
|
||||
## 5. Install valkey
|
||||
|
||||
## Install valkey
|
||||
|
||||
```bash
|
||||
apt install -y valkey
|
||||
systemctl enable valkey-server
|
||||
systemctl start valkey-server
|
||||
```
|
||||
## 6. Install deno (for frontend build)
|
||||
|
||||
## Install deno (for frontend build)
|
||||
|
||||
```bash
|
||||
curl -fsSL https://deno.land/install.sh | sh
|
||||
export PATH="$HOME/.deno/bin:$PATH"
|
||||
echo 'export PATH="$HOME/.deno/bin:$PATH"' >> ~/.bashrc
|
||||
```
|
||||
## 7. Clone and Build Tranquil PDS
|
||||
|
||||
## Clone and Build Tranquil PDS
|
||||
|
||||
```bash
|
||||
cd /opt
|
||||
git clone https://tangled.org/lewis.moe/bspds-sandbox tranquil-pds
|
||||
git clone https://tangled.org/tranquil.farm/tranquil-pds tranquil-pds
|
||||
cd tranquil-pds
|
||||
cd frontend
|
||||
deno task build
|
||||
cd ..
|
||||
cargo build --release
|
||||
```
|
||||
## 8. Install sqlx-cli and Run Migrations
|
||||
|
||||
## Install sqlx-cli and Run Migrations
|
||||
|
||||
```bash
|
||||
cargo install sqlx-cli --no-default-features --features postgres
|
||||
export DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds"
|
||||
sqlx migrate run
|
||||
```
|
||||
## 9. Configure Tranquil PDS
|
||||
|
||||
## Configure Tranquil PDS
|
||||
|
||||
```bash
|
||||
mkdir -p /etc/tranquil-pds
|
||||
cp /opt/tranquil-pds/.env.example /etc/tranquil-pds/tranquil-pds.env
|
||||
chmod 600 /etc/tranquil-pds/tranquil-pds.env
|
||||
```
|
||||
|
||||
Edit `/etc/tranquil-pds/tranquil-pds.env` and fill in your values. Generate secrets with:
|
||||
```bash
|
||||
openssl rand -base64 48
|
||||
```
|
||||
## 10. Create Systemd Service
|
||||
|
||||
## Install Frontend Files
|
||||
|
||||
```bash
|
||||
mkdir -p /var/www/tranquil-pds
|
||||
cp -r /opt/tranquil-pds/frontend/dist/* /var/www/tranquil-pds/
|
||||
chown -R www-data:www-data /var/www/tranquil-pds
|
||||
```
|
||||
|
||||
## Create Systemd Service
|
||||
|
||||
```bash
|
||||
useradd -r -s /sbin/nologin tranquil-pds
|
||||
cp /opt/tranquil-pds/target/release/tranquil-pds /usr/local/bin/
|
||||
mkdir -p /var/lib/tranquil-pds
|
||||
cp -r /opt/tranquil-pds/frontend/dist /var/lib/tranquil-pds/frontend
|
||||
chown -R tranquil-pds:tranquil-pds /var/lib/tranquil-pds
|
||||
|
||||
cat > /etc/systemd/system/tranquil-pds.service << 'EOF'
|
||||
[Unit]
|
||||
Description=Tranquil PDS - AT Protocol PDS
|
||||
@@ -122,26 +153,52 @@ Type=simple
|
||||
User=tranquil-pds
|
||||
Group=tranquil-pds
|
||||
EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env
|
||||
Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend
|
||||
ExecStart=/usr/local/bin/tranquil-pds
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
systemctl daemon-reload
|
||||
systemctl enable tranquil-pds
|
||||
systemctl start tranquil-pds
|
||||
```
|
||||
## 11. Install and Configure nginx
|
||||
|
||||
## Install and Configure nginx
|
||||
|
||||
```bash
|
||||
apt install -y nginx certbot python3-certbot-nginx
|
||||
|
||||
cat > /etc/nginx/sites-available/tranquil-pds << 'EOF'
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name pds.example.com;
|
||||
server_name pds.example.com *.pds.example.com;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name pds.example.com *.pds.example.com;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/pds.example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/pds.example.com/privkey.pem;
|
||||
|
||||
client_max_body_size 10G;
|
||||
|
||||
root /var/www/tranquil-pds;
|
||||
|
||||
location /xrpc/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
@@ -151,16 +208,94 @@ server {
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location /oauth/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300;
|
||||
proxy_send_timeout 300;
|
||||
}
|
||||
|
||||
location /.well-known/ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /metrics {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /logo {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ~ ^/u/[^/]+/did\.json$ {
|
||||
proxy_pass http://127.0.0.1:3000;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /homepage.html /index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
ln -s /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/
|
||||
|
||||
ln -sf /etc/nginx/sites-available/tranquil-pds /etc/nginx/sites-enabled/
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
mkdir -p /var/www/acme
|
||||
nginx -t
|
||||
systemctl reload nginx
|
||||
```
|
||||
## 12. Obtain Wildcard SSL Certificate
|
||||
User handles are served as subdomains (e.g., `alice.pds.example.com`), so you need a wildcard certificate.
|
||||
|
||||
## Obtain Wildcard SSL Certificate
|
||||
|
||||
User handles are served as subdomains (eg., `alice.pds.example.com`), so you need a wildcard certificate.
|
||||
|
||||
Wildcard certs require DNS-01 validation. If your DNS provider has a certbot plugin:
|
||||
```bash
|
||||
@@ -175,10 +310,16 @@ For manual DNS validation (works with any provider):
|
||||
certbot certonly --manual --preferred-challenges dns \
|
||||
-d pds.example.com -d '*.pds.example.com'
|
||||
```
|
||||
|
||||
Follow the prompts to add TXT records to your DNS. Note: manual mode doesn't auto-renew.
|
||||
|
||||
After obtaining the cert, update nginx to use it and reload.
|
||||
## 13. Configure Firewall
|
||||
After obtaining the cert, reload nginx:
|
||||
```bash
|
||||
systemctl reload nginx
|
||||
```
|
||||
|
||||
## Configure Firewall
|
||||
|
||||
```bash
|
||||
apt install -y ufw
|
||||
ufw allow ssh
|
||||
@@ -186,17 +327,22 @@ ufw allow 80/tcp
|
||||
ufw allow 443/tcp
|
||||
ufw enable
|
||||
```
|
||||
## 14. Verify Installation
|
||||
|
||||
## Verify Installation
|
||||
|
||||
```bash
|
||||
systemctl status tranquil-pds
|
||||
curl -s https://pds.example.com/xrpc/_health | jq
|
||||
curl -s https://pds.example.com/.well-known/atproto-did
|
||||
```
|
||||
|
||||
## Maintenance
|
||||
|
||||
View logs:
|
||||
```bash
|
||||
journalctl -u tranquil-pds -f
|
||||
```
|
||||
|
||||
Update Tranquil PDS:
|
||||
```bash
|
||||
cd /opt/tranquil-pds
|
||||
@@ -205,10 +351,11 @@ cd frontend && deno task build && cd ..
|
||||
cargo build --release
|
||||
systemctl stop tranquil-pds
|
||||
cp target/release/tranquil-pds /usr/local/bin/
|
||||
cp -r frontend/dist /var/lib/tranquil-pds/frontend
|
||||
cp -r frontend/dist/* /var/www/tranquil-pds/
|
||||
DATABASE_URL="postgres://tranquil_pds:your-secure-password@localhost:5432/pds" sqlx migrate run
|
||||
systemctl start tranquil-pds
|
||||
```
|
||||
|
||||
Backup database:
|
||||
```bash
|
||||
sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql
|
||||
@@ -216,10 +363,10 @@ sudo -u postgres pg_dump pds > /var/backups/pds-$(date +%Y%m%d).sql
|
||||
|
||||
## Custom Homepage
|
||||
|
||||
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.
|
||||
Drop a `homepage.html` in `/var/www/tranquil-pds/` and it becomes your landing page. Account dashboard is at `/app/` so you won't break anything.
|
||||
|
||||
```bash
|
||||
cat > /var/lib/tranquil-pds/frontend/homepage.html << 'EOF'
|
||||
cat > /var/www/tranquil-pds/homepage.html << 'EOF'
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
FROM denoland/deno:alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . ./
|
||||
RUN deno task build
|
||||
|
||||
FROM nginx:1.29-alpine
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
@@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 8080;
|
||||
listen [::]:8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
default_type application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
try_files /oauth/client-metadata.json =404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /homepage.html /index.html;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
default_type application/json;
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
try_files /oauth/client-metadata.json =404;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
location = / {
|
||||
try_files /homepage.html /index.html;
|
||||
}
|
||||
|
||||
location /app/ {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
@@ -440,7 +440,7 @@
|
||||
<a href="/app/register" class="btn primary" id="heroPrimary"
|
||||
>Join This Server</a>
|
||||
<a
|
||||
href="https://tangled.org/lewis.moe/bspds-sandbox"
|
||||
href="https://tangled.org/tranquil.farm/tranquil-pds"
|
||||
class="btn secondary"
|
||||
id="heroSecondary"
|
||||
target="_blank"
|
||||
@@ -461,9 +461,9 @@
|
||||
<div class="feature">
|
||||
<h3>Real security</h3>
|
||||
<p>
|
||||
Sign in with passkeys, add two-factor authentication, set up
|
||||
backup codes, and mark devices you trust. Your account stays
|
||||
yours.
|
||||
Sign in with passkeys or SSO, add two-factor authentication,
|
||||
set up backup codes, and mark devices you trust. Your account
|
||||
stays yours.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -546,7 +546,7 @@
|
||||
<a href="/app/register" class="btn primary" id="footerPrimary"
|
||||
>Join This Server</a>
|
||||
<a
|
||||
href="https://tangled.org/lewis.moe/bspds-sandbox"
|
||||
href="https://tangled.org/tranquil.farm/tranquil-pds"
|
||||
class="btn secondary"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"client_id": "https://__PDS_HOSTNAME__/oauth/client-metadata.json",
|
||||
"client_name": "PDS Account Manager",
|
||||
"client_uri": "https://__PDS_HOSTNAME__",
|
||||
"redirect_uris": [
|
||||
"https://__PDS_HOSTNAME__/app/",
|
||||
"https://__PDS_HOSTNAME__/app/migrate"
|
||||
],
|
||||
"grant_types": ["authorization_code", "refresh_token"],
|
||||
"response_types": ["code"],
|
||||
"scope": "atproto transition:generic repo:* blob:*/* rpc:* rpc:com.atproto.server.createAccount?aud=* account:*?action=manage identity:*",
|
||||
"token_endpoint_auth_method": "none",
|
||||
"application_type": "web",
|
||||
"dpop_bound_access_tokens": true
|
||||
}
|
||||
@@ -160,7 +160,7 @@
|
||||
"signal": "Signal",
|
||||
"signalNumber": "Signal Phone Number",
|
||||
"signalNumberPlaceholder": "+1234567890",
|
||||
"signalNumberHint": "Include country code (e.g., +1 for US)",
|
||||
"signalNumberHint": "Include country code (eg., +1 for US)",
|
||||
"notConfigured": "not configured",
|
||||
"inviteCode": "Invite Code",
|
||||
"inviteCodePlaceholder": "Enter your invite code",
|
||||
@@ -263,7 +263,7 @@
|
||||
"saveFailed": "Failed to save DID document",
|
||||
"loadFailed": "Failed to load DID document",
|
||||
"invalidMultibase": "Public key must be a valid multibase string starting with 'z'",
|
||||
"invalidHandle": "Handle must be an at:// URI (e.g., at://handle.example.com)",
|
||||
"invalidHandle": "Handle must be an at:// URI (eg., at://handle.example.com)",
|
||||
"helpTitle": "What is this?",
|
||||
"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)."
|
||||
},
|
||||
@@ -385,7 +385,7 @@
|
||||
"title": "App Passwords",
|
||||
"description": "App passwords let you sign in to third-party apps without giving them your main password. Each app password can be revoked individually.",
|
||||
"createNew": "Create New App Password",
|
||||
"appNamePlaceholder": "App name (e.g., Graysky, Skeets)",
|
||||
"appNamePlaceholder": "App name (eg., Graysky, Skeets)",
|
||||
"created": "App Password Created",
|
||||
"createdMessage": "Copy this password now. You won't be able to see it again.",
|
||||
"yourPasswords": "Your App Passwords",
|
||||
@@ -452,7 +452,7 @@
|
||||
"adding": "Adding...",
|
||||
"noPasskeys": "No passkeys registered",
|
||||
"passkeyName": "Passkey name",
|
||||
"passkeyNamePlaceholder": "e.g., MacBook Pro, iPhone",
|
||||
"passkeyNamePlaceholder": "eg., MacBook Pro, iPhone",
|
||||
"register": "Register",
|
||||
"registering": "Registering...",
|
||||
"rename": "Rename",
|
||||
@@ -1007,7 +1007,7 @@
|
||||
"infoAppAccess": "Using third-party apps",
|
||||
"infoAppAccessDesc": "After creating your account, you will receive an app password. Use this to sign in to Bluesky apps and other AT Protocol clients.",
|
||||
"passkeyNameLabel": "Passkey Name (optional)",
|
||||
"passkeyNamePlaceholder": "e.g., MacBook Touch ID",
|
||||
"passkeyNamePlaceholder": "eg., MacBook Touch ID",
|
||||
"passkeyNameHint": "A friendly name to identify this passkey",
|
||||
"passkeyPrompt": "Click the button below to create your passkey. You'll be prompted to use:",
|
||||
"passkeyPromptBullet1": "Touch ID or Face ID",
|
||||
@@ -1279,7 +1279,7 @@
|
||||
"checkingAvailability": "Checking availability...",
|
||||
"handleAvailable": "Handle is available!",
|
||||
"handleTaken": "Handle is already taken",
|
||||
"handleHint": "You can also use your own domain by entering the full handle (e.g., alice.mydomain.com)",
|
||||
"handleHint": "You can also use your own domain by entering the full handle (eg., alice.mydomain.com)",
|
||||
"email": "Email Address",
|
||||
"authMethod": "Authentication Method",
|
||||
"authPassword": "Password",
|
||||
@@ -1320,7 +1320,7 @@
|
||||
"title": "Set Up Your Passkey",
|
||||
"desc": "Your email has been verified. Now set up your passkey for secure, passwordless login.",
|
||||
"nameLabel": "Passkey Name (optional)",
|
||||
"namePlaceholder": "e.g., MacBook Pro, iPhone",
|
||||
"namePlaceholder": "eg., MacBook Pro, iPhone",
|
||||
"nameHint": "A friendly name to identify this passkey",
|
||||
"instructions": "Click the button below to register your passkey. Your device will prompt you to use biometrics (fingerprint, Face ID) or a security key.",
|
||||
"register": "Register Passkey",
|
||||
@@ -1418,7 +1418,7 @@
|
||||
"title": "Enter Your DID",
|
||||
"desc": "Enter the DID of the account you want to restore.",
|
||||
"label": "Your DID",
|
||||
"hint": "Your decentralized identifier (e.g., did:plc:abc123...)"
|
||||
"hint": "Your decentralized identifier (eg., did:plc:abc123...)"
|
||||
},
|
||||
"uploadCar": {
|
||||
"title": "Upload Repository Backup",
|
||||
|
||||
@@ -23,7 +23,7 @@ test-unit:
|
||||
SQLX_OFFLINE=true cargo test --test dpop_unit --test validation_edge_cases --test scope_edge_cases
|
||||
|
||||
test-auth:
|
||||
./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
|
||||
./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
|
||||
|
||||
test-admin:
|
||||
./scripts/run-tests.sh --test admin_email --test admin_invite --test admin_moderation --test admin_search --test admin_stats
|
||||
@@ -81,8 +81,9 @@ podman-down:
|
||||
podman compose down
|
||||
podman-logs:
|
||||
podman compose logs -f
|
||||
podman-build:
|
||||
podman compose build
|
||||
container-build:
|
||||
podman build -t tranquil-pds:latest .
|
||||
podman build -t tranquil-pds-frontend:latest ./frontend
|
||||
|
||||
frontend-dev:
|
||||
. ~/.deno/env && cd frontend && deno task dev
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
worker_connections 4096;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time uct="$upstream_connect_time" '
|
||||
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript
|
||||
application/xml application/xml+rss text/javascript application/activity+json;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
|
||||
upstream backend {
|
||||
server tranquil-pds:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
|
||||
upstream frontend {
|
||||
server frontend:80;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
listen [::]:443 ssl;
|
||||
http2 on;
|
||||
server_name _;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/privkey.pem;
|
||||
|
||||
client_max_body_size 10G;
|
||||
|
||||
location /xrpc/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
|
||||
location = /oauth/client-metadata.json {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header Accept-Encoding "";
|
||||
sub_filter_once off;
|
||||
sub_filter_types application/json;
|
||||
sub_filter '__PDS_HOSTNAME__' $host;
|
||||
}
|
||||
|
||||
location /oauth/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 300;
|
||||
proxy_send_timeout 300;
|
||||
}
|
||||
|
||||
location /.well-known/ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /metrics {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /health {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /robots.txt {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location = /logo {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
location ~ ^/u/[^/]+/did\.json$ {
|
||||
proxy_pass http://backend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location / {
|
||||
proxy_pass http://frontend;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
worker_processes auto;
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
events {
|
||||
worker_connections 4096;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for" '
|
||||
'rt=$request_time uct="$upstream_connect_time" '
|
||||
'uht="$upstream_header_time" urt="$upstream_response_time"';
|
||||
access_log /var/log/nginx/access.log main;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css text/xml application/json application/javascript
|
||||
application/xml application/xml+rss text/javascript application/activity+json;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
|
||||
ssl_prefer_server_ciphers off;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 1d;
|
||||
ssl_session_tickets off;
|
||||
ssl_stapling on;
|
||||
ssl_stapling_verify on;
|
||||
upstream tranquil-pds {
|
||||
server tranquil-pds:3000;
|
||||
keepalive 32;
|
||||
}
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
location /.well-known/acme-challenge/ {
|
||||
root /var/www/acme;
|
||||
}
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
listen [::]:443 ssl http2;
|
||||
server_name _;
|
||||
ssl_certificate /etc/nginx/certs/live/${PDS_HOSTNAME}/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/live/${PDS_HOSTNAME}/privkey.pem;
|
||||
client_max_body_size 10G;
|
||||
location / {
|
||||
proxy_pass http://tranquil-pds;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
proxy_buffering off;
|
||||
proxy_request_buffering off;
|
||||
}
|
||||
location /xrpc/com.atproto.sync.subscribeRepos {
|
||||
proxy_pass http://tranquil-pds;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
proxy_send_timeout 86400;
|
||||
proxy_buffering off;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,5 +9,5 @@ scrape_configs:
|
||||
|
||||
- job_name: 'tranquil-pds'
|
||||
static_configs:
|
||||
- targets: ['app:3000']
|
||||
- targets: ['tranquil-pds:3000']
|
||||
metrics_path: /metrics
|
||||
@@ -117,7 +117,7 @@ echo " IPv4: ${IPV4}"
|
||||
[[ -n "$IPV6" ]] && echo " IPv6: ${IPV6}"
|
||||
echo ""
|
||||
|
||||
read -p "Enter your PDS domain (e.g., pds.example.com): " PDS_DOMAIN
|
||||
read -p "Enter your PDS domain (eg., pds.example.com): " PDS_DOMAIN
|
||||
if [[ -z "$PDS_DOMAIN" ]]; then
|
||||
log_error "Domain cannot be empty"
|
||||
exit 1
|
||||
@@ -296,7 +296,7 @@ fi
|
||||
|
||||
log_info "Cloning Tranquil PDS..."
|
||||
if [[ ! -d /opt/tranquil-pds ]]; then
|
||||
git clone https://tangled.org/lewis.moe/bspds-sandbox /opt/tranquil-pds
|
||||
git clone https://tangled.org/tranquil.farm/tranquil-pds /opt/tranquil-pds
|
||||
else
|
||||
cd /opt/tranquil-pds && git pull
|
||||
fi
|
||||
@@ -417,7 +417,6 @@ Type=simple
|
||||
User=tranquil-pds
|
||||
Group=tranquil-pds
|
||||
EnvironmentFile=/etc/tranquil-pds/tranquil-pds.env
|
||||
Environment=FRONTEND_DIR=/var/lib/tranquil-pds/frontend
|
||||
ExecStart=/usr/local/bin/tranquil-pds
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
@@ -479,7 +478,7 @@ log_success "Firewall configured"
|
||||
echo ""
|
||||
log_info "Obtaining wildcard SSL certificate..."
|
||||
echo ""
|
||||
echo "User handles are served as subdomains (e.g., alice.${PDS_DOMAIN}),"
|
||||
echo "User handles are served as subdomains (eg., alice.${PDS_DOMAIN}),"
|
||||
echo "so you need a wildcard certificate. This requires DNS validation."
|
||||
echo ""
|
||||
echo "You'll need to add a TXT record to your DNS when prompted."
|
||||
|
||||
@@ -48,15 +48,15 @@ start_infra() {
|
||||
--name "${CONTAINER_PREFIX}-minio" \
|
||||
-e MINIO_ROOT_USER=minioadmin \
|
||||
-e MINIO_ROOT_PASSWORD=minioadmin \
|
||||
-P \
|
||||
-p 9000 \
|
||||
--label tranquil_pds_test=true \
|
||||
minio/minio:latest server /data >/dev/null
|
||||
cgr.dev/chainguard/minio:latest server /data >/dev/null
|
||||
echo "Starting Valkey..."
|
||||
$CONTAINER_CMD run -d \
|
||||
--name "${CONTAINER_PREFIX}-valkey" \
|
||||
-P \
|
||||
--label tranquil_pds_test=true \
|
||||
valkey/valkey:8-alpine >/dev/null
|
||||
valkey/valkey:9-alpine >/dev/null
|
||||
echo "Waiting for services to be ready..."
|
||||
sleep 2
|
||||
PG_PORT=$($CONTAINER_CMD port "${CONTAINER_PREFIX}-postgres" 5432 | head -1 | cut -d: -f2)
|
||||
@@ -86,10 +86,10 @@ start_infra() {
|
||||
echo "Creating MinIO buckets..."
|
||||
$CONTAINER_CMD run --rm --network host \
|
||||
-e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \
|
||||
minio/mc:latest mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true
|
||||
cgr.dev/chainguard/minio-client:latest-dev mb minio/test-bucket --ignore-existing >/dev/null 2>&1 || true
|
||||
$CONTAINER_CMD run --rm --network host \
|
||||
-e MC_HOST_minio="http://minioadmin:minioadmin@127.0.0.1:${MINIO_PORT}" \
|
||||
minio/mc:latest mb minio/test-backups --ignore-existing >/dev/null 2>&1 || true
|
||||
cgr.dev/chainguard/minio-client:latest-dev mb minio/test-backups --ignore-existing >/dev/null 2>&1 || true
|
||||
cat > "$INFRA_FILE" << EOF
|
||||
export DATABASE_URL="postgres://postgres:postgres@127.0.0.1:${PG_PORT}/postgres"
|
||||
export TEST_DB_PORT="${PG_PORT}"
|
||||
|
||||
Reference in New Issue
Block a user