diff --git a/CHANGELOG.md b/CHANGELOG.md index f198b146..7f7c6771 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ## v1.8.12-alpha (2026-09-11) +- **Fresh IndeedHub installs no longer share a fleet-wide encryption root.** The API now generates a persistent per-node AES master secret and shares it with the media worker through the platform's protected secret environment. Existing nodes migrate the exact legacy value they are already using before any container can be recreated, preserving access to encrypted data; an unreadable or empty existing root fails safely instead of being silently replaced. The manifest path, retired fallback installer, and container repair script follow the same rule. + - **The Companion download advertises and re-announces the APK it actually serves.** The Discover banner and its install prompt now share the no-cache APK metadata, visibly report Companion 0.5.32 build 52, and remember dismissal per Android build rather than forever, so an existing browser gets one useful update prompt when the APK changes. The ISO gate reads the expected version from the Android build itself instead of accepting the stale 0.5.28 payload. - **GitWorkshop's dependency audit is clean.** The pinned upstream client keeps its separately reviewable Archipelago integration patch and now applies a deterministic dependency patch: safe lock refreshes plus targeted `fflate`, React Router, and Vitest upgrades remove all ten production advisories and all eight development advisories. A clean install reports zero vulnerabilities; type-check, all 152 upstream unit tests, and the exact Archipelago subpath build pass. diff --git a/apps/indeedhub-api/manifest.yml b/apps/indeedhub-api/manifest.yml index 1c6ad706..05b1fbb5 100644 --- a/apps/indeedhub-api/manifest.yml +++ b/apps/indeedhub-api/manifest.yml @@ -19,14 +19,15 @@ app: pull_policy: if-not-present network: indeedhub-net network_aliases: [api] - # The JWT signing secret is owned here (no backend container owns it); the - # db + minio passwords are owned by indeedhub-postgres / indeedhub-minio and - # only consumed here. ensure_generated_secrets no-ops when a file already - # exists, so live values on .228 are preserved (postgres pw is fixed at - # PGDATA init — regenerating would lock the API out). + # The JWT signing secret and stable envelope-encryption root are owned here; + # the db + minio passwords are owned by indeedhub-postgres / indeedhub-minio + # and only consumed here. Existing nodes migrate the legacy AES value into + # the secret file once, while fresh nodes receive a unique per-node value. generated_secrets: - name: indeedhub-jwt kind: hex32 + - name: indeedhub-aes-master + kind: hex16 secret_env: - key: DATABASE_PASSWORD secret_file: indeedhub-db-password @@ -34,6 +35,8 @@ app: secret_file: indeedhub-minio-password - key: NOSTR_JWT_SECRET secret_file: indeedhub-jwt + - key: AES_MASTER_SECRET + secret_file: indeedhub-aes-master dependencies: - app_id: indeedhub-postgres @@ -67,9 +70,6 @@ app: - S3_PRIVATE_BUCKET_NAME=indeedhub-private - S3_PUBLIC_BUCKET_URL=/storage - NOSTR_JWT_EXPIRES_IN=7d - # Fixed across the fleet (envelope-encryption master key baked by the legacy - # installer); not node-specific, so a plain env literal, not a secret. - - AES_MASTER_SECRET=0123456789abcdef0123456789abcdef - ENVIRONMENT=production health_check: diff --git a/apps/indeedhub-ffmpeg/manifest.yml b/apps/indeedhub-ffmpeg/manifest.yml index 35c72cd2..89a028e0 100644 --- a/apps/indeedhub-ffmpeg/manifest.yml +++ b/apps/indeedhub-ffmpeg/manifest.yml @@ -22,6 +22,8 @@ app: secret_file: indeedhub-db-password - key: AWS_SECRET_KEY secret_file: indeedhub-minio-password + - key: AES_MASTER_SECRET + secret_file: indeedhub-aes-master dependencies: - app_id: indeedhub-api @@ -51,4 +53,3 @@ app: - S3_PUBLIC_BUCKET_NAME=indeedhub-public - S3_PRIVATE_BUCKET_NAME=indeedhub-private - ENVIRONMENT=production - - AES_MASTER_SECRET=0123456789abcdef0123456789abcdef diff --git a/core/archipelago/src/api/rpc/package/stacks.rs b/core/archipelago/src/api/rpc/package/stacks.rs index fd503af5..00d813db 100644 --- a/core/archipelago/src/api/rpc/package/stacks.rs +++ b/core/archipelago/src/api/rpc/package/stacks.rs @@ -1559,6 +1559,31 @@ impl RpcHandler { self.set_install_progress("indeedhub", n_images, n_images) .await; + // The retired installer injected one fleet-wide AES root directly in + // the API/worker environment. Detect those consumers before removing + // anything, then persist the legacy value exactly once so an upgrade + // cannot orphan encrypted data. A genuinely fresh fallback install + // receives a random per-node root instead. + let mut had_existing_crypto_consumer = false; + for name in [ + "indeedhub-api", + "indeedhub-ffmpeg", + "indeedhub-build_api_1", + "indeedhub-build_ffmpeg-worker_1", + ] { + let status = + podman_stack_status(&["container", "exists", name], PODMAN_STACK_PROBE_TIMEOUT) + .await?; + had_existing_crypto_consumer |= status.success(); + } + let secrets_dir = self.config.data_dir.join("secrets"); + crate::container::secrets::ensure_indeedhub_aes_master_secret( + &secrets_dir, + had_existing_crypto_consumer, + ) + .context("preparing IndeedHub encryption root")?; + let aes_master = crate::container::secrets::indeedhub_aes_master_secret(&secrets_dir)?; + // Remove any leftover containers from a previous partial install (or // from the first-boot frontend stub that used to race the installer). // Without this, `podman run --name indeedhub` fails on name conflict @@ -1759,7 +1784,7 @@ impl RpcHandler { "-e".to_string(), "NOSTR_JWT_EXPIRES_IN=7d".to_string(), "-e".to_string(), - "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef".to_string(), + format!("AES_MASTER_SECRET={aes_master}"), "-e".to_string(), "ENVIRONMENT=production".to_string(), format!("{registry}/indeedhub-api:1.0.0"), @@ -1810,7 +1835,7 @@ impl RpcHandler { "-e".to_string(), "ENVIRONMENT=production".to_string(), "-e".to_string(), - "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef".to_string(), + format!("AES_MASTER_SECRET={aes_master}"), format!("{registry}/indeedhub-ffmpeg:1.0.0"), ], &tmp_env, diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index 1ea169ee..628db61f 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -3565,6 +3565,54 @@ impl ProdContainerOrchestrator { Ok(()) } + /// Materialise IndeedHub's AES root before the generic generated-secret + /// pass. Old installers injected one known value directly into the API and + /// worker environments, so an upgrade with either consumer still present + /// must persist that value before container drift can recreate them. With + /// no existing consumer this is a fresh install and receives random bytes. + async fn ensure_indeedhub_aes_master(&self, manifest: &AppManifest) -> Result<()> { + if manifest.app.id != "indeedhub-api" { + return Ok(()); + } + + let secret_path = self + .secrets_dir + .join(crate::container::secrets::INDEEDHUB_AES_SECRET_NAME); + let preserve_legacy = if secret_path.exists() { + // The secret helper validates the existing file and, critically, + // refuses to replace a damaged encryption root. + false + } else { + let consumers = [ + "indeedhub-api", + "indeedhub-ffmpeg", + "indeedhub-build_api_1", + "indeedhub-build_ffmpeg-worker_1", + ]; + self.runtime + .list_containers() + .await + .context("detecting an existing IndeedHub encryption-key consumer")? + .iter() + .any(|container| { + let name = container.name.trim_start_matches('/'); + consumers.contains(&name) + }) + }; + + if crate::container::secrets::ensure_indeedhub_aes_master_secret( + &self.secrets_dir, + preserve_legacy, + )? { + tracing::info!( + app = "indeedhub-api", + path = %secret_path.display(), + "Persisted the legacy IndeedHub encryption root for upgrade compatibility" + ); + } + Ok(()) + } + async fn resolve_dynamic_env(&self, manifest: &mut AppManifest) -> Result<()> { // Idempotency guard: partitioning already ran on this instance. // Re-running would re-taint against an environment that no longer @@ -3573,6 +3621,11 @@ impl ProdContainerOrchestrator { if !manifest.app.container.secret_env_refs.is_empty() { return Ok(()); } + // IndeedHub's data-encryption root needs an upgrade-aware first pass: + // generic generation alone would replace the fleet-wide legacy value + // and make previously encrypted data unreadable. + self.ensure_indeedhub_aes_master(manifest).await?; + // Materialise any manifest-declared generated secrets before they're // read below. This is the single chokepoint every install/reconcile // path funnels through, so an app's secrets exist by the time its @@ -5627,6 +5680,52 @@ app: "app:\n id: fedimint-gateway\n name: Fedimint Gateway\n version: 0.10.0\n container:\n image: x:1\n generated_secrets:\n - name: fedimint-gateway-hash\n kind: bcrypt\n secret_env:\n - key: FEDI_HASH\n secret_file: fedimint-gateway-hash\n" } + fn indeedhub_api_manifest_yaml() -> &'static str { + "app:\n id: indeedhub-api\n name: IndeedHub API\n version: 1.0.0\n container:\n image: x:1\n generated_secrets:\n - name: indeedhub-aes-master\n kind: hex16\n secret_env:\n - key: AES_MASTER_SECRET\n secret_file: indeedhub-aes-master\n" + } + + #[tokio::test] + async fn existing_indeedhub_consumer_gets_migration_compatible_root() { + let rt = Arc::new(MockRuntime::default()); + rt.set_state("indeedhub-api", ContainerState::Running); + let mut orch = orch_with(rt).await; + let tmp = tempfile::TempDir::new().unwrap(); + orch.set_secrets_dir(tmp.path().to_path_buf()); + + let mut manifest = AppManifest::parse(indeedhub_api_manifest_yaml()).unwrap(); + orch.resolve_dynamic_env(&mut manifest).await.unwrap(); + let resolved = manifest + .app + .container + .secret_env_refs + .iter() + .find(|entry| entry.env_key == "AES_MASTER_SECRET") + .unwrap(); + assert_eq!(resolved.value.len(), 32); + assert!(tmp.path().join("indeedhub-aes-master").exists()); + assert!( + crate::container::secrets::ensure_indeedhub_aes_master_secret(tmp.path(), true).is_ok(), + "the migrated file remains valid and stable" + ); + } + + #[tokio::test] + async fn fresh_indeedhub_install_gets_random_root() { + let rt = Arc::new(MockRuntime::default()); + let mut orch = orch_with(rt).await; + let tmp = tempfile::TempDir::new().unwrap(); + orch.set_secrets_dir(tmp.path().to_path_buf()); + + let mut manifest = AppManifest::parse(indeedhub_api_manifest_yaml()).unwrap(); + orch.resolve_dynamic_env(&mut manifest).await.unwrap(); + let first = crate::container::secrets::indeedhub_aes_master_secret(tmp.path()).unwrap(); + + let other = tempfile::TempDir::new().unwrap(); + crate::container::secrets::ensure_indeedhub_aes_master_secret(other.path(), false).unwrap(); + let second = crate::container::secrets::indeedhub_aes_master_secret(other.path()).unwrap(); + assert_ne!(first, second, "fresh installs must receive per-node roots"); + } + /// FED-07. Rotating a compromised credential leaves the RUNNING container /// holding the old value, so the rotation must flag the app for recreate. /// Without the flag the drift check skips it as restart-sensitive and the diff --git a/core/archipelago/src/container/secrets.rs b/core/archipelago/src/container/secrets.rs index 86d99b70..517d087b 100644 --- a/core/archipelago/src/container/secrets.rs +++ b/core/archipelago/src/container/secrets.rs @@ -140,6 +140,79 @@ fn random_base64(bytes: usize) -> String { /// daemon read `fedimint-gateway-hash`). pub const GATEWAY_HASH_SECRET_NAME: &str = "fedimint-gateway-hash"; +/// Canonical filename for IndeedHub's envelope-encryption root. API and media +/// worker must receive the same stable value: changing it after data has been +/// encrypted can make that data unreadable. +pub const INDEEDHUB_AES_SECRET_NAME: &str = "indeedhub-aes-master"; + +/// The fleet-wide value used by the legacy IndeedHub installers. It remains +/// here only for the one-way migration of an already-installed stack: those +/// nodes must persist the value they have been using before the manifest +/// starts reading it from a file. Fresh installs must never receive it. +const KNOWN_LEGACY_INDEEDHUB_AES_MASTER: &str = "0123456789abcdef0123456789abcdef"; + +/// Ensure IndeedHub has a stable encryption root. +/// +/// `preserve_legacy` is true only when an API/worker container already exists, +/// proving this is an upgrade from the installer that shipped the known legacy +/// value. In that case we persist that value once so recreating the containers +/// does not orphan encrypted data. A fresh installation gets 16 random bytes +/// encoded as 32 hex characters. +/// +/// Unlike ordinary generated credentials, an existing-but-empty or unreadable +/// encryption root is never self-healed by rotation: replacement could destroy +/// access to data, so this fails loudly and leaves the file untouched. +/// Returns true only when the legacy migration value was written. +pub fn ensure_indeedhub_aes_master_secret( + secrets_dir: &Path, + preserve_legacy: bool, +) -> Result { + fs::create_dir_all(secrets_dir) + .with_context(|| format!("creating secrets dir {}", secrets_dir.display()))?; + let path = secrets_dir.join(INDEEDHUB_AES_SECRET_NAME); + + if path.exists() { + let value = fs::read_to_string(&path).with_context(|| { + format!( + "reading IndeedHub encryption root {} (refusing to replace it)", + path.display() + ) + })?; + if value.trim().is_empty() { + anyhow::bail!( + "IndeedHub encryption root {} is empty; refusing to replace a potentially \ + data-bearing key", + path.display() + ); + } + return Ok(false); + } + + if preserve_legacy { + write_secret(&path, KNOWN_LEGACY_INDEEDHUB_AES_MASTER)?; + return Ok(true); + } + + let spec = GeneratedSecret { + name: INDEEDHUB_AES_SECRET_NAME.to_string(), + kind: SecretGenKind::Hex16, + }; + ensure_one(secrets_dir, &spec)?; + Ok(false) +} + +/// Read the stable IndeedHub encryption root after it has been materialised. +pub fn indeedhub_aes_master_secret(secrets_dir: &Path) -> Result { + let path = secrets_dir.join(INDEEDHUB_AES_SECRET_NAME); + let value = fs::read_to_string(&path) + .with_context(|| format!("reading IndeedHub encryption root {}", path.display()))?; + let value = value.trim(); + if value.is_empty() { + anyhow::bail!("IndeedHub encryption root {} is empty", path.display()); + } + Ok(value.to_string()) +} + /// Detection-only denylist of bcrypt hashes that shipped as hardcoded /// fallback credentials in this repository before FED-07. `t9YjjxkiktrlYvjajB /// /zgOMDnSNVg4HqrbDqh47u7Jf42whNdxNqC` was substituted for the Fedimint @@ -356,6 +429,63 @@ mod tests { ); } + #[test] + fn indeedhub_fresh_installs_get_distinct_per_node_encryption_roots() { + let dir_a = tempfile::tempdir().unwrap(); + let dir_b = tempfile::tempdir().unwrap(); + + assert!(!ensure_indeedhub_aes_master_secret(dir_a.path(), false).unwrap()); + assert!(!ensure_indeedhub_aes_master_secret(dir_b.path(), false).unwrap()); + let value_a = indeedhub_aes_master_secret(dir_a.path()).unwrap(); + let value_b = indeedhub_aes_master_secret(dir_b.path()).unwrap(); + + assert_eq!(value_a.len(), 32); + assert!(value_a.chars().all(|c| c.is_ascii_hexdigit())); + assert_ne!(value_a, KNOWN_LEGACY_INDEEDHUB_AES_MASTER); + assert_ne!(value_a, value_b, "fresh nodes must not share an AES root"); + let mode = std::fs::metadata(dir_a.path().join(INDEEDHUB_AES_SECRET_NAME)) + .unwrap() + .permissions() + .mode() + & 0o777; + assert_eq!(mode, 0o600); + } + + #[test] + fn indeedhub_existing_install_persists_legacy_root_once() { + let dir = tempfile::tempdir().unwrap(); + assert!(ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap()); + assert_eq!( + indeedhub_aes_master_secret(dir.path()).unwrap(), + KNOWN_LEGACY_INDEEDHUB_AES_MASTER + ); + assert!( + !ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap(), + "a second migration pass must be a no-op" + ); + } + + #[test] + fn indeedhub_existing_unique_root_is_never_rotated() { + let dir = tempfile::tempdir().unwrap(); + ensure_indeedhub_aes_master_secret(dir.path(), false).unwrap(); + let before = indeedhub_aes_master_secret(dir.path()).unwrap(); + + assert!(!ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap()); + assert_eq!(before, indeedhub_aes_master_secret(dir.path()).unwrap()); + } + + #[test] + fn indeedhub_empty_root_fails_without_overwriting() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join(INDEEDHUB_AES_SECRET_NAME); + std::fs::write(&path, "").unwrap(); + + let err = ensure_indeedhub_aes_master_secret(dir.path(), true).unwrap_err(); + assert!(err.to_string().contains("refusing to replace")); + assert_eq!(std::fs::read(&path).unwrap(), b""); + } + #[test] fn gateway_credential_fresh_generation_verifies_and_is_0600() { let dir = tempfile::tempdir().unwrap(); diff --git a/releases/app-catalog.json b/releases/app-catalog.json index 333d42e6..d161c85e 100644 --- a/releases/app-catalog.json +++ b/releases/app-catalog.json @@ -1,37 +1,92 @@ { + "schema": 1, + "updated": "2026-09-11", "apps": { "adguardhome": { + "version": "v0.107.79", "image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79", "manifest": { "app": { + "id": "adguardhome", + "name": "AdGuard Home", + "version": "v0.107.79", + "upstream": { + "kind": "github", + "repo": "AdguardTeam/AdGuardHome" + }, + "description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.", "container": { "image": "source.archipelago-foundation.org/lfg2025/adguardhome:v0.107.79", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Network-wide ad and tracker blocking: a DNS server that filters every device on your LAN, with a web console for rules and client management.", + "resources": { + "memory_limit": "512Mi", + "disk_limit": "1Gi" + }, + "security": { + "capabilities": [ + "NET_BIND_SERVICE" + ], + "readonly_root": false, + "no_new_privileges": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 3030, + "container": 3000, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "open", + "auth_rationale": "AdGuard Home enforces its own admin login on the console, and the first-run wizard must answer before any account exists." + }, + { + "host": 53, + "container": 53, + "protocol": "udp", + "auth": "none", + "auth_rationale": "Plain DNS answers unauthenticated by protocol: resolvers and clients send queries directly; a login challenge would make DNS unreachable." + }, + { + "host": 53, + "container": 53, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "DNS-over-TCP fallback (truncated responses, zone transfers); same protocol-level requirement as the UDP port." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/adguardhome", + "target": "/opt/adguardhome", + "options": [ + "rw" + ] + } + ], "environment": [], "health_check": { + "type": "tcp", "endpoint": "localhost:3030", "interval": "30s", - "retries": 3, "timeout": "5s", - "type": "tcp" + "retries": 3 }, - "id": "adguardhome", "interfaces": { "main": { - "description": "AdGuard Home web console", "name": "Admin console", - "path": "/", + "description": "AdGuard Home web console", + "type": "ui", "port": 3030, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { @@ -39,116 +94,72 @@ "category": "networking", "repo": "https://github.com/AdguardTeam/AdGuardHome", "tier": "optional" - }, - "name": "AdGuard Home", - "ports": [ - { - "auth": "open", - "auth_rationale": "AdGuard Home enforces its own admin login on the console, and the first-run wizard must answer before any account exists.", - "bind": "127.0.0.1", - "container": 3000, - "host": 3030, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Plain DNS answers unauthenticated by protocol: resolvers and clients send queries directly; a login challenge would make DNS unreachable.", - "container": 53, - "host": 53, - "protocol": "udp" - }, - { - "auth": "none", - "auth_rationale": "DNS-over-TCP fallback (truncated responses, zone transfers); same protocol-level requirement as the UDP port.", - "container": 53, - "host": 53, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [ - "NET_BIND_SERVICE" - ], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "AdguardTeam/AdGuardHome" - }, - "version": "v0.107.79", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/adguardhome", - "target": "/opt/adguardhome", - "type": "bind" - } - ] + } } - }, - "version": "v0.107.79" + } }, "aiui": { + "version": "0.1.0", "manifest": { "app": { + "id": "aiui", + "name": "AI Assistant", + "version": "0.1.0", + "upstream": { + "kind": "internal" + }, + "description": "Conversational AI interface for Archipelago. Quarantined \u2014 communicates only via context broker.", + "internal": true, "container": { "image": "localhost/archipelago-aiui:latest", "pull_policy": "always" }, - "description": "Conversational AI interface for Archipelago. Quarantined — communicates only via context broker.", - "health_check": { - "endpoint": "http://localhost:80", - "interval": "60s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "aiui", - "internal": true, - "name": "AI Assistant", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 80, - "host": 5180, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 1, - "disk_limit": "1Gi", - "memory_limit": "512Mi" + "memory_limit": "512Mi", + "disk_limit": "1Gi" }, "security": { - "apparmor_profile": "aiui", "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, "readonly_root": true, + "no_new_privileges": true, + "user": 1000, "seccomp_profile": "default", - "user": 1000 + "network_policy": "isolated", + "apparmor_profile": "aiui" }, - "upstream": { - "kind": "internal" - }, - "version": "0.1.0" + "ports": [ + { + "host": 5180, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], + "health_check": { + "type": "http", + "endpoint": "http://localhost:80", + "path": "/", + "interval": "60s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "0.1.0" + } }, "alby-hub": { + "version": "1.23.0", "manifest": { "app": { + "id": "alby-hub", + "name": "Alby Hub", + "version": "1.23.0", + "upstream": { + "kind": "github", + "repo": "getAlby/hub" + }, + "description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect \u2014 one hub, every app pays through it.", "category": "money", "container": { "image": "source.archipelago-foundation.org/lfg2025/alby-hub:v1.24.0", @@ -159,96 +170,179 @@ "storage": "1Gi" } ], - "description": "Self-custodial Lightning wallet hub. Runs its own Lightning node on your Archipelago and connects your apps to it over Nostr Wallet Connect — one hub, every app pays through it.", + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "2Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "network_policy": "bridge" + }, + "ports": [ + { + "host": 8187, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/alby-hub", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "WORK_DIR=/data", "PORT=8080", "LOG_LEVEL=info" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8080", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 5 }, - "id": "alby-hub", "interfaces": { "main": { - "description": "Alby Hub wallet interface", "name": "Web UI", + "description": "Alby Hub wallet interface", + "type": "ui", "port": 8187, - "protocol": "http", - "type": "ui" + "protocol": "http" } }, "metadata": { + "icon": "/assets/img/app-icons/alby-hub.svg", + "repo": "https://github.com/getAlby/hub", + "tier": "optional", + "launch": { + "open_in_new_tab": false + }, "features": [ "Self-custodial Lightning node (LDK) with a friendly wallet UI", "Connect wallets and apps via Nostr Wallet Connect (NWC)", "Per-app budgets and isolated sub-wallets", "Works with the Alby browser extension and mobile app" - ], - "icon": "/assets/img/app-icons/alby-hub.svg", - "launch": { - "open_in_new_tab": false - }, - "repo": "https://github.com/getAlby/hub", - "tier": "optional" + ] + } + } + } + }, + "archipelago-source": { + "version": "0.4.0", + "manifest": { + "app": { + "id": "archipelago-source", + "name": "GitWorkshop", + "version": "0.4.0", + "upstream": { + "kind": "github", + "repo": "DanConwayDev/gitworkshop" }, - "name": "Alby Hub", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 8187, - "protocol": "tcp" + "description": "Get Archipelago's source, clone it with ngit, and contribute issues, patches, and reviews over Nostr using the upstream GitWorkshop client.", + "category": "development", + "container": { + "build": { + "context": "/opt/archipelago/docker/archipelago-source", + "dockerfile": "Dockerfile", + "tag": "localhost/archipelago-source:local" } - ], + }, "resources": { "cpu_limit": 1, - "disk_limit": "2Gi", - "memory_limit": "512Mi" + "memory_limit": "64Mi", + "disk_limit": "64Mi" }, "security": { "capabilities": [], - "network_policy": "bridge", + "readonly_root": true, "no_new_privileges": true, - "readonly_root": true + "network_policy": "host" }, - "upstream": { - "kind": "github", - "repo": "getAlby/hub" - }, - "version": "1.23.0", + "ports": [ + { + "host": 8337, + "container": 8337, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated", + "session_passthrough": true + } + ], "volumes": [ { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/alby-hub", - "target": "/data", - "type": "bind" + "type": "tmpfs", + "target": "/tmp", + "tmpfs_options": "rw,noexec,nosuid,size=16m,mode=1777" } - ] + ], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:8337", + "path": "/healthz", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "GitWorkshop", + "description": "NIP-34 repository browser, issues, pull requests, and review", + "type": "ui", + "port": 8337, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/gitworkshop-dc36db6.svg", + "author": "GitWorkshop contributors", + "repo": "https://github.com/DanConwayDev/gitworkshop", + "maintainer_npub": "npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg", + "tier": "optional", + "launch": { + "requires_host_frame": false + }, + "features": [ + "NIP-34 repository discovery and browsing", + "Bandwidth-efficient Git explorer over GRASP", + "Nostr issues, pull requests, and code review", + "NIP-07 extension and NIP-46 remote-signer support", + "Archipelago node identity through explicit signing consent" + ] + } } - }, - "version": "1.23.0" + } }, "archy-btcpay-db": { + "version": "15.17", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "none", - "sync_required": false + "id": "archy-btcpay-db", + "name": "BTCPay Postgres", + "version": "15.17", + "upstream": { + "kind": "dockerhub", + "repo": "library/postgres" }, + "description": "Postgres backend for BTCPay and NBXplorer.", "container": { - "data_uid": "100998:100998", "image": "source.archipelago-foundation.org/lfg2025/postgres:15.17", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", + "data_uid": "100998:100998", "secret_env": [ { "key": "POSTGRES_PASSWORD", @@ -261,24 +355,9 @@ "storage": "20Gi" } ], - "description": "Postgres backend for BTCPay and NBXplorer.", - "environment": [ - "POSTGRES_DB=btcpay", - "POSTGRES_USER=btcpay" - ], - "health_check": { - "endpoint": "localhost:5432", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "archy-btcpay-db", - "name": "BTCPay Postgres", - "ports": [], "resources": { - "disk_limit": "20Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "20Gi" }, "security": { "capabilities": [ @@ -288,40 +367,55 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/postgres" - }, - "version": "15.17", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/postgres-btcpay", "target": "/var/lib/postgresql/data", - "type": "bind" + "options": [ + "rw" + ] } - ] - } - }, - "version": "15.17" - }, - "archy-mempool-db": { - "manifest": { - "app": { + ], + "environment": [ + "POSTGRES_DB=btcpay", + "POSTGRES_USER=btcpay" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:5432", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, "bitcoin_integration": { "rpc_access": "none", "sync_required": false + } + } + } + }, + "archy-mempool-db": { + "version": "11.4.10", + "manifest": { + "app": { + "id": "archy-mempool-db", + "name": "Mempool MariaDB", + "version": "11.4.10", + "upstream": { + "kind": "dockerhub", + "repo": "library/mariadb" }, + "description": "MariaDB backend for the mempool explorer stack.", "container": { - "data_uid": "100998:100998", "image": "source.archipelago-foundation.org/lfg2025/mariadb:11.4.10", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", + "data_uid": "100998:100998", "secret_env": [ { "key": "MYSQL_PASSWORD", @@ -338,24 +432,9 @@ "storage": "20Gi" } ], - "description": "MariaDB backend for the mempool explorer stack.", - "environment": [ - "MYSQL_DATABASE=mempool", - "MYSQL_USER=mempool" - ], - "health_check": { - "endpoint": "localhost:3306", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "archy-mempool-db", - "name": "Mempool MariaDB", - "ports": [], "resources": { - "disk_limit": "20Gi", - "memory_limit": "512Mi" + "memory_limit": "512Mi", + "disk_limit": "20Gi" }, "security": { "capabilities": [ @@ -365,99 +444,114 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/mariadb" - }, - "version": "11.4.10", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/mysql-mempool", "target": "/var/lib/mysql", - "type": "bind" + "options": [ + "rw" + ] } - ] - } - }, - "version": "11.4.10" - }, - "archy-mempool-web": { - "manifest": { - "app": { + ], + "environment": [ + "MYSQL_DATABASE=mempool", + "MYSQL_USER=mempool" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:3306", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, "bitcoin_integration": { "rpc_access": "none", "sync_required": false + } + } + } + }, + "archy-mempool-web": { + "version": "3.0.1", + "manifest": { + "app": { + "id": "archy-mempool-web", + "name": "Mempool Web", + "version": "3.0.1", + "upstream": { + "kind": "github", + "repo": "mempool/mempool" }, + "description": "Frontend web UI for mempool explorer.", + "container_name": "mempool", "container": { "image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1", - "network": "archy-net", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "archy-net" }, - "container_name": "mempool", "dependencies": [ { "app_id": "mempool-api", "version": ">=3.0.0" } ], - "description": "Frontend web UI for mempool explorer.", - "environment": [ - "FRONTEND_HTTP_PORT=8080", - "BACKEND_MAINNET_HTTP_HOST=mempool-api" - ], - "health_check": { - "endpoint": "http://127.0.0.1:8080", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "archy-mempool-web", - "name": "Mempool Web", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 4080, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "512Mi" }, "security": { "capabilities": [], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "mempool/mempool" + "ports": [ + { + "host": 4080, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "environment": [ + "FRONTEND_HTTP_PORT=8080", + "BACKEND_MAINNET_HTTP_HOST=mempool-api" + ], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:8080", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 }, - "version": "3.0.1" + "bitcoin_integration": { + "rpc_access": "none", + "sync_required": false + } } - }, - "version": "3.0.1" + } }, "archy-nbxplorer": { + "version": "2.6.0", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "read-only", - "sync_required": true + "id": "archy-nbxplorer", + "name": "NBXplorer", + "version": "2.6.0", + "upstream": { + "kind": "github", + "repo": "dgarage/NBXplorer" }, + "description": "BTCPay blockchain indexer service.", "container": { "image": "source.archipelago-foundation.org/lfg2025/nbxplorer:2.6.0", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "NBXPLORER_BTCRPCPASSWORD", @@ -479,7 +573,34 @@ "version": ">=15.17" } ], - "description": "BTCPay blockchain indexer service.", + "resources": { + "memory_limit": "2Gi", + "disk_limit": "20Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 32838, + "container": 32838, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/nbxplorer", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "NBXPLORER_DATADIR=/data", "NBXPLORER_NETWORK=mainnet", @@ -492,307 +613,128 @@ "NBXPLORER_POSTGRES=Username=btcpay;Password=${BTCPAY_DB_PASS};Host=archy-btcpay-db;Port=5432;Database=nbxplorer" ], "health_check": { + "type": "http", "endpoint": "http://localhost:32838", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" + "retries": 5 }, - "id": "archy-nbxplorer", - "name": "NBXplorer", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 32838, - "host": 32838, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "dgarage/NBXplorer" - }, - "version": "2.6.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/nbxplorer", - "target": "/data", - "type": "bind" - } - ] + "bitcoin_integration": { + "rpc_access": "read-only", + "sync_required": true + } } - }, - "version": "2.6.0" + } }, "barkd": { + "version": "0.3.0", "manifest": { "app": { + "id": "barkd", + "name": "Ark Wallet", + "version": "0.3.0", + "upstream": { + "kind": "gitlab", + "repo": "ark-bitcoin/bark" + }, + "description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.", "container": { - "data_uid": "1000:1000", + "image": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0", + "pull_policy": "if-not-present", + "network": "archy-net", "generated_secrets": [ { - "kind": "hex32", - "name": "barkd-secret" + "name": "barkd-secret", + "kind": "hex32" } ], - "image": "source.archipelago-foundation.org/lfg2025/barkd:0.3.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BARKD_SECRET", "secret_file": "barkd-secret" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Ark protocol wallet daemon (barkd). Lets the node hold self-custodial off-chain bitcoin via an Ark server; the wallet talks to it over a local REST API. Signet by default while Ark matures.", + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "1Gi" + }, + "security": { + "readonly_root": true, + "network_policy": "bridge" + }, + "ports": [ + { + "host": 3535, + "container": 3535, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/barkd", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "BARKD_DATADIR=/data", "BARKD_BIND_HOST=0.0.0.0", "BARKD_BIND_PORT=3535" ], "health_check": { + "type": "tcp", "endpoint": "localhost:3535", "interval": "30s", - "retries": 3, "timeout": "5s", - "type": "tcp" - }, - "id": "barkd", - "name": "Ark Wallet", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 3535, - "host": 3535, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 1, - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "network_policy": "bridge", - "readonly_root": true - }, - "upstream": { - "kind": "gitlab", - "repo": "ark-bitcoin/bark" - }, - "version": "0.3.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/barkd", - "target": "/data", - "type": "bind" - } - ] + "retries": 3 + } } - }, - "version": "0.3.0" + } }, "bitcoin-core": { + "version": "latest", "manifest": { "app": { - "bitcoin_integration": { - "pruning_support": true, - "rpc_access": "admin", - "sync_required": true, - "testnet_support": false - }, - "container": { - "custom_args": [ - "BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then\n echo \"archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF\" >&2;\nfi; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi" - ], - "data_uid": "100101:100101", - "derived_env": [ - { - "key": "DISK_GB", - "template": "{{DISK_GB}}" - } - ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4", - "network": "archy-net", - "pull_policy": "if-not-present", - "secret_env": [ - { - "key": "BITCOIN_RPC_PASS", - "secret_file": "bitcoin-rpc-password" - }, - { - "key": "BITCOIN_RPC_TXRELAY_RPCAUTH", - "secret_file": "bitcoin-rpc-txrelay-rpcauth" - } - ] - }, - "container_name": "bitcoin-core", - "dependencies": [ - { - "storage": "500Gi" - } - ], - "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.", - "environment": [ - "BITCOIN_RPC_USER=archipelago" - ], - "health_check": { - "endpoint": "localhost:8332", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, "id": "bitcoin-core", "name": "Bitcoin Core", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 8332, - "host": 8332, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP.", - "container": 8333, - "host": 8333, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 0, - "disk_limit": "500Gi", - "memory_limit": "4Gi" - }, - "security": { - "capabilities": [ - "CHOWN", - "FOWNER", - "SETUID", - "SETGID", - "DAC_OVERRIDE" - ], - "network_policy": "isolated", - "readonly_root": false - }, + "version": "28.4.0", "upstream": { "kind": "github", "repo": "bitcoin/bitcoin" }, - "version": "28.4.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/bitcoin", - "target": "/home/bitcoin/.bitcoin", - "type": "bind" - } - ] - } - }, - "version": "latest", - "versions": [ - { - "default": true, - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:latest", - "version": "latest" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:31.0", - "version": "31.0" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:30.2", - "version": "30.2" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.3", - "version": "29.3" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.2", - "version": "29.2" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4", - "version": "28.4.0" - }, - { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:27.2", - "version": "27.2" - }, - { - "deprecated": true, - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:26.2", - "version": "26.2" - }, - { - "deprecated": true, - "image": "source.archipelago-foundation.org/lfg2025/bitcoin:25.2", - "version": "25.2" - } - ] - }, - "bitcoin-knots": { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210", - "manifest": { - "app": { - "bitcoin_integration": { - "pruning_support": true, - "rpc_access": "admin", - "sync_required": true, - "testnet_support": false - }, + "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.", + "container_name": "bitcoin-core", "container": { + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4", + "pull_policy": "if-not-present", + "network": "archy-net", + "entrypoint": [ + "sh", + "-lc" + ], "custom_args": [ - "BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then\n echo \"archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF\" >&2;\nfi; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi" + "BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then\n echo \"archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF\" >&2;\nfi; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=1024 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi" ], - "data_uid": "100101:100101", "derived_env": [ { "key": "DISK_GB", "template": "{{DISK_GB}}" } ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BITCOIN_RPC_PASS", @@ -802,47 +744,18 @@ "key": "BITCOIN_RPC_TXRELAY_RPCAUTH", "secret_file": "bitcoin-rpc-txrelay-rpcauth" } - ] + ], + "data_uid": "100101:100101" }, - "container_name": "bitcoin-knots", "dependencies": [ { "storage": "500Gi" } ], - "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.", - "environment": [ - "BITCOIN_RPC_USER=archipelago" - ], - "health_check": { - "endpoint": "localhost:8332", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "bitcoin-knots", - "name": "Bitcoin Knots", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 8332, - "host": 8332, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP.", - "container": 8333, - "host": 8333, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 0, - "disk_limit": "500Gi", - "memory_limit": "8Gi" + "memory_limit": "4Gi", + "disk_limit": "500Gi" }, "security": { "capabilities": [ @@ -852,51 +765,235 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, + "ports": [ + { + "host": 8332, + "container": 8332, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + }, + { + "host": 8333, + "container": 8333, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/bitcoin", + "target": "/home/bitcoin/.bitcoin", + "options": [ + "rw" + ] + } + ], + "environment": [ + "BITCOIN_RPC_USER=archipelago" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8332", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true, + "testnet_support": false, + "pruning_support": true + } + } + }, + "versions": [ + { + "version": "latest", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:latest", + "default": true + }, + { + "version": "31.0", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:31.0" + }, + { + "version": "30.2", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:30.2" + }, + { + "version": "29.3", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.3" + }, + { + "version": "29.2", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:29.2" + }, + { + "version": "28.4.0", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:28.4" + }, + { + "version": "27.2", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:27.2" + }, + { + "version": "26.2", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:26.2", + "deprecated": true + }, + { + "version": "25.2", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin:25.2", + "deprecated": true + } + ] + }, + "bitcoin-knots": { + "version": "29.3.knots20260210", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210", + "manifest": { + "app": { + "id": "bitcoin-knots", + "name": "Bitcoin Knots", + "version": "28.1.0", "upstream": { "kind": "github", "repo": "bitcoinknots/bitcoin" }, - "version": "28.1.0", + "description": "Full Bitcoin Knots node with dynamic prune/full-mode startup based on host disk.", + "container_name": "bitcoin-knots", + "container": { + "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210", + "pull_policy": "if-not-present", + "network": "archy-net", + "entrypoint": [ + "sh", + "-lc" + ], + "custom_args": [ + "BITCOIND=\"$(command -v bitcoind || true)\"; if [ -z \"$BITCOIND\" ]; then\n BITCOIND=\"$(find /opt -path '*/bin/bitcoind' -type f 2>/dev/null | sort | tail -n 1)\";\nfi; if [ -z \"$BITCOIND\" ]; then\n echo \"bitcoind not found in image\" >&2;\n exit 127;\nfi; RPC_USER=\"$(printenv BITCOIN_RPC_USER)\"; RPC_PASS=\"$(printenv BITCOIN_RPC_PASS)\"; RPC_CONF=\"/tmp/rpc.conf\"; umask 077; { echo \"rpcuser=$RPC_USER\"; echo \"rpcpassword=$RPC_PASS\"; } > \"$RPC_CONF\"; if [ -f /home/bitcoin/.bitcoin/bitcoin.conf ]; then\n echo \"archipelago: ignoring legacy datadir bitcoin.conf; RPC config comes from $RPC_CONF\" >&2;\nfi; RPC_TXRELAY_AUTH=\"$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)\"; DISK_GB_VALUE=\"$(printenv DISK_GB || true)\"; RPC_HEADROOM=\"-rpcthreads=16 -rpcworkqueue=256\"; RPC_TXRELAY_FLAGS=\"-rpcwhitelistdefault=0\"; if [ -n \"$RPC_TXRELAY_AUTH\" ]; then\n RPC_TXRELAY_FLAGS=\"$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips\";\nfi; if [ \"${DISK_GB_VALUE:-0}\" -lt 1000 ]; then\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -prune=50000 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=2048 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -conf=\"$RPC_CONF\" -allowignoredconf=1 -printtoconsole=0 -server=1 -txindex=1 -rpcallowip=0.0.0.0/0 -rpcbind=0.0.0.0:8332 -listen=1 -bind=0.0.0.0:8333 -dbcache=4096 -par=0 -maxconnections=125 $RPC_HEADROOM $RPC_TXRELAY_FLAGS;\nfi" + ], + "derived_env": [ + { + "key": "DISK_GB", + "template": "{{DISK_GB}}" + } + ], + "secret_env": [ + { + "key": "BITCOIN_RPC_PASS", + "secret_file": "bitcoin-rpc-password" + }, + { + "key": "BITCOIN_RPC_TXRELAY_RPCAUTH", + "secret_file": "bitcoin-rpc-txrelay-rpcauth" + } + ], + "data_uid": "100101:100101" + }, + "dependencies": [ + { + "storage": "500Gi" + } + ], + "resources": { + "cpu_limit": 0, + "memory_limit": "8Gi", + "disk_limit": "500Gi" + }, + "security": { + "capabilities": [ + "CHOWN", + "FOWNER", + "SETUID", + "SETGID", + "DAC_OVERRIDE" + ], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8332, + "container": 8332, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + }, + { + "host": 8333, + "container": 8333, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Bitcoin p2p gossip. Peers are anonymous by design and speak the Bitcoin wire protocol, not HTTP." + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/bitcoin", "target": "/home/bitcoin/.bitcoin", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "BITCOIN_RPC_USER=archipelago" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8332", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true, + "testnet_support": false, + "pruning_support": true + } } }, - "version": "29.3.knots20260210", "versions": [ { - "default": true, + "version": "29.3.knots20260210", "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260210", - "version": "29.3.knots20260210" + "default": true }, { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260508", - "version": "29.3.knots20260508" + "version": "29.3.knots20260508", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260508" }, { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260507", - "version": "29.3.knots20260507" + "version": "29.3.knots20260507", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.3.knots20260507" }, { - "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.2.knots20251110", - "version": "29.2.knots20251110" + "version": "29.2.knots20251110", + "image": "source.archipelago-foundation.org/lfg2025/bitcoin-knots:29.2.knots20251110" } ] }, "bitcoin-ui": { + "version": "1.7.123-alpha", "image": "source.archipelago-foundation.org/lfg2025/bitcoin-ui:1.7.123-alpha", "manifest": { "app": { + "id": "bitcoin-ui", + "name": "Bitcoin UI", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Archipelago-native HTTP proxy + static site for interacting with the\nBitcoin Core / Bitcoin Knots JSON-RPC. Runs nginx inside a container\nand reverse-proxies /bitcoin-rpc/ to 127.0.0.1:8332 on the host. The\nupstream Authorization header is substituted from\n/var/lib/archipelago/secrets/bitcoin-rpc-password by the prod\norchestrator's pre-start hook, rendered into an nginx.conf that is\nbind-mounted read-only at container start.\n", "container": { "build": { "context": "/opt/archipelago/docker/bitcoin-ui", @@ -909,80 +1006,119 @@ "app_id": "bitcoin-core" } ], - "description": "Archipelago-native HTTP proxy + static site for interacting with the\nBitcoin Core / Bitcoin Knots JSON-RPC. Runs nginx inside a container\nand reverse-proxies /bitcoin-rpc/ to 127.0.0.1:8332 on the host. The\nupstream Authorization header is substituted from\n/var/lib/archipelago/secrets/bitcoin-rpc-password by the prod\norchestrator's pre-start hook, rendered into an nginx.conf that is\nbind-mounted read-only at container start.\n", - "environment": [], - "health_check": { - "endpoint": "http://127.0.0.1:8334", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "bitcoin-ui", - "name": "Bitcoin UI", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8334, - "host": 8334, - "protocol": "tcp", - "session_passthrough": true - } - ], "resources": { "memory_limit": "128Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, + "ports": [ + { + "host": 8334, + "container": 8334, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated", + "session_passthrough": true + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/bitcoin-ui/nginx.conf", + "target": "/etc/nginx/conf.d/default.conf", + "options": [ + "ro" + ] + } + ], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:8334", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } + } + } + }, + "botfights": { + "version": "1.2.11", + "manifest": { + "app": { + "id": "botfights", + "name": "BotFights", + "version": "1.2.11", "upstream": { "kind": "internal" }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "ro" - ], - "source": "/var/lib/archipelago/bitcoin-ui/nginx.conf", - "target": "/etc/nginx/conf.d/default.conf", - "type": "bind" - } - ] - } - }, - "version": "1.7.123-alpha" - }, - "botfights": { - "manifest": { - "app": { + "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.", "category": "community", "container": { - "data_uid": "999:999", - "generated_secrets": [ - { - "kind": "hex32", - "name": "botfights-jwt-secret" - } - ], "image": "source.archipelago-foundation.org/lfg2025/botfights:1.2.11", "pull_policy": "always", + "generated_secrets": [ + { + "name": "botfights-jwt-secret", + "kind": "hex32" + } + ], "secret_env": [ { "key": "JWT_SECRET", "secret_file": "botfights-jwt-secret" } - ] + ], + "data_uid": "999:999" }, "dependencies": [ { "storage": "500Mi" } ], - "description": "Bot competition arena with 2-player arcade fighting mode. AI bots battle in trivia challenges while humans duke it out with controllers. Built for Bitcoiners.", + "resources": { + "cpu_limit": 2, + "memory_limit": "512Mi", + "disk_limit": "500Mi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 999, + "seccomp_profile": "default", + "network_policy": "bridge", + "apparmor_profile": "default" + }, + "ports": [ + { + "host": 9100, + "container": 9100, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/botfights", + "target": "/app/server/data" + }, + { + "type": "tmpfs", + "target": "/tmp", + "options": [ + "rw", + "noexec", + "nosuid", + "size=64m" + ] + } + ], "environment": [ "NODE_ENV=production", "PORT=9100", @@ -990,30 +1126,29 @@ "ARCHY_EMBEDDED=1" ], "health_check": { + "type": "http", "endpoint": "http://localhost:9100", - "interval": "30s", "path": "/api/health", - "retries": 3, - "start_period": "30s", + "interval": "30s", "timeout": "10s", - "type": "http" + "retries": 3, + "start_period": "30s" }, - "id": "botfights", "interfaces": { "main": { - "description": "Bot arena and arcade fighter with controller support", "name": "Web UI", - "path": "/", + "description": "Bot arena and arcade fighter with controller support", + "type": "ui", "port": 9100, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { "author": "Dorian", + "repo": "https://botfights.net", "icon": "/assets/img/app-icons/botfights.svg", "license": "MIT", - "repo": "https://botfights.net", "tags": [ "bitcoin", "gaming", @@ -1023,82 +1158,35 @@ "competition", "controller" ] - }, - "name": "BotFights", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 9100, - "host": 9100, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "500Mi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "default", - "capabilities": [], - "network_policy": "bridge", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 999 - }, - "upstream": { - "kind": "internal" - }, - "version": "1.2.11", - "volumes": [ - { - "source": "/var/lib/archipelago/botfights", - "target": "/app/server/data", - "type": "bind" - }, - { - "options": [ - "rw", - "noexec", - "nosuid", - "size=64m" - ], - "target": "/tmp", - "type": "tmpfs" - } - ] + } } - }, - "version": "1.2.11" + } }, "btcpay": { + "version": "2.4.3", "image": "docker.io/btcpayserver/btcpayserver:2.4.3", "images": { - "archy-btcpay-db": "source.archipelago-foundation.org/lfg2025/postgres:15.17", + "btcpay-server": "docker.io/btcpayserver/btcpayserver:2.4.3", "archy-nbxplorer": "source.archipelago-foundation.org/lfg2025/nbxplorer:2.6.0", - "btcpay-server": "docker.io/btcpayserver/btcpayserver:2.4.3" - }, - "version": "2.4.3" + "archy-btcpay-db": "source.archipelago-foundation.org/lfg2025/postgres:15.17" + } }, "btcpay-server": { + "version": "2.4.3", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "read-only", - "sync_required": true + "id": "btcpay-server", + "name": "BTCPay Server", + "version": "2.4.3", + "upstream": { + "kind": "github", + "repo": "btcpayserver/btcpayserver" }, + "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.", "container": { - "derived_env": [ - { - "key": "BTCPAY_HOST", - "template": "{{HOST_IP}}:23000" - } - ], "image": "docker.io/btcpayserver/btcpayserver:2.4.3", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "BTCPAY_BTCRPCPASSWORD", @@ -1110,8 +1198,14 @@ }, { "key": "BTCPAY_BTCLIGHTNING", - "optional": true, - "secret_file": "btcpay-lnd-connection" + "secret_file": "btcpay-lnd-connection", + "optional": true + } + ], + "derived_env": [ + { + "key": "BTCPAY_HOST", + "template": "{{HOST_IP}}:23000" } ] }, @@ -1129,7 +1223,36 @@ "version": ">=2.6.0" } ], - "description": "Self-hosted Bitcoin payment processor. Accept Bitcoin payments without intermediaries.", + "resources": { + "cpu_limit": 2, + "memory_limit": "2Gi", + "disk_limit": "20Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 23000, + "container": 49392, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "open", + "auth_rationale": "BTCPay enforces its own login for administration, and its checkout, invoice and webhook endpoints are designed to be reached by anonymous payers and payment processors." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/btcpay", + "target": "/datadir", + "options": [ + "rw" + ] + } + ], "environment": [ "ASPNETCORE_URLS=http://0.0.0.0:49392", "BTCPAY_PROTOCOL=http", @@ -1141,80 +1264,51 @@ "BTCPAY_POSTGRES=Username=btcpay;Password=${BTCPAY_DB_PASS};Host=archy-btcpay-db;Port=5432;Database=btcpay" ], "health_check": { + "type": "http", "endpoint": "http://localhost:49392", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" + "retries": 5 }, - "id": "btcpay-server", - "interfaces": { - "main": { - "description": "BTCPay Server dashboard", - "name": "Web UI", - "path": "/", - "port": 23000, - "protocol": "http", - "type": "ui" - } + "bitcoin_integration": { + "rpc_access": "read-only", + "sync_required": true }, "lightning_integration": { - "invoice_management": true, - "payment_processing": false + "payment_processing": false, + "invoice_management": true + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "BTCPay Server dashboard", + "type": "ui", + "port": 23000, + "protocol": "http", + "path": "/" + } }, "metadata": { "launch": { "open_in_new_tab": true } - }, - "name": "BTCPay Server", - "ports": [ - { - "auth": "open", - "auth_rationale": "BTCPay enforces its own login for administration, and its checkout, invoice and webhook endpoints are designed to be reached by anonymous payers and payment processors.", - "bind": "127.0.0.1", - "container": 49392, - "host": 23000, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "btcpayserver/btcpayserver" - }, - "version": "2.4.3", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/btcpay", - "target": "/datadir", - "type": "bind" - } - ] + } } - }, - "version": "2.4.3" + } }, "core-lightning": { + "version": "23.08.2", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true + "id": "core-lightning", + "name": "Core Lightning (CLN)", + "version": "23.08.2", + "upstream": { + "kind": "github", + "repo": "ElementsProject/lightning" }, + "description": "Lightning Network implementation in C. Lightweight alternative to LND.", "container": { "image": "elementsproject/lightningd:v23.08.2", "image_signature": "cosign://...", @@ -1226,7 +1320,48 @@ "version": ">=26.0" } ], - "description": "Lightning Network implementation in C. Lightweight alternative to LND.", + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "5Gi" + }, + "security": { + "capabilities": [ + "NET_BIND_SERVICE" + ], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "core-lightning" + }, + "ports": [ + { + "host": 9736, + "container": 9735, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself." + }, + { + "host": 9835, + "container": 9835, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Core Lightning gRPC, authenticated by mutual TLS client certificates." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/core-lightning", + "target": "/home/clightning/.lightning", + "options": [ + "rw" + ] + } + ], "environment": [ "BITCOIND_RPCURL=http://bitcoin-core:8332", "BITCOIND_RPCUSER=${BITCOIN_RPC_USER}", @@ -1234,163 +1369,125 @@ "NETWORK=bitcoin" ], "health_check": { + "type": "exec", "endpoint": "lightning-cli getinfo", "interval": "30s", - "retries": 3, "timeout": "5s", - "type": "exec" + "retries": 3 + }, + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true }, - "id": "core-lightning", "lightning_integration": { "channel_management": true, "payment_routing": true - }, - "name": "Core Lightning (CLN)", - "ports": [ - { - "auth": "none", - "auth_rationale": "Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.", - "container": 9735, - "host": 9736, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Core Lightning gRPC, authenticated by mutual TLS client certificates.", - "container": 9835, - "host": 9835, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 1, - "disk_limit": "5Gi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "core-lightning", - "capabilities": [ - "NET_BIND_SERVICE" - ], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "upstream": { - "kind": "github", - "repo": "ElementsProject/lightning" - }, - "version": "23.08.2", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/core-lightning", - "target": "/home/clightning/.lightning", - "type": "bind" - } - ] + } } - }, - "version": "23.08.2" + } }, "cuprate": { + "version": "0.1.0-preview", "manifest": { "app": { + "id": "cuprate", + "name": "Cuprate", + "version": "0.1.0-preview", + "upstream": { + "kind": "github", + "repo": "Cuprate/cuprate" + }, + "description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.", "category": "money", "container": { + "image": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14", + "pull_policy": "if-not-present", + "network": "archy-net", "custom_args": [ "--config-file", "/home/cuprate/Cuprated.toml" ], - "data_uid": "1000:1000", - "image": "source.archipelago-foundation.org/lfg2025/cuprate:0.1.0-preview-18-g618ff14", - "network": "archy-net", - "pull_policy": "if-not-present" + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "300Gi" } ], - "description": "Alternative Monero node implementation in Rust. Independently validates Monero consensus rules, providing a layer of security and redundancy for the network.", - "files": [ - { - "content": "network = \"Mainnet\"\nfast_sync = true\ntarget_max_memory = 8589934592\n\n[rpc.restricted]\nenable = true\n\n[tracing.stdout]\nlevel = \"info\"\n\n[tracing.file]\nlevel = \"info\"\nmax_log_files = 14\n", - "overwrite": false, - "path": "/var/lib/archipelago/cuprate/Cuprated.toml" - } - ], - "health_check": { - "endpoint": "localhost:18090", - "interval": "30s", - "retries": 3, - "start_period": "5m", - "timeout": "5s", - "type": "tcp" - }, - "id": "cuprate", - "metadata": { - "author": "Cuprate", - "category": "money", - "icon": "/assets/img/app-icons/cuprate.svg", - "repo": "https://github.com/Cuprate/cuprate", - "tier": "optional" - }, - "name": "Cuprate", - "ports": [ - { - "auth": "none", - "auth_rationale": "Monero p2p gossip. Peers are anonymous by design and speak the Monero wire protocol, not HTTP.", - "container": 18080, - "host": 18183, - "protocol": "tcp" - }, - { - "auth": "open", - "auth_rationale": "Monero restricted RPC — the subset upstream considers safe for public/remote-node use. Wallets (Feather, monero-wallet-rpc, GUI) connect directly over plain HTTP JSON-RPC and cannot complete a browser login or hold a dashboard session cookie.", - "container": 18089, - "host": 18090, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 0, - "disk_limit": "300Gi", - "memory_limit": "10Gi" + "memory_limit": "10Gi", + "disk_limit": "300Gi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": true, "no_new_privileges": true, - "readonly_root": true + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "Cuprate/cuprate" - }, - "version": "0.1.0-preview", + "ports": [ + { + "host": 18183, + "container": 18080, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Monero p2p gossip. Peers are anonymous by design and speak the Monero wire protocol, not HTTP." + }, + { + "host": 18090, + "container": 18089, + "protocol": "tcp", + "auth": "open", + "auth_rationale": "Monero restricted RPC \u2014 the subset upstream considers safe for public/remote-node use. Wallets (Feather, monero-wallet-rpc, GUI) connect directly over plain HTTP JSON-RPC and cannot complete a browser login or hold a dashboard session cookie." + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/cuprate", "target": "/home/cuprate", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "files": [ + { + "path": "/var/lib/archipelago/cuprate/Cuprated.toml", + "content": "network = \"Mainnet\"\nfast_sync = true\ntarget_max_memory = 8589934592\n\n[rpc.restricted]\nenable = true\n\n[tracing.stdout]\nlevel = \"info\"\n\n[tracing.file]\nlevel = \"info\"\nmax_log_files = 14\n", + "overwrite": false + } + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:18090", + "interval": "30s", + "timeout": "5s", + "retries": 3, + "start_period": "5m" + }, + "metadata": { + "icon": "/assets/img/app-icons/cuprate.svg", + "category": "money", + "tier": "optional", + "author": "Cuprate", + "repo": "https://github.com/Cuprate/cuprate" + } } - }, - "version": "0.1.0-preview" + } }, "electrs-ui": { + "version": "1.7.123-alpha", "image": "source.archipelago-foundation.org/lfg2025/electrs-ui:1.7.123-alpha", "manifest": { "app": { + "id": "electrs-ui", + "name": "Electrs UI", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Archipelago-native HTTP frontend for electrs/electrumx status. Runs\nnginx inside a container, serves static assets, and proxies\n/electrs-status to the archipelago backend on 127.0.0.1:5678.\n", "container": { "build": { "context": "/opt/archipelago/docker/electrs-ui", @@ -1399,65 +1496,61 @@ } }, "dependencies": [], - "description": "Archipelago-native HTTP frontend for electrs/electrumx status. Runs\nnginx inside a container, serves static assets, and proxies\n/electrs-status to the archipelago backend on 127.0.0.1:5678.\n", - "environment": [], - "health_check": { - "endpoint": "http://127.0.0.1:50002", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "electrs-ui", - "name": "Electrs UI", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 50002, - "host": 50002, - "protocol": "tcp", - "session_passthrough": true - } - ], "resources": { "memory_limit": "64Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "upstream": { - "kind": "internal" - }, - "version": "1.0.0", - "volumes": [] + "ports": [ + { + "host": 50002, + "container": 50002, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated", + "session_passthrough": true + } + ], + "volumes": [], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:50002", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "1.7.123-alpha" + } }, "electrumx": { + "version": "v1.18.0", "image": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0", "manifest": { "app": { - "bitcoin_integration": { - "pruning_support": false, - "rpc_access": "read-only", - "sync_required": true + "id": "electrumx", + "name": "ElectrumX", + "version": "1.18.0", + "upstream": { + "kind": "github", + "repo": "spesmilo/electrumx" }, + "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.", "container": { - "custom_args": [ - "for h in bitcoin-knots bitcoin-core; do if getent hosts \"$h\" >/dev/null 2>&1; then BTC_HOST=\"$h\"; break; fi; done; export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@${BTC_HOST:-bitcoin-knots}:8332/\"; exec electrumx_server" - ], + "image": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0", + "pull_policy": "if-not-present", + "network": "archy-net", "data_uid": "1000:1000", "entrypoint": [ "sh", "-lc" ], - "image": "source.archipelago-foundation.org/lfg2025/electrumx:v1.18.0", - "network": "archy-net", - "pull_policy": "if-not-present", + "custom_args": [ + "for h in bitcoin-knots bitcoin-core; do if getent hosts \"$h\" >/dev/null 2>&1; then BTC_HOST=\"$h\"; break; fi; done; export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@${BTC_HOST:-bitcoin-knots}:8332/\"; exec electrumx_server" + ], "secret_env": [ { "key": "BITCOIN_RPC_PASS", @@ -1475,7 +1568,37 @@ }, "bitcoin:archival" ], - "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.", + "resources": { + "cpu_limit": 0, + "memory_limit": "6Gi", + "disk_limit": "50Gi" + }, + "security": { + "capabilities": [ + "DAC_OVERRIDE" + ], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 50001, + "container": 50001, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Electrum wire protocol over TCP. Electrum wallets speak it directly and cannot hold a session cookie." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/electrumx", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "COIN=Bitcoin", "DB_DIRECTORY=/data", @@ -1483,78 +1606,55 @@ "CACHE_MB=1024", "MAX_SEND=10000000" ], - "health_check": { - "endpoint": "localhost:50001", - "interval": "30s", - "retries": 3, - "start_period": "10m", - "timeout": "5s", - "type": "tcp" - }, - "id": "electrumx", "interfaces": { "main": { - "description": "ElectrumX server status and connection details", "name": "Web UI", + "description": "ElectrumX server status and connection details", + "type": "ui", "port": 50002, - "protocol": "http", - "type": "ui" + "protocol": "http" } }, - "name": "ElectrumX", - "ports": [ - { - "auth": "none", - "auth_rationale": "Electrum wire protocol over TCP. Electrum wallets speak it directly and cannot hold a session cookie.", - "container": 50001, - "host": 50001, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 0, - "disk_limit": "50Gi", - "memory_limit": "6Gi" + "health_check": { + "type": "tcp", + "endpoint": "localhost:50001", + "interval": "30s", + "timeout": "5s", + "retries": 3, + "start_period": "10m" }, - "security": { - "capabilities": [ - "DAC_OVERRIDE" - ], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "spesmilo/electrumx" - }, - "version": "1.18.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/electrumx", - "target": "/data", - "type": "bind" - } - ] + "bitcoin_integration": { + "rpc_access": "read-only", + "sync_required": true, + "pruning_support": false + } } - }, - "version": "v1.18.0" + } }, "fedimint": { + "version": "v0.10.1", "image": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.1", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true + "id": "fedimint", + "name": "Fedimint Guardian", + "version": "0.10.0", + "upstream": { + "kind": "github", + "repo": "fedimint/fedimint" }, + "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.1", + "pull_policy": "if-not-present", + "network": "archy-net", + "entrypoint": [ + "sh", + "-lc" + ], "custom_args": [ "until state=\"$(curl -sS --connect-timeout 5 -m 45 -u \"$FM_BITCOIND_USERNAME:$FM_BITCOIND_PASSWORD\" -H \"Content-Type: application/json\" --data-binary '{\"jsonrpc\":\"1.0\",\"id\":\"fedimint-wait\",\"method\":\"getblockchaininfo\",\"params\":[]}' \"$FM_BITCOIND_URL/\")\" && echo \"$state\" | grep -q '\"initialblockdownload\":false'; do\n echo \"Waiting for Bitcoin RPC sync at $FM_BITCOIND_URL...\";\n sleep 30;\ndone;\nexec fedimintd" ], - "data_uid": "1000:1000", "derived_env": [ { "key": "FM_P2P_URL", @@ -1569,19 +1669,13 @@ "template": "http://{{BITCOIN_HOST}}:8332" } ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "source.archipelago-foundation.org/lfg2025/fedimintd:v0.10.1", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "FM_BITCOIND_PASSWORD", "secret_file": "bitcoin-rpc-password" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { @@ -1592,7 +1686,49 @@ "storage": "20Gi" } ], - "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.", + "resources": { + "cpu_limit": 4, + "memory_limit": "4Gi", + "disk_limit": "20Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8173, + "container": 8173, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Fedimint guardian consensus. Other guardians speak the federation's own authenticated protocol here; a login page would break consensus." + }, + { + "host": 8174, + "container": 8174, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Fedimint guardian API for federation clients, which authenticate to the federation itself and cannot hold a browser session." + }, + { + "host": 8177, + "container": 8175, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/fedimint", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "FM_DATA_DIR=/data", "FM_BITCOIND_USERNAME=archipelago", @@ -1602,132 +1738,69 @@ "FM_BIND_UI=0.0.0.0:8175" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8175", - "interval": "30s", "path": "/", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "fedimint", "interfaces": { "main": { - "description": "Fedimint Guardian wait/proxy UI", "name": "Guardian UI", - "path": "/", + "description": "Fedimint Guardian wait/proxy UI", + "type": "ui", "port": 8175, "protocol": "http", - "type": "ui" + "path": "/" } }, - "name": "Fedimint Guardian", - "ports": [ - { - "auth": "none", - "auth_rationale": "Fedimint guardian consensus. Other guardians speak the federation's own authenticated protocol here; a login page would break consensus.", - "container": 8173, - "host": 8173, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Fedimint guardian API for federation clients, which authenticate to the federation itself and cannot hold a browser session.", - "container": 8174, - "host": 8174, - "protocol": "tcp" - }, - { - "auth": "local", - "bind": "127.0.0.1", - "container": 8175, - "host": 8177, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 4, - "disk_limit": "20Gi", - "memory_limit": "4Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": true - }, - "upstream": { - "kind": "github", - "repo": "fedimint/fedimint" - }, - "version": "0.10.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/fedimint", - "target": "/data", - "type": "bind" - } - ] + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true + } } - }, - "version": "v0.10.1" + } }, "fedimint-clientd": { + "version": "0.8.0", "manifest": { "app": { + "id": "fedimint-clientd", + "name": "Fedimint Client", + "version": "0.8.0", + "upstream": { + "kind": "github", + "repo": "fedimint/fedimint-clientd" + }, + "description": "Fedimint ecash client daemon (fmcd). Lets the node hold Fedimint ecash and join federations; the wallet talks to it over a local REST API.", "container": { - "data_uid": "1000:1000", + "image": "source.archipelago-foundation.org/lfg2025/fmcd:0.8.1", + "pull_policy": "if-not-present", + "network": "archy-net", "generated_secrets": [ { - "kind": "hex16", - "name": "fmcd-password" + "name": "fmcd-password", + "kind": "hex16" } ], - "image": "source.archipelago-foundation.org/lfg2025/fmcd:0.8.1", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "FMCD_PASSWORD", "secret_file": "fmcd-password" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "2Gi" } ], - "description": "Fedimint ecash client daemon (fmcd). Lets the node hold Fedimint ecash and join federations; the wallet talks to it over a local REST API.", - "environment": [ - "FMCD_ADDR=0.0.0.0:8080", - "FMCD_MODE=rest", - "FMCD_DATA_DIR=/data", - "FMCD_INVITE_CODE=fed11qgqyj3mfwfhksw309uuxywtxxfjrjc35xuexverpxdsnxcnrxucxvenzveskgc3kvvun2c34xp3k2ep38yunzdpexcekxe3hvd3rvvmx8pnrvdenx5mnzvtzqqqjqt0t6pc3s5z0ynqjw9s4njf6svwgu59kweawc0vvrddcjeemw6yyn4pcdp" - ], - "health_check": { - "endpoint": "localhost:8080", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "fedimint-clientd", - "name": "Fedimint Client", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 8080, - "host": 8178, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 1, - "disk_limit": "2Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "2Gi" }, "security": { "capabilities": [ @@ -1737,60 +1810,80 @@ "SETUID", "SETGID" ], - "network_policy": "bridge", - "readonly_root": true + "readonly_root": true, + "network_policy": "bridge" }, - "upstream": { - "kind": "github", - "repo": "fedimint/fedimint-clientd" - }, - "version": "0.8.0", + "ports": [ + { + "host": 8178, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/fmcd", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "FMCD_ADDR=0.0.0.0:8080", + "FMCD_MODE=rest", + "FMCD_DATA_DIR=/data", + "FMCD_INVITE_CODE=fed11qgqyj3mfwfhksw309uuxywtxxfjrjc35xuexverpxdsnxcnrxucxvenzveskgc3kvvun2c34xp3k2ep38yunzdpexcekxe3hvd3rvvmx8pnrvdenx5mnzvtzqqqjqt0t6pc3s5z0ynqjw9s4njf6svwgu59kweawc0vvrddcjeemw6yyn4pcdp" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8080", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "0.8.0" + } }, "fedimint-gateway": { + "version": "v0.10.1", "image": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.1", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true + "id": "fedimint-gateway", + "name": "Fedimint Gateway", + "version": "0.10.0", + "upstream": { + "kind": "github", + "repo": "fedimint/fedimint" }, + "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.1", + "pull_policy": "if-not-present", + "network": "archy-net", + "entrypoint": [ + "sh", + "-lc" + ], "custom_args": [ "if [ -f /lnd/tls.cert ] && [ -f /lnd/data/chain/bitcoin/mainnet/admin.macaroon ]; then\n exec gatewayd --data-dir /data --listen 0.0.0.0:8176 --bcrypt-password-hash \"$FEDI_HASH\" --network bitcoin --bitcoind-url \"$FM_BITCOIND_URL\" --bitcoind-username \"$FM_BITCOIND_USERNAME\" --bitcoind-password \"$FM_BITCOIND_PASSWORD\" lnd --lnd-rpc-host lnd:10009 --lnd-tls-cert /lnd/tls.cert --lnd-macaroon /lnd/data/chain/bitcoin/mainnet/admin.macaroon;\nelse\n exec gatewayd --data-dir /data --listen 0.0.0.0:8176 --bcrypt-password-hash \"$FEDI_HASH\" --network bitcoin --bitcoind-url \"$FM_BITCOIND_URL\" --bitcoind-username \"$FM_BITCOIND_USERNAME\" --bitcoind-password \"$FM_BITCOIND_PASSWORD\" ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway;\nfi" ], - "data_uid": "1000:1000", "derived_env": [ { "key": "FM_BITCOIND_URL", "template": "http://{{BITCOIN_HOST}}:8332" } ], - "entrypoint": [ - "sh", - "-lc" - ], "generated_secrets": [ { - "kind": "bcrypt", - "name": "fedimint-gateway-hash" + "name": "fedimint-gateway-hash", + "kind": "bcrypt" } ], - "image": "source.archipelago-foundation.org/lfg2025/gatewayd:v0.10.1", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "FM_BITCOIND_PASSWORD", @@ -1800,7 +1893,8 @@ "key": "FEDI_HASH", "secret_file": "fedimint-gateway-hash" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { @@ -1812,120 +1906,99 @@ "version": ">=0.10.0" } ], - "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.", + "resources": { + "cpu_limit": 2, + "memory_limit": "2Gi", + "disk_limit": "10Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8176, + "container": 8176, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash) and reached by federation peers and clients that cannot hold a browser session." + }, + { + "host": 9737, + "container": 9737, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and encrypts the connection itself." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/fedimint-gateway", + "target": "/data", + "options": [ + "rw" + ] + }, + { + "type": "bind", + "source": "/var/lib/archipelago/lnd", + "target": "/lnd", + "options": [ + "ro" + ] + } + ], "environment": [ "FM_BITCOIND_USERNAME=archipelago" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8176", - "interval": "30s", "path": "/", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "fedimint-gateway", - "name": "Fedimint Gateway", - "ports": [ - { - "auth": "none", - "auth_rationale": "Fedimint gateway API, protected by its own bcrypt password (--bcrypt-password-hash) and reached by federation peers and clients that cannot hold a browser session.", - "container": 8176, - "host": 8176, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "LDK Lightning p2p for the gateway. The BOLT-8 noise handshake authenticates and encrypts the connection itself.", - "container": 9737, - "host": 9737, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": true - }, - "upstream": { - "kind": "github", - "repo": "fedimint/fedimint" - }, - "version": "0.10.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/fedimint-gateway", - "target": "/data", - "type": "bind" - }, - { - "options": [ - "ro" - ], - "source": "/var/lib/archipelago/lnd", - "target": "/lnd", - "type": "bind" - } - ] + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true + } } - }, - "version": "v0.10.1" + } }, "filebrowser": { + "version": "v2.63.23", "image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "none", - "sync_required": false + "id": "filebrowser", + "name": "File Browser", + "version": "2.63.23", + "upstream": { + "kind": "github", + "repo": "filebrowser/filebrowser" }, + "description": "Baseline Archipelago file manager service.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23", + "pull_policy": "if-not-present", + "network": "archy-net", "custom_args": [ "--config", "/data/.filebrowser.json" ], - "data_uid": "100000:100000", - "image": "source.archipelago-foundation.org/lfg2025/filebrowser:v2.63.23", - "network": "archy-net", - "pull_policy": "if-not-present" + "data_uid": "100000:100000" }, "dependencies": [ { "storage": "10Gi" } ], - "description": "Baseline Archipelago file manager service.", - "environment": [], - "health_check": { - "endpoint": "http://localhost:80", - "interval": "30s", - "path": "/health", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "filebrowser", - "name": "File Browser", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 80, - "host": 8083, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -1936,39 +2009,63 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "filebrowser/filebrowser" - }, - "version": "2.63.23", + "ports": [ + { + "host": 8083, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/filebrowser", "target": "/srv", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/filebrowser-data", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://localhost:80", + "path": "/health", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "bitcoin_integration": { + "rpc_access": "none", + "sync_required": false + } } - }, - "version": "v2.63.23" + } }, "fips-ui": { + "version": "1.0.0", "manifest": { "app": { + "id": "fips-ui", + "name": "FIPS Mesh", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Archipelago-native dashboard for the FIPS mesh transport. Runs nginx\ninside a container with host networking, serves a static dashboard on\n:8336, and reverse-proxies /rpc/v1 to the archipelago backend on\n127.0.0.1:5678. All FIPS controls (status, seed anchors, reconnect,\nrestart, and stable-channel daemon updates) go through the existing\nfips.* RPC methods, authenticated by the browser's own archipelago\nsession \u2014 there is no separate secret to manage.\n", "container": { "build": { "context": "/opt/archipelago/docker/fips-ui", @@ -1976,47 +2073,48 @@ "tag": "localhost/fips-ui:local" } }, - "description": "Archipelago-native dashboard for the FIPS mesh transport. Runs nginx\ninside a container with host networking, serves a static dashboard on\n:8336, and reverse-proxies /rpc/v1 to the archipelago backend on\n127.0.0.1:5678. All FIPS controls (status, seed anchors, reconnect,\nrestart, and stable-channel daemon updates) go through the existing\nfips.* RPC methods, authenticated by the browser's own archipelago\nsession — there is no separate secret to manage.\n", - "environment": [], - "health_check": { - "endpoint": "http://127.0.0.1:8336", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "fips-ui", - "name": "FIPS Mesh", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8336, - "host": 8336, - "protocol": "tcp", - "session_passthrough": true - } - ], "resources": { "memory_limit": "128Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "upstream": { - "kind": "internal" - }, - "version": "1.0.0", - "volumes": [] + "ports": [ + { + "host": 8336, + "container": 8336, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated", + "session_passthrough": true + } + ], + "volumes": [], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:8336", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "1.0.0" + } }, "gitea": { + "version": "1.27.3", "manifest": { "app": { + "id": "gitea", + "name": "Gitea", + "version": "1.27.3", + "upstream": { + "kind": "github", + "repo": "go-gitea/gitea" + }, + "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.", "category": "development", "container": { "image": "source.archipelago-foundation.org/lfg2025/gitea:1.27.3", @@ -2024,82 +2122,12 @@ }, "dependencies": [ { - "storage": "500Mi" - } - ], - "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.", - "environment": [ - "GITEA__database__DB_TYPE=sqlite3", - "GITEA__server__SSH_PORT=2222", - "GITEA__server__SSH_LISTEN_PORT=22", - "GITEA__server__LFS_START_SERVER=true", - "GITEA__packages__ENABLED=true", - "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", - "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true" - ], - "health_check": { - "endpoint": "http://localhost:3000", - "interval": "120s", - "path": "/", - "retries": 5, - "timeout": "30s", - "type": "http" - }, - "id": "gitea", - "interfaces": { - "main": { - "description": "Gitea web interface", - "name": "Web UI", - "path": "/", - "port": 3001, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "features": [ - "Git repositories with web UI", - "Built-in container/package registry", - "Issue tracking and pull requests", - "CI/CD via Gitea Actions", - "Lightweight SQLite deployment" - ], - "icon": "/assets/img/app-icons/gitea.svg", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://gitea.com", - "tier": "optional" - }, - "name": "Gitea", - "nginx_proxy": { - "extra_headers": [ - "proxy_hide_header X-Frame-Options", - "proxy_hide_header Content-Security-Policy" - ], - "listen": 3000, - "proxy_pass": "http://127.0.0.1:3001" - }, - "ports": [ - { - "auth": "open", - "auth_rationale": "Gitea enforces its own account login on every page and API route; git clients authenticate with basic-auth/tokens and cannot complete a browser login challenge.", - "bind": "127.0.0.1", - "container": 3000, - "host": 3001, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Git over SSH, authenticated by the user's own SSH keypair. Not HTTP, so the gate cannot serve a login page here.", - "container": 22, - "host": 2222, - "protocol": "tcp" + "storage": "50Gi" } ], "resources": { - "disk_limit": "500Mi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "50Gi" }, "security": { "capabilities": [ @@ -2110,53 +2138,159 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "bridge", + "readonly_root": false, "no_new_privileges": false, - "readonly_root": false + "network_policy": "bridge" }, - "upstream": { - "kind": "github", - "repo": "go-gitea/gitea" - }, - "version": "1.27.3", - "volumes": [ + "ports": [ { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/gitea/data", - "target": "/data", - "type": "bind" + "host": 3001, + "container": 3000, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "open", + "auth_rationale": "Gitea enforces its own account login on every page and API route; git clients authenticate with basic-auth/tokens and cannot complete a browser login challenge." }, { + "host": 2222, + "container": 22, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Git over SSH, authenticated by the user's own SSH keypair. Not HTTP, so the gate cannot serve a login page here." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/gitea/data", + "target": "/data", "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/gitea/config", "target": "/etc/gitea", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "GITEA__database__DB_TYPE=sqlite3", + "GITEA__server__SSH_PORT=2222", + "GITEA__server__SSH_LISTEN_PORT=22", + "GITEA__server__LFS_START_SERVER=true", + "GITEA__packages__ENABLED=true", + "GITEA__packages__LIMIT_TOTAL_OWNER_SIZE=-1", + "GITEA__packages__LIMIT_SIZE_CONTAINER=-1", + "GITEA__repository_0x2Erelease__FILE_MAX_SIZE=10240", + "GITEA__repository_0x2Erelease__MAX_FILES=20", + "GITEA__repository__ENABLE_PUSH_CREATE_USER=true", + "GITEA__repository__ENABLE_PUSH_CREATE_ORG=true" + ], + "health_check": { + "type": "http", + "endpoint": "http://localhost:3000", + "path": "/", + "interval": "120s", + "timeout": "30s", + "retries": 5 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Gitea web interface", + "type": "ui", + "port": 3001, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/gitea.svg", + "repo": "https://gitea.com", + "tier": "optional", + "launch": { + "open_in_new_tab": true + }, + "features": [ + "Git repositories with web UI", + "Built-in container/package registry", + "Issue tracking and pull requests", + "CI/CD via Gitea Actions", + "Lightweight SQLite deployment" + ] + }, + "nginx_proxy": { + "listen": 3000, + "proxy_pass": "http://127.0.0.1:3001", + "extra_headers": [ + "proxy_hide_header X-Frame-Options", + "proxy_hide_header Content-Security-Policy" + ] + } } - }, - "version": "1.27.3" + } }, "grafana": { + "version": "10.2.0", "image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0", "manifest": { "app": { + "id": "grafana", + "name": "Grafana", + "version": "10.2.0", + "upstream": { + "kind": "github", + "repo": "grafana/grafana" + }, + "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.", "container": { - "data_uid": "472:472", "image": "source.archipelago-foundation.org/lfg2025/grafana:10.2.0", "image_signature": "cosign://...", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "data_uid": "472:472" }, "dependencies": [ { "storage": "5Gi" } ], - "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.", + "resources": { + "cpu_limit": 2, + "memory_limit": "1Gi", + "disk_limit": "5Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "grafana" + }, + "ports": [ + { + "host": 3000, + "container": 3000, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/grafana", + "target": "/var/lib/grafana", + "options": [ + "rw" + ] + } + ], "environment": [ "GF_SECURITY_ADMIN_USER=admin", "GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}", @@ -2164,125 +2298,50 @@ "GF_INSTALL_PLUGINS=" ], "health_check": { + "type": "http", "endpoint": "http://localhost:3000", - "interval": "30s", "path": "/api/health", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" + "retries": 5 }, - "id": "grafana", "metadata": { "launch": { "open_in_new_tab": true } - }, - "name": "Grafana", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 3000, - "host": 3000, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "5Gi", - "memory_limit": "1Gi" - }, - "security": { - "apparmor_profile": "grafana", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "upstream": { - "kind": "github", - "repo": "grafana/grafana" - }, - "version": "10.2.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/grafana", - "target": "/var/lib/grafana", - "type": "bind" - } - ] + } } - }, - "version": "10.2.0" + } }, "homeassistant": { + "version": "2026.8.3", "image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3", "manifest": { "app": { + "id": "homeassistant", + "name": "Home Assistant", + "version": "2026.8.3", + "upstream": { + "kind": "github", + "repo": "home-assistant/core" + }, + "description": "Open source home automation platform. Control and monitor your smart home devices.", "container": { "image": "source.archipelago-foundation.org/lfg2025/home-assistant:2026.8.3", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "10Gi" } ], - "description": "Open source home automation platform. Control and monitor your smart home devices.", - "devices": [], - "environment": [ - "TZ=UTC" - ], - "health_check": { - "endpoint": "localhost:8123", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "homeassistant", - "interfaces": { - "main": { - "description": "Home Assistant dashboard", - "name": "Web UI", - "path": "/", - "port": 8123, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Home Assistant", - "category": "home", - "icon": "/assets/img/app-icons/homeassistant.png", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/home-assistant/core" - }, - "name": "Home Assistant", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8123, - "host": 8123, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "512Mi" + "memory_limit": "512Mi", + "disk_limit": "10Gi" }, "security": { - "apparmor_profile": "home-assistant", "capabilities": [ "CHOWN", "FOWNER", @@ -2292,44 +2351,88 @@ "NET_BIND_SERVICE", "NET_RAW" ], - "network_policy": "isolated", - "no_new_privileges": true, "readonly_root": false, + "no_new_privileges": true, + "user": 1000, "seccomp_profile": "default", - "user": 1000 + "network_policy": "isolated", + "apparmor_profile": "home-assistant" }, - "upstream": { - "kind": "github", - "repo": "home-assistant/core" - }, - "version": "2026.8.3", + "ports": [ + { + "host": 8123, + "container": 8123, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/home-assistant", "target": "/config", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "devices": [], + "environment": [ + "TZ=UTC" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8123", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Home Assistant dashboard", + "type": "ui", + "port": 8123, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/homeassistant.png", + "category": "home", + "author": "Home Assistant", + "repo": "https://github.com/home-assistant/core", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "2026.8.3" + } }, "immich": { + "version": "release", "image": "source.archipelago-foundation.org/lfg2025/immich-server:release", "images": { + "immich_server": "source.archipelago-foundation.org/lfg2025/immich-server:release", "immich_postgres": "source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", - "immich_redis": "source.archipelago-foundation.org/lfg2025/redis:7.4.8", - "immich_server": "source.archipelago-foundation.org/lfg2025/immich-server:release" + "immich_redis": "source.archipelago-foundation.org/lfg2025/redis:7.4.8" }, "manifest": { "app": { + "id": "immich", + "name": "Immich", + "version": "2.7.4", + "upstream": { + "kind": "github", + "repo": "immich-app/immich" + }, + "description": "Self-hosted photo and video backup with mobile apps and search.", + "container_name": "immich_server", "container": { "image": "source.archipelago-foundation.org/lfg2025/immich-server:release", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "DB_PASSWORD", @@ -2337,7 +2440,6 @@ } ] }, - "container_name": "immich_server", "dependencies": [ { "app_id": "immich-postgres" @@ -2349,7 +2451,38 @@ "storage": "200Gi" } ], - "description": "Self-hosted photo and video backup with mobile apps and search.", + "resources": { + "memory_limit": "2Gi", + "disk_limit": "200Gi" + }, + "security": { + "capabilities": [ + "CHOWN", + "DAC_OVERRIDE", + "FOWNER" + ], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 2283, + "container": 2283, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/immich", + "target": "/usr/src/app/upload", + "options": [ + "rw" + ] + } + ], "environment": [ "DB_HOSTNAME=immich_postgres", "DB_USERNAME=postgres", @@ -2358,85 +2491,55 @@ "UPLOAD_LOCATION=/usr/src/app/upload" ], "health_check": { + "type": "http", "endpoint": "http://localhost:2283", - "interval": "30s", "path": "/api/server/ping", - "retries": 20, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 20 }, - "id": "immich", "interfaces": { "main": { - "description": "Immich photo library", "name": "Web UI", - "path": "/", + "description": "Immich photo library", + "type": "ui", "port": 2283, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { "launch": { "open_in_new_tab": true } - }, - "name": "Immich", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 2283, - "host": 2283, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "200Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [ - "CHOWN", - "DAC_OVERRIDE", - "FOWNER" - ], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "immich-app/immich" - }, - "version": "2.7.4", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/immich", - "target": "/usr/src/app/upload", - "type": "bind" - } - ] + } } - }, - "version": "release" + } }, "immich-postgres": { + "version": "14-vectorchord0.4.3-pgvectors0.2.0", "manifest": { "app": { + "id": "immich-postgres", + "name": "Immich Postgres", + "version": "14-vectorchord0.4.3-pgvectors0.2.0", + "upstream": { + "kind": "ghcr", + "repo": "immich-app/postgres" + }, + "description": "Postgres (pgvecto.rs / vectorchord) backend for Immich.", + "container_name": "immich_postgres", "container": { + "image": "source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", + "pull_policy": "if-not-present", + "network": "archy-net", "data_uid": "100998:100998", "generated_secrets": [ { - "kind": "hex32", - "name": "immich-db-password" + "name": "immich-db-password", + "kind": "hex32" } ], - "image": "source.archipelago-foundation.org/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "POSTGRES_PASSWORD", @@ -2444,30 +2547,14 @@ } ] }, - "container_name": "immich_postgres", "dependencies": [ { "storage": "40Gi" } ], - "description": "Postgres (pgvecto.rs / vectorchord) backend for Immich.", - "environment": [ - "POSTGRES_USER=postgres", - "POSTGRES_DB=immich" - ], - "health_check": { - "endpoint": "localhost:5432", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "immich-postgres", - "name": "Immich Postgres", - "ports": [], "resources": { - "disk_limit": "40Gi", - "memory_limit": "2Gi" + "memory_limit": "2Gi", + "disk_limit": "40Gi" }, "security": { "capabilities": [ @@ -2477,50 +2564,53 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "ghcr", - "repo": "immich-app/postgres" - }, - "version": "14-vectorchord0.4.3-pgvectors0.2.0", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/immich-db", "target": "/var/lib/postgresql/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "POSTGRES_USER=postgres", + "POSTGRES_DB=immich" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:5432", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "14-vectorchord0.4.3-pgvectors0.2.0" + } }, "immich-redis": { + "version": "7-alpine", "manifest": { "app": { - "container": { - "image": "source.archipelago-foundation.org/lfg2025/valkey:7-alpine", - "network": "archy-net", - "pull_policy": "if-not-present" - }, - "container_name": "immich_redis", - "dependencies": [], - "description": "Valkey (Redis-compatible) cache for Immich.", - "environment": [], - "health_check": { - "endpoint": "localhost:6379", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, "id": "immich-redis", "name": "Immich Redis", - "ports": [], + "version": "7-alpine", + "upstream": { + "kind": "dockerhub", + "repo": "valkey/valkey" + }, + "description": "Valkey (Redis-compatible) cache for Immich.", + "container_name": "immich_redis", + "container": { + "image": "source.archipelago-foundation.org/lfg2025/valkey:7-alpine", + "pull_policy": "if-not-present", + "network": "archy-net" + }, + "dependencies": [], "resources": { "memory_limit": "128Mi" }, @@ -2529,20 +2619,24 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "valkey/valkey" - }, - "version": "7-alpine", - "volumes": [] + "ports": [], + "volumes": [], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:6379", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "7-alpine" + } }, "indeedhub": { + "version": "1.0.0", "image": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0", "images": { "indeedhub": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0", @@ -2551,13 +2645,20 @@ }, "manifest": { "app": { + "id": "indeedhub", + "name": "IndeeHub", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.", "category": "community", + "container_name": "indeedhub", "container": { "image": "source.archipelago-foundation.org/lfg2025/indeedhub:1.0.0", - "network": "indeedhub-net", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "indeedhub-net" }, - "container_name": "indeedhub", "dependencies": [ { "app_id": "indeedhub-api" @@ -2566,16 +2667,52 @@ "storage": "1Gi" } ], - "description": "Bitcoin documentary streaming platform featuring God Bless Bitcoin and other educational content about Bitcoin, sovereignty, and decentralized technology. Sign in with your Nostr identity.", - "environment": [], - "health_check": { - "endpoint": "localhost:7777", - "interval": "30s", - "retries": 5, - "start_period": "30s", - "timeout": "5s", - "type": "tcp" + "resources": { + "memory_limit": "512Mi", + "disk_limit": "1Gi" }, + "security": { + "capabilities": [ + "CHOWN", + "DAC_OVERRIDE", + "SETGID", + "SETUID" + ], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 7778, + "container": 7777, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "tmpfs", + "target": "/run", + "options": [ + "rw", + "nosuid", + "nodev", + "size=16m" + ] + }, + { + "type": "tmpfs", + "target": "/var/cache/nginx", + "options": [ + "rw", + "nosuid", + "nodev", + "size=32m" + ] + } + ], + "environment": [], "hooks": { "post_install": [ { @@ -2588,10 +2725,17 @@ }, { "copy_from_host": { - "dest": "/usr/share/nginx/html/nostr-provider.js", - "src": "web-ui/nostr-provider.js" + "src": "web-ui/nostr-provider.js", + "dest": "/usr/share/nginx/html/nostr-provider.js" } }, + { + "exec": [ + "sh", + "-c", + "grep -qF 'location = /nostr-provider.js {' /etc/nginx/conf.d/default.conf || sed -i '/location = /sw.js {/i\\ location = /nostr-provider.js {\\n add_header Cache-Control \"no-cache, no-store, must-revalidate\";\\n expires off;\\n }\\n' /etc/nginx/conf.d/default.conf" + ] + }, { "exec": [ "sh", @@ -2599,6 +2743,22 @@ "grep -q nostr-provider /etc/nginx/conf.d/default.conf || sed -i 's###' /etc/nginx/conf.d/default.conf" ] }, + { + "exec": [ + "sed", + "-i", + "s#tab-signer-v2#tab-signer-v4#g; s#tab-signer-v3#tab-signer-v4#g", + "/etc/nginx/conf.d/default.conf" + ] + }, + { + "exec": [ + "sed", + "-i", + "s#src=\"/nostr-provider.js\"#src=\"/nostr-provider.js?v=tab-signer-v4\"#g", + "/etc/nginx/conf.d/default.conf" + ] + }, { "exec": [ "nginx", @@ -2608,22 +2768,30 @@ } ] }, - "id": "indeedhub", + "health_check": { + "type": "tcp", + "endpoint": "localhost:7777", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "30s" + }, "interfaces": { "main": { - "description": "Stream Bitcoin documentaries with Nostr identity", "name": "Web UI", - "path": "/", + "description": "Stream Bitcoin documentaries with Nostr identity", + "type": "ui", "port": 7778, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { "author": "Indeehub Team", "icon": "/assets/img/app-icons/indeedhub.png", - "license": "MIT", + "website": "https://indeedhub.com", "repo": "https://github.com/indeedhub/indeedhub", + "license": "MIT", "tags": [ "bitcoin", "documentary", @@ -2631,80 +2799,41 @@ "media", "education", "nostr" - ], - "website": "https://indeedhub.com" - }, - "name": "IndeeHub", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 7777, - "host": 7778, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [ - "CHOWN", - "DAC_OVERRIDE", - "SETGID", - "SETUID" - ], - "network_policy": "isolated", - "readonly_root": false - }, + ] + } + } + } + }, + "indeedhub-api": { + "version": "1.0.0", + "manifest": { + "app": { + "id": "indeedhub-api", + "name": "IndeedHub API", + "version": "1.0.0", "upstream": { "kind": "internal" }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw", - "nosuid", - "nodev", - "size=16m" - ], - "target": "/run", - "type": "tmpfs" - }, - { - "options": [ - "rw", - "nosuid", - "nodev", - "size=32m" - ], - "target": "/var/cache/nginx", - "type": "tmpfs" - } - ] - } - }, - "version": "1.0.0" - }, - "indeedhub-api": { - "manifest": { - "app": { + "description": "IndeedHub backend API (Nostr auth, media, payments).", "category": "community", + "container_name": "indeedhub-api", "container": { - "generated_secrets": [ - { - "kind": "hex32", - "name": "indeedhub-jwt" - } - ], "image": "source.archipelago-foundation.org/lfg2025/indeedhub-api:1.0.0", + "pull_policy": "if-not-present", "network": "indeedhub-net", "network_aliases": [ "api" ], - "pull_policy": "if-not-present", + "generated_secrets": [ + { + "name": "indeedhub-jwt", + "kind": "hex32" + }, + { + "name": "indeedhub-aes-master", + "kind": "hex16" + } + ], "secret_env": [ { "key": "DATABASE_PASSWORD", @@ -2717,10 +2846,13 @@ { "key": "NOSTR_JWT_SECRET", "secret_file": "indeedhub-jwt" + }, + { + "key": "AES_MASTER_SECRET", + "secret_file": "indeedhub-aes-master" } ] }, - "container_name": "indeedhub-api", "dependencies": [ { "app_id": "indeedhub-postgres" @@ -2732,7 +2864,16 @@ "app_id": "indeedhub-minio" } ], - "description": "IndeedHub backend API (Nostr auth, media, payments).", + "resources": { + "memory_limit": "2Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [], + "volumes": [], "environment": [ "PORT=4000", "DATABASE_HOST=postgres", @@ -2748,44 +2889,35 @@ "S3_PRIVATE_BUCKET_NAME=indeedhub-private", "S3_PUBLIC_BUCKET_URL=/storage", "NOSTR_JWT_EXPIRES_IN=7d", - "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef", "ENVIRONMENT=production" ], "health_check": { + "type": "tcp", "endpoint": "localhost:4000", "interval": "30s", - "retries": 10, "timeout": "5s", - "type": "tcp" - }, - "id": "indeedhub-api", - "name": "IndeedHub API", - "ports": [], - "resources": { - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, + "retries": 10 + } + } + } + }, + "indeedhub-ffmpeg": { + "version": "1.0.0", + "manifest": { + "app": { + "id": "indeedhub-ffmpeg", + "name": "IndeedHub FFmpeg Worker", + "version": "1.0.0", "upstream": { "kind": "internal" }, - "version": "1.0.0", - "volumes": [] - } - }, - "version": "1.0.0" - }, - "indeedhub-ffmpeg": { - "manifest": { - "app": { + "description": "IndeedHub background media transcoding worker.", "category": "community", + "container_name": "indeedhub-ffmpeg", "container": { "image": "source.archipelago-foundation.org/lfg2025/indeedhub-ffmpeg:1.0.0", - "network": "indeedhub-net", "pull_policy": "if-not-present", + "network": "indeedhub-net", "secret_env": [ { "key": "DATABASE_PASSWORD", @@ -2794,16 +2926,28 @@ { "key": "AWS_SECRET_KEY", "secret_file": "indeedhub-minio-password" + }, + { + "key": "AES_MASTER_SECRET", + "secret_file": "indeedhub-aes-master" } ] }, - "container_name": "indeedhub-ffmpeg", "dependencies": [ { "app_id": "indeedhub-api" } ], - "description": "IndeedHub background media transcoding worker.", + "resources": { + "memory_limit": "4Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [], + "volumes": [], "environment": [ "DATABASE_HOST=postgres", "DATABASE_PORT=5432", @@ -2816,50 +2960,42 @@ "AWS_ACCESS_KEY=indeeadmin", "S3_PUBLIC_BUCKET_NAME=indeedhub-public", "S3_PRIVATE_BUCKET_NAME=indeedhub-private", - "ENVIRONMENT=production", - "AES_MASTER_SECRET=0123456789abcdef0123456789abcdef" - ], - "id": "indeedhub-ffmpeg", - "name": "IndeedHub FFmpeg Worker", - "ports": [], - "resources": { - "memory_limit": "4Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "internal" - }, - "version": "1.0.0", - "volumes": [] + "ENVIRONMENT=production" + ] } - }, - "version": "1.0.0" + } }, "indeedhub-minio": { + "version": "RELEASE.2024-11-07T00-52-20Z", "manifest": { "app": { + "id": "indeedhub-minio", + "name": "IndeedHub MinIO", + "version": "RELEASE.2024-11-07T00-52-20Z", + "upstream": { + "kind": "github", + "repo": "minio/minio" + }, + "description": "MinIO S3-compatible object storage for IndeedHub media.", "category": "community", + "container_name": "indeedhub-minio", "container": { + "image": "source.archipelago-foundation.org/lfg2025/minio:RELEASE.2024-11-07T00-52-20Z", + "pull_policy": "if-not-present", + "network": "indeedhub-net", + "network_aliases": [ + "minio" + ], "custom_args": [ "server", "/data" ], "generated_secrets": [ { - "kind": "hex32", - "name": "indeedhub-minio-password" + "name": "indeedhub-minio-password", + "kind": "hex32" } ], - "image": "source.archipelago-foundation.org/lfg2025/minio:RELEASE.2024-11-07T00-52-20Z", - "network": "indeedhub-net", - "network_aliases": [ - "minio" - ], - "pull_policy": "if-not-present", "secret_env": [ { "key": "MINIO_ROOT_PASSWORD", @@ -2867,72 +3003,72 @@ } ] }, - "container_name": "indeedhub-minio", "dependencies": [ { "storage": "50Gi" } ], - "description": "MinIO S3-compatible object storage for IndeedHub media.", + "resources": { + "memory_limit": "1Gi", + "disk_limit": "50Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [], + "volumes": [ + { + "type": "volume", + "source": "indeedhub-minio-data", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MINIO_ROOT_USER=indeeadmin" ], "health_check": { + "type": "http", "endpoint": "http://localhost:9000", - "interval": "30s", "path": "/minio/health/live", - "retries": 5, + "interval": "30s", "timeout": "5s", - "type": "http" - }, - "id": "indeedhub-minio", - "name": "IndeedHub MinIO", - "ports": [], - "resources": { - "disk_limit": "50Gi", - "memory_limit": "1Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "minio/minio" - }, - "version": "RELEASE.2024-11-07T00-52-20Z", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "indeedhub-minio-data", - "target": "/data", - "type": "volume" - } - ] + "retries": 5 + } } - }, - "version": "RELEASE.2024-11-07T00-52-20Z" + } }, "indeedhub-postgres": { + "version": "16.13-alpine", "manifest": { "app": { + "id": "indeedhub-postgres", + "name": "IndeedHub Postgres", + "version": "16.13-alpine", + "upstream": { + "kind": "dockerhub", + "repo": "library/postgres" + }, + "description": "Postgres database backend for IndeedHub.", "category": "community", + "container_name": "indeedhub-postgres", "container": { - "generated_secrets": [ - { - "kind": "hex32", - "name": "indeedhub-db-password" - } - ], "image": "source.archipelago-foundation.org/lfg2025/postgres:16.13-alpine", + "pull_policy": "if-not-present", "network": "indeedhub-net", "network_aliases": [ "postgres" ], - "pull_policy": "if-not-present", + "generated_secrets": [ + { + "name": "indeedhub-db-password", + "kind": "hex32" + } + ], "secret_env": [ { "key": "POSTGRES_PASSWORD", @@ -2940,30 +3076,14 @@ } ] }, - "container_name": "indeedhub-postgres", "dependencies": [ { "storage": "10Gi" } ], - "description": "Postgres database backend for IndeedHub.", - "environment": [ - "POSTGRES_USER=indeedhub", - "POSTGRES_DB=indeedhub" - ], - "health_check": { - "endpoint": "localhost:5432", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "indeedhub-postgres", - "name": "IndeedHub Postgres", - "ports": [], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -2973,58 +3093,61 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/postgres" - }, - "version": "16.13-alpine", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "volume", "source": "indeedhub-postgres-data", "target": "/var/lib/postgresql/data", - "type": "volume" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "POSTGRES_USER=indeedhub", + "POSTGRES_DB=indeedhub" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:5432", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "16.13-alpine" + } }, "indeedhub-redis": { + "version": "7.4.8-alpine", "manifest": { "app": { + "id": "indeedhub-redis", + "name": "IndeedHub Redis", + "version": "7.4.8-alpine", + "upstream": { + "kind": "dockerhub", + "repo": "library/redis" + }, + "description": "Redis queue/cache backend for IndeedHub.", "category": "community", + "container_name": "indeedhub-redis", "container": { "image": "source.archipelago-foundation.org/lfg2025/redis:7.4.8-alpine", + "pull_policy": "if-not-present", "network": "indeedhub-net", "network_aliases": [ "redis" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "indeedhub-redis", "dependencies": [ { "storage": "1Gi" } ], - "description": "Redis queue/cache backend for IndeedHub.", - "environment": [], - "health_check": { - "endpoint": "localhost:6379", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "indeedhub-redis", - "name": "IndeedHub Redis", - "ports": [], "resources": { "memory_limit": "256Mi" }, @@ -3035,139 +3158,115 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/redis" - }, - "version": "7.4.8-alpine", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "volume", "source": "indeedhub-redis-data", "target": "/data", - "type": "volume" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:6379", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "7.4.8-alpine" + } }, "indeedhub-relay": { + "version": "0.9.0", "manifest": { "app": { + "id": "indeedhub-relay", + "name": "IndeedHub Nostr Relay", + "version": "0.9.0", + "upstream": { + "kind": "github", + "repo": "scsibug/nostr-rs-relay" + }, + "description": "nostr-rs-relay backing IndeedHub's Nostr identity + comments.", "category": "community", + "container_name": "indeedhub-relay", "container": { "image": "source.archipelago-foundation.org/lfg2025/nostr-rs-relay:0.10.0", + "pull_policy": "if-not-present", "network": "indeedhub-net", "network_aliases": [ "relay" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "indeedhub-relay", "dependencies": [ { "storage": "2Gi" } ], - "description": "nostr-rs-relay backing IndeedHub's Nostr identity + comments.", - "environment": [], - "health_check": { - "endpoint": "localhost:8080", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "indeedhub-relay", - "name": "IndeedHub Nostr Relay", - "ports": [], "resources": { - "disk_limit": "2Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "2Gi" }, "security": { "capabilities": [], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "scsibug/nostr-rs-relay" - }, - "version": "0.9.0", + "ports": [], "volumes": [ { - "options": [ - "rw" - ], + "type": "volume", "source": "indeedhub-relay-data", "target": "/usr/src/app/db", - "type": "volume" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8080", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "0.9.0" + } }, "jellyfin": { + "version": "10.11.11", "image": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11", "manifest": { "app": { + "id": "jellyfin", + "name": "Jellyfin", + "version": "10.8.13", + "upstream": { + "kind": "github", + "repo": "jellyfin/jellyfin" + }, + "description": "Free media server. Stream movies, music, and photos.", "container": { "image": "source.archipelago-foundation.org/lfg2025/jellyfin:10.11.11", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "10Gi" } ], - "description": "Free media server. Stream movies, music, and photos.", - "environment": [], - "health_check": { - "endpoint": "localhost:8096", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "jellyfin", - "interfaces": { - "main": { - "description": "Jellyfin media dashboard", - "name": "Web UI", - "path": "/", - "port": 8096, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Jellyfin", - "category": "data", - "icon": "/assets/img/app-icons/jellyfin.webp", - "repo": "https://github.com/jellyfin/jellyfin" - }, - "name": "Jellyfin", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8096, - "host": 8096, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -3177,61 +3276,93 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "jellyfin/jellyfin" - }, - "version": "10.8.13", + "ports": [ + { + "host": 8096, + "container": 8096, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/jellyfin/config", "target": "/config", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/jellyfin/cache", "target": "/cache", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:8096", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Jellyfin media dashboard", + "type": "ui", + "port": 8096, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/jellyfin.webp", + "category": "data", + "author": "Jellyfin", + "repo": "https://github.com/jellyfin/jellyfin" + } } - }, - "version": "10.11.11" + } }, "lnd": { + "version": "v0.21.2-beta", "image": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true + "id": "lnd", + "name": "LND", + "version": "0.21.2", + "upstream": { + "kind": "github", + "repo": "lightningnetwork/lnd" }, + "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", "container": { - "data_uid": "100000:100000", + "image": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta", + "pull_policy": "if-not-present", + "network": "archy-net", "derived_env": [ { "key": "BITCOIND_HOST", "template": "{{BITCOIN_HOST}}" } ], - "image": "source.archipelago-foundation.org/lfg2025/lnd:v0.21.2-beta", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BITCOIND_RPCPASS", "secret_file": "bitcoin-rpc-password" } - ] + ], + "data_uid": "100000:100000" }, "dependencies": [ { @@ -3239,51 +3370,10 @@ "version": ">=26.0" } ], - "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", - "environment": [ - "BITCOIND_RPCUSER=archipelago", - "NETWORK=mainnet" - ], - "health_check": { - "endpoint": "localhost:10009", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "lnd", - "lightning_integration": { - "channel_management": true, - "payment_routing": true - }, - "name": "LND", - "ports": [ - { - "auth": "none", - "auth_rationale": "Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself.", - "container": 9735, - "host": 9735, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "LND gRPC, authenticated by macaroon over TLS. Zeus and other remote wallets depend on reaching this directly.", - "container": 10009, - "host": 10009, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client.", - "container": 8080, - "host": 18080, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -3294,32 +3384,76 @@ "DAC_OVERRIDE", "NET_RAW" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "lightningnetwork/lnd" - }, - "version": "0.21.2", + "ports": [ + { + "host": 9735, + "container": 9735, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Lightning p2p. The BOLT-8 noise handshake authenticates and encrypts the channel itself." + }, + { + "host": 10009, + "container": 10009, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "LND gRPC, authenticated by macaroon over TLS. Zeus and other remote wallets depend on reaching this directly." + }, + { + "host": 18080, + "container": 8080, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "LND REST, authenticated by macaroon over TLS. A browser login page would break Zeus and every non-browser wallet client." + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/lnd", "target": "/root/.lnd", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "BITCOIND_RPCUSER=archipelago", + "NETWORK=mainnet" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:10009", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "bitcoin_integration": { + "rpc_access": "admin", + "sync_required": true + }, + "lightning_integration": { + "channel_management": true, + "payment_routing": true + } } - }, - "version": "v0.21.2-beta" + } }, "lnd-ui": { + "version": "1.7.123-alpha", "image": "source.archipelago-foundation.org/lfg2025/lnd-ui:1.7.123-alpha", "manifest": { "app": { + "id": "lnd-ui", + "name": "LND UI", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Archipelago-native HTTP frontend for LND. Runs nginx inside a\ncontainer and serves static assets. LND connection info is fetched\nvia an absolute URL that the host nginx routes to the archipelago\nbackend on 127.0.0.1:5678, so no upstream auth is baked in.\n", "container": { "build": { "context": "/opt/archipelago/docker/lnd-ui", @@ -3332,57 +3466,54 @@ "app_id": "lnd" } ], - "description": "Archipelago-native HTTP frontend for LND. Runs nginx inside a\ncontainer and serves static assets. LND connection info is fetched\nvia an absolute URL that the host nginx routes to the archipelago\nbackend on 127.0.0.1:5678, so no upstream auth is baked in.\n", - "environment": [], - "health_check": { - "endpoint": "http://127.0.0.1:18083", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "lnd-ui", - "name": "LND UI", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 18083, - "host": 18083, - "protocol": "tcp", - "session_passthrough": true - } - ], "resources": { "memory_limit": "64Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "upstream": { - "kind": "internal" - }, - "version": "1.0.0", - "volumes": [] + "ports": [ + { + "host": 18083, + "container": 18083, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated", + "session_passthrough": true + } + ], + "volumes": [], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:18083", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "1.7.123-alpha" + } }, "mempool": { + "version": "v3.3.1", "image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1", "images": { - "archy-mempool-db": "source.archipelago-foundation.org/lfg2025/mariadb:11.4.10", "archy-mempool-web": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1", - "mempool-api": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.3.1" + "mempool-api": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.3.1", + "archy-mempool-db": "source.archipelago-foundation.org/lfg2025/mariadb:11.4.10" }, "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "read-only", - "sync_required": true + "id": "mempool", + "name": "Mempool Explorer", + "version": "3.0.0", + "upstream": { + "kind": "github", + "repo": "mempool/mempool" }, + "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.", "container": { "image": "source.archipelago-foundation.org/lfg2025/mempool-frontend:v3.3.1", "image_signature": "cosign://...", @@ -3398,7 +3529,39 @@ }, "bitcoin:archival" ], - "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.", + "resources": { + "cpu_limit": 2, + "memory_limit": "2Gi", + "disk_limit": "20Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "mempool" + }, + "ports": [ + { + "host": 4080, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/mempool", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MEMPOOL_BACKEND=electrum", "MEMPOOL_BITCOIN_HOST=bitcoin-core", @@ -3407,75 +3570,42 @@ "MEMPOOL_BITCOIN_PASSWORD=${BITCOIN_RPC_PASSWORD}" ], "health_check": { + "type": "http", "endpoint": "http://localhost:4080", - "interval": "30s", "path": "/api/health", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" - }, - "id": "mempool", - "name": "Mempool Explorer", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 4080, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "apparmor_profile": "mempool", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 + "retries": 3 }, + "bitcoin_integration": { + "rpc_access": "read-only", + "sync_required": true + } + } + } + }, + "mempool-api": { + "version": "3.0.0", + "manifest": { + "app": { + "id": "mempool-api", + "name": "Mempool API", + "version": "3.0.0", "upstream": { "kind": "github", "repo": "mempool/mempool" }, - "version": "3.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/mempool", - "target": "/data", - "type": "bind" - } - ] - } - }, - "version": "v3.3.1" - }, - "mempool-api": { - "manifest": { - "app": { - "bitcoin_integration": { - "pruning_support": false, - "rpc_access": "read-only", - "sync_required": true - }, + "description": "Backend API for mempool explorer.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.3.1", + "pull_policy": "if-not-present", + "network": "archy-net", "derived_env": [ { "key": "CORE_RPC_HOST", "template": "{{BITCOIN_HOST}}" } ], - "image": "source.archipelago-foundation.org/lfg2025/mempool-backend:v3.3.1", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "CORE_RPC_PASSWORD", @@ -3502,7 +3632,34 @@ }, "bitcoin:archival" ], - "description": "Backend API for mempool explorer.", + "resources": { + "memory_limit": "2Gi", + "disk_limit": "20Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8999, + "container": 8999, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/mempool", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MEMPOOL_BACKEND=electrum", "ELECTRUM_HOST=electrumx", @@ -3516,68 +3673,46 @@ "DATABASE_USERNAME=mempool" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8999", - "interval": "30s", "path": "/api/v1/backend-info", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "mempool-api", - "name": "Mempool API", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 8999, - "host": 8999, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "mempool/mempool" - }, - "version": "3.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/mempool", - "target": "/data", - "type": "bind" - } - ] + "bitcoin_integration": { + "rpc_access": "read-only", + "sync_required": true, + "pruning_support": false + } } - }, - "version": "3.0.0" + } }, "netbird": { + "version": "2.38.0", "manifest": { "app": { + "id": "netbird", + "name": "NetBird", + "version": "2.38.0", + "upstream": { + "kind": "dockerhub", + "repo": "library/nginx" + }, + "description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point \u2014 a TLS proxy in front of the dashboard + server.", "category": "networking", + "container_name": "netbird", "container": { + "image": "docker.io/library/nginx:1.31.4-alpine", + "pull_policy": "if-not-present", + "network": "netbird-net", "generated_certs": [ { "crt": "/var/lib/archipelago/netbird/tls.crt", "key": "/var/lib/archipelago/netbird/tls.key" } - ], - "image": "docker.io/library/nginx:1.31.4-alpine", - "network": "netbird-net", - "pull_policy": "if-not-present" + ] }, - "container_name": "netbird", "dependencies": [ { "app_id": "netbird-server" @@ -3589,57 +3724,6 @@ "storage": "1Gi" } ], - "description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN. The user-facing entry point — a TLS proxy in front of the dashboard + server.", - "environment": [], - "files": [ - { - "content": "server {\n listen 443 ssl;\n server_name _;\n\n # netbird's dashboard needs a secure context (window.crypto.subtle for\n # OIDC PKCE), so the proxy terminates TLS with a self-signed cert (#15).\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n\n # Rootless Podman can hand a container a new IP across restarts/reboots.\n # nginx resolves a literal upstream name ONCE at startup and caches it,\n # so after the IP moves every request 502s with \"host unreachable\"\n # (issue #15, observed live on .198: nginx pinned to a dead\n # netbird-dashboard IP). Fix: point `resolver` at the netbird-net\n # gateway (Podman's aardvark DNS) and use VARIABLE upstreams, which\n # forces nginx to re-resolve the container names at request time.\n resolver {{NETWORK_GATEWAY}} valid=10s ipv6=off;\n\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_http_version 1.1;\n\n location ~ ^/(relay|ws-proxy/) {\n set $nb_server netbird-server;\n proxy_pass http://$nb_server:80;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n proxy_read_timeout 1d;\n }\n\n location ~ ^/(api|oauth2)(/|$) {\n # The dashboard is a SPA whose API/OIDC base URL is baked at build\n # time to one host:port. A single box is reached via several\n # addresses, so those fetches are cross-origin and the browser\n # blocks them with no Access-Control-Allow-Origin (#15, live on\n # .198). Reflect the caller's Origin and answer the CORS preflight.\n if ($request_method = OPTIONS) {\n add_header Access-Control-Allow-Origin $http_origin always;\n add_header Access-Control-Allow-Credentials true always;\n add_header Access-Control-Allow-Methods \"GET, POST, PUT, PATCH, DELETE, OPTIONS\" always;\n add_header Access-Control-Allow-Headers \"Authorization, Content-Type, Accept\" always;\n add_header Access-Control-Max-Age 86400 always;\n add_header Content-Length 0;\n return 204;\n }\n add_header Access-Control-Allow-Origin $http_origin always;\n add_header Access-Control-Allow-Credentials true always;\n add_header Access-Control-Allow-Methods \"GET, POST, PUT, PATCH, DELETE, OPTIONS\" always;\n add_header Access-Control-Allow-Headers \"Authorization, Content-Type, Accept\" always;\n set $nb_server netbird-server;\n proxy_pass http://$nb_server:80;\n }\n\n location ~ ^/(signalexchange\\.SignalExchange|management\\.ManagementService|management\\.ProxyService)/ {\n set $nb_server netbird-server;\n grpc_pass grpc://$nb_server:80;\n grpc_read_timeout 1d;\n grpc_send_timeout 1d;\n }\n\n # OIDC callback routes are client-side SPA routes with NO prebuilt page\n # in the dashboard bundle, so proxying them straight through 404s —\n # which crashes the dashboard's auth init and shows \"Unauthenticated\"\n # with dead buttons (#15, live on .198: /nb-auth + /nb-silent-auth\n # returned 404). Serve index.html at these paths (URL unchanged) so\n # react-oidc boots and completes the login / silent-SSO.\n location ~ ^/(nb-auth|nb-silent-auth) {\n set $nb_dashboard netbird-dashboard;\n rewrite ^.*$ /index.html break;\n proxy_pass http://$nb_dashboard:80;\n }\n\n location / {\n set $nb_dashboard netbird-dashboard;\n proxy_pass http://$nb_dashboard:80;\n }\n}\n", - "overwrite": true, - "path": "/var/lib/archipelago/netbird/nginx.conf" - } - ], - "health_check": { - "endpoint": "localhost:443", - "interval": "30s", - "retries": 5, - "start_period": "20s", - "timeout": "5s", - "type": "tcp" - }, - "id": "netbird", - "interfaces": { - "main": { - "description": "Manage your self-hosted NetBird mesh VPN", - "name": "Dashboard", - "path": "/", - "port": 8087, - "protocol": "https", - "type": "ui" - } - }, - "metadata": { - "author": "NetBird", - "icon": "/assets/img/app-icons/netbird.svg", - "license": "BSD-3-Clause", - "repo": "https://github.com/netbirdio/netbird", - "tags": [ - "networking", - "vpn", - "wireguard", - "mesh" - ], - "website": "https://netbird.io" - }, - "name": "NetBird", - "ports": [ - { - "auth": "none", - "auth_rationale": "NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP, so fronting this port would break the secure context the dashboard requires (issue #15) and the certificate clients pin.", - "container": 443, - "host": 8087, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "256Mi" }, @@ -3651,49 +3735,107 @@ "SETUID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/nginx" - }, - "version": "2.38.0", + "ports": [ + { + "host": 8087, + "container": 443, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "NetBird dashboard over TLS, with its own login. The gate speaks plain HTTP, so fronting this port would break the secure context the dashboard requires (issue #15) and the certificate clients pin." + } + ], "volumes": [ { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/netbird/nginx.conf", "target": "/etc/nginx/conf.d/default.conf", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/netbird/tls.crt", "target": "/etc/nginx/tls.crt", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/netbird/tls.key", "target": "/etc/nginx/tls.key", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "environment": [], + "files": [ + { + "path": "/var/lib/archipelago/netbird/nginx.conf", + "overwrite": true, + "content": "server {\n listen 443 ssl;\n server_name _;\n\n # netbird's dashboard needs a secure context (window.crypto.subtle for\n # OIDC PKCE), so the proxy terminates TLS with a self-signed cert (#15).\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n\n # Rootless Podman can hand a container a new IP across restarts/reboots.\n # nginx resolves a literal upstream name ONCE at startup and caches it,\n # so after the IP moves every request 502s with \"host unreachable\"\n # (issue #15, observed live on .198: nginx pinned to a dead\n # netbird-dashboard IP). Fix: point `resolver` at the netbird-net\n # gateway (Podman's aardvark DNS) and use VARIABLE upstreams, which\n # forces nginx to re-resolve the container names at request time.\n resolver {{NETWORK_GATEWAY}} valid=10s ipv6=off;\n\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n proxy_http_version 1.1;\n\n location ~ ^/(relay|ws-proxy/) {\n set $nb_server netbird-server;\n proxy_pass http://$nb_server:80;\n proxy_set_header Upgrade $http_upgrade;\n proxy_set_header Connection \"upgrade\";\n proxy_read_timeout 1d;\n }\n\n location ~ ^/(api|oauth2)(/|$) {\n # The dashboard is a SPA whose API/OIDC base URL is baked at build\n # time to one host:port. A single box is reached via several\n # addresses, so those fetches are cross-origin and the browser\n # blocks them with no Access-Control-Allow-Origin (#15, live on\n # .198). Reflect the caller's Origin and answer the CORS preflight.\n if ($request_method = OPTIONS) {\n add_header Access-Control-Allow-Origin $http_origin always;\n add_header Access-Control-Allow-Credentials true always;\n add_header Access-Control-Allow-Methods \"GET, POST, PUT, PATCH, DELETE, OPTIONS\" always;\n add_header Access-Control-Allow-Headers \"Authorization, Content-Type, Accept\" always;\n add_header Access-Control-Max-Age 86400 always;\n add_header Content-Length 0;\n return 204;\n }\n add_header Access-Control-Allow-Origin $http_origin always;\n add_header Access-Control-Allow-Credentials true always;\n add_header Access-Control-Allow-Methods \"GET, POST, PUT, PATCH, DELETE, OPTIONS\" always;\n add_header Access-Control-Allow-Headers \"Authorization, Content-Type, Accept\" always;\n set $nb_server netbird-server;\n proxy_pass http://$nb_server:80;\n }\n\n location ~ ^/(signalexchange\\.SignalExchange|management\\.ManagementService|management\\.ProxyService)/ {\n set $nb_server netbird-server;\n grpc_pass grpc://$nb_server:80;\n grpc_read_timeout 1d;\n grpc_send_timeout 1d;\n }\n\n # OIDC callback routes are client-side SPA routes with NO prebuilt page\n # in the dashboard bundle, so proxying them straight through 404s \u2014\n # which crashes the dashboard's auth init and shows \"Unauthenticated\"\n # with dead buttons (#15, live on .198: /nb-auth + /nb-silent-auth\n # returned 404). Serve index.html at these paths (URL unchanged) so\n # react-oidc boots and completes the login / silent-SSO.\n location ~ ^/(nb-auth|nb-silent-auth) {\n set $nb_dashboard netbird-dashboard;\n rewrite ^.*$ /index.html break;\n proxy_pass http://$nb_dashboard:80;\n }\n\n location / {\n set $nb_dashboard netbird-dashboard;\n proxy_pass http://$nb_dashboard:80;\n }\n}\n" + } + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:443", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "20s" + }, + "interfaces": { + "main": { + "name": "Dashboard", + "description": "Manage your self-hosted NetBird mesh VPN", + "type": "ui", + "port": 8087, + "protocol": "https", + "path": "/" + } + }, + "metadata": { + "author": "NetBird", + "icon": "/assets/img/app-icons/netbird.svg", + "website": "https://netbird.io", + "repo": "https://github.com/netbirdio/netbird", + "license": "BSD-3-Clause", + "tags": [ + "networking", + "vpn", + "wireguard", + "mesh" + ] + } } - }, - "version": "2.38.0" + } }, "netbird-dashboard": { + "version": "2.38.0", "manifest": { "app": { + "id": "netbird-dashboard", + "name": "NetBird Dashboard", + "version": "2.38.0", + "upstream": { + "kind": "github", + "repo": "netbirdio/dashboard" + }, + "description": "NetBird management dashboard (SPA). Internal stack member served through the netbird proxy.", "category": "networking", + "container_name": "netbird-dashboard", "container": { + "image": "docker.io/netbirdio/dashboard:v2.38.0", + "pull_policy": "if-not-present", + "network": "netbird-net", + "network_aliases": [ + "netbird-dashboard" + ], "derived_env": [ { "key": "NETBIRD_MGMT_API_ENDPOINT", @@ -3707,21 +3849,29 @@ "key": "AUTH_AUTHORITY", "template": "https://{{HOST_IP}}:8087/oauth2" } - ], - "image": "docker.io/netbirdio/dashboard:v2.38.0", - "network": "netbird-net", - "network_aliases": [ - "netbird-dashboard" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "netbird-dashboard", "dependencies": [ { "app_id": "netbird-server" } ], - "description": "NetBird management dashboard (SPA). Internal stack member served through the netbird proxy.", + "resources": { + "memory_limit": "256Mi" + }, + "security": { + "capabilities": [ + "CHOWN", + "DAC_OVERRIDE", + "SETGID", + "SETUID", + "NET_BIND_SERVICE" + ], + "readonly_root": false, + "network_policy": "isolated" + }, + "ports": [], + "volumes": [], "environment": [ "AUTH_AUDIENCE=netbird-dashboard", "AUTH_CLIENT_ID=netbird-dashboard", @@ -3735,132 +3885,69 @@ "LETSENCRYPT_DOMAIN=none" ], "health_check": { + "type": "tcp", "endpoint": "localhost:80", "interval": "30s", - "retries": 5, - "start_period": "20s", "timeout": "5s", - "type": "tcp" + "retries": 5, + "start_period": "20s" }, - "id": "netbird-dashboard", "metadata": { "author": "NetBird", "icon": "/assets/img/app-icons/netbird.svg", - "license": "BSD-3-Clause", + "website": "https://netbird.io", "repo": "https://github.com/netbirdio/dashboard", + "license": "BSD-3-Clause", "tags": [ "networking", "vpn", "dashboard" - ], - "website": "https://netbird.io" - }, - "name": "NetBird Dashboard", - "ports": [], - "resources": { - "memory_limit": "256Mi" - }, - "security": { - "capabilities": [ - "CHOWN", - "DAC_OVERRIDE", - "SETGID", - "SETUID", - "NET_BIND_SERVICE" - ], - "network_policy": "isolated", - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "netbirdio/dashboard" - }, - "version": "2.38.0", - "volumes": [] + ] + } } - }, - "version": "2.38.0" + } }, "netbird-server": { + "version": "0.71.2", "manifest": { "app": { + "id": "netbird-server", + "name": "NetBird Server", + "version": "0.71.2", + "upstream": { + "kind": "github", + "repo": "netbirdio/netbird" + }, + "description": "NetBird combined management / signal / relay server with an embedded identity provider and STUN. Backend for the self-hosted NetBird mesh VPN.", "category": "networking", + "container_name": "netbird-server", "container": { - "custom_args": [ - "--config", - "/etc/netbird/config.yaml" - ], - "generated_secrets": [ - { - "kind": "base64", - "name": "netbird-relay-auth-secret" - }, - { - "kind": "base64", - "name": "netbird-store-encryption-key" - } - ], "image": "docker.io/netbirdio/netbird-server:0.71.2", + "pull_policy": "if-not-present", "network": "netbird-net", "network_aliases": [ "netbird-server" ], - "pull_policy": "if-not-present" + "generated_secrets": [ + { + "name": "netbird-relay-auth-secret", + "kind": "base64" + }, + { + "name": "netbird-store-encryption-key", + "kind": "base64" + } + ], + "custom_args": [ + "--config", + "/etc/netbird/config.yaml" + ] }, - "container_name": "netbird-server", "dependencies": [ { "storage": "1Gi" } ], - "description": "NetBird combined management / signal / relay server with an embedded identity provider and STUN. Backend for the self-hosted NetBird mesh VPN.", - "environment": [], - "files": [ - { - "content": "server:\n listenAddress: \":80\"\n exposedAddress: \"https://{{HOST_IP}}:8087\"\n stunPorts:\n - 3478\n metricsPort: 9090\n healthcheckAddress: \":9000\"\n logLevel: \"info\"\n logFile: \"console\"\n authSecret: \"{{secret:netbird-relay-auth-secret}}\"\n dataDir: \"/var/lib/netbird\"\n auth:\n issuer: \"https://{{HOST_IP}}:8087/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n dashboardRedirectURIs:\n - \"https://{{HOST_IP}}:8087/nb-auth\"\n - \"https://{{HOST_IP}}:8087/nb-silent-auth\"\n dashboardPostLogoutRedirectURIs:\n - \"https://{{HOST_IP}}:8087/\"\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n store:\n engine: \"sqlite\"\n encryptionKey: \"{{secret:netbird-store-encryption-key}}\"\n", - "overwrite": true, - "path": "/var/lib/archipelago/netbird/config.yaml" - } - ], - "health_check": { - "endpoint": "localhost:80", - "interval": "30s", - "retries": 10, - "start_period": "30s", - "timeout": "5s", - "type": "tcp" - }, - "id": "netbird-server", - "metadata": { - "author": "NetBird", - "icon": "/assets/img/app-icons/netbird.svg", - "license": "BSD-3-Clause", - "repo": "https://github.com/netbirdio/netbird", - "tags": [ - "networking", - "vpn", - "wireguard", - "mesh" - ], - "website": "https://netbird.io" - }, - "name": "NetBird Server", - "ports": [ - { - "auth": "none", - "auth_rationale": "NetBird management API and its OIDC issuer. Enrolled devices authenticate themselves with setup keys and JWTs, and they cannot hold a browser session — a login page here would disconnect every VPN client on the network.", - "container": 80, - "host": 8086, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "STUN over UDP for NAT traversal; it must answer unauthenticated probes to do its job at all.", - "container": 3478, - "host": 3478, - "protocol": "udp" - } - ], "resources": { "memory_limit": "1Gi" }, @@ -3868,92 +3955,101 @@ "capabilities": [ "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "netbirdio/netbird" - }, - "version": "0.71.2", - "volumes": [ + "ports": [ { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/netbird/data", - "target": "/var/lib/netbird", - "type": "bind" + "host": 8086, + "container": 80, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "NetBird management API and its OIDC issuer. Enrolled devices authenticate themselves with setup keys and JWTs, and they cannot hold a browser session \u2014 a login page here would disconnect every VPN client on the network." }, { + "host": 3478, + "container": 3478, + "protocol": "udp", + "auth": "none", + "auth_rationale": "STUN over UDP for NAT traversal; it must answer unauthenticated probes to do its job at all." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/netbird/data", + "target": "/var/lib/netbird", "options": [ - "ro" - ], + "rw" + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/netbird/config.yaml", "target": "/etc/netbird/config.yaml", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "environment": [], + "files": [ + { + "path": "/var/lib/archipelago/netbird/config.yaml", + "overwrite": true, + "content": "server:\n listenAddress: \":80\"\n exposedAddress: \"https://{{HOST_IP}}:8087\"\n stunPorts:\n - 3478\n metricsPort: 9090\n healthcheckAddress: \":9000\"\n logLevel: \"info\"\n logFile: \"console\"\n authSecret: \"{{secret:netbird-relay-auth-secret}}\"\n dataDir: \"/var/lib/netbird\"\n auth:\n issuer: \"https://{{HOST_IP}}:8087/oauth2\"\n localAuthDisabled: false\n signKeyRefreshEnabled: false\n dashboardRedirectURIs:\n - \"https://{{HOST_IP}}:8087/nb-auth\"\n - \"https://{{HOST_IP}}:8087/nb-silent-auth\"\n dashboardPostLogoutRedirectURIs:\n - \"https://{{HOST_IP}}:8087/\"\n cliRedirectURIs:\n - \"http://localhost:53000/\"\n store:\n engine: \"sqlite\"\n encryptionKey: \"{{secret:netbird-store-encryption-key}}\"\n" + } + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:80", + "interval": "30s", + "timeout": "5s", + "retries": 10, + "start_period": "30s" + }, + "metadata": { + "author": "NetBird", + "icon": "/assets/img/app-icons/netbird.svg", + "website": "https://netbird.io", + "repo": "https://github.com/netbirdio/netbird", + "license": "BSD-3-Clause", + "tags": [ + "networking", + "vpn", + "wireguard", + "mesh" + ] + } } - }, - "version": "0.71.2" + } }, "nextcloud": { + "version": "29", "image": "source.archipelago-foundation.org/lfg2025/nextcloud:29", "manifest": { "app": { + "id": "nextcloud", + "name": "Nextcloud", + "version": "29", + "upstream": { + "kind": "github", + "repo": "nextcloud/server" + }, + "description": "Your own private cloud. File sync, calendars, contacts.", "container": { "image": "source.archipelago-foundation.org/lfg2025/nextcloud:29", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "10Gi" } ], - "description": "Your own private cloud. File sync, calendars, contacts.", - "environment": [], - "health_check": { - "endpoint": "localhost:80", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "nextcloud", - "interfaces": { - "main": { - "description": "Nextcloud file and collaboration dashboard", - "name": "Web UI", - "path": "/", - "port": 8085, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Nextcloud", - "category": "data", - "icon": "/assets/img/app-icons/nextcloud.webp", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/nextcloud/server" - }, - "name": "Nextcloud", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 80, - "host": 8085, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -3963,60 +4059,141 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "nextcloud/server" - }, - "version": "29", + "ports": [ + { + "host": 8085, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/nextcloud", "target": "/var/www/html", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:80", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Nextcloud file and collaboration dashboard", + "type": "ui", + "port": 8085, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/nextcloud.webp", + "category": "data", + "author": "Nextcloud", + "repo": "https://github.com/nextcloud/server", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "29" + } }, "nginx-proxy-manager": { + "version": "latest", "image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest", "manifest": { "app": { + "id": "nginx-proxy-manager", + "name": "Nginx Proxy Manager", + "version": "2.12.1", + "upstream": { + "kind": "github", + "repo": "NginxProxyManager/nginx-proxy-manager" + }, + "description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration \u2014 the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).", "container": { "image": "source.archipelago-foundation.org/lfg2025/nginx-proxy-manager:latest", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Reverse proxy with SSL. Beautiful web interface for managing proxies. On a node, this manages its admin UI and upstream configuration — the proxy's own :80/:443 listeners are not published (the node's web server owns those ports).", + "resources": { + "memory_limit": "512Mi", + "disk_limit": "1Gi" + }, + "security": { + "capabilities": [ + "CHOWN", + "SETUID", + "SETGID", + "DAC_OVERRIDE", + "NET_BIND_SERVICE" + ], + "readonly_root": false, + "no_new_privileges": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8081, + "container": 81, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "open", + "auth_rationale": "Nginx Proxy Manager enforces its own admin account on every page; the initial setup wizard also has to answer before any account exists." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/nginx-proxy-manager", + "target": "/data", + "options": [ + "rw" + ] + }, + { + "type": "bind", + "source": "/var/lib/archipelago/nginx-proxy-manager/letsencrypt", + "target": "/etc/letsencrypt", + "options": [ + "rw" + ] + } + ], "environment": [], "health_check": { + "type": "tcp", "endpoint": "localhost:81", "interval": "30s", - "retries": 3, "timeout": "5s", - "type": "tcp" + "retries": 3 }, - "id": "nginx-proxy-manager", "interfaces": { "main": { - "description": "Nginx Proxy Manager admin interface", "name": "Admin UI", - "path": "/", + "description": "Nginx Proxy Manager admin interface", + "type": "ui", "port": 8081, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { @@ -4025,77 +4202,67 @@ "icon": "/assets/img/app-icons/nginx.svg", "repo": "https://github.com/NginxProxyManager/nginx-proxy-manager", "tier": "optional" - }, - "name": "Nginx Proxy Manager", - "ports": [ - { - "auth": "open", - "auth_rationale": "Nginx Proxy Manager enforces its own admin account on every page; the initial setup wizard also has to answer before any account exists.", - "bind": "127.0.0.1", - "container": 81, - "host": 8081, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [ - "CHOWN", - "SETUID", - "SETGID", - "DAC_OVERRIDE", - "NET_BIND_SERVICE" - ], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "NginxProxyManager/nginx-proxy-manager" - }, - "version": "2.12.1", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/nginx-proxy-manager", - "target": "/data", - "type": "bind" - }, - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/nginx-proxy-manager/letsencrypt", - "target": "/etc/letsencrypt", - "type": "bind" - } - ] + } } - }, - "version": "latest" + } }, "nostr-rs-relay": { + "version": "0.10.0", "image": "source.archipelago-foundation.org/lfg2025/nostr-rs-relay:0.10.0", "manifest": { "app": { + "id": "nostr-rs-relay", + "name": "Nostr Relay (Rust)", + "version": "0.10.0", + "upstream": { + "kind": "github", + "repo": "scsibug/nostr-rs-relay" + }, + "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.", "container": { - "data_uid": "1000:1000", "image": "scsibug/nostr-rs-relay:0.10.0", "image_signature": "cosign://...", - "pull_policy": "verify-signature" + "pull_policy": "verify-signature", + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "10Gi" } ], - "description": "High-performance Nostr relay written in Rust. Host your own decentralized social media relay and earn networking profits.", + "resources": { + "cpu_limit": 2, + "memory_limit": "1Gi", + "disk_limit": "10Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "nostr-relay" + }, + "ports": [ + { + "host": 18081, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/nostr-relay", + "target": "/usr/src/app/db", + "options": [ + "rw" + ] + } + ], "environment": [ "RELAY_NAME=Archipelago Nostr Relay", "RELAY_DESCRIPTION=Self-hosted Nostr relay on Archipelago", @@ -4103,213 +4270,181 @@ "MAX_SUBSCRIPTIONS=100" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8080", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" + "retries": 5 }, - "id": "nostr-rs-relay", - "name": "Nostr Relay (Rust)", "nostr_integration": { - "event_storage": "sqlite", + "relay_type": "public", "monetization_enabled": true, - "relay_type": "public" - }, - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 18081, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "1Gi" - }, - "security": { - "apparmor_profile": "nostr-relay", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "upstream": { - "kind": "github", - "repo": "scsibug/nostr-rs-relay" - }, - "version": "0.10.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/nostr-relay", - "target": "/usr/src/app/db", - "type": "bind" - } - ] + "event_storage": "sqlite" + } } - }, - "version": "0.10.0" + } }, "ollama": { + "version": "latest", "image": "source.archipelago-foundation.org/lfg2025/ollama:latest", "manifest": { "app": { + "id": "ollama", + "name": "Ollama", + "version": "0.5.4", + "upstream": { + "kind": "github", + "repo": "ollama/ollama" + }, + "description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware \u2014 served on the node's loopback for the AI assistant (Settings \u2192 Claude Auth \u2192 model backend), never exposed to the network.", "container": { "image": "source.archipelago-foundation.org/lfg2025/ollama:latest", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "50Gi" } ], - "description": "Run large language models locally. Download and run AI models like Llama, Mistral on your own hardware — served on the node's loopback for the AI assistant (Settings → Claude Auth → model backend), never exposed to the network.", - "environment": [], - "health_check": { - "endpoint": "localhost:11434", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "ollama", - "metadata": { - "author": "Ollama", - "category": "community", - "icon": "/assets/img/app-icons/ollama.png", - "repo": "https://github.com/ollama/ollama", - "tier": "optional" - }, - "name": "Ollama", - "ports": [ - { - "auth": "local", - "bind": "127.0.0.1", - "container": 11434, - "host": 11434, - "protocol": "tcp" - } - ], "resources": { "disk_limit": "50Gi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "ollama/ollama" - }, - "version": "0.5.4", + "ports": [ + { + "host": 11434, + "container": 11434, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "local" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/ollama", "target": "/root/.ollama", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:11434", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "metadata": { + "author": "Ollama", + "category": "community", + "icon": "/assets/img/app-icons/ollama.png", + "repo": "https://github.com/ollama/ollama", + "tier": "optional" + } } - }, - "version": "latest" + } }, "phoenixd": { + "version": "0.9.0", "manifest": { "app": { + "id": "phoenixd", + "name": "phoenixd", + "version": "0.9.0", + "upstream": { + "kind": "github", + "repo": "ACINQ/phoenixd" + }, + "description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own \u2014 it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.", "category": "money", "container": { - "data_uid": "1000:1000", "image": "source.archipelago-foundation.org/lfg2025/phoenixd:0.9.0", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "500Mi" } ], - "description": "Headless Lightning daemon by ACINQ (the Phoenix wallet team). No screen of its own — it exposes a small local API that other apps and tools use to send and receive Lightning payments. Channel liquidity is managed automatically for a fee.", + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "1Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "network_policy": "bridge" + }, + "ports": [ + { + "host": 9740, + "container": 9740, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "none", + "auth_rationale": "Loopback-only JSON API, not a web page. Every request is authenticated by the http password phoenixd generates in its own data directory on first run; the app gate's browser login page would break the API clients this port exists for." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/phoenixd", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "PHOENIX_DATADIR=/data" ], "health_check": { + "type": "tcp", "endpoint": "localhost:9740", "interval": "30s", - "retries": 5, "timeout": "5s", - "type": "tcp" + "retries": 5 }, - "id": "phoenixd", "metadata": { + "icon": "/assets/img/app-icons/phoenixd.svg", + "repo": "https://github.com/ACINQ/phoenixd", + "tier": "optional", "features": [ - "Ultra-light Lightning node — no bitcoin node required", + "Ultra-light Lightning node \u2014 no bitcoin node required", "Automated channel and liquidity management (fees apply)", "Simple HTTP API + websockets for payments", "Backed by the team behind the Phoenix mobile wallet" - ], - "icon": "/assets/img/app-icons/phoenixd.svg", - "repo": "https://github.com/ACINQ/phoenixd", - "tier": "optional" - }, - "name": "phoenixd", - "ports": [ - { - "auth": "none", - "auth_rationale": "Loopback-only JSON API, not a web page. Every request is authenticated by the http password phoenixd generates in its own data directory on first run; the app gate's browser login page would break the API clients this port exists for.", - "bind": "127.0.0.1", - "container": 9740, - "host": 9740, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 1, - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [], - "network_policy": "bridge", - "no_new_privileges": true, - "readonly_root": true - }, - "upstream": { - "kind": "github", - "repo": "ACINQ/phoenixd" - }, - "version": "0.9.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/phoenixd", - "target": "/data", - "type": "bind" - } - ] + ] + } } - }, - "version": "0.9.0" + } }, "photoprism": { + "version": "240915", "image": "source.archipelago-foundation.org/lfg2025/photoprism:240915", "manifest": { "app": { + "id": "photoprism", + "name": "PhotoPrism", + "version": "240915", + "upstream": { + "kind": "github", + "repo": "photoprism/photoprism" + }, + "description": "AI-powered photo management with facial recognition.", "container": { "image": "source.archipelago-foundation.org/lfg2025/photoprism:240915", "pull_policy": "if-not-present" @@ -4319,51 +4454,9 @@ "storage": "10Gi" } ], - "description": "AI-powered photo management with facial recognition.", - "environment": [ - "PHOTOPRISM_ADMIN_PASSWORD=archipelago", - "PHOTOPRISM_DEFAULT_LOCALE=en" - ], - "health_check": { - "endpoint": "localhost:2342", - "interval": "60s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "photoprism", - "interfaces": { - "main": { - "description": "PhotoPrism photo library", - "name": "Web UI", - "path": "/", - "port": 2342, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "PhotoPrism", - "category": "data", - "icon": "/assets/img/app-icons/photoprism.svg", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/photoprism/photoprism" - }, - "name": "PhotoPrism", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 2342, - "host": 2342, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -4371,47 +4464,89 @@ "SETUID", "SETGID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "photoprism/photoprism" - }, - "version": "240915", + "ports": [ + { + "host": 2342, + "container": 2342, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/photoprism", "target": "/photoprism/storage", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "PHOTOPRISM_ADMIN_PASSWORD=archipelago", + "PHOTOPRISM_DEFAULT_LOCALE=en" + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:2342", + "interval": "60s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "PhotoPrism photo library", + "type": "ui", + "port": 2342, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/photoprism.svg", + "category": "data", + "author": "PhotoPrism", + "repo": "https://github.com/photoprism/photoprism", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "240915" + } }, "pine": { + "version": "1.3.0", "manifest": { "app": { + "id": "pine", + "name": "Pine", + "version": "1.3.0", + "upstream": { + "kind": "dockerhub", + "repo": "library/nginx" + }, + "description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node \u2014 block height, sync, peers, Lightning balance \u2014 and, when a Claude API key is set, anything else.", "category": "home", + "container_name": "pine", "container": { + "image": "docker.io/library/nginx:1.31.4-alpine", + "pull_policy": "if-not-present", + "network": "archy-net", + "network_aliases": [ + "pine" + ], "generated_certs": [ { "crt": "/var/lib/archipelago/pine/tls.crt", "key": "/var/lib/archipelago/pine/tls.key" } - ], - "image": "docker.io/library/nginx:1.31.4-alpine", - "network": "archy-net", - "network_aliases": [ - "pine" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "pine", "dependencies": [ { "app_id": "pine-whisper" @@ -4426,73 +4561,6 @@ "storage": "128Mi" } ], - "description": "A private voice assistant for your home. Pine runs speech-to-text (Whisper), text-to-speech (Piper) and wake-word detection (openWakeWord) on your own node and pairs with a PineVoice satellite speaker, so Home Assistant Assist works locally with nothing sent to the cloud. Ask it about your node — block height, sync, peers, Lightning balance — and, when a Claude API key is set, anything else.", - "environment": [], - "files": [ - { - "content": "server {\n listen 80;\n server_name _;\n return 301 https://$host:10381$request_uri;\n}\nserver {\n listen 443 ssl;\n server_name _;\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n root /usr/share/nginx/html;\n index index.html;\n # Live node facts for the status card — proxied to the node's\n # public status tier so the (https) page can fetch same-origin.\n location = /node-status {\n proxy_pass http://host.containers.internal:80/api/pine/status;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_connect_timeout 5s;\n proxy_read_timeout 10s;\n }\n location / { try_files $uri $uri/ /index.html; }\n}\n", - "overwrite": true, - "path": "/var/lib/archipelago/pine/nginx.conf" - }, - { - "content": "\n\n\n \n \n Pine — connect your speaker\n \n\n\n
\n
\n \n \n \n \n \n \n

