feat: docs tweaks & standalone frontend

This commit is contained in:
lewis
2026-01-18 16:41:41 +00:00
committed by Tangled
parent b3ec7feb96
commit 158628f1a7
31 changed files with 844 additions and 468 deletions
+2 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"]
+2 -2
View File
@@ -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
+5 -3
View File
@@ -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,
+2 -35
View File
@@ -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,
})
}
+1 -1
View File
@@ -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"
);
}
+92 -3
View File
@@ -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
+1 -6
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+9
View File
@@ -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
+38
View File
@@ -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;
}
}
+38
View File
@@ -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;
}
}
+5 -5
View File
@@ -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
}
+8 -8
View File
@@ -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",
+4 -3
View File
@@ -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
+170
View File
@@ -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;
}
}
}
-87
View File
@@ -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
+3 -4
View File
@@ -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."
+5 -5
View File
@@ -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}"