Pine

\n

Connect your speaker — everything stays on your node.

\n
\n\n
\n
Whisperspeech-to-text ready on :10300
\n
Pipertext-to-speech ready on :10200
\n
Wake word“Hey Jarvis” on the speaker (openWakeWord on :10400)
\n
Speakerput it in pairing mode — ring LED blinking yellow
\n
\n\n
\n
Nodechecking…
\n
Bitcoin—
\n
Peers—
\n
\n\n
\n This page isn’t running over HTTPS, so the browser blocks Bluetooth.\n Open it via its https://…:10380 address (accept the self-signed\n certificate) and the button below will work.\n
\n\n
\n \n \n \n \n \n
Ready. Click the button, then pick “PineVoice” in the Bluetooth popup.
\n
\n\n

After WiFi joins, one manual step remains — pair the\n speaker in Home Assistant: Settings → Devices & services →\n Add Wyoming Protocol, host = the speaker’s IP, port\n 10700. Whisper, Piper, openWakeWord and the Assist pipeline\n are wired up automatically when Pine installs. Wake word:\n “Hey Jarvis.” Ask node things like “what’s the block\n height?”, “how many peers?”, “is the node\n synced?” or “what’s my lightning balance?” — and when a\n Claude API key is set on the node, anything else gets answered by\n Claude. New mesh messages are announced on the speaker too.

\n

Troubleshooting: if it hears you (LED reacts) but answers\n are silent, unplug and replug the speaker — an interrupted answer can\n wedge its audio output until it reboots.

\n
\n\n \n\n\n", - "overwrite": true, - "path": "/var/lib/archipelago/pine/index.html" - } - ], - "health_check": { - "endpoint": "localhost:443", - "interval": "30s", - "retries": 5, - "start_period": "10s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine", - "interfaces": { - "main": { - "description": "Connect your speaker to WiFi and check the voice assistant", - "name": "Pine", - "path": "/", - "port": 10380, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Archipelago", - "category": "home", - "icon": "/assets/img/app-icons/pine.svg", - "launch": { - "open_in_new_tab": true - }, - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming", - "tags": [ - "home", - "voice", - "assistant", - "privacy" - ], - "website": "https://github.com/rhasspy/wyoming" - }, - "name": "Pine", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 80, - "host": 10380, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would break the secure context navigator.bluetooth needs for WiFi provisioning. The plain-HTTP entry point (10380) is gated, and it is what the UI opens.", - "container": 443, - "host": 10381, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "64Mi" }, @@ -4504,228 +4572,302 @@ "SETUID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "library/nginx" - }, - "version": "1.3.0", + "ports": [ + { + "host": 10380, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + }, + { + "host": 10381, + "container": 443, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Pine's TLS listener. The gate speaks plain HTTP, so fronting this port would break the secure context navigator.bluetooth needs for WiFi provisioning. The plain-HTTP entry point (10380) is gated, and it is what the UI opens." + } + ], "volumes": [ { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/pine/nginx.conf", "target": "/etc/nginx/conf.d/default.conf", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/tls.crt", "target": "/etc/nginx/tls.crt", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/tls.key", "target": "/etc/nginx/tls.key", - "type": "bind" - }, - { "options": [ "ro" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/pine/index.html", "target": "/usr/share/nginx/html/index.html", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "environment": [], + "files": [ + { + "path": "/var/lib/archipelago/pine/nginx.conf", + "overwrite": true, + "content": "server {\n listen 80;\n server_name _;\n return 301 https://$host:10381$request_uri;\n}\nserver {\n listen 443 ssl;\n server_name _;\n ssl_certificate /etc/nginx/tls.crt;\n ssl_certificate_key /etc/nginx/tls.key;\n root /usr/share/nginx/html;\n index index.html;\n # Live node facts for the status card \u2014 proxied to the node's\n # public status tier so the (https) page can fetch same-origin.\n location = /node-status {\n proxy_pass http://host.containers.internal:80/api/pine/status;\n proxy_http_version 1.1;\n proxy_set_header Host $host;\n proxy_connect_timeout 5s;\n proxy_read_timeout 10s;\n }\n location / { try_files $uri $uri/ /index.html; }\n}\n" + }, + { + "path": "/var/lib/archipelago/pine/index.html", + "overwrite": true, + "content": "\n\n\n \n \n Pine \u2014 connect your speaker\n \n\n\n
\n
\n \n \n \n \n \n \n

Pine

\n

Connect your speaker \u2014 everything stays on your node.

\n
\n\n
\n
Whisperspeech-to-text ready on :10300
\n
Pipertext-to-speech ready on :10200
\n
Wake word\u201cHey Jarvis\u201d on the speaker (openWakeWord on :10400)
\n
Speakerput it in pairing mode \u2014 ring LED blinking yellow
\n
\n\n
\n
Nodechecking\u2026
\n
Bitcoin\u2014
\n
Peers\u2014
\n
\n\n
\n This page isn\u2019t running over HTTPS, so the browser blocks Bluetooth.\n Open it via its https://\u2026:10380 address (accept the self-signed\n certificate) and the button below will work.\n
\n\n
\n \n \n \n \n \n
Ready. Click the button, then pick \u201cPineVoice\u201d in the Bluetooth popup.
\n
\n\n

After WiFi joins, one manual step remains \u2014 pair the\n speaker in Home Assistant: Settings \u2192 Devices & services \u2192\n Add Wyoming Protocol, host = the speaker\u2019s IP, port\n 10700. Whisper, Piper, openWakeWord and the Assist pipeline\n are wired up automatically when Pine installs. Wake word:\n \u201cHey Jarvis.\u201d Ask node things like \u201cwhat\u2019s the block\n height?\u201d, \u201chow many peers?\u201d, \u201cis the node\n synced?\u201d or \u201cwhat\u2019s my lightning balance?\u201d \u2014 and when a\n Claude API key is set on the node, anything else gets answered by\n Claude. New mesh messages are announced on the speaker too.

\n

Troubleshooting: if it hears you (LED reacts) but answers\n are silent, unplug and replug the speaker \u2014 an interrupted answer can\n wedge its audio output until it reboots.

\n
\n\n \n\n\n" + } + ], + "health_check": { + "type": "tcp", + "endpoint": "localhost:443", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "10s" + }, + "interfaces": { + "main": { + "name": "Pine", + "description": "Connect your speaker to WiFi and check the voice assistant", + "type": "ui", + "port": 10380, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "author": "Archipelago", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming", + "repo": "https://github.com/rhasspy/wyoming", + "license": "MIT", + "category": "home", + "launch": { + "open_in_new_tab": true + }, + "tags": [ + "home", + "voice", + "assistant", + "privacy" + ] + } } - }, - "version": "1.3.0" + } }, "pine-openwakeword": { + "version": "2.1.0", "manifest": { "app": { + "id": "pine-openwakeword", + "name": "Pine Wake Word (openWakeWord)", + "version": "2.1.0", + "upstream": { + "kind": "github", + "repo": "rhasspy/wyoming-openwakeword" + }, + "description": "Wyoming-protocol openWakeWord wake-word engine. Internal Pine voice-assistant stack member \u2014 lets Assist pipelines run wake-word detection on the node (groundwork for the custom \"Yo Archy\" wake word; stock models like \"ok nabu\" ship with the image).", "category": "home", + "container_name": "pine-openwakeword", "container": { + "image": "docker.io/rhasspy/wyoming-openwakeword:2.1.0", + "pull_policy": "if-not-present", + "network": "archy-net", + "network_aliases": [ + "pine-openwakeword" + ], "custom_args": [ "--preload-model", "ok_nabu", "--custom-model-dir", "/custom" - ], - "image": "docker.io/rhasspy/wyoming-openwakeword:2.1.0", - "network": "archy-net", - "network_aliases": [ - "pine-openwakeword" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "pine-openwakeword", "dependencies": [ { "storage": "512Mi" } ], - "description": "Wyoming-protocol openWakeWord wake-word engine. Internal Pine voice-assistant stack member — lets Assist pipelines run wake-word detection on the node (groundwork for the custom \"Yo Archy\" wake word; stock models like \"ok nabu\" ship with the image).", + "resources": { + "memory_limit": "512Mi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "no_new_privileges": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 10400, + "container": 10400, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/pine-openwakeword", + "target": "/custom", + "options": [ + "rw" + ] + } + ], "environment": [], "health_check": { + "type": "tcp", "endpoint": "localhost:10400", "interval": "30s", - "retries": 5, - "start_period": "30s", "timeout": "5s", - "type": "tcp" + "retries": 5, + "start_period": "30s" }, - "id": "pine-openwakeword", "metadata": { "author": "Rhasspy / Home Assistant", "icon": "/assets/img/app-icons/pine.svg", - "license": "MIT", + "website": "https://github.com/rhasspy/wyoming-openwakeword", "repo": "https://github.com/rhasspy/wyoming-openwakeword", + "license": "MIT", "tags": [ "home", "voice", "wake-word", "wyoming" - ], - "website": "https://github.com/rhasspy/wyoming-openwakeword" - }, - "name": "Pine Wake Word (openWakeWord)", - "ports": [ - { - "auth": "none", - "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.", - "container": 10400, - "host": 10400, - "protocol": "tcp" - } - ], - "resources": { - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "rhasspy/wyoming-openwakeword" - }, - "version": "2.1.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/pine-openwakeword", - "target": "/custom", - "type": "bind" - } - ] + ] + } } - }, - "version": "2.1.0" + } }, "pine-piper": { + "version": "2.4.2", "manifest": { "app": { + "id": "pine-piper", + "name": "Pine Piper (TTS)", + "version": "2.4.2", + "upstream": { + "kind": "github", + "repo": "rhasspy/wyoming-piper" + }, + "description": "Wyoming-protocol Piper text-to-speech engine. Internal Pine voice-assistant stack member \u2014 gives Home Assistant Assist a natural voice for spoken responses on the PineVoice satellite.", "category": "home", + "container_name": "pine-piper", "container": { - "custom_args": [ - "--voice", - "en_GB-alba-medium" - ], "image": "docker.io/rhasspy/wyoming-piper:2.4.2", + "pull_policy": "if-not-present", "network": "archy-net", "network_aliases": [ "pine-piper" ], - "pull_policy": "if-not-present" + "custom_args": [ + "--voice", + "en_GB-alba-medium" + ] }, - "container_name": "pine-piper", "dependencies": [ { "storage": "1Gi" } ], - "description": "Wyoming-protocol Piper text-to-speech engine. Internal Pine voice-assistant stack member — gives Home Assistant Assist a natural voice for spoken responses on the PineVoice satellite.", - "environment": [], - "health_check": { - "endpoint": "localhost:10200", - "interval": "30s", - "retries": 5, - "start_period": "60s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine-piper", - "metadata": { - "author": "Rhasspy / Home Assistant", - "icon": "/assets/img/app-icons/pine.svg", - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming-piper", - "tags": [ - "home", - "voice", - "text-to-speech", - "wyoming" - ], - "website": "https://github.com/rhasspy/wyoming-piper" - }, - "name": "Pine Piper (TTS)", - "ports": [ - { - "auth": "none", - "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.", - "container": 10200, - "host": 10200, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "512Mi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "rhasspy/wyoming-piper" - }, - "version": "2.4.2", + "ports": [ + { + "host": 10200, + "container": 10200, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable." + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/pine-piper", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:10200", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "60s" + }, + "metadata": { + "author": "Rhasspy / Home Assistant", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming-piper", + "repo": "https://github.com/rhasspy/wyoming-piper", + "license": "MIT", + "tags": [ + "home", + "voice", + "text-to-speech", + "wyoming" + ] + } } - }, - "version": "2.4.2" + } }, "pine-whisper": { + "version": "3.6.0", "manifest": { "app": { + "id": "pine-whisper", + "name": "Pine Whisper (STT)", + "version": "3.6.0", + "upstream": { + "kind": "dockerhub", + "repo": "rhasspy/wyoming-whisper" + }, + "description": "Wyoming-protocol faster-whisper speech-to-text engine. Internal Pine voice-assistant stack member \u2014 turns speech captured by a PineVoice satellite into text for Home Assistant Assist.", "category": "home", + "container_name": "pine-whisper", "container": { + "image": "docker.io/rhasspy/wyoming-whisper:3.6.0", + "pull_policy": "if-not-present", + "network": "archy-net", + "network_aliases": [ + "pine-whisper" + ], "custom_args": [ "--model", "base-int8", @@ -4733,135 +4875,93 @@ "en", "--beam-size", "1" - ], - "image": "docker.io/rhasspy/wyoming-whisper:3.6.0", - "network": "archy-net", - "network_aliases": [ - "pine-whisper" - ], - "pull_policy": "if-not-present" + ] }, - "container_name": "pine-whisper", "dependencies": [ { "storage": "2Gi" } ], - "description": "Wyoming-protocol faster-whisper speech-to-text engine. Internal Pine voice-assistant stack member — turns speech captured by a PineVoice satellite into text for Home Assistant Assist.", - "environment": [], - "health_check": { - "endpoint": "localhost:10300", - "interval": "30s", - "retries": 5, - "start_period": "60s", - "timeout": "5s", - "type": "tcp" - }, - "id": "pine-whisper", - "metadata": { - "author": "Rhasspy / Home Assistant", - "icon": "/assets/img/app-icons/pine.svg", - "license": "MIT", - "repo": "https://github.com/rhasspy/wyoming-faster-whisper", - "tags": [ - "home", - "voice", - "speech-to-text", - "wyoming" - ], - "website": "https://github.com/rhasspy/wyoming-faster-whisper" - }, - "name": "Pine Whisper (STT)", - "ports": [ - { - "auth": "none", - "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable.", - "container": 10300, - "host": 10300, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "2Gi" }, "security": { "capabilities": [], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "upstream": { - "kind": "dockerhub", - "repo": "rhasspy/wyoming-whisper" - }, - "version": "3.6.0", + "ports": [ + { + "host": 10300, + "container": 10300, + "protocol": "tcp", + "auth": "none", + "auth_rationale": "Wyoming voice protocol, a binary local-only stream consumed by Home Assistant; not HTTP and not browser-reachable." + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/pine-whisper", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:10300", + "interval": "30s", + "timeout": "5s", + "retries": 5, + "start_period": "60s" + }, + "metadata": { + "author": "Rhasspy / Home Assistant", + "icon": "/assets/img/app-icons/pine.svg", + "website": "https://github.com/rhasspy/wyoming-faster-whisper", + "repo": "https://github.com/rhasspy/wyoming-faster-whisper", + "license": "MIT", + "tags": [ + "home", + "voice", + "speech-to-text", + "wyoming" + ] + } } - }, - "version": "3.6.0" + } }, "portainer": { + "version": "2.45.0", "image": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0", "manifest": { "app": { + "id": "portainer", + "name": "Portainer", + "version": "2.45.0", + "upstream": { + "kind": "github", + "repo": "portainer/portainer" + }, + "description": "Container management web UI for the local Podman socket.", "category": "development", "container": { - "data_uid": "1000:1000", "image": "source.archipelago-foundation.org/lfg2025/portainer:2.45.0", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "data_uid": "1000:1000" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Container management web UI for the local Podman socket.", - "environment": [], - "id": "portainer", - "interfaces": { - "main": { - "description": "Portainer web interface", - "name": "Web UI", - "path": "/", - "port": 9000, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "features": [ - "Container management dashboard", - "Local Podman socket access", - "Compose stack storage" - ], - "icon": "/assets/img/app-icons/portainer.webp", - "launch": { - "open_in_new_tab": true - }, - "tier": "optional" - }, - "name": "Portainer", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 9000, - "host": 9000, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4870,48 +4970,82 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "portainer/portainer" - }, - "version": "2.45.0", + "ports": [ + { + "host": 9000, + "container": 9000, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/portainer", "target": "/data", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/var/lib/archipelago/portainer/compose", "target": "/data/compose", - "type": "bind" - }, - { "options": [ "rw" - ], + ] + }, + { + "type": "bind", "source": "/run/user/1000/podman/podman.sock", "target": "/var/run/docker.sock", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "interfaces": { + "main": { + "name": "Web UI", + "description": "Portainer web interface", + "type": "ui", + "port": 9000, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/portainer.webp", + "tier": "optional", + "launch": { + "open_in_new_tab": true + }, + "features": [ + "Container management dashboard", + "Local Podman socket access", + "Compose stack storage" + ] + } } - }, - "version": "2.45.0" + } }, "router": { + "version": "1.0.0", "manifest": { "app": { + "id": "router", + "name": "Mesh Router", + "version": "1.0.0", + "upstream": { + "kind": "internal" + }, + "description": "Mesh routing and local network management. Provides device discovery, routing, and network topology visualization.", "container": { "image": "archipelago/router:1.0.0", "image_signature": "cosign://...", @@ -4922,101 +5056,102 @@ "storage": "500Mi" } ], - "description": "Mesh routing and local network management. Provides device discovery, routing, and network topology visualization.", + "resources": { + "cpu_limit": 2, + "memory_limit": "512Mi", + "disk_limit": "500Mi" + }, + "security": { + "capabilities": [ + "NET_ADMIN", + "NET_RAW" + ], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "host", + "apparmor_profile": "router" + }, + "ports": [ + { + "host": 8084, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + }, + { + "host": 5353, + "container": 5353, + "protocol": "udp", + "auth": "none", + "auth_rationale": "mDNS is UDP multicast service discovery; gating it would break .local name resolution for every device on the LAN." + }, + { + "host": 1900, + "container": 1900, + "protocol": "udp", + "auth": "none", + "auth_rationale": "SSDP/UPnP discovery is UDP multicast \u2014 there is no HTTP request to gate and no client that could hold a session." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/router", + "target": "/app/data", + "options": [ + "rw" + ] + }, + { + "type": "bind", + "source": "/var/run/dbus", + "target": "/var/run/dbus", + "options": [ + "ro" + ] + } + ], "environment": [ "NETWORK_INTERFACE=eth0", "MESH_ENABLED=true", "DEVICE_DISCOVERY=true" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8084", - "interval": "30s", "path": "/health", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "router", - "name": "Mesh Router", "networking": { - "device_discovery": true, - "local_network_access": true, "mesh_enabled": true, + "local_network_access": true, + "device_discovery": true, "routing_protocols": [ "olsr", "babel" ] - }, - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 8084, - "protocol": "tcp" - }, - { - "auth": "none", - "auth_rationale": "mDNS is UDP multicast service discovery; gating it would break .local name resolution for every device on the LAN.", - "container": 5353, - "host": 5353, - "protocol": "udp" - }, - { - "auth": "none", - "auth_rationale": "SSDP/UPnP discovery is UDP multicast — there is no HTTP request to gate and no client that could hold a session.", - "container": 1900, - "host": 1900, - "protocol": "udp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "500Mi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "router", - "capabilities": [ - "NET_ADMIN", - "NET_RAW" - ], - "network_policy": "host", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "upstream": { - "kind": "internal" - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/router", - "target": "/app/data", - "type": "bind" - }, - { - "options": [ - "ro" - ], - "source": "/var/run/dbus", - "target": "/var/run/dbus", - "type": "bind" - } - ] + } } - }, - "version": "1.0.0" + } }, "searxng": { + "version": "latest", "image": "source.archipelago-foundation.org/lfg2025/searxng:latest", "manifest": { "app": { + "id": "searxng", + "name": "SearXNG", + "version": "1.0.0", + "upstream": { + "kind": "github", + "repo": "searxng/searxng" + }, + "description": "Privacy-respecting metasearch engine. Search the web without tracking.", "container": { "image": "source.archipelago-foundation.org/lfg2025/searxng:latest", "pull_policy": "if-not-present" @@ -5026,66 +5161,66 @@ "storage": "2Gi" } ], - "description": "Privacy-respecting metasearch engine. Search the web without tracking.", + "resources": { + "cpu_limit": 2, + "memory_limit": "1Gi", + "disk_limit": "2Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "searxng" + }, + "ports": [ + { + "host": 8888, + "container": 8080, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/searxng", + "target": "/etc/searxng", + "options": [ + "rw" + ] + } + ], "environment": [ "SEARXNG_HOSTNAME=localhost", "SEARXNG_BIND_ADDRESS=0.0.0.0:8080" ], "health_check": { + "type": "http", "endpoint": "http://localhost:8080", - "interval": "30s", "path": "/", - "retries": 5, + "interval": "30s", "timeout": "30s", - "type": "http" - }, - "id": "searxng", - "name": "SearXNG", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 8080, - "host": 8888, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "2Gi", - "memory_limit": "1Gi" - }, - "security": { - "apparmor_profile": "searxng", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "upstream": { - "kind": "github", - "repo": "searxng/searxng" - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/searxng", - "target": "/etc/searxng", - "type": "bind" - } - ] + "retries": 5 + } } - }, - "version": "latest" + } }, "strfry": { + "version": "1.1.2", "manifest": { "app": { + "id": "strfry", + "name": "Strfry Nostr Relay", + "version": "1.1.2", + "upstream": { + "kind": "github", + "repo": "hoytech/strfry" + }, + "description": "Lightweight Nostr relay written in C++. Alternative to nostr-rs-relay with lower resource usage.", "container": { "image": "dockurr/strfry:1.1.2", "image_signature": "cosign://...", @@ -5096,116 +5231,144 @@ "storage": "5Gi" } ], - "description": "Lightweight Nostr relay written in C++. Alternative to nostr-rs-relay with lower resource usage.", - "files": [ - { - "content": "##\n## Default strfry config\n##\n\n# Directory that contains the strfry LMDB database (restart required)\ndb = \"./strfry-db/\"\n\ndbParams {\n # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)\n maxreaders = 256\n\n # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)\n mapsize = 10995116277760\n\n # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)\n noReadAhead = false\n}\n\nevents {\n # Maximum size of normalised JSON, in bytes\n maxEventSize = 65536\n\n # Events newer than this will be rejected\n rejectEventsNewerThanSeconds = 900\n\n # Events older than this will be rejected\n rejectEventsOlderThanSeconds = 94608000\n\n # Ephemeral events older than this will be rejected\n rejectEphemeralEventsOlderThanSeconds = 60\n\n # Ephemeral events will be deleted from the DB when older than this\n ephemeralEventsLifetimeSeconds = 300\n\n # Maximum number of tags allowed\n maxNumTags = 2000\n\n # Maximum size for tag values, in bytes\n maxTagValSize = 1024\n}\n\nrelay {\n # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)\n bind = \"0.0.0.0\"\n\n # Port to open for the nostr websocket protocol (restart required)\n port = 7777\n\n # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)\n nofiles = 0\n\n # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)\n realIpHeader = \"\"\n\n info {\n # NIP-11: Name of this server. Short/descriptive (< 30 characters)\n name = \"Archipelago Strfry Relay\"\n\n # NIP-11: Detailed information about relay, free-form\n description = \"Self-hosted strfry Nostr relay on Archipelago.\"\n\n # NIP-11: Administrative nostr pubkey, for contact purposes\n pubkey = \"\"\n\n # NIP-11: Alternative administrative contact (email, website, etc)\n contact = \"\"\n\n # NIP-11: URL pointing to an image to be used as an icon for the relay\n icon = \"\"\n\n # List of supported lists as JSON array, or empty string to use default. Example: \"[1,2]\"\n nips = \"\"\n }\n\n # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)\n maxWebsocketPayloadSize = 131072\n\n # Maximum number of filters allowed in a REQ\n maxReqFilterSize = 200\n\n # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)\n autoPingSeconds = 55\n\n # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)\n enableTcpKeepalive = false\n\n # How much uninterrupted CPU time a REQ query should get during its DB scan\n queryTimesliceBudgetMicroseconds = 10000\n\n # Maximum records that can be returned per filter\n maxFilterLimit = 500\n\n # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time\n maxSubsPerConnection = 20\n\n writePolicy {\n # If non-empty, path to an executable script that implements the writePolicy plugin logic\n plugin = \"/app/write-policy.py\"\n }\n\n compression {\n # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)\n enabled = true\n\n # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)\n slidingWindow = true\n }\n\n logging {\n # Dump all incoming messages\n dumpInAll = false\n\n # Dump all incoming EVENT messages\n dumpInEvents = false\n\n # Dump all incoming REQ/CLOSE messages\n dumpInReqs = false\n\n # Log performance metrics for initial REQ database scans\n dbScanPerf = false\n\n # Log reason for invalid event rejection? Can be disabled to silence excessive logging\n invalidEvents = true\n }\n\n numThreads {\n # Ingester threads: route incoming requests, validate events/sigs (restart required)\n ingester = 3\n\n # reqWorker threads: Handle initial DB scan for events (restart required)\n reqWorker = 3\n\n # reqMonitor threads: Handle filtering of new events (restart required)\n reqMonitor = 3\n\n # negentropy threads: Handle negentropy protocol messages (restart required)\n negentropy = 2\n }\n\n negentropy {\n # Support negentropy protocol messages\n enabled = true\n\n # Maximum records that sync will process before returning an error\n maxSyncEvents = 1000000\n }\n}\n", - "overwrite": true, - "path": "/var/lib/archipelago/strfry-config/strfry.conf" - } - ], - "health_check": { - "endpoint": "http://127.0.0.1:7777", - "interval": "30s", - "path": "/health", - "retries": 3, - "timeout": "5s", - "type": "http" + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "5Gi" }, - "id": "strfry", - "name": "Strfry Nostr Relay", - "nostr_integration": { - "monetization_enabled": true, - "relay_type": "public" + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "nostr-relay" }, "ports": [ { - "auth": "gated", - "bind": "127.0.0.1", - "container": 7777, "host": 8090, - "protocol": "tcp" + "container": 7777, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" } ], - "resources": { - "cpu_limit": 1, - "disk_limit": "5Gi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "nostr-relay", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default" - }, - "upstream": { - "kind": "github", - "repo": "hoytech/strfry" - }, - "version": "1.1.2", "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/strfry", "target": "/app/strfry-db", - "type": "bind" + "options": [ + "rw" + ] }, { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/strfry-config/strfry.conf", "target": "/etc/strfry.conf", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "files": [ + { + "path": "/var/lib/archipelago/strfry-config/strfry.conf", + "overwrite": true, + "content": "##\n## Default strfry config\n##\n\n# Directory that contains the strfry LMDB database (restart required)\ndb = \"./strfry-db/\"\n\ndbParams {\n # Maximum number of threads/processes that can simultaneously have LMDB transactions open (restart required)\n maxreaders = 256\n\n # Size of mmap() to use when loading LMDB (default is 10TB, does *not* correspond to disk-space used) (restart required)\n mapsize = 10995116277760\n\n # Disables read-ahead when accessing the LMDB mapping. Reduces IO activity when DB size is larger than RAM. (restart required)\n noReadAhead = false\n}\n\nevents {\n # Maximum size of normalised JSON, in bytes\n maxEventSize = 65536\n\n # Events newer than this will be rejected\n rejectEventsNewerThanSeconds = 900\n\n # Events older than this will be rejected\n rejectEventsOlderThanSeconds = 94608000\n\n # Ephemeral events older than this will be rejected\n rejectEphemeralEventsOlderThanSeconds = 60\n\n # Ephemeral events will be deleted from the DB when older than this\n ephemeralEventsLifetimeSeconds = 300\n\n # Maximum number of tags allowed\n maxNumTags = 2000\n\n # Maximum size for tag values, in bytes\n maxTagValSize = 1024\n}\n\nrelay {\n # Interface to listen on. Use 0.0.0.0 to listen on all interfaces (restart required)\n bind = \"0.0.0.0\"\n\n # Port to open for the nostr websocket protocol (restart required)\n port = 7777\n\n # Set OS-limit on maximum number of open files/sockets (if 0, don't attempt to set) (restart required)\n nofiles = 0\n\n # HTTP header that contains the client's real IP, before reverse proxying (ie x-real-ip) (MUST be all lower-case)\n realIpHeader = \"\"\n\n info {\n # NIP-11: Name of this server. Short/descriptive (< 30 characters)\n name = \"Archipelago Strfry Relay\"\n\n # NIP-11: Detailed information about relay, free-form\n description = \"Self-hosted strfry Nostr relay on Archipelago.\"\n\n # NIP-11: Administrative nostr pubkey, for contact purposes\n pubkey = \"\"\n\n # NIP-11: Alternative administrative contact (email, website, etc)\n contact = \"\"\n\n # NIP-11: URL pointing to an image to be used as an icon for the relay\n icon = \"\"\n\n # List of supported lists as JSON array, or empty string to use default. Example: \"[1,2]\"\n nips = \"\"\n }\n\n # Maximum accepted incoming websocket frame size (should be larger than max event) (restart required)\n maxWebsocketPayloadSize = 131072\n\n # Maximum number of filters allowed in a REQ\n maxReqFilterSize = 200\n\n # Websocket-level PING message frequency (should be less than any reverse proxy idle timeouts) (restart required)\n autoPingSeconds = 55\n\n # If TCP keep-alive should be enabled (detect dropped connections to upstream reverse proxy)\n enableTcpKeepalive = false\n\n # How much uninterrupted CPU time a REQ query should get during its DB scan\n queryTimesliceBudgetMicroseconds = 10000\n\n # Maximum records that can be returned per filter\n maxFilterLimit = 500\n\n # Maximum number of subscriptions (concurrent REQs) a connection can have open at any time\n maxSubsPerConnection = 20\n\n writePolicy {\n # If non-empty, path to an executable script that implements the writePolicy plugin logic\n plugin = \"/app/write-policy.py\"\n }\n\n compression {\n # Use permessage-deflate compression if supported by client. Reduces bandwidth, but slight increase in CPU (restart required)\n enabled = true\n\n # Maintain a sliding window buffer for each connection. Improves compression, but uses more memory (restart required)\n slidingWindow = true\n }\n\n logging {\n # Dump all incoming messages\n dumpInAll = false\n\n # Dump all incoming EVENT messages\n dumpInEvents = false\n\n # Dump all incoming REQ/CLOSE messages\n dumpInReqs = false\n\n # Log performance metrics for initial REQ database scans\n dbScanPerf = false\n\n # Log reason for invalid event rejection? Can be disabled to silence excessive logging\n invalidEvents = true\n }\n\n numThreads {\n # Ingester threads: route incoming requests, validate events/sigs (restart required)\n ingester = 3\n\n # reqWorker threads: Handle initial DB scan for events (restart required)\n reqWorker = 3\n\n # reqMonitor threads: Handle filtering of new events (restart required)\n reqMonitor = 3\n\n # negentropy threads: Handle negentropy protocol messages (restart required)\n negentropy = 2\n }\n\n negentropy {\n # Support negentropy protocol messages\n enabled = true\n\n # Maximum records that sync will process before returning an error\n maxSyncEvents = 1000000\n }\n}\n" + } + ], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:7777", + "path": "/health", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "nostr_integration": { + "relay_type": "public", + "monetization_enabled": true + } } - }, - "version": "1.1.2" + } }, "tailscale": { + "version": "stable", "image": "source.archipelago-foundation.org/lfg2025/tailscale:stable", "manifest": { "app": { + "id": "tailscale", + "name": "Tailscale", + "version": "1.78.0", + "upstream": { + "kind": "github", + "repo": "tailscale/tailscale" + }, + "description": "Zero-config VPN with WireGuard mesh networking.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/tailscale:stable", + "pull_policy": "if-not-present", + "network": "pasta", "entrypoint": [ "sh", "-c", "tailscaled --tun=userspace-networking & for i in $(seq 1 30); do [ -S /var/run/tailscale/tailscaled.sock ] && break; sleep 1; done; tailscale web --listen 0.0.0.0:8240 & wait" - ], - "image": "source.archipelago-foundation.org/lfg2025/tailscale:stable", - "network": "pasta", - "pull_policy": "if-not-present" + ] }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Zero-config VPN with WireGuard mesh networking.", + "resources": { + "memory_limit": "512Mi", + "disk_limit": "1Gi" + }, + "security": { + "capabilities": [], + "readonly_root": false, + "no_new_privileges": true, + "network_policy": "isolated" + }, + "ports": [ + { + "host": 8240, + "container": 8240, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "open", + "auth_rationale": "Tailscale's web console authenticates against the tailnet account for all administrative actions; the node's cookie challenge would be a second, redundant login." + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/tailscale", + "target": "/var/lib/tailscale", + "options": [ + "rw" + ] + } + ], "environment": [ "TS_STATE_DIR=/var/lib/tailscale" ], "health_check": { + "type": "tcp", "endpoint": "localhost:8240", "interval": "30s", - "retries": 3, "timeout": "5s", - "type": "tcp" + "retries": 3 }, - "id": "tailscale", "interfaces": { "main": { - "description": "Tailscale web console", "name": "Web console", - "path": "/", + "description": "Tailscale web console", + "type": "ui", "port": 8240, "protocol": "http", - "type": "ui" + "path": "/" } }, "metadata": { @@ -5214,102 +5377,41 @@ "icon": "/assets/img/app-icons/tailscale.webp", "repo": "https://github.com/tailscale/tailscale", "tier": "recommended" - }, - "name": "Tailscale", - "ports": [ - { - "auth": "open", - "auth_rationale": "Tailscale's web console authenticates against the tailnet account for all administrative actions; the node's cookie challenge would be a second, redundant login.", - "bind": "127.0.0.1", - "container": 8240, - "host": 8240, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "1Gi", - "memory_limit": "512Mi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": false - }, - "upstream": { - "kind": "github", - "repo": "tailscale/tailscale" - }, - "version": "1.78.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/tailscale", - "target": "/var/lib/tailscale", - "type": "bind" - } - ] + } } - }, - "version": "stable" + } }, "uptime-kuma": { + "version": "1", "image": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1", "manifest": { "app": { + "id": "uptime-kuma", + "name": "Uptime Kuma", + "version": "1.23.0", + "upstream": { + "kind": "github", + "repo": "louislam/uptime-kuma" + }, + "description": "Self-hosted uptime monitoring.", "container": { + "image": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1", + "pull_policy": "if-not-present", + "network": "pasta", "custom_args": [ "--", "node", "server/server.js" - ], - "image": "source.archipelago-foundation.org/lfg2025/uptime-kuma:1", - "network": "pasta", - "pull_policy": "if-not-present" + ] }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Self-hosted uptime monitoring.", - "environment": [ - "TZ=UTC" - ], - "health_check": { - "endpoint": "localhost:3001", - "interval": "30s", - "path": "/", - "retries": 3, - "timeout": "5s", - "type": "http" - }, - "id": "uptime-kuma", - "metadata": { - "author": "Uptime Kuma", - "category": "data", - "icon": "/assets/img/app-icons/uptime-kuma.webp", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/louislam/uptime-kuma", - "tier": "recommended" - }, - "name": "Uptime Kuma", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 3001, - "host": 3002, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -5318,85 +5420,78 @@ "SETUID", "SETGID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "louislam/uptime-kuma" - }, - "version": "1.23.0", + "ports": [ + { + "host": 3002, + "container": 3001, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/uptime-kuma", "target": "/app/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [ + "TZ=UTC" + ], + "health_check": { + "type": "http", + "endpoint": "localhost:3001", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "metadata": { + "icon": "/assets/img/app-icons/uptime-kuma.webp", + "category": "data", + "tier": "recommended", + "author": "Uptime Kuma", + "repo": "https://github.com/louislam/uptime-kuma", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "1" + } }, "vaultwarden": { + "version": "1.37.2-alpine", "image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine", "manifest": { "app": { + "id": "vaultwarden", + "name": "Vaultwarden", + "version": "1.37.2", + "upstream": { + "kind": "github", + "repo": "dani-garcia/vaultwarden" + }, + "description": "Self-hosted password vault with zero-knowledge encryption.", "container": { "image": "source.archipelago-foundation.org/lfg2025/vaultwarden:1.37.2-alpine", - "network": "pasta", - "pull_policy": "if-not-present" + "pull_policy": "if-not-present", + "network": "pasta" }, "dependencies": [ { "storage": "1Gi" } ], - "description": "Self-hosted password vault with zero-knowledge encryption.", - "environment": [], - "health_check": { - "endpoint": "localhost:80", - "interval": "30s", - "retries": 3, - "timeout": "5s", - "type": "tcp" - }, - "id": "vaultwarden", - "interfaces": { - "main": { - "description": "Vaultwarden web vault", - "name": "Web UI", - "path": "/", - "port": 8082, - "protocol": "http", - "type": "ui" - } - }, - "metadata": { - "author": "Vaultwarden", - "category": "data", - "icon": "/assets/img/app-icons/vaultwarden.webp", - "launch": { - "open_in_new_tab": true - }, - "repo": "https://github.com/dani-garcia/vaultwarden", - "tier": "recommended" - }, - "name": "Vaultwarden", - "ports": [ - { - "auth": "gated", - "bind": "127.0.0.1", - "container": 80, - "host": 8082, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -5405,31 +5500,89 @@ "SETGID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "upstream": { - "kind": "github", - "repo": "dani-garcia/vaultwarden" - }, - "version": "1.37.2", + "ports": [ + { + "host": 8082, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1", + "auth": "gated" + } + ], "volumes": [ { - "options": [ - "rw" - ], + "type": "bind", "source": "/var/lib/archipelago/vaultwarden", "target": "/data", - "type": "bind" + "options": [ + "rw" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "tcp", + "endpoint": "localhost:80", + "interval": "30s", + "timeout": "5s", + "retries": 3 + }, + "interfaces": { + "main": { + "name": "Web UI", + "description": "Vaultwarden web vault", + "type": "ui", + "port": 8082, + "protocol": "http", + "path": "/" + } + }, + "metadata": { + "icon": "/assets/img/app-icons/vaultwarden.webp", + "category": "data", + "tier": "recommended", + "author": "Vaultwarden", + "repo": "https://github.com/dani-garcia/vaultwarden", + "launch": { + "open_in_new_tab": true + } + } } - }, - "version": "1.37.2-alpine" + } } }, - "schema": 1, - "signature": "3e87496a7197177ea295eba416cd1ed9a2c41ddca3328a160b1db2c65d39ce813c2b1e63df1313680a8e48e0113e2bbe6118df01df4a777bd33b189e7ef69206", - "signed_by": "did:key:z6Mkfu5LT8d4DjETtrkATvHh9Dvcbnr7zBCUwfau8Sw7DLWT", - "updated": "2026-09-03" + "featured": { + "id": "indeedhub", + "banner": "/assets/img/featured/indeedhub-banner.jpg", + "headline": "Stream Sovereignty", + "description": "Bitcoin documentaries with Nostr identity.", + "tag": "NOSTR IDENTITY // YOUR NODE" + }, + "storefront": { + "popular": [ + "bitcoin-knots", + "lnd", + "btcpay-server", + "mempool", + "filebrowser", + "homeassistant" + ], + "promotions": [ + { + "id": "archipelago-source", + "banner": "/assets/img/featured/archipelago-source-banner.webp", + "eyebrow": "open source", + "headline": "Your node. Your source.", + "description": "Install GitWorkshop to browse Archipelago's code from your own node, clone it with ngit, and contribute issues, patches, and reviews over Nostr.", + "tag": "NGIT // NOSTR // NO SILO", + "path": "/npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/archy", + "launchLabel": "Open GitWorkshop", + "installLabel": "Install GitWorkshop", + "detailsLabel": "How contribution works \u2192" + } + ] + } } diff --git a/scripts/fix-indeedhub-containers.sh b/scripts/fix-indeedhub-containers.sh index 8a873a18..59a7563e 100755 --- a/scripts/fix-indeedhub-containers.sh +++ b/scripts/fix-indeedhub-containers.sh @@ -21,6 +21,63 @@ echo "Node IP: $NODE_IP" NETWORK="indeedhub-build_indeedhub-network" +# Preserve every credential before any existing stack member is removed. Old +# installs carried some only in container environments; fresh repairs get +# random per-node values. Never print a value into repair logs. +SECRETS_DIR="/var/lib/archipelago/secrets" +ensure_secret_from_container_env() { + secret_name="$1" + random_bytes="$2" + shift 2 + secret_path="$SECRETS_DIR/$secret_name" + if [ -e "$secret_path" ] && { [ ! -r "$secret_path" ] || [ ! -s "$secret_path" ]; }; then + echo "ERROR: $secret_name exists but is unreadable or empty; refusing to replace it." + exit 1 + fi + if [ ! -e "$secret_path" ]; then + recovered="" + for source in "$@"; do + c="${source%%:*}" + env_key="${source#*:}" + if podman container exists "$c" 2>/dev/null; then + candidate=$(podman inspect "$c" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | sed -n "s/^${env_key}=//p" | head -1) + if [ -n "$candidate" ]; then + recovered="$candidate" + break + fi + fi + done + if [ -z "$recovered" ]; then + recovered=$(openssl rand -hex "$random_bytes") + fi + mkdir -p "$SECRETS_DIR" + umask 077 + secret_tmp=$(mktemp "$SECRETS_DIR/.${secret_name}.XXXXXX") + printf '%s' "$recovered" > "$secret_tmp" + chmod 600 "$secret_tmp" + mv "$secret_tmp" "$secret_path" + fi +} + +ensure_secret_from_container_env indeedhub-aes-master 16 \ + indeedhub-api:AES_MASTER_SECRET indeedhub-ffmpeg:AES_MASTER_SECRET \ + indeedhub-build_api_1:AES_MASTER_SECRET indeedhub-build_ffmpeg-worker_1:AES_MASTER_SECRET +ensure_secret_from_container_env indeedhub-db-password 24 \ + indeedhub-api:DATABASE_PASSWORD indeedhub-ffmpeg:DATABASE_PASSWORD \ + indeedhub-build_api_1:DATABASE_PASSWORD indeedhub-build_ffmpeg-worker_1:DATABASE_PASSWORD \ + indeedhub-postgres:POSTGRES_PASSWORD +ensure_secret_from_container_env indeedhub-minio-password 24 \ + indeedhub-api:AWS_SECRET_KEY indeedhub-ffmpeg:AWS_SECRET_KEY \ + indeedhub-build_api_1:AWS_SECRET_KEY indeedhub-build_ffmpeg-worker_1:AWS_SECRET_KEY \ + indeedhub-minio:MINIO_ROOT_PASSWORD +ensure_secret_from_container_env indeedhub-jwt 32 \ + indeedhub-api:NOSTR_JWT_SECRET indeedhub-build_api_1:NOSTR_JWT_SECRET + +AES_MASTER_SECRET=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-aes-master") +DATABASE_PASSWORD=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-db-password") +MINIO_ROOT_PASSWORD=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-minio-password") +NOSTR_JWT_SECRET=$(tr -d '\r\n' < "$SECRETS_DIR/indeedhub-jwt") + # Load custom images if tar exists if [ -f /tmp/indeedhub-images.tar ]; then echo "Loading custom images from tar..." @@ -62,7 +119,7 @@ podman run -d --name indeedhub-postgres \ --network "$NETWORK" --network-alias postgres \ -v indeedhub-postgres-data:/var/lib/postgresql/data \ -e POSTGRES_USER=indeedhub \ - -e POSTGRES_PASSWORD=indeehhub-archy-2026 \ + -e POSTGRES_PASSWORD="$DATABASE_PASSWORD" \ -e POSTGRES_DB=indeedhub \ "$INDEEDHUB_POSTGRES_IMAGE" @@ -92,7 +149,7 @@ podman run -d --name indeedhub-minio \ --network "$NETWORK" --network-alias minio \ -v indeedhub-minio-data:/data \ -e MINIO_ROOT_USER=indeeadmin \ - -e MINIO_ROOT_PASSWORD=indeeadmin2026 \ + -e MINIO_ROOT_PASSWORD="$MINIO_ROOT_PASSWORD" \ "${MINIO_IMAGE}" \ server /data --console-address ":9001" @@ -116,7 +173,7 @@ podman run -d --name indeedhub-build_api_1 \ -e DATABASE_HOST=postgres \ -e DATABASE_PORT=5432 \ -e DATABASE_USER=indeedhub \ - -e DATABASE_PASSWORD=indeehhub-archy-2026 \ + -e DATABASE_PASSWORD="$DATABASE_PASSWORD" \ -e DATABASE_NAME=indeedhub \ -e QUEUE_HOST=redis \ -e QUEUE_PORT=6379 \ @@ -124,7 +181,7 @@ podman run -d --name indeedhub-build_api_1 \ -e S3_ENDPOINT=http://minio:9000 \ -e AWS_REGION=us-east-1 \ -e AWS_ACCESS_KEY=indeeadmin \ - -e AWS_SECRET_KEY=indeeadmin2026 \ + -e AWS_SECRET_KEY="$MINIO_ROOT_PASSWORD" \ -e S3_PRIVATE_BUCKET_NAME=indeedhub-private \ -e S3_PUBLIC_BUCKET_NAME=indeedhub-public \ -e S3_PUBLIC_BUCKET_URL=/storage \ @@ -132,9 +189,9 @@ podman run -d --name indeedhub-build_api_1 \ -e "BTCPAY_API_KEY=" \ -e "BTCPAY_STORE_ID=" \ -e "BTCPAY_WEBHOOK_SECRET=" \ - -e NOSTR_JWT_SECRET=archipelago-indeehhub-jwt-secret-2026 \ + -e NOSTR_JWT_SECRET="$NOSTR_JWT_SECRET" \ -e NOSTR_JWT_EXPIRES_IN=7d \ - -e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \ + -e AES_MASTER_SECRET="$AES_MASTER_SECRET" \ -e "ADMIN_API_KEY=" \ -e NODE_OPTIONS=--max-old-space-size=1024 \ --health-cmd "wget --no-verbose --tries=1 --spider http://localhost:4000/nostr-auth/health || exit 1" \ @@ -154,7 +211,7 @@ podman run -d --name indeedhub-build_ffmpeg-worker_1 \ -e DATABASE_HOST=postgres \ -e DATABASE_PORT=5432 \ -e DATABASE_USER=indeedhub \ - -e DATABASE_PASSWORD=indeehhub-archy-2026 \ + -e DATABASE_PASSWORD="$DATABASE_PASSWORD" \ -e DATABASE_NAME=indeedhub \ -e QUEUE_HOST=redis \ -e QUEUE_PORT=6379 \ @@ -162,11 +219,11 @@ podman run -d --name indeedhub-build_ffmpeg-worker_1 \ -e S3_ENDPOINT=http://minio:9000 \ -e AWS_REGION=us-east-1 \ -e AWS_ACCESS_KEY=indeeadmin \ - -e AWS_SECRET_KEY=indeeadmin2026 \ + -e AWS_SECRET_KEY="$MINIO_ROOT_PASSWORD" \ -e S3_PRIVATE_BUCKET_NAME=indeedhub-private \ -e S3_PUBLIC_BUCKET_NAME=indeedhub-public \ -e S3_PUBLIC_BUCKET_URL=/storage \ - -e AES_MASTER_SECRET=0123456789abcdef0123456789abcdef \ + -e AES_MASTER_SECRET="$AES_MASTER_SECRET" \ localhost/indeedhub-build_ffmpeg-worker:local # 7. IndeedHub Frontend