diff --git a/.gitmodules b/.gitmodules index 755a2411..b79b5f6c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "indeedhub"] path = indeedhub - url = https://git.tx1138.com/lfg2025/indeehub.git + url = http://146.59.87.168:3000/lfg2025/indeehub.git diff --git a/app-catalog/README.md b/app-catalog/README.md index 9e723be2..e3367209 100644 --- a/app-catalog/README.md +++ b/app-catalog/README.md @@ -21,7 +21,7 @@ Add an entry to `catalog.json`: "icon": "/assets/img/app-icons/my-app.svg", "author": "Author", "category": "data", - "dockerImage": "git.tx1138.com/lfg2025/my-app:1.0.0", + "dockerImage": "146.59.87.168:3000/lfg2025/my-app:1.0.0", "repoUrl": "https://github.com/...", "containerConfig": { "ports": ["8080:8080"], diff --git a/app-catalog/catalog.json b/app-catalog/catalog.json index 24b358de..ae5eb198 100644 --- a/app-catalog/catalog.json +++ b/app-catalog/catalog.json @@ -172,7 +172,7 @@ "author": "File Browser", "category": "data", "tier": "core", - "dockerImage": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", + "dockerImage": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", "repoUrl": "https://github.com/filebrowser/filebrowser", "containerConfig": { "ports": [ @@ -285,7 +285,7 @@ "icon": "/assets/img/app-icons/fedimint.png", "author": "Fedimint", "category": "money", - "dockerImage": "git.tx1138.com/lfg2025/gatewayd:v0.10.0", + "dockerImage": "146.59.87.168:3000/lfg2025/gatewayd:v0.10.0", "repoUrl": "https://github.com/fedimint/fedimint", "containerConfig": { "ports": [ diff --git a/apps/btcpay-server/manifest.yml b/apps/btcpay-server/manifest.yml index 4f0d9af0..8f42a95c 100644 --- a/apps/btcpay-server/manifest.yml +++ b/apps/btcpay-server/manifest.yml @@ -13,6 +13,12 @@ app: secret_file: bitcoin-rpc-password - key: BTCPAY_DB_PASS secret_file: btcpay-db-password + # Internal LND node. Generated by the daemon (lnd macaroon as hex + + # tls.cert thumbprint) — see container::lnd::ensure_btcpay_lnd_connection_secret. + # Optional: nodes without LND run btcpay without an internal node. + - key: BTCPAY_BTCLIGHTNING + secret_file: btcpay-lnd-connection + optional: true derived_env: - key: BTCPAY_HOST template: "{{HOST_IP}}:23000" @@ -50,6 +56,10 @@ app: - ASPNETCORE_URLS=http://0.0.0.0:49392 - BTCPAY_PROTOCOL=http - BTCPAY_CHAINS=btc + # Plugins must live on the persistent volume: the image default + # (/root/.btcpayserver/Plugins) is container-local, so every recreate + # silently wiped installed plugins. + - BTCPAY_PLUGINDIR=/datadir/Plugins - BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838 - BTCPAY_BTCRPCURL=http://bitcoin-knots:8332 - BTCPAY_BTCRPCUSER=archipelago diff --git a/apps/indeedhub/push-to-registry.sh b/apps/indeedhub/push-to-registry.sh index aa746deb..5818bc0f 100755 --- a/apps/indeedhub/push-to-registry.sh +++ b/apps/indeedhub/push-to-registry.sh @@ -12,7 +12,7 @@ set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" FRONTEND_DIR="${INDEEHUB_FRONTEND:-$HOME/Projects/indeehub-frontend}" VERSION="${1:-latest}" -REGISTRY="${REGISTRY:-git.tx1138.com}" +REGISTRY="${REGISTRY:-146.59.87.168:3000}" NAMESPACE="${NAMESPACE:-lfg2025}" IMAGE_NAME="indeedhub" RUNTIME="${RUNTIME:-podman}" diff --git a/core/Cargo.lock b/core/Cargo.lock index 68425044..486e02d0 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -140,6 +140,7 @@ dependencies = [ "reqwest 0.11.27", "sd-notify", "serde", + "serde_bytes", "serde_json", "serde_yaml", "serial2-tokio", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 2d96c82b..33de594a 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -110,6 +110,7 @@ hkdf = "0.12.4" # Transport abstraction (Phase 2: mesh as federation transport) ciborium = "0.2.2" +serde_bytes = "0.11" reed-solomon-erasure = "6.0" mdns-sd = "0.18" diff --git a/core/archipelago/src/api/handler/mod.rs b/core/archipelago/src/api/handler/mod.rs index a86839bd..20d8207f 100644 --- a/core/archipelago/src/api/handler/mod.rs +++ b/core/archipelago/src/api/handler/mod.rs @@ -126,15 +126,15 @@ impl ApiHandler { } /// Server-side fetch of the upstream app catalog so the browser can - /// load it without fighting CORS (git.tx1138.com emits no ACAO) or + /// load it without fighting CORS (upstream Gitea emits no ACAO) or /// CSP (the fallback IP-port URL isn't in `connect-src`). The upstream /// list is derived from the operator's configured container registries /// so switching mirrors in Settings changes the App Store source too — /// each active registry contributes one Gitea `raw/branch/main/catalog.json` /// URL (http or https per `tls_verify`), tried in priority order. - /// If registry config can't be loaded, falls back to the legacy - /// hardcoded pair so the App Store still renders on nodes that haven't - /// persisted a registry config yet. 15s total timeout. + /// If registry config can't be loaded, falls back to the hardcoded OVH + /// URL so the App Store still renders on nodes that haven't persisted + /// a registry config yet. 15s total timeout. async fn handle_app_catalog_proxy(&self) -> Result> { let mut upstreams: Vec = Vec::new(); if let Ok(config) = crate::container::registry::load_registries(&self.config.data_dir).await @@ -155,10 +155,6 @@ impl ApiHandler { "http://146.59.87.168:3000/lfg2025/app-catalog/raw/branch/main/catalog.json" .to_string(), ); - upstreams.push( - "https://git.tx1138.com/lfg2025/app-catalog/raw/branch/main/catalog.json" - .to_string(), - ); } let client = match reqwest::Client::builder() @@ -527,7 +523,7 @@ impl ApiHandler { // App-catalog proxy — fetches catalog.json from the configured // upstream URLs server-side so the browser doesn't hit CORS - // (git.tx1138.com has no ACAO header) or CSP (IP-port upstream + // (upstream Gitea has no ACAO header) or CSP (IP-port upstream // falls outside `connect-src`). Session-authenticated so only // the logged-in node owner can spin up fetches. (Method::GET, "/api/app-catalog") => { diff --git a/core/archipelago/src/container/image_policy.rs b/core/archipelago/src/container/image_policy.rs index e8baf589..ea433d43 100644 --- a/core/archipelago/src/container/image_policy.rs +++ b/core/archipelago/src/container/image_policy.rs @@ -5,11 +5,12 @@ //! 1.8.0 hardening plan). /// Registries images may be pulled from with an explicit host part. +/// (git.tx1138.com was removed 2026-07-10: the host is retired and must +/// never be pulled through again.) pub const TRUSTED_REGISTRIES: &[&str] = &[ "docker.io", "ghcr.io", "localhost", - "git.tx1138.com", "146.59.87.168:3000", ]; @@ -58,13 +59,19 @@ mod tests { "docker.io/library/nginx:1.25", "ghcr.io/owner/app:latest", "localhost/archy-dev:1", - "git.tx1138.com/lfg2025/x:2", "146.59.87.168:3000/archy/bitcoin-knots:28.1", ] { assert!(is_valid_docker_image(img), "{img} should be accepted"); } } + #[test] + fn rejects_retired_tx1138_registry() { + // Retired 2026-07-10 — refs through the dead host must be refused + // at the pull site, not time out against it. + assert!(!is_valid_docker_image("git.tx1138.com/lfg2025/x:2")); + } + #[test] fn accepts_docker_hub_shorthand() { for img in ["nginx", "grafana/grafana:11.2.0", "lightninglabs/lnd:v0.18"] { diff --git a/core/archipelago/src/container/image_versions.rs b/core/archipelago/src/container/image_versions.rs index cf543aed..b52b577b 100644 --- a/core/archipelago/src/container/image_versions.rs +++ b/core/archipelago/src/container/image_versions.rs @@ -234,7 +234,7 @@ pub fn available_update_for_images(pinned: &str, running_image: &str) -> Option< } /// Extract version tag from a full image reference. -/// e.g. "git.tx1138.com/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta" +/// e.g. "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta" → "v0.18.4-beta" /// Returns "latest" if no tag or tag is empty. pub fn extract_version_from_image(image: &str) -> String { // Split off the tag after the last colon, but only if it comes after the last slash @@ -328,11 +328,11 @@ mod tests { #[test] fn test_extract_version() { assert_eq!( - extract_version_from_image("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"), + extract_version_from_image("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"), "v0.18.4-beta" ); assert_eq!( - extract_version_from_image("git.tx1138.com/lfg2025/grafana:10.2.0"), + extract_version_from_image("146.59.87.168:3000/lfg2025/grafana:10.2.0"), "10.2.0" ); assert_eq!( @@ -340,7 +340,7 @@ mod tests { "latest" ); assert_eq!( - extract_version_from_image("git.tx1138.com/lfg2025/bitcoin-knots:latest"), + extract_version_from_image("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"), "latest" ); } @@ -352,7 +352,7 @@ mod tests { "lfg2025/lnd" ); assert_eq!( - image_without_registry_or_tag("git.tx1138.com/lfg2025/lnd:v0.18.4-beta"), + image_without_registry_or_tag("146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta"), "lfg2025/lnd" ); } @@ -369,7 +369,7 @@ mod tests { assert_eq!( available_update_for_images( "146.59.87.168:3000/lfg2025/nextcloud:29", - "git.tx1138.com/lfg2025/nextcloud:29", + "146.59.87.168:3000/lfg2025/nextcloud:29", ), None ); @@ -389,7 +389,7 @@ mod tests { #[test] fn test_parse_image_versions() { let content = r#" -ARCHY_REGISTRY="git.tx1138.com/lfg2025" +ARCHY_REGISTRY="146.59.87.168:3000/lfg2025" LND_IMAGE="$ARCHY_REGISTRY/lnd:v0.18.4-beta" GRAFANA_IMAGE="$ARCHY_REGISTRY/grafana:10.2.0" # comment @@ -398,11 +398,11 @@ NOT_AN_IMAGE="something" let parsed = parse_image_versions(content); assert_eq!( parsed.get("LND_IMAGE"), - Some(&"git.tx1138.com/lfg2025/lnd:v0.18.4-beta".to_string()) + Some(&"146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta".to_string()) ); assert_eq!( parsed.get("GRAFANA_IMAGE"), - Some(&"git.tx1138.com/lfg2025/grafana:10.2.0".to_string()) + Some(&"146.59.87.168:3000/lfg2025/grafana:10.2.0".to_string()) ); assert!(!parsed.contains_key("NOT_AN_IMAGE")); assert!(!parsed.contains_key("ARCHY_REGISTRY")); diff --git a/core/archipelago/src/container/lnd.rs b/core/archipelago/src/container/lnd.rs index df9ef5f0..8772cdfe 100644 --- a/core/archipelago/src/container/lnd.rs +++ b/core/archipelago/src/container/lnd.rs @@ -670,6 +670,73 @@ fn has_required_lnd_flags(conf: &str, rpc_pass: &str, bitcoin_host: &str) -> boo .all(|needle| conf.lines().any(|line| line.trim() == *needle)) } +/// Secret file consumed by btcpay-server's optional `BTCPAY_BTCLIGHTNING` +/// secret_env (see apps/btcpay-server/manifest.yml). +const BTCPAY_LND_CONNECTION_SECRET: &str = "btcpay-lnd-connection"; + +/// Materialise the BTCPay→internal-LND connection-string secret. +/// +/// LND's datadir is owned by its container subuid (100999 on a stock node), +/// so btcpay cannot bind-mount the macaroon — EACCES across the userns +/// boundary. The connection string therefore carries the macaroon inline as +/// hex, delivered by reference through the podman secret store. +/// +/// No-op when LND isn't provisioned yet (missing tls.cert or macaroon) — +/// btcpay's secret_env entry is `optional`, so it simply starts without an +/// internal Lightning node and picks it up on a later reconcile tick. +/// Rewrites when the pinned cert thumbprint no longer matches (LND TLS cert +/// rotation). Macaroon rotation without cert rotation is not auto-detected +/// (reading the macaroon needs sudo; probing it every tick is not worth the +/// churn) — delete the secret file once to force regeneration. +pub async fn ensure_btcpay_lnd_connection_secret(secrets_dir: &std::path::Path) -> Result<()> { + let cert_path = format!("{DEFAULT_DATA_DIR}/tls.cert"); + let pem = match fs::read_to_string(&cert_path).await { + Ok(s) => s, + Err(_) => return Ok(()), // LND not installed/provisioned yet + }; + let thumbprint = + cert_sha256_thumbprint(&pem).context("computing LND tls.cert thumbprint")?; + + let target = secrets_dir.join(BTCPAY_LND_CONNECTION_SECRET); + // Fast path (no sudo): existing secret already pins the current cert. + if let Ok(existing) = fs::read_to_string(&target).await { + if !existing.trim().is_empty() + && existing.contains(&format!("certthumbprint={thumbprint}")) + { + return Ok(()); + } + } + + let macaroon_path = format!("{DEFAULT_DATA_DIR}/data/chain/bitcoin/mainnet/admin.macaroon"); + if !file_exists_as_root(&macaroon_path).await { + return Ok(()); // wallet not created yet; next tick retries + } + let macaroon = read_file_as_root(&macaroon_path).await?; + let value = format!( + "type=lnd-rest;server=https://lnd:8080/;macaroon={};certthumbprint={}", + hex::encode(macaroon), + thumbprint + ); + crate::container::secrets::write_secret_file(&target, &value) + .context("writing btcpay-lnd-connection secret") +} + +/// SHA256 over the DER certificate body (matches +/// `openssl x509 -fingerprint -sha256` without colons) — the format BTCPay's +/// `certthumbprint=` connection-string parameter expects. +fn cert_sha256_thumbprint(pem: &str) -> Result { + use sha2::{Digest, Sha256}; + let b64: String = pem + .lines() + .filter(|l| !l.starts_with("-----")) + .collect::>() + .join(""); + let der = base64::engine::general_purpose::STANDARD + .decode(b64.trim()) + .context("decoding tls.cert PEM body")?; + Ok(hex::encode_upper(Sha256::digest(&der))) +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/archipelago/src/container/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index a04ff1ab..7e3ca2e5 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -2251,7 +2251,7 @@ impl ProdContainerOrchestrator { // Reconcile recreates route through here too: an unreachable // registry must not brick an app whose exact image:tag is // already in local storage (boot reconcile of archy-btcpay-db - // /archy-nbxplorer with git.tx1138.com dead, 2026-07-10). + // /archy-nbxplorer with the (since-retired) upstream registry dead, 2026-07-10). // Same exists-first semantics as // ensure_resolved_source_available and the quadlets' // Pull=never. @@ -3162,6 +3162,17 @@ impl ProdContainerOrchestrator { .await .context("ensuring bitcoin tx-relay credentials")?; } + if app_id == "btcpay-server" { + // Derived secret (LND macaroon + cert thumbprint), consumed by an + // `optional` secret_env — btcpay must still start when LND is + // absent or the derivation fails, so log-and-continue. + if let Err(e) = + crate::container::lnd::ensure_btcpay_lnd_connection_secret(&self.secrets_dir) + .await + { + tracing::warn!(error = %e, "btcpay-lnd-connection secret not generated; btcpay will run without the internal LND node"); + } + } // Other app secrets (fmcd-password, fedimint-gateway-hash, …) are now // declared as `generated_secrets` in their manifests and materialised // generically in `resolve_dynamic_env` — no per-app code here. @@ -4837,7 +4848,7 @@ app: name: File Browser version: 1.0.0 container: - image: git.tx1138.com/lfg2025/filebrowser:v2.27.0 + image: 146.59.87.168:3000/lfg2025/filebrowser:v2.27.0 custom_args: - --config - /data/.filebrowser.json @@ -4861,7 +4872,7 @@ app: name: LND version: 1.0.0 container: - image: git.tx1138.com/lfg2025/lnd:v0.18.4-beta + image: 146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta secret_env: - key: BITCOIND_RPCPASS secret_file: bitcoin-rpc-password @@ -4948,7 +4959,7 @@ app: async fn install_fresh_skips_pull_when_image_local() { // An unreachable registry must not brick a reconcile recreate whose // exact image:tag is already in local storage (archy-btcpay-db / - // archy-nbxplorer boot reconcile with git.tx1138.com dead, + // archy-nbxplorer boot reconcile with the (since-retired) upstream registry dead, // 2026-07-10). let rt = Arc::new(MockRuntime::default()); rt.mark_image_present("docker.io/bitcoin/knots:28"); diff --git a/core/archipelago/src/container/registry.rs b/core/archipelago/src/container/registry.rs index c413e25f..72d2338c 100644 --- a/core/archipelago/src/container/registry.rs +++ b/core/archipelago/src/container/registry.rs @@ -11,12 +11,17 @@ use tokio::fs; const REGISTRY_FILE: &str = "config/registries.json"; const OVH_REGISTRY_URL: &str = "146.59.87.168:3000/lfg2025"; -const TX1138_REGISTRY_URL: &str = "git.tx1138.com/lfg2025"; +/// Retired registry host (release server retired 2026-06-13; the registry +/// frontend was fully dead by 2026-07-10 — 500 on every /v2 manifest read). +/// Never a default, never force-enabled; stripped from saved configs on +/// load. The literal exists ONLY so the strip can match — nothing may pull +/// through this host. +const RETIRED_TX1138_HOST: &str = "git.tx1138.com"; /// A single container registry. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Registry { - /// Registry URL (e.g., "git.tx1138.com/lfg2025" or "146.59.87.168:3000/lfg2025"). + /// Registry URL (e.g., "146.59.87.168:3000/lfg2025"). pub url: String, /// Human-readable name. pub name: String, @@ -43,22 +48,13 @@ pub struct RegistryConfig { impl Default for RegistryConfig { fn default() -> Self { Self { - registries: vec![ - Registry { - url: OVH_REGISTRY_URL.to_string(), - name: "Server 1 (OVH)".to_string(), - tls_verify: false, - enabled: true, - priority: 0, - }, - Registry { - url: TX1138_REGISTRY_URL.to_string(), - name: "Server 2 (tx1138)".to_string(), - tls_verify: true, - enabled: true, - priority: 10, - }, - ], + registries: vec![Registry { + url: OVH_REGISTRY_URL.to_string(), + name: "Server 1 (OVH)".to_string(), + tls_verify: false, + enabled: true, + priority: 0, + }], } } } @@ -72,7 +68,7 @@ impl RegistryConfig { } /// Rewrite an image reference to use a specific registry. - /// E.g., "git.tx1138.com/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025" + /// E.g., "docker.io/lfg2025/bitcoin-knots:latest" with registry "146.59.87.168:3000/lfg2025" /// becomes "146.59.87.168:3000/lfg2025/bitcoin-knots:latest". pub fn rewrite_image(&self, image: &str, registry: &Registry) -> String { // Extract the image name (last component after the org/namespace) @@ -83,7 +79,7 @@ impl RegistryConfig { } /// Extract the image name from a full image reference. -/// "git.tx1138.com/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest" +/// "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" -> "bitcoin-knots:latest" /// "docker.io/gitea/gitea:1.23" -> "gitea:1.23" fn extract_image_name(image: &str) -> &str { // Split by '/' and take the last segment (image:tag) @@ -118,6 +114,12 @@ pub async fn load_registries(data_dir: &Path) -> Result { config .registries .retain(|r| !r.url.contains("23.182.128.160")); + // Same treatment for the retired tx1138 registry (was Server 2 in older + // defaults): strip it on load so nothing ever pulls through the dead + // host again. + config + .registries + .retain(|r| !r.url.contains(RETIRED_TX1138_HOST)); let mut changed = config.registries.len() != before; // Migrate: any default registry URL that isn't already in the @@ -178,12 +180,6 @@ fn force_ovh_registry_primary(config: &mut RegistryConfig) { registry.enabled = true; registry.priority = 0; } - TX1138_REGISTRY_URL => { - registry.name = "Server 2 (tx1138)".to_string(); - registry.tls_verify = true; - registry.enabled = true; - registry.priority = 10; - } _ => { if registry.priority <= 10 { registry.priority = registry.priority.saturating_add(20); @@ -216,7 +212,7 @@ mod tests { #[test] fn test_extract_image_name() { assert_eq!( - extract_image_name("git.tx1138.com/lfg2025/bitcoin-knots:latest"), + extract_image_name("146.59.87.168:3000/lfg2025/bitcoin-knots:latest"), "bitcoin-knots:latest" ); assert_eq!( @@ -229,11 +225,11 @@ mod tests { #[test] fn test_rewrite_image() { let config = RegistryConfig::default(); - // Default primary is now the OVH VPS (index 0). A tx1138-hardcoded - // image rewrites to OVH when asked for the primary mirror. + // An image hardcoded to some other registry rewrites to OVH when + // asked for the primary mirror. let primary = &config.registries[0]; assert_eq!( - config.rewrite_image("git.tx1138.com/lfg2025/bitcoin-knots:latest", primary), + config.rewrite_image("docker.io/lfg2025/bitcoin-knots:latest", primary), "146.59.87.168:3000/lfg2025/bitcoin-knots:latest" ); } @@ -242,15 +238,51 @@ mod tests { fn test_active_registries_sorted() { let config = RegistryConfig::default(); let active = config.active_registries(); - assert_eq!(active.len(), 2); - assert!(active[0].priority <= active[1].priority); + assert_eq!(active.len(), 1); + assert_eq!(active[0].url, OVH_REGISTRY_URL); } #[tokio::test] async fn test_load_default() { let tmp = TempDir::new().unwrap(); let config = load_registries(tmp.path()).await.unwrap(); - assert_eq!(config.registries.len(), 2); + assert_eq!(config.registries.len(), 1); + } + + #[tokio::test] + async fn test_load_strips_retired_tx1138_registry() { + // Nodes provisioned before the retirement have the tx1138 registry + // baked into their saved config (was Server 2). It must be stripped + // on load and never re-added by the defaults merge. + let tmp = TempDir::new().unwrap(); + let config = RegistryConfig { + registries: vec![ + Registry { + url: format!("{RETIRED_TX1138_HOST}/lfg2025"), + name: "Server 2 (tx1138)".into(), + tls_verify: true, + enabled: true, + priority: 10, + }, + Registry { + url: OVH_REGISTRY_URL.into(), + name: "Server 1 (OVH)".into(), + tls_verify: false, + enabled: true, + priority: 0, + }, + ], + }; + save_registries(tmp.path(), &config).await.unwrap(); + let loaded = load_registries(tmp.path()).await.unwrap(); + assert!( + !loaded + .registries + .iter() + .any(|r| r.url.contains(RETIRED_TX1138_HOST)), + "retired tx1138 registry must be stripped on load; got {:?}", + loaded.registries + ); } #[tokio::test] @@ -266,6 +298,6 @@ mod tests { }); save_registries(tmp.path(), &config).await.unwrap(); let loaded = load_registries(tmp.path()).await.unwrap(); - assert_eq!(loaded.registries.len(), 3); + assert_eq!(loaded.registries.len(), 2); } } diff --git a/core/archipelago/src/container/secrets.rs b/core/archipelago/src/container/secrets.rs index b866d954..74eb72da 100644 --- a/core/archipelago/src/container/secrets.rs +++ b/core/archipelago/src/container/secrets.rs @@ -102,6 +102,17 @@ fn random_base64(bytes: usize) -> String { base64::engine::general_purpose::STANDARD.encode(buf) } +/// Write an externally computed secret value (0600, atomic). For derived +/// secrets that aren't random generators — e.g. the btcpay internal-LND +/// connection string assembled in `container::lnd`. +pub(crate) fn write_secret_file(path: &Path, value: &str) -> Result<()> { + if let Some(dir) = path.parent() { + fs::create_dir_all(dir) + .with_context(|| format!("creating secrets dir {}", dir.display()))?; + } + write_secret(path, value) +} + /// Atomically write a `0600` secret: a temp file in the same dir (so the rename /// is atomic), fsynced, then renamed over the target. fn write_secret(path: &Path, value: &str) -> Result<()> { diff --git a/core/archipelago/src/main.rs b/core/archipelago/src/main.rs index 5a80230e..c33d36c1 100644 --- a/core/archipelago/src/main.rs +++ b/core/archipelago/src/main.rs @@ -72,6 +72,7 @@ mod state; mod storage_crypto; mod streaming; mod swarm; +mod tollgate_sweep; mod totp; mod transport; mod trust; diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index 48eb94f0..58fc5eda 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -537,6 +537,28 @@ impl Server { }); } + // Periodic TollGate ecash sweep. tollgate-wrt keeps its own separate + // Cashu wallet on the router — customer payments never land in this + // node's wallet on their own, and its Lightning auto-payout config is + // independent and easy to leave misconfigured. This drains whatever + // TollGate has collected straight into the local wallet on a timer, + // sidestepping Lightning payout configuration entirely. + { + let data_dir = config.data_dir.clone(); + tokio::spawn(async move { + tokio::time::sleep(Duration::from_secs(30)).await; + let mut interval = tokio::time::interval(Duration::from_secs(300)); + loop { + interval.tick().await; + match crate::tollgate_sweep::sweep_once(&data_dir).await { + Ok(0) => {} + Ok(swept) => info!(sats = swept, "tollgate wallet sweep complete"), + Err(e) => debug!(error = %e, "tollgate wallet sweep (non-fatal)"), + } + } + }); + } + // Initialize container scanner — discovers installed apps from Podman/Docker { let scanner = create_docker_scanner(&config).await?; diff --git a/core/archipelago/src/tollgate_sweep.rs b/core/archipelago/src/tollgate_sweep.rs new file mode 100644 index 00000000..ed4aa878 --- /dev/null +++ b/core/archipelago/src/tollgate_sweep.rs @@ -0,0 +1,81 @@ +use std::path::Path; + +use anyhow::{Context, Result}; +use archipelago_openwrt::router::Router; +use tracing::{info, warn}; + +use crate::network::router as net_router; +use crate::wallet::ecash; + +/// Pull whatever TollGate has collected in its on-router Cashu wallet into +/// this node's own wallet. +/// +/// `tollgate-wrt` keeps a completely separate Cashu wallet on the router +/// (`/etc/tollgate/wallet.db`) — customer payments land there, never in this +/// node's wallet directly. Its own Lightning auto-payout is configured +/// independently in `/etc/tollgate/identities.json`, which is easy to leave +/// pointed at the wrong (or a placeholder) address and easy to lose track of. +/// This sweep sidesteps that entirely: periodically drain the router's +/// TollGate wallet to Cashu tokens (`tollgate wallet drain cashu`, over SSH) +/// and receive them straight into the local wallet via the same path the +/// "Receive ecash" UI uses. +/// +/// Returns the total sats swept in (0 if there was nothing to do, including +/// when no router is configured or it doesn't have TollGate installed). +pub async fn sweep_once(data_dir: &Path) -> Result { + let cfg = net_router::load_router_config(data_dir).await?; + if !cfg.configured { + return Ok(0); + } + let ssh_user = cfg.username.clone().unwrap_or_else(|| "root".to_string()); + let ssh_password = cfg.password.clone().unwrap_or_default(); + + let router = Router::connect_password(&cfg.address, 22, &ssh_user, &ssh_password) + .context("connect to router for tollgate wallet sweep")?; + + // `tollgate` CLI missing (no TollGate installed here) is a normal, + // expected case, not an error — just nothing to sweep. + let (balance_out, code) = router.run("tollgate wallet balance 2>/dev/null")?; + if code != 0 { + return Ok(0); + } + let balance_sats = balance_out + .lines() + .find_map(|l| l.trim().strip_prefix("balance_sats:")) + .and_then(|v| v.trim().parse::().ok()) + .unwrap_or(0); + if balance_sats == 0 { + return Ok(0); + } + + let (drain_out, drain_code) = router.run("tollgate wallet drain cashu 2>&1")?; + if drain_code != 0 { + anyhow::bail!("tollgate wallet drain cashu failed: {}", drain_out.trim()); + } + + // The CLI has no machine-readable output mode; pull tokens out of its + // " Token: cashuB..." lines. + let mut received_total = 0u64; + for line in drain_out.lines() { + let Some(token) = line.trim().strip_prefix("Token:") else { + continue; + }; + let token = token.trim(); + if token.is_empty() { + continue; + } + match ecash::receive_token(data_dir, token).await { + Ok(amount) => { + received_total += amount; + info!(amount_sats = amount, "swept TollGate ecash into local wallet"); + } + Err(e) => { + // The token is still in this log line if this happens — not + // silently lost, just needs manual `wallet.ecash-receive`. + warn!(error = %e, token, "failed to receive swept TollGate token"); + } + } + } + + Ok(received_total) +} diff --git a/core/archipelago/src/update.rs b/core/archipelago/src/update.rs index 10fe1246..b1c9f9e6 100644 --- a/core/archipelago/src/update.rs +++ b/core/archipelago/src/update.rs @@ -2132,9 +2132,9 @@ mod tests { fn test_manifest_origin_parses_https() { assert_eq!( manifest_origin( - "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json" + "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json" ), - Some("https://git.tx1138.com".to_string()) + Some("https://releases.example.com".to_string()) ); } @@ -2151,7 +2151,7 @@ mod tests { #[test] fn test_manifest_origin_rejects_garbage() { assert_eq!(manifest_origin("not a url"), None); - assert_eq!(manifest_origin("ftp://git.tx1138.com/x"), None); + assert_eq!(manifest_origin("ftp://releases.example.com/x"), None); } #[test] @@ -2165,7 +2165,7 @@ mod tests { name: "archipelago".into(), current_version: "1.7.25-alpha".into(), new_version: "1.7.26-alpha".into(), - download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(), + download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/archipelago".into(), sha256: "x".into(), size_bytes: 1, blake3: None, @@ -2174,7 +2174,7 @@ mod tests { name: "frontend".into(), current_version: "1.7.25-alpha".into(), new_version: "1.7.26-alpha".into(), - download_url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(), + download_url: "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/v1.7.26-alpha/frontend.tar.gz".into(), sha256: "y".into(), size_bytes: 2, blake3: None, @@ -2218,6 +2218,8 @@ mod tests { label: "Server 1 (OVH)".to_string(), }, UpdateMirror { + // Deliberately the retired host: this fixture exists to prove + // load_mirrors strips it. url: "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json" .to_string(), label: "Server 2 (tx1138)".to_string(), @@ -2406,7 +2408,7 @@ mod tests { rollback_available: false, schedule: UpdateSchedule::Manual, manifest_mirror: Some( - "https://git.tx1138.com/lfg2025/archy/raw/branch/main/releases/manifest.json" + "https://releases.example.com/lfg2025/archy/raw/branch/main/releases/manifest.json" .to_string(), ), manifest_signed: false, diff --git a/core/archipelago/src/wallet/cashu.rs b/core/archipelago/src/wallet/cashu.rs index 17865853..5a4fc8ed 100644 --- a/core/archipelago/src/wallet/cashu.rs +++ b/core/archipelago/src/wallet/cashu.rs @@ -1,6 +1,6 @@ //! Cashu token format (NUT-00) — serialization and deserialization. //! -//! Supports the cashuA (V3) token format: +//! Emits the cashuA (V3) token format: //! cashuA //! //! Token JSON structure: @@ -8,13 +8,54 @@ //! "token": [{ "mint": "", "proofs": [{ "amount": u64, "id": "", "secret": "", "C": "" }] }], //! "memo": "" //! } +//! +//! Also accepts (decode-only) the cashuB (V4) CBOR format many wallets emit +//! by default now: +//! cashuB +//! CBOR map keys are the spec's single-letter names (t/i/p/a/s/c/m/u/d/w), +//! not the JSON names above. `i` (keyset id) and `c` (signature) are raw +//! bytes on the wire; we hex-encode them into `Proof` to match the V3 +//! convention so the rest of the wallet doesn't need to know which version +//! a token arrived in. use anyhow::{Context, Result}; use bitcoin::secp256k1::PublicKey; use serde::{Deserialize, Serialize}; -/// Prefix for V3 tokens. +/// Prefix for V3 (JSON) tokens. const CASHU_A_PREFIX: &str = "cashuA"; +/// Prefix for V4 (CBOR) tokens. +const CASHU_B_PREFIX: &str = "cashuB"; + +/// Raw CBOR shape of a V4 proof — field names are the spec's map keys. +#[derive(Debug, Deserialize)] +struct ProofV4 { + a: u64, + s: String, + #[serde(with = "serde_bytes")] + c: Vec, + // DLEQ proof ("d") and witness ("w") aren't verified or stored by this + // wallet; accept and discard them rather than fail on the field. +} + +/// Raw CBOR shape of a V4 token entry (one keyset's worth of proofs). +#[derive(Debug, Deserialize)] +struct TokenEntryV4 { + #[serde(with = "serde_bytes")] + i: Vec, + p: Vec, +} + +/// Raw CBOR shape of a full V4 token — single mint per token, unlike V3. +#[derive(Debug, Deserialize)] +struct TokenV4 { + t: Vec, + m: String, + #[serde(default)] + u: Option, + #[serde(default, rename = "d")] + memo: Option, +} /// A single Cashu proof (a signed token for a specific denomination). #[derive(Debug, Clone, Serialize, Deserialize)] @@ -100,31 +141,65 @@ impl CashuToken { Ok(format!("{}{}", CASHU_A_PREFIX, encoded)) } - /// Decode a cashuA token string. + /// Decode a cashuA (V3 JSON) or cashuB (V4 CBOR) token string. pub fn deserialize(token_str: &str) -> Result { - let payload = token_str - .strip_prefix(CASHU_A_PREFIX) - .ok_or_else(|| anyhow::anyhow!("Token must start with '{}'", CASHU_A_PREFIX))?; + if let Some(payload) = token_str.strip_prefix(CASHU_B_PREFIX) { + return Self::deserialize_v4(payload); + } - use base64::Engine; - let decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD - .decode(payload) - .or_else(|_| { - // Try standard base64 as fallback (some implementations use it) - base64::engine::general_purpose::URL_SAFE.decode(payload) - }) - .or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload)) - .context("Invalid base64 in cashuA token")?; + let payload = token_str.strip_prefix(CASHU_A_PREFIX).ok_or_else(|| { + anyhow::anyhow!( + "Token must start with '{}' or '{}'", + CASHU_A_PREFIX, + CASHU_B_PREFIX + ) + })?; + let decoded = decode_token_base64(payload).context("Invalid base64 in cashuA token")?; let json_str = String::from_utf8(decoded).context("Invalid UTF-8 in decoded token")?; let token: CashuToken = serde_json::from_str(&json_str).context("Invalid JSON in cashuA token")?; - // Structural validation - if token.token.is_empty() { + token.validate()?; + Ok(token) + } + + /// Decode a cashuB (V4 CBOR) token payload (prefix already stripped). + fn deserialize_v4(payload: &str) -> Result { + let decoded = decode_token_base64(payload).context("Invalid base64 in cashuB token")?; + + let v4: TokenV4 = + ciborium::from_reader(decoded.as_slice()).context("Invalid CBOR in cashuB token")?; + + let proofs = v4 + .t + .into_iter() + .flat_map(|entry| { + let keyset_id = hex::encode(&entry.i); + entry.p.into_iter().map(move |p| Proof { + amount: p.a, + id: keyset_id.clone(), + secret: p.s, + c: hex::encode(&p.c), + }) + }) + .collect(); + + let token = CashuToken { + token: vec![TokenEntry { mint: v4.m, proofs }], + memo: v4.memo, + unit: v4.u, + }; + token.validate()?; + Ok(token) + } + + /// Structural validation shared by both token versions. + fn validate(&self) -> Result<()> { + if self.token.is_empty() { anyhow::bail!("Token has no entries"); } - for entry in &token.token { + for entry in &self.token { if entry.mint.is_empty() { anyhow::bail!("Token entry has empty mint URL"); } @@ -143,11 +218,20 @@ impl CashuToken { } } } - - Ok(token) + Ok(()) } } +/// Decode a token's base64 payload, trying URL-safe-no-pad first (the spec +/// default) and falling back to other alphabets some implementations use. +fn decode_token_base64(payload: &str) -> Result, base64::DecodeError> { + use base64::Engine; + base64::engine::general_purpose::URL_SAFE_NO_PAD + .decode(payload) + .or_else(|_| base64::engine::general_purpose::URL_SAFE.decode(payload)) + .or_else(|_| base64::engine::general_purpose::STANDARD.decode(payload)) +} + /// Keyset info returned by a mint. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct KeysetInfo { @@ -303,11 +387,63 @@ mod tests { } #[test] - fn test_deserialize_rejects_invalid_prefix() { + fn test_deserialize_rejects_unknown_prefix() { + let result = CashuToken::deserialize("cashuZabc123"); + assert!(result.is_err()); + } + + #[test] + fn test_deserialize_rejects_malformed_v4_cbor() { let result = CashuToken::deserialize("cashuBabc123"); assert!(result.is_err()); } + #[test] + fn test_deserialize_v4_cbor_token() { + // Hand-built (not via our own encoder) to verify we actually match + // the NUT-00 V4 wire format: single-letter CBOR map keys, raw-byte + // keyset id ("i") and signature ("c"). + use base64::Engine; + use ciborium::value::Value; + + let keyset_id = vec![0x00u8, 0x9a, 0x1f, 0x29, 0x32, 0x53, 0xe4, 0x1e]; + let sig = hex::decode( + "02a9acc1e48c25eeeb9289b5031cc57da9fe72f3fe2861d94ec4da0e7f6c2b4e24", + ) + .unwrap(); + + let proof = Value::Map(vec![ + (Value::from("a"), Value::from(8u64)), + (Value::from("s"), Value::from("abcdef1234567890")), + (Value::from("c"), Value::from(sig.clone())), + ]); + let entry = Value::Map(vec![ + (Value::from("i"), Value::from(keyset_id.clone())), + (Value::from("p"), Value::Array(vec![proof])), + ]); + let token = Value::Map(vec![ + (Value::from("t"), Value::Array(vec![entry])), + (Value::from("m"), Value::from("http://127.0.0.1:8175")), + (Value::from("u"), Value::from("sat")), + (Value::from("d"), Value::from("test token")), + ]); + + let mut buf = Vec::new(); + ciborium::into_writer(&token, &mut buf).unwrap(); + let encoded = format!( + "cashuB{}", + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&buf) + ); + + let decoded = CashuToken::deserialize(&encoded).unwrap(); + assert_eq!(decoded.total_amount(), 8); + assert_eq!(decoded.token[0].mint, "http://127.0.0.1:8175"); + assert_eq!(decoded.token[0].proofs[0].secret, "abcdef1234567890"); + assert_eq!(decoded.token[0].proofs[0].id, hex::encode(&keyset_id)); + assert_eq!(decoded.token[0].proofs[0].c, hex::encode(&sig)); + assert_eq!(decoded.memo, Some("test token".to_string())); + } + #[test] fn test_amount_to_denominations() { assert_eq!(amount_to_denominations(0), Vec::::new()); diff --git a/core/container/src/manifest.rs b/core/container/src/manifest.rs index 85a51cf8..b1720350 100644 --- a/core/container/src/manifest.rs +++ b/core/container/src/manifest.rs @@ -279,6 +279,12 @@ pub struct DerivedEnv { pub struct SecretEnv { pub key: String, pub secret_file: String, + /// When true, a missing/unreadable/empty secret skips this entry instead + /// of failing the whole resolution. For integrations that exist on some + /// nodes only (btcpay's internal-LND connection string: nodes without + /// LND must still run btcpay, just without the internal node). + #[serde(default)] + pub optional: bool, } /// A fully resolved secret env entry, produced at apply time. `value` lives @@ -1353,10 +1359,18 @@ impl ContainerConfig { ) -> Result, ManifestError> { let mut out = Vec::with_capacity(self.secret_env.len()); for e in &self.secret_env { - let v = provider.read(&e.secret_file)?; + let v = match provider.read(&e.secret_file) { + Ok(v) => v, + Err(_) if e.optional => continue, + Err(err) => return Err(err), + }; // An empty secret produces e.g. `-rpcpassword=` and crashes - // the container on auth before logs are useful. Fail loud. + // the container on auth before logs are useful. Fail loud — + // unless the entry is optional, where empty means "not set". if v.trim().is_empty() { + if e.optional { + continue; + } return Err(ManifestError::Invalid(format!( "secret_env {} resolved to empty value (file: {})", e.key, e.secret_file @@ -2034,10 +2048,12 @@ app: SecretEnv { key: "FM_BITCOIND_PASSWORD".to_string(), secret_file: "bitcoin-rpc-password".to_string(), + optional: false, }, SecretEnv { key: "FM_GATEWAY_PASSWORD".to_string(), secret_file: "fedimint-gateway-password".to_string(), + optional: false, }, ], generated_secrets: vec![], @@ -2079,6 +2095,7 @@ app: secret_env: vec![SecretEnv { key: "BITCOIN_RPC_PASS".to_string(), secret_file: "bitcoin-rpc-password".to_string(), + optional: false, }], generated_secrets: vec![], generated_certs: vec![], @@ -2099,6 +2116,51 @@ app: } } + #[test] + fn resolve_secret_env_skips_missing_or_empty_optional_entries() { + let c = ContainerConfig { + image: Some("x:latest".to_string()), + image_signature: None, + pull_policy: "if-not-present".to_string(), + build: None, + network: None, + network_aliases: vec![], + custom_args: vec![], + entrypoint: None, + derived_env: vec![], + secret_env: vec![ + SecretEnv { + key: "REQUIRED".to_string(), + secret_file: "present".to_string(), + optional: false, + }, + SecretEnv { + key: "OPT_MISSING".to_string(), + secret_file: "does-not-exist".to_string(), + optional: true, + }, + SecretEnv { + key: "OPT_EMPTY".to_string(), + secret_file: "empty".to_string(), + optional: true, + }, + ], + generated_secrets: vec![], + generated_certs: vec![], + data_uid: None, + secret_env_refs: vec![], + secret_env_hash: None, + }; + let p = MapSecretsProvider { + data: HashMap::from([ + ("present".to_string(), "value".to_string()), + ("empty".to_string(), " \n".to_string()), + ]), + }; + let out = c.resolve_secret_env(&p).unwrap(); + assert_eq!(out, vec!["REQUIRED=value".to_string()]); + } + #[test] fn unsafe_manifest_values_are_rejected() { let cases = [ diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index 0c584e85..d69f5b0f 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -885,8 +885,9 @@ mod tests { assert!(!image_uses_insecure_registry( "23.182.128.160:3000/lfg2025/filebrowser:v2.27.0" )); + // HTTPS registries never match the insecure list. assert!(!image_uses_insecure_registry( - "git.tx1138.com/lfg2025/bitcoin-knots:latest" + "ghcr.io/lfg2025/bitcoin-knots:latest" )); assert!(!image_uses_insecure_registry( "docker.io/library/nginx:latest" diff --git a/core/openwrt/src/opkg.rs b/core/openwrt/src/opkg.rs index 84d889e0..296715ba 100644 --- a/core/openwrt/src/opkg.rs +++ b/core/openwrt/src/opkg.rs @@ -87,4 +87,31 @@ impl Router { self.run_ok(&format!("/usr/bin/opkg remove {}", package))?; Ok(()) } + + /// Install a standard OpenWrt package via whichever manager is active. + /// + /// Unlike `tollgate-module-basic-go` itself (which falls back to manual + /// `.ipk` extraction and therefore skips dependency resolution — see + /// `tollgate::install`), packages like `nodogsplash` are in every + /// upstream OpenWrt feed, so a plain `apk add` / `opkg install` works + /// even in `ApkNative` mode. + pub fn install_package(&self, pkg_mgr: PkgManager, package: &str) -> Result<()> { + match pkg_mgr { + PkgManager::Opkg => self.opkg_install(package), + PkgManager::ApkNative => self.apk_install(package), + } + } + + /// `apk add `, skipping if already installed. + pub fn apk_install(&self, package: &str) -> Result<()> { + let (_, code) = self.run(&format!("apk info -e {} >/dev/null 2>&1", package))?; + if code == 0 { + info!("[{}] {} already installed", self.host, package); + return Ok(()); + } + + info!("[{}] apk add {}", self.host, package); + self.run_ok(&format!("/usr/bin/apk add {}", package))?; + Ok(()) + } } diff --git a/core/openwrt/src/router.rs b/core/openwrt/src/router.rs index 088ec9c5..97b1a84c 100644 --- a/core/openwrt/src/router.rs +++ b/core/openwrt/src/router.rs @@ -1,6 +1,6 @@ use anyhow::{Context, Result}; use ssh2::Session; -use std::io::Read; +use std::io::{Read, Write}; use std::net::TcpStream; use std::path::Path; use tracing::debug; @@ -84,4 +84,22 @@ impl Router { .context("read /etc/openwrt_release — is this an OpenWrt device?")?; Ok(release) } + + /// Upload file contents to the router over SCP, overwriting any existing + /// file at `remote_path`. Used for config files that aren't UCI-backed + /// (e.g. `/etc/tollgate/config.json`), where `uci_*` helpers don't apply. + pub fn upload_file(&self, remote_path: &str, contents: &[u8]) -> Result<()> { + let mut channel = self + .session + .scp_send(Path::new(remote_path), 0o644, contents.len() as u64, None) + .with_context(|| format!("scp_send to {}", remote_path))?; + channel + .write_all(contents) + .with_context(|| format!("write contents to {}", remote_path))?; + channel.send_eof().context("scp send_eof")?; + channel.wait_eof().context("scp wait_eof")?; + channel.close().context("scp close")?; + channel.wait_close().context("scp wait_close")?; + Ok(()) + } } diff --git a/core/openwrt/src/tollgate/config.rs b/core/openwrt/src/tollgate/config.rs index 005e2cae..d6761d54 100644 --- a/core/openwrt/src/tollgate/config.rs +++ b/core/openwrt/src/tollgate/config.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use crate::Router; @@ -40,7 +40,11 @@ impl Default for TollGateConfig { /// Write TollGate UCI configuration and commit. /// -/// Maps TIP-01 / TIP-02 fields onto UCI keys used by tollgate-module-basic-go. +/// `tollgate-wrt` never reads UCI — see `apply_daemon_config` below for the +/// config it actually consumes. These `tollgate.main.*` keys exist only for +/// this project's own status display / detection probes (`uci get +/// tollgate.main.enabled` etc.); changing pricing or the mint here has no +/// effect on what the daemon advertises or accepts. pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> { router.uci_apply( "tollgate", @@ -57,3 +61,39 @@ pub fn apply(router: &Router, cfg: &TollGateConfig) -> Result<()> { )?; Ok(()) } + +/// Write the config `tollgate-wrt` actually reads: `/etc/tollgate/config.json` +/// (schema `v0.0.6`/`v0.0.7`, see `config_manager` in the upstream Go source). +/// +/// Merges into whatever config.json already exists (the daemon writes a +/// default on first boot) rather than overwriting it wholesale — fields this +/// project doesn't manage (`profit_share`, `upstream_detector`, +/// `upstream_session_manager`/`chandler`, `relays`, ...) must survive +/// re-provisioning. +/// +/// Must run before the daemon is (re)started — it only reads this file at +/// startup, it does not hot-reload. +pub fn apply_daemon_config(router: &Router, cfg: &TollGateConfig) -> Result<()> { + let existing = router.run_ok("cat /etc/tollgate/config.json 2>/dev/null || echo '{}'")?; + let mut doc: serde_json::Value = + serde_json::from_str(existing.trim()).unwrap_or_else(|_| serde_json::json!({})); + + doc["metric"] = serde_json::json!("milliseconds"); + doc["step_size"] = serde_json::json!(cfg.step_size_ms); + doc["accepted_mints"] = serde_json::json!([{ + "url": cfg.mint_url, + "min_balance": 64, + "balance_tolerance_percent": 10, + "payout_interval_seconds": 60, + "min_payout_amount": 128, + "price_per_step": cfg.price_sats, + "price_unit": "sats", + "purchase_min_steps": cfg.min_steps, + }]); + + let json_str = serde_json::to_string_pretty(&doc).context("serialize config.json")?; + router + .upload_file("/etc/tollgate/config.json", json_str.as_bytes()) + .context("upload /etc/tollgate/config.json")?; + Ok(()) +} diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index f694e97d..d92b1c55 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -1,21 +1,27 @@ pub mod config; pub mod install; +pub mod nodogsplash; pub mod wifi; pub use config::TollGateConfig; pub use install::install_tollgate; pub use wifi::provision_ssid; -use anyhow::Result; +use anyhow::{Context, Result}; use tracing::info; use crate::{opkg::PkgManager, Router}; /// Full TollGate provisioning sequence: /// 1. Install tollgate-module-basic-go -/// 2. Write TollGate UCI config (pricing, mint URL) -/// 3. Create the pay-as-you-go WiFi SSID -/// 4. Restart affected services +/// 2. Install NoDogSplash and immediately stop it (its postinst auto-starts +/// it against `br-lan` by default — see `nodogsplash::install_and_stop`) +/// 3. Write TollGate config: UCI (status/detection only) + the JSON file the +/// daemon actually reads +/// 4. Create the pay-as-you-go WiFi SSID and its dedicated bridge/network +/// 5. Configure NoDogSplash to gate that bridge (now that it exists) — +/// client gating; tollgate-wrt has no enforcement code of its own +/// 6. Restart affected services pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { info!("[{}] Starting TollGate provisioning", router.host); @@ -29,9 +35,41 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { install::install_tollgate_apk_native(router)?; } } + + // NoDogSplash is a hard runtime dependency of tollgate-wrt (upstream's + // package declares `+nodogsplash`), but neither install path above pulls + // it in: the opkg fast path only resolves deps against a real feed, and + // the raw .ipk-extraction fallback (used whenever the package isn't in a + // feed, and always on ApkNative) skips dependency resolution entirely. + // Without it, tollgate-wrt runs and accepts payments but never actually + // blocks unpaid clients. Install + stop happens before anything else so + // its auto-started default config (gating br-lan) is live for as little + // time as possible. + nodogsplash::install_and_stop(router, pkg_mgr) + .context("install nodogsplash — tollgate-wrt cannot gate clients without it")?; + + // Wire NoDogSplash's webroot to TollGate's actual payment portal instead + // of the generic stock splash page it ships with. Confirmed live: without + // this, "click continue" on the stock page authorizes the client via + // NoDogSplash's own built-in handler with zero payment involved. + nodogsplash::install_captive_portal_symlink(router).context( + "wire up TollGate's captive portal — without it NoDogSplash serves its own \ + generic splash page, which authorizes clients on click with no payment", + )?; + config::apply(router, config)?; wifi::provision_ssid(router, config)?; + // Must come after provision_ssid (which creates br-tollgate) and before + // the daemon restart below — config.json is only read at startup. + config::apply_daemon_config(router, config) + .context("write /etc/tollgate/config.json — tollgate-wrt reads this, not UCI")?; + // Also must come after provision_ssid: points gatewayinterface at + // br-tollgate, which provision_ssid is what creates. + nodogsplash::configure(router, config) + .context("configure nodogsplash — tollgate-wrt cannot gate clients without it")?; + restart_services(router, config.enabled)?; + nodogsplash::restart(router)?; info!("[{}] TollGate provisioning complete", router.host); Ok(()) @@ -57,5 +95,27 @@ fn restart_services(router: &Router, enabled: bool) -> Result<()> { // Reload wireless so wireless.tollgate.disabled takes effect on the radio — // `network restart` alone doesn't reliably reconfigure wifi interfaces. router.run_ok("wifi down 2>&1; wifi up 2>&1")?; + // Observed live, twice, in two different ways: netifd can lose the race + // to claim br-tollgate as the wifi vif attaches to it during the restart + // above. The first time it showed up as netifd reporting + // "up: false, DEVICE_CLAIM_FAILED"; the second time netifd reported the + // interface up with its address assigned while the kernel-level device + // genuinely had none (`ip -4 addr show br-tollgate` empty) — dnsmasq + // logged "DHCP packet received on br-tollgate which has no address" and + // silently dropped every DISCOVER. A single blind ifdown/ifup isn't + // trustworthy here — verify the address actually landed at the kernel + // level (not just what netifd claims) and retry the cycle if not, since + // NoDogSplash refuses to start against an interface that isn't really up + // and dnsmasq will silently refuse to answer DHCP without erroring loudly. + router.run_ok( + "sleep 2; \ + for i in 1 2 3 4 5; do \ + ifdown tollgate 2>&1; sleep 1; ifup tollgate 2>&1; sleep 2; \ + ip -4 addr show br-tollgate 2>/dev/null | grep -q 'inet ' && break; \ + echo \"br-tollgate has no kernel-level IPv4 address after cycle $i, retrying\"; \ + done; \ + ip -4 addr show br-tollgate 2>/dev/null | grep -q 'inet ' || \ + { echo 'br-tollgate never got a kernel-level IPv4 address after 5 cycles'; exit 1; }" + )?; Ok(()) } diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs new file mode 100644 index 00000000..82bafed4 --- /dev/null +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -0,0 +1,139 @@ +use anyhow::{Context, Result}; + +use crate::opkg::PkgManager; +use crate::tollgate::TollGateConfig; +use crate::Router; + +/// Install NoDogSplash and immediately stop it, before configuring anything. +/// +/// The OpenWrt package's postinst auto-enables and starts nodogsplash on +/// install using its stock default config — critically, `gatewayinterface` +/// defaults to `br-lan`. On a fresh install that window is real: NoDogSplash +/// only manages IPv4 iptables, so anything plugged into `br-lan` (e.g. an +/// admin's own management box) silently loses IPv4 connectivity (DHCP still +/// listens, but the gate blocks the client until ndsctl authorizes its MAC) +/// until we get a chance to repoint it — a full network re-scan can take +/// long enough for that to matter. Stopping it right after install, before +/// `configure()` ever runs, closes that window as early as possible. +/// +/// `tollgate-wrt` delegates all MAC authorization and gate open/close to +/// NoDogSplash via `ndsctl` — it has no firewall/netfilter code of its own +/// (confirmed: its binary has no `nft`/`ipset`/`iptables` calls at all). +/// Upstream's package therefore hard-depends on `+nodogsplash`, but neither +/// of our install paths (see `tollgate::install`) pull it in automatically. +pub fn install_and_stop(router: &Router, pkg_mgr: PkgManager) -> Result<()> { + router + .install_package(pkg_mgr, "nodogsplash") + .context("install nodogsplash — required by tollgate-wrt for client gating")?; + router.run_ok("/etc/init.d/nodogsplash stop || true")?; + Ok(()) +} + +/// Point NoDogSplash's webroot at TollGate's own splash page instead of the +/// generic stock one NoDogSplash ships with. +/// +/// Confirmed live: without this, NoDogSplash serves its own bundled +/// click-to-continue splash page — clicking "Continue" calls NDS's built-in +/// auth handler directly and authorizes the client with zero payment +/// involved. TollGate's actual payment UI (a QR/Cashu-token entry SPA) lives +/// at `/etc/tollgate/tollgate-captive-portal-site` — the .ipk's data payload +/// stages it there (see `packaging/files/tollgate-captive-portal-site/` in +/// the upstream repo), it's just never wired up as NoDogSplash's webroot. +/// +/// Mirrors upstream's own `90-tollgate-captive-portal-symlink` uci-defaults +/// script exactly (symlink swap, not a `webroot` UCI override) — confirmed +/// live that setting `option webroot` directly instead causes NoDogSplash to +/// 500 on every request, for reasons not fully understood (worth filing +/// upstream, but the symlink approach is what's actually shipped/tested). +pub fn install_captive_portal_symlink(router: &Router) -> Result<()> { + let (_, exists) = router.run("test -d /etc/tollgate/tollgate-captive-portal-site")?; + if exists != 0 { + anyhow::bail!( + "/etc/tollgate/tollgate-captive-portal-site missing — expected to be staged \ + by the tollgate-wrt package install" + ); + } + + router.run_ok( + "if [ -L /etc/nodogsplash/htdocs ]; then \ + true; \ + else \ + if [ -d /etc/nodogsplash/htdocs ]; then \ + mv /etc/nodogsplash/htdocs /etc/nodogsplash/htdocs.backup; \ + fi; \ + rm -rf /etc/nodogsplash/htdocs; \ + ln -sf /etc/tollgate/tollgate-captive-portal-site /etc/nodogsplash/htdocs; \ + fi" + )?; + Ok(()) +} + +/// Configure NoDogSplash to gate the dedicated `br-tollgate` bridge (see +/// `wifi::provision_network`), not `br-lan` — the paid SSID here lives on its +/// own isolated network/subnet rather than the canonical upstream layout +/// where it's bridged into `lan`. +/// +/// Must run after `wifi::provision_ssid` has created `br-tollgate` — pointing +/// `gatewayinterface` at a bridge that doesn't exist yet is at best a no-op +/// and at worst leaves NoDogSplash in a confused state. +pub fn configure(router: &Router, cfg: &TollGateConfig) -> Result<()> { + router.run_ok("touch /etc/config/nodogsplash")?; + + // The nodogsplash package's own uci-defaults populate an anonymous + // `@nodogsplash[0]` section on first install, pointed at `br-lan` (its + // stock default — see `install_and_stop`). NoDogSplash supports multiple + // simultaneous gateway instances, one per config section, so leaving this + // in place alongside our own `nodogsplash.main` doesn't get overridden by + // it — it starts a *second* instance gating br-lan for real. Delete it; + // `main` is the only instance this project manages. + let _ = router.uci_delete("nodogsplash.@nodogsplash[0]"); + + router.uci_set("nodogsplash.main", "nodogsplash")?; + router.uci_set("nodogsplash.main.enabled", "1")?; + router.uci_set("nodogsplash.main.gatewayinterface", "br-tollgate")?; + router.uci_set("nodogsplash.main.gatewayname", &format!("{} Portal", cfg.ssid))?; + router.uci_set("nodogsplash.main.gatewaydomainname", "TollGate.lan")?; + router.uci_set("nodogsplash.main.gatewayport", "2050")?; + + // Pre-auth "walled garden": traffic an unauthenticated client must still + // reach before ndsctl authorizes their MAC. `uci_delete` + rebuild (rather + // than only adding our own entries) is deliberate — the stock package + // config ships a `users_to_router` default of its own (DNS, DHCP, plus + // SSH/Telnet to the router), and `uci_set`/`add_list` on an existing + // *named* section does not clear an inherited default list, so without + // an explicit delete first, re-provisioning would silently keep + // whatever was there before. + // + // DNS (53) and DHCP (67, udp) are carried over from that stock default — + // without them a client can't even get an IP or resolve the portal + // domain before authenticating (confirmed live: omitting udp/67 here + // broke DHCP entirely for new clients on the archipelago SSID). 2121 + // (TollGate payment) and 2050 (NDS's own splash portal) are ours. + // SSH/Telnet (22/23) are deliberately *not* carried over — the stock + // default exposes router shell access to every unauthenticated device + // on a public pay-as-you-go network, which is a bad default here. + let _ = router.uci_delete("nodogsplash.main.users_to_router"); + router.uci_add_list("nodogsplash.main.users_to_router", "allow udp port 53")?; + router.uci_add_list("nodogsplash.main.users_to_router", "allow tcp port 53")?; + router.uci_add_list("nodogsplash.main.users_to_router", "allow udp port 67")?; + router.uci_add_list("nodogsplash.main.users_to_router", "allow tcp port 2121")?; + router.uci_add_list("nodogsplash.main.users_to_router", "allow tcp port 2050")?; + + // Post-auth (paid) clients get full access — matches the stock package + // default (`list authenticated_users 'allow all'`), which our from-scratch + // named section never carried over. Under this router's default-ACCEPT + // FORWARD policy an empty list happens to behave the same, but that's an + // accident of this specific setup, not something to depend on. + let _ = router.uci_delete("nodogsplash.main.authenticated_users"); + router.uci_add_list("nodogsplash.main.authenticated_users", "allow all")?; + + router.uci_commit(Some("nodogsplash"))?; + Ok(()) +} + +/// (Re)start the nodogsplash service so config changes and gate state take effect. +pub fn restart(router: &Router) -> Result<()> { + router.run_ok("/etc/init.d/nodogsplash enable")?; + router.run_ok("/etc/init.d/nodogsplash restart || /etc/init.d/nodogsplash start")?; + Ok(()) +} diff --git a/core/openwrt/src/tollgate/wifi.rs b/core/openwrt/src/tollgate/wifi.rs index 03baea1d..5d3428bd 100644 --- a/core/openwrt/src/tollgate/wifi.rs +++ b/core/openwrt/src/tollgate/wifi.rs @@ -38,14 +38,29 @@ pub fn provision_ssid(router: &Router, cfg: &TollGateConfig) -> Result<()> { } /// Add a `tollgate` network interface (isolated LAN for TollGate clients). +/// +/// Binds to a named bridge device (`br-tollgate`) rather than leaving the +/// wifi-iface as the network's raw device — NoDogSplash's `gatewayinterface` +/// needs a stable, known interface name to gate (see `nodogsplash::provision`), +/// and the driver-assigned name of a bare wifi vif (e.g. `phy0-ap0`) isn't +/// guaranteed across hardware. fn provision_network(router: &Router) -> Result<()> { router.uci_apply( "network", &[ + ("network.tollgate_bridge", "device"), + ("network.tollgate_bridge.type", "bridge"), + ("network.tollgate_bridge.name", "br-tollgate"), ("network.tollgate", "interface"), + ("network.tollgate.device", "br-tollgate"), ("network.tollgate.proto", "static"), ("network.tollgate.ipaddr", "192.168.99.1"), ("network.tollgate.netmask", "255.255.255.0"), + // NoDogSplash only manages IPv4 iptables rules. If IPv6 RA/DHCPv6 + // stays enabled, clients get routable IPv6 addresses and their OS + // validates connectivity (and browses freely) over IPv6, bypassing + // the portal entirely. See OpenTollGate/tollgate-module-basic-go#148. + ("network.tollgate.ip6assign", "0"), ], )?; @@ -58,6 +73,8 @@ fn provision_network(router: &Router) -> Result<()> { ("dhcp.tollgate.start", "100"), ("dhcp.tollgate.limit", "150"), ("dhcp.tollgate.leasetime", "5m"), + ("dhcp.tollgate.ra", "disabled"), + ("dhcp.tollgate.dhcpv6", "disabled"), ], )?; @@ -66,8 +83,11 @@ fn provision_network(router: &Router) -> Result<()> { /// Add firewall zone for the tollgate interface. /// -/// TollGate itself gates forwarding via iptables; the firewall zone isolates -/// tollgate clients from other LAN segments. +/// This zone only isolates tollgate clients from other LAN segments and +/// opens the payment port to the router. Per-client forwarding to WAN is +/// actually gated by NoDogSplash's own iptables rules (via `ndsctl`), not by +/// anything in this static firewall config — `tollgate-wrt` has no netfilter +/// code of its own. See `nodogsplash::provision`. fn provision_firewall(router: &Router) -> Result<()> { // Zone router.uci_apply( diff --git a/docs/1.8.0-RELEASE-HARDENING-PLAN.md b/docs/1.8.0-RELEASE-HARDENING-PLAN.md index 18c6ba54..e3f39431 100644 --- a/docs/1.8.0-RELEASE-HARDENING-PLAN.md +++ b/docs/1.8.0-RELEASE-HARDENING-PLAN.md @@ -304,7 +304,7 @@ media (latest artifact only one minor behind). - [ ] 🟠 **Sign + checksum the ISO.** Pipeline ends at `xorriso` with no `SHA256SUMS`, no GPG/minisign, no Secure Boot (`BOOTX64.EFI` is unsigned though `grub-efi-amd64-signed` is installed). Emit + sign checksums; wire signed Secure Boot. -- [ ] 🟠 **Registries over HTTPS in the image too** — `146.59.87.168:3000` / `git.tx1138.com` +- [ ] 🟠 **Registries over HTTPS in the image too** — `146.59.87.168:3000` are baked `insecure=true`/`tls_verify:false` (`:216`, `:2308`). (Ties to §A.) - [ ] 🟡 **Add `unattended-upgrades` + a default-deny nftables firewall** (allow 22/80/443 + mesh/WG). Neither exists today; OS packages drift until reflash and there is no host diff --git a/docs/container-architecture.html b/docs/container-architecture.html index 2a0fe468..ce1e6e96 100644 --- a/docs/container-architecture.html +++ b/docs/container-architecture.html @@ -842,7 +842,7 @@

Registry

    -
  • Private registry at git.tx1138.com/lfg2025/
  • +
  • Private registry at 146.59.87.168:3000/lfg2025/
  • HTTPS (self-hosted Gitea)
  • All images pre-pulled into registry; nodes pull on first boot
diff --git a/docs/dht-distribution-design.md b/docs/dht-distribution-design.md index 4746596f..a209c4c7 100644 --- a/docs/dht-distribution-design.md +++ b/docs/dht-distribution-design.md @@ -65,7 +65,7 @@ origin": ### IndeeHub (the streaming target) - Original platform (not a fork). Working source: `~/Projects/Indeedhub Prototype/` - (Vue 3 + NestJS). Submodule `git.tx1138.com/lfg2025/indeehub.git` (host retired — + (Vue 3 + NestJS). Submodule `146.59.87.168:3000/lfg2025/indeehub.git` (repointed off the retired host — needs a live remote). In `archy`: image-only, `apps/indeedhub/manifest.yml` pulls `146.59.87.168:3000/lfg2025/indeedhub:1.0.0` (+ `-api`, `-ffmpeg`, postgres, redis, minio, nostr-rs-relay). diff --git a/image-recipe/_archived/.gitea-workflows/build-iso-dev.yml b/image-recipe/_archived/.gitea-workflows/build-iso-dev.yml index 9fa87689..a05c7cd8 100644 --- a/image-recipe/_archived/.gitea-workflows/build-iso-dev.yml +++ b/image-recipe/_archived/.gitea-workflows/build-iso-dev.yml @@ -112,7 +112,7 @@ jobs: run: | sudo mkdir -p /etc/containers/registries.conf.d echo '[[registry]] - location = "git.tx1138.com" + location = "146.59.87.168:3000" insecure = true' | sudo tee /etc/containers/registries.conf.d/archipelago.conf - name: Build unbundled ISO diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index 3868dabb..f9b8da41 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -221,7 +221,7 @@ location = "146.59.87.168:3000" insecure = true [[registry]] -location = "git.tx1138.com" +location = "146.59.87.168:3000" insecure = true REGCONF fi @@ -1158,7 +1158,7 @@ fi # If built against a newer GLIBC, the binary will fail at runtime. # Rebuild with: FROM debian:13 AS builder echo " Extracting NostrVPN binary..." -_NVPN_IMG="${NOSTR_VPN_IMAGE:-git.tx1138.com/lfg2025/nostr-vpn:v0.3.7}" +_NVPN_IMG="${NOSTR_VPN_IMAGE:-146.59.87.168:3000/lfg2025/nostr-vpn:v0.3.7}" NVPN_IMAGE_ID="$($CONTAINER_CMD images -q "$_NVPN_IMG" 2>/dev/null)" if [ -z "$NVPN_IMAGE_ID" ]; then $CONTAINER_CMD pull "$_NVPN_IMG" 2>/dev/null || true @@ -1189,11 +1189,11 @@ fi # Extract nostr-rs-relay binary from container image (native system service for VPN signaling) echo " Extracting nostr-rs-relay binary..." -RELAY_IMAGE="$($CONTAINER_CMD images -q git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null)" +RELAY_IMAGE="$($CONTAINER_CMD images -q 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null)" if [ -z "$RELAY_IMAGE" ]; then - $CONTAINER_CMD pull git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null || true + $CONTAINER_CMD pull 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null || true fi -RELAY_CONTAINER=$($CONTAINER_CMD create git.tx1138.com/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null) || true +RELAY_CONTAINER=$($CONTAINER_CMD create 146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0 2>/dev/null) || true if [ -n "$RELAY_CONTAINER" ]; then $CONTAINER_CMD cp "$RELAY_CONTAINER:/usr/local/bin/nostr-rs-relay" "$ARCH_DIR/bin/nostr-rs-relay" 2>/dev/null && \ chmod +x "$ARCH_DIR/bin/nostr-rs-relay" && \ @@ -2319,7 +2319,7 @@ location = "146.59.87.168:3000" insecure = true [[registry]] -location = "git.tx1138.com" +location = "146.59.87.168:3000" insecure = true REGCONF chown -R 1000:1000 /mnt/target/home/archipelago/.config @@ -2330,7 +2330,7 @@ cat > /mnt/target/var/lib/archipelago/config/registries.json <<'DYNREG' { "registries": [ {"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Primary", "tls_verify": false, "enabled": true, "priority": 0}, - {"url": "git.tx1138.com/lfg2025", "name": "Archipelago Fallback", "tls_verify": true, "enabled": true, "priority": 10} + {"url": "146.59.87.168:3000/lfg2025", "name": "Archipelago Fallback", "tls_verify": true, "enabled": true, "priority": 10} ] } DYNREG @@ -2475,7 +2475,7 @@ if [ -d "$REPO_DIR/.git" ]; then exit 0 # Already cloned fi echo "[update] Cloning Archipelago repo for self-updates..." -su - archipelago -c "git clone https://git.tx1138.com/lfg2025/archy $REPO_DIR" 2>/dev/null || { +su - archipelago -c "git clone https://146.59.87.168:3000/lfg2025/archy $REPO_DIR" 2>/dev/null || { echo "[update] Git clone failed (network?). Updates will retry on next boot." exit 0 } diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 2f34a33e..1e227760 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -83,25 +83,33 @@ xset s noblank 2>/dev/null || true pkill -u archipelago -f 'chromium.*localhost' 2>/dev/null || true sleep 1 -# GPU vs headless (#36). On a real kiosk display with a GPU, GPU rasterization is -# fast. On a GPU-less / headless server (no /dev/dri), --enable-gpu-rasterization -# forces GPU paths that fall back to software compositing and SPIN a full core at -# ~92% CPU, saturating the node. Detect the GPU and pick safe flags accordingly. +# GPU vs headless (#36, choppy-audio incident 2026-06-28). --enable-gpu-rasterization +# spins a dedicated GPU process at 55-92% CPU even on real GPU hardware (Intel HD 5500) +# because under X11 it falls back to software compositing anyway — that CPU +# starvation is what caused choppy HDMI audio. --in-process-gpu avoids the +# separate process; GpuRasterization is also disabled via --disable-features below. +# On a GPU-less / headless server (no /dev/dri), disable GPU entirely instead. if [ -e /dev/dri/card0 ] || [ -e /dev/dri/renderD128 ]; then - GPU_FLAGS="--enable-gpu-rasterization --num-raster-threads=2" + GPU_FLAGS="--in-process-gpu --num-raster-threads=1" else GPU_FLAGS="--disable-gpu --num-raster-threads=1" fi +ARCHIPELAGO_UID=$(id -u archipelago) + while true; do - sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago chromium --kiosk \ + # XDG_RUNTIME_DIR must be passed explicitly — without it Chromium's audio + # backend can't find PipeWire-Pulse's socket at /run/user//pulse/native, + # falls back to raw ALSA "default", fails to connect, and produces no audio + # at all with no visible error (--noerrdialogs suppresses it). + sudo -u archipelago env DISPLAY=:0 HOME=/home/archipelago XDG_RUNTIME_DIR=/run/user/$ARCHIPELAGO_UID chromium --kiosk \ --app=http://localhost/kiosk?safe_area_x=${KIOSK_SAFE_AREA_X_PX:-0}\&safe_area_y=${KIOSK_SAFE_AREA_Y_PX:-0} \ --noerrdialogs \ --disable-infobars \ --disable-translate \ --no-first-run \ --check-for-update-interval=31536000 \ - --disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled \ + --disable-features=TranslateUI,MetricsReporting,AutofillServerCommunication,PasswordManagerEnabled,GpuRasterization \ --disable-session-crashed-bubble \ --disable-save-password-bubble \ --disable-suggestions-service \ diff --git a/image-recipe/configs/archipelago-kiosk.service b/image-recipe/configs/archipelago-kiosk.service index ad996448..b388c4f5 100644 --- a/image-recipe/configs/archipelago-kiosk.service +++ b/image-recipe/configs/archipelago-kiosk.service @@ -25,8 +25,11 @@ RestartSec=5 # backend (it caused the .198 receive timeout + deploy storms). Cap CPU + memory # so a runaway kiosk can never take the whole machine down; Delegate so the cap # also binds the chromium/Xorg children in this unit's cgroup. +# CPUQuota=75% (0.75 cores) was too tight even for normal playback — the kiosk +# was throttled ~40% of the time, which is what caused choppy HDMI audio on +# archy-x250-exp (2026-06-28 incident). 200% (2 cores) gives enough headroom. Delegate=yes -CPUQuota=75% +CPUQuota=200% # Raised from 1500M/1200M: a Framework (Tiger Lake) kiosk sat at 806M used / # 1.1G peak, riding the old MemoryHigh reclaim-throttle line — the throttling # itself was the perceived UI lag. Keep Max well above real peaks; High stays diff --git a/image-recipe/configs/asound.conf b/image-recipe/configs/asound.conf new file mode 100644 index 00000000..8a1f1f07 --- /dev/null +++ b/image-recipe/configs/asound.conf @@ -0,0 +1,7 @@ +pcm.!default { + type pulse + hint.description "Default ALSA Device (via PulseAudio)" +} +ctl.!default { + type pulse +} diff --git a/image-recipe/scripts/install-podman.sh b/image-recipe/scripts/install-podman.sh index 0977bd8e..e772a4e0 100755 --- a/image-recipe/scripts/install-podman.sh +++ b/image-recipe/scripts/install-podman.sh @@ -40,10 +40,10 @@ EOF # Configure registries (use Docker Hub and quay.io) mkdir -p /home/archipelago/.config/containers/registries.conf.d cat > /home/archipelago/.config/containers/registries.conf <

Easy sources

-

Use images from Docker Hub, GHCR, git.tx1138.com, the VPS2 Gitea registry, or localhost. Good first candidates: Excalidraw, Stirling PDF, FreshRSS, Wallabag, HedgeDoc, CyberChef, Mealie, or PairDrop.

+

Use images from Docker Hub, GHCR, the VPS2 Gitea registry (146.59.87.168:3000), or localhost. Good first candidates: Excalidraw, Stirling PDF, FreshRSS, Wallabag, HedgeDoc, CyberChef, Mealie, or PairDrop.

diff --git a/neode-ui/src/views/Home.vue b/neode-ui/src/views/Home.vue index d733404e..245988bb 100644 --- a/neode-ui/src/views/Home.vue +++ b/neode-ui/src/views/Home.vue @@ -519,6 +519,35 @@ const walletTransactions = ref([]) function openInMempool(txHash: string) { router.push({ name: 'app-session', params: { appId: 'mempool' }, query: { path: `/tx/${txHash}` } }) } +// wallet.ecash-history's shape (see handle_wallet_ecash_history in +// api/rpc/wallet.rs) — distinct from the LND-shaped WalletTransaction used +// elsewhere in this file, so it's mapped into that shape below rather than +// widening WalletTransaction itself with a pile of ecash-only fields. +interface EcashTransaction { + id: string + tx_type: 'send' | 'receive' + amount_sats: number + timestamp: string + description: string + mint_url: string + peer: string + kind: 'cashu' | 'fedimint' +} + +function ecashToWalletTransaction(tx: EcashTransaction): WalletTransaction { + return { + tx_hash: tx.id, + amount_sats: tx.amount_sats, + direction: tx.tx_type === 'receive' ? 'incoming' : 'outgoing', + num_confirmations: 1, + time_stamp: Math.floor(new Date(tx.timestamp).getTime() / 1000), + total_fees: 0, + dest_addresses: [], + label: tx.description, + block_height: 0, + } +} + async function loadWeb5Status() { // A transient RPC timeout must NOT flash the balance to 0 ("wallet says 0 when // there is a balance"). On failure keep the last-known value — the refs start @@ -526,7 +555,15 @@ async function loadWeb5Status() { try { const res = await rpcClient.call<{ balance_sats: number; channel_balance_sats: number }>({ method: 'lnd.getinfo', timeout: 5000 }); walletOnchain.value = res.balance_sats || 0; walletLightning.value = res.channel_balance_sats || 0; walletConnected.value = true } catch { walletConnected.value = false } try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.ecash-balance', timeout: 5000 }); walletEcash.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ } try { const res = await rpcClient.call<{ balance_sats: number }>({ method: 'wallet.fedimint-balance', timeout: 5000 }); walletFedimint.value = res.balance_sats ?? 0 } catch { /* keep last-known balance */ } - try { const res = await rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 }); walletTransactions.value = res.transactions || [] } catch { /* keep last-known transactions */ } + // Merge LND transactions with ecash/Fedimint history (wallet.ecash-history + // already unifies both) — previously only LND transactions were fetched + // here, so any Cashu or Fedimint receive (e.g. a TollGate payment) never + // appeared in the Transactions modal even though the balance included it. + let lndTxs: WalletTransaction[] = [] + try { const res = await rpcClient.call<{ transactions: WalletTransaction[]; incoming_pending_count: number }>({ method: 'lnd.gettransactions', timeout: 5000 }); lndTxs = res.transactions || [] } catch { /* keep last-known transactions */ } + let ecashTxs: WalletTransaction[] = [] + try { const res = await rpcClient.call<{ transactions: EcashTransaction[] }>({ method: 'wallet.ecash-history', timeout: 5000 }); ecashTxs = (res.transactions || []).map(ecashToWalletTransaction) } catch { /* keep last-known transactions */ } + walletTransactions.value = [...lndTxs, ...ecashTxs].sort((a, b) => b.time_stamp - a.time_stamp) } // System stats diff --git a/neode-ui/src/views/discover/curatedApps.ts b/neode-ui/src/views/discover/curatedApps.ts index 68016e9f..b48d5757 100644 --- a/neode-ui/src/views/discover/curatedApps.ts +++ b/neode-ui/src/views/discover/curatedApps.ts @@ -24,7 +24,7 @@ const CATALOG_TTL = 60 * 60 * 1000 // 1 hour cache /** Catalog URLs tried in order. First success wins. * Primary is the backend proxy (`/api/app-catalog`) — server-side fetch - * bypasses CORS on git.tx1138.com and CSP restrictions on the IP-port + * bypasses CORS on the upstream Gitea and CSP restrictions on the IP-port * fallback. If the backend is offline (mid-restart etc.) we fall back * to the static copy baked into the frontend build. */ const CATALOG_URLS = [ diff --git a/neode-ui/src/views/settings/AccountInfoSection.vue b/neode-ui/src/views/settings/AccountInfoSection.vue index 165e38c5..d0b2bc61 100644 --- a/neode-ui/src/views/settings/AccountInfoSection.vue +++ b/neode-ui/src/views/settings/AccountInfoSection.vue @@ -1405,7 +1405,7 @@ init()

Updates survive network hiccups. Downloads now resume from exactly where a dropped connection left off, and retry up to 6 times with increasing gaps between attempts, instead of restarting from byte zero or giving up.

The download progress bar now shows real progress. Instead of a fake number that creeps to 95% and freezes, you see the actual bytes arriving, and it continues to update correctly even if you navigate away and come back.

-

Update check itself retries on slow responses. If git.tx1138.com is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date.

+

Update check itself retries on slow responses. If the release server is momentarily overloaded, the node tries three times with a five-second wait between attempts before concluding you're up to date.

@@ -1591,7 +1591,7 @@ init() Apr 11, 2026
-

Migrated container registry to git.tx1138.com

+

Migrated container registry to the self-hosted Gitea

diff --git a/releases/app-catalog.json b/releases/app-catalog.json index d4a0b8e9..720df197 100644 --- a/releases/app-catalog.json +++ b/releases/app-catalog.json @@ -1,67 +1,70 @@ { + "schema": 1, + "updated": "2026-07-10", "apps": { "adguardhome": { - "image": "146.59.87.168:3000/lfg2025/adguardhome:v0.107.55", - "version": "v0.107.55" + "version": "v0.107.55", + "image": "146.59.87.168:3000/lfg2025/adguardhome:v0.107.55" }, "aiui": { + "version": "0.1.0", "manifest": { "app": { + "id": "aiui", + "name": "AI Assistant", + "version": "0.1.0", + "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": [ - { - "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" }, - "version": "0.1.0" + "ports": [ + { + "host": 5180, + "container": 80, + "protocol": "tcp", + "bind": "127.0.0.1" + } + ], + "health_check": { + "type": "http", + "endpoint": "http://localhost:80", + "path": "/", + "interval": "60s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "0.1.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", + "description": "Postgres backend for BTCPay and NBXplorer.", "container": { - "data_uid": "100998:100998", - "image": "git.tx1138.com/lfg2025/postgres:15.17", - "network": "archy-net", + "image": "146.59.87.168:3000/lfg2025/postgres:15.17", "pull_policy": "if-not-present", + "network": "archy-net", + "data_uid": "100998:100998", "secret_env": [ { "key": "POSTGRES_PASSWORD", @@ -74,24 +77,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": [ @@ -101,36 +89,51 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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", + "description": "MariaDB backend for the mempool explorer stack.", "container": { - "data_uid": "100998:100998", - "image": "git.tx1138.com/lfg2025/mariadb:11.4.10", - "network": "archy-net", + "image": "146.59.87.168:3000/lfg2025/mariadb:11.4.10", "pull_policy": "if-not-present", + "network": "archy-net", + "data_uid": "100998:100998", "secret_env": [ { "key": "MYSQL_PASSWORD", @@ -147,24 +150,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": [ @@ -174,89 +162,104 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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", + "description": "Frontend web UI for mempool explorer.", + "container_name": "mempool", "container": { "image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.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": [ - { - "container": 8080, - "host": 4080, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "512Mi" }, "security": { "capabilities": [], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "3.0.1" + "ports": [ + { + "host": 4080, + "container": 8080, + "protocol": "tcp" + } + ], + "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 + }, + "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", + "description": "BTCPay blockchain indexer service.", "container": { - "image": "git.tx1138.com/lfg2025/nbxplorer:2.6.0", - "network": "archy-net", + "image": "146.59.87.168:3000/lfg2025/nbxplorer:2.6.0", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "NBXPLORER_BTCRPCPASSWORD", @@ -278,7 +281,32 @@ "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/nbxplorer", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "NBXPLORER_DATADIR=/data", "NBXPLORER_NETWORK=mainnet", @@ -291,73 +319,46 @@ "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": [ - { - "container": 32838, - "host": 32838, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "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" + } }, "bitcoin-core": { + "version": "latest", "manifest": { "app": { - "bitcoin_integration": { - "pruning_support": true, - "rpc_access": "admin", - "sync_required": true, - "testnet_support": false - }, + "id": "bitcoin-core", + "name": "Bitcoin Core", + "version": "28.4.0", + "description": "Reference Bitcoin Core node with dynamic prune/full-mode startup based on host disk.", + "container_name": "bitcoin-core", "container": { + "image": "146.59.87.168:3000/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_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 -noconf -printtoconsole=0 -server=1 -prune=550 -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 -rpcuser=\"$RPC_USER\" -rpcpassword=\"$RPC_PASS\";\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -noconf -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 -rpcuser=\"$RPC_USER\" -rpcpassword=\"$RPC_PASS\";\nfi" ], - "data_uid": "100101:100101", "derived_env": [ { "key": "DISK_GB", "template": "{{DISK_GB}}" } ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "146.59.87.168:3000/lfg2025/bitcoin:28.4", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BITCOIN_RPC_PASS", @@ -367,44 +368,18 @@ "key": "BITCOIN_RPC_TXRELAY_RPCAUTH", "secret_file": "bitcoin-rpc-txrelay-rpcauth" } - ] + ], + "data_uid": "100101:100101" }, - "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": [ - { - "bind": "127.0.0.1", - "container": 8332, - "host": 8332, - "protocol": "tcp" - }, - { - "container": 8333, - "host": 8333, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 0, - "disk_limit": "500Gi", - "memory_limit": "4Gi" + "memory_limit": "4Gi", + "disk_limit": "500Gi" }, "security": { "capabilities": [ @@ -414,93 +389,119 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "28.4.0", + "ports": [ + { + "host": 8332, + "container": 8332, + "protocol": "tcp", + "bind": "127.0.0.1" + }, + { + "host": 8333, + "container": 8333, + "protocol": "tcp" + } + ], "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": "latest", "versions": [ { - "default": true, + "version": "latest", "image": "146.59.87.168:3000/lfg2025/bitcoin:latest", - "version": "latest" + "default": true }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:31.0", - "version": "31.0" + "version": "31.0", + "image": "146.59.87.168:3000/lfg2025/bitcoin:31.0" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:30.2", - "version": "30.2" + "version": "30.2", + "image": "146.59.87.168:3000/lfg2025/bitcoin:30.2" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:29.3", - "version": "29.3" + "version": "29.3", + "image": "146.59.87.168:3000/lfg2025/bitcoin:29.3" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:29.2", - "version": "29.2" + "version": "29.2", + "image": "146.59.87.168:3000/lfg2025/bitcoin:29.2" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:28.4", - "version": "28.4.0" + "version": "28.4.0", + "image": "146.59.87.168:3000/lfg2025/bitcoin:28.4" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin:27.2", - "version": "27.2" + "version": "27.2", + "image": "146.59.87.168:3000/lfg2025/bitcoin:27.2" }, { - "deprecated": true, + "version": "26.2", "image": "146.59.87.168:3000/lfg2025/bitcoin:26.2", - "version": "26.2" + "deprecated": true }, { - "deprecated": true, + "version": "25.2", "image": "146.59.87.168:3000/lfg2025/bitcoin:25.2", - "version": "25.2" + "deprecated": true } ] }, "bitcoin-knots": { + "version": "latest", "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:latest", "manifest": { "app": { - "bitcoin_integration": { - "pruning_support": true, - "rpc_access": "admin", - "sync_required": true, - "testnet_support": false - }, + "id": "bitcoin-knots", + "name": "Bitcoin Knots", + "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": "146.59.87.168:3000/lfg2025/bitcoin-knots:latest", + "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_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 -noconf -printtoconsole=0 -server=1 -prune=550 -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 -rpcuser=\"$RPC_USER\" -rpcpassword=\"$RPC_PASS\";\nelse\n exec \"$BITCOIND\" -datadir=/home/bitcoin/.bitcoin -noconf -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 -rpcuser=\"$RPC_USER\" -rpcpassword=\"$RPC_PASS\";\nfi" ], - "data_uid": "100101:100101", "derived_env": [ { "key": "DISK_GB", "template": "{{DISK_GB}}" } ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:latest", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BITCOIN_RPC_PASS", @@ -510,44 +511,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": [ - { - "bind": "127.0.0.1", - "container": 8332, - "host": 8332, - "protocol": "tcp" - }, - { - "container": 8333, - "host": 8333, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 0, - "disk_limit": "500Gi", - "memory_limit": "8Gi" + "memory_limit": "8Gi", + "disk_limit": "500Gi" }, "security": { "capabilities": [ @@ -557,51 +532,83 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "28.1.0", + "ports": [ + { + "host": 8332, + "container": 8332, + "protocol": "tcp", + "bind": "127.0.0.1" + }, + { + "host": 8333, + "container": 8333, + "protocol": "tcp" + } + ], "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": "latest", "versions": [ { - "default": true, + "version": "latest", "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260508", - "version": "latest" + "default": true }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260508", - "version": "29.3.knots20260508" + "version": "29.3.knots20260508", + "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260508" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260507", - "version": "29.3.knots20260507" + "version": "29.3.knots20260507", + "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260507" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260210", - "version": "29.3.knots20260210" + "version": "29.3.knots20260210", + "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.3.knots20260210" }, { - "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.2.knots20251110", - "version": "29.2.knots20251110" + "version": "29.2.knots20251110", + "image": "146.59.87.168:3000/lfg2025/bitcoin-knots:29.2.knots20251110" } ] }, "bitcoin-ui": { + "version": "1.7.84-alpha", "image": "146.59.87.168:3000/lfg2025/bitcoin-ui:1.7.84-alpha", "manifest": { "app": { + "id": "bitcoin-ui", + "name": "Bitcoin UI", + "version": "1.0.0", + "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", @@ -614,44 +621,44 @@ "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": [], "resources": { "memory_limit": "128Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "version": "1.0.0", + "ports": [], "volumes": [ { - "options": [ - "ro" - ], + "type": "bind", "source": "/var/lib/archipelago/bitcoin-ui/nginx.conf", "target": "/etc/nginx/conf.d/default.conf", - "type": "bind" + "options": [ + "ro" + ] } - ] + ], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:8334", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "1.7.84-alpha" + } }, "botfights": { + "version": "1.1.0", "manifest": { "app": { + "id": "botfights", + "name": "BotFights", + "version": "1.1.0", + "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": { "image": "146.59.87.168:3000/lfg2025/botfights:1.1.0", @@ -662,35 +669,71 @@ "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": 1001, + "seccomp_profile": "default", + "network_policy": "bridge", + "apparmor_profile": "default" + }, + "ports": [ + { + "host": 9100, + "container": 9100, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "botfights-data", + "target": "/app/server/data" + }, + { + "type": "tmpfs", + "target": "/tmp", + "options": [ + "rw", + "noexec", + "nosuid", + "size=64m" + ] + } + ], "environment": [ "NODE_ENV=production" ], "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", @@ -700,77 +743,31 @@ "competition", "controller" ] - }, - "name": "BotFights", - "ports": [ - { - "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": 1001 - }, - "version": "1.1.0", - "volumes": [ - { - "source": "botfights-data", - "target": "/app/server/data", - "type": "bind" - }, - { - "options": [ - "rw", - "noexec", - "nosuid", - "size=64m" - ], - "target": "/tmp", - "type": "tmpfs" - } - ] + } } - }, - "version": "1.1.0" + } }, "btcpay": { + "version": "2.3.9", "image": "docker.io/btcpayserver/btcpayserver:2.3.9", "images": { - "archy-btcpay-db": "146.59.87.168:3000/lfg2025/postgres:15.17", + "btcpay-server": "docker.io/btcpayserver/btcpayserver:2.3.9", "archy-nbxplorer": "146.59.87.168:3000/lfg2025/nbxplorer:2.6.0", - "btcpay-server": "docker.io/btcpayserver/btcpayserver:2.3.9" - }, - "version": "2.3.9" + "archy-btcpay-db": "146.59.87.168:3000/lfg2025/postgres:15.17" + } }, "btcpay-server": { + "version": "2.3.9", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "read-only", - "sync_required": true - }, + "id": "btcpay-server", + "name": "BTCPay Server", + "version": "2.3.9", + "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.3.9", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "BTCPAY_BTCRPCPASSWORD", @@ -779,6 +776,17 @@ { "key": "BTCPAY_DB_PASS", "secret_file": "btcpay-db-password" + }, + { + "key": "BTCPAY_BTCLIGHTNING", + "secret_file": "btcpay-lnd-connection", + "optional": true + } + ], + "derived_env": [ + { + "key": "BTCPAY_HOST", + "template": "{{HOST_IP}}:23000" } ] }, @@ -796,84 +804,85 @@ "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/btcpay", + "target": "/datadir", + "options": [ + "rw" + ] + } + ], "environment": [ "ASPNETCORE_URLS=http://0.0.0.0:49392", "BTCPAY_PROTOCOL=http", "BTCPAY_CHAINS=btc", + "BTCPAY_PLUGINDIR=/datadir/Plugins", "BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838", "BTCPAY_BTCRPCURL=http://bitcoin-knots:8332", "BTCPAY_BTCRPCUSER=archipelago", "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": [ - { - "container": 49392, - "host": 23000, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "version": "2.3.9", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/btcpay", - "target": "/datadir", - "type": "bind" - } - ] + } } - }, - "version": "2.3.9" + } }, "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", + "description": "Lightning Network implementation in C. Lightweight alternative to LND.", "container": { "image": "elementsproject/lightningd:v23.08.2", "image_signature": "cosign://...", @@ -885,7 +894,44 @@ "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" + }, + { + "host": 9835, + "container": 9835, + "protocol": "tcp" + } + ], + "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}", @@ -893,68 +939,35 @@ "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": [ - { - "container": 9735, - "host": 9736, - "protocol": "tcp" - }, - { - "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 - }, - "version": "23.08.2", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/core-lightning", - "target": "/home/clightning/.lightning", - "type": "bind" - } - ] + } } - }, - "version": "23.08.2" + } }, "cryptpad": { - "image": "146.59.87.168:3000/lfg2025/cryptpad:2024.12.0", - "version": "2024.12.0" + "version": "2024.12.0", + "image": "146.59.87.168:3000/lfg2025/cryptpad:2024.12.0" }, "did-wallet": { + "version": "1.0.0", "manifest": { "app": { + "id": "did-wallet", + "name": "Web5 DID Wallet", + "version": "1.0.0", + "description": "Web5 wallet with Decentralized Identifier (DID) support. Manage your digital identity and Web5 assets.", "container": { "image": "archipelago/did-wallet:1.0.0", "image_signature": "cosign://...", @@ -965,65 +978,65 @@ "storage": "2Gi" } ], - "description": "Web5 wallet with Decentralized Identifier (DID) support. Manage your digital identity and Web5 assets.", + "resources": { + "cpu_limit": 1, + "memory_limit": "512Mi", + "disk_limit": "2Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "did-wallet" + }, + "ports": [ + { + "host": 8088, + "container": 8080, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/did-wallet", + "target": "/app/wallet", + "options": [ + "rw" + ] + } + ], "environment": [ "WALLET_STORAGE=/app/wallet" ], "health_check": { - "endpoint": "http://localhost:8083", - "interval": "30s", + "type": "http", + "endpoint": "http://127.0.0.1:8080", "path": "/health", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "did-wallet", - "name": "Web5 DID Wallet", - "ports": [ - { - "container": 8080, - "host": 8083, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 1, - "disk_limit": "2Gi", - "memory_limit": "512Mi" - }, - "security": { - "apparmor_profile": "did-wallet", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/did-wallet", - "target": "/app/wallet", - "type": "bind" - } - ], "web5_integration": { - "bitcoin_integration": true, "did_support": true, - "wallet_functionality": true + "wallet_functionality": true, + "bitcoin_integration": true } } - }, - "version": "1.0.0" + } }, "electrs-ui": { + "version": "latest", "image": "146.59.87.168:3000/lfg2025/electrs-ui:latest", "manifest": { "app": { + "id": "electrs-ui", + "name": "Electrs UI", + "version": "1.0.0", + "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", @@ -1032,53 +1045,48 @@ } }, "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": [], "resources": { "memory_limit": "64Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "version": "1.0.0", - "volumes": [] + "ports": [], + "volumes": [], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:50002", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "latest" + } }, "electrumx": { + "version": "v1.18.0", "image": "146.59.87.168:3000/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", + "description": "Electrum server indexing Bitcoin chain data for lightweight wallet queries.", "container": { - "custom_args": [ - "export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@bitcoin-knots:8332/\"; exec electrumx_server" - ], + "image": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0", + "pull_policy": "if-not-present", + "network": "archy-net", "data_uid": "1000:1000", "entrypoint": [ "sh", "-lc" ], - "image": "146.59.87.168:3000/lfg2025/electrumx:v1.18.0", - "network": "archy-net", - "pull_policy": "if-not-present", + "custom_args": [ + "export DAEMON_URL=\"http://archipelago:$(printenv BITCOIN_RPC_PASS)@bitcoin-knots:8332/\"; exec electrumx_server" + ], "secret_env": [ { "key": "BITCOIN_RPC_PASS", @@ -1096,7 +1104,35 @@ }, "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/electrumx", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "COIN=Bitcoin", "DB_DIRECTORY=/data", @@ -1104,72 +1140,51 @@ "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": [ - { - "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 - }, - "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.0", "image": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true - }, + "id": "fedimint", + "name": "Fedimint Guardian", + "version": "0.10.0", + "description": "Federated Bitcoin minting service with built-in Guardian UI. Privacy-preserving Bitcoin custody.", "container": { + "image": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", + "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", @@ -1180,19 +1195,13 @@ "template": "ws://{{HOST_MDNS}}:8174" } ], - "entrypoint": [ - "sh", - "-lc" - ], - "image": "146.59.87.168:3000/lfg2025/fedimintd:v0.10.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "FM_BITCOIND_PASSWORD", "secret_file": "bitcoin-rpc-password" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { @@ -1203,7 +1212,43 @@ "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" + }, + { + "host": 8174, + "container": 8174, + "protocol": "tcp" + }, + { + "host": 8177, + "container": 8175, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/fedimint", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "FM_DATA_DIR=/data", "FM_BITCOIND_URL=http://bitcoin-knots:8332", @@ -1214,120 +1259,65 @@ "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": [ - { - "container": 8173, - "host": 8173, - "protocol": "tcp" - }, - { - "container": 8174, - "host": 8174, - "protocol": "tcp" - }, - { - "container": 8175, - "host": 8177, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 4, - "disk_limit": "20Gi", - "memory_limit": "4Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": true - }, - "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.0" + } }, "fedimint-clientd": { + "version": "0.8.0", "manifest": { "app": { + "id": "fedimint-clientd", + "name": "Fedimint Client", + "version": "0.8.0", + "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": "146.59.87.168:3000/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": "146.59.87.168:3000/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": [ - { - "container": 8080, - "host": 8178, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 1, - "disk_limit": "2Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "2Gi" }, "security": { "capabilities": [ @@ -1337,50 +1327,68 @@ "SETUID", "SETGID" ], - "network_policy": "bridge", - "readonly_root": true + "readonly_root": true, + "network_policy": "bridge" }, - "version": "0.8.0", + "ports": [ + { + "host": 8178, + "container": 8080, + "protocol": "tcp" + } + ], "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.0", "image": "146.59.87.168:3000/lfg2025/gatewayd:v0.10.0", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true - }, + "id": "fedimint-gateway", + "name": "Fedimint Gateway", + "version": "0.10.0", + "description": "Fedimint gateway service with automatic LND-or-LDK backend selection.", "container": { - "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 http://host.archipelago:8332 --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 http://host.archipelago:8332 --bitcoind-username \"$FM_BITCOIND_USERNAME\" --bitcoind-password \"$FM_BITCOIND_PASSWORD\" ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway;\nfi" - ], - "data_uid": "1000:1000", + "image": "146.59.87.168:3000/lfg2025/gatewayd:v0.10.0", + "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 http://host.archipelago:8332 --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 http://host.archipelago:8332 --bitcoind-username \"$FM_BITCOIND_USERNAME\" --bitcoind-password \"$FM_BITCOIND_PASSWORD\" ldk --ldk-lightning-port 9737 --ldk-alias archipelago-gateway;\nfi" + ], "generated_secrets": [ { - "kind": "bcrypt", - "name": "fedimint-gateway-hash" + "name": "fedimint-gateway-hash", + "kind": "bcrypt" } ], - "image": "git.tx1138.com/lfg2025/gatewayd:v0.10.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "FM_BITCOIND_PASSWORD", @@ -1390,7 +1398,8 @@ "key": "FEDI_HASH", "secret_file": "fedimint-gateway-hash" } - ] + ], + "data_uid": "1000:1000" }, "dependencies": [ { @@ -1402,110 +1411,91 @@ "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" + }, + { + "host": 9737, + "container": 9737, + "protocol": "tcp" + } + ], + "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": [ - { - "container": 8176, - "host": 8176, - "protocol": "tcp" - }, - { - "container": 9737, - "host": 9737, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": true - }, - "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.0" + } }, "filebrowser": { + "version": "v2.27.0", "image": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "none", - "sync_required": false - }, + "id": "filebrowser", + "name": "File Browser", + "version": "2.27.0", + "description": "Baseline Archipelago file manager service.", "container": { + "image": "146.59.87.168:3000/lfg2025/filebrowser:v2.27.0", + "pull_policy": "if-not-present", + "network": "archy-net", "custom_args": [ "--config", "/data/.filebrowser.json" ], - "data_uid": "100000:100000", - "image": "git.tx1138.com/lfg2025/filebrowser:v2.27.0", - "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": [ - { - "container": 80, - "host": 8083, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -1516,39 +1506,62 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "2.27.0", + "ports": [ + { + "host": 8083, + "container": 80, + "protocol": "tcp" + } + ], "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.27.0" + } }, "fips": { - "image": "146.59.87.168:3000/lfg2025/fips:v0.1.0", - "version": "v0.1.0" + "version": "v0.1.0", + "image": "146.59.87.168:3000/lfg2025/fips:v0.1.0" }, "fips-ui": { + "version": "1.0.0", "manifest": { "app": { + "id": "fips-ui", + "name": "FIPS Mesh", + "version": "1.0.0", + "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", @@ -1556,35 +1569,35 @@ "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": [], "resources": { "memory_limit": "128Mi" }, "security": { - "network_policy": "host", - "readonly_root": false + "readonly_root": false, + "network_policy": "host" }, - "version": "1.0.0", - "volumes": [] + "ports": [], + "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.23", "manifest": { "app": { + "id": "gitea", + "name": "Gitea", + "version": "1.23", + "description": "Self-hosted Git service with built-in container registry, CI/CD, and package hosting.", "category": "development", "container": { "image": "docker.io/gitea/gitea:1.23", @@ -1595,74 +1608,9 @@ "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": [ - { - "container": 3000, - "host": 3001, - "protocol": "tcp" - }, - { - "container": 22, - "host": 2222, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "500Mi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "500Mi" }, "security": { "capabilities": [ @@ -1673,49 +1621,144 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "bridge", + "readonly_root": false, "no_new_privileges": false, - "readonly_root": false + "network_policy": "bridge" }, - "version": "1.23", - "volumes": [ + "ports": [ { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/gitea/data", - "target": "/data", - "type": "bind" + "host": 3001, + "container": 3000, + "protocol": "tcp" }, { + "host": 2222, + "container": 22, + "protocol": "tcp" + } + ], + "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__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.23" + } }, "grafana": { + "version": "10.2.0", "image": "146.59.87.168:3000/lfg2025/grafana:10.2.0", "manifest": { "app": { + "id": "grafana", + "name": "Grafana", + "version": "10.2.0", + "description": "Analytics and monitoring platform. Visualize metrics and create dashboards.", "container": { - "data_uid": "472:472", "image": "grafana/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" + } + ], + "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}", @@ -1723,117 +1766,46 @@ "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": [ - { - "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 - }, - "version": "10.2.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/grafana", - "target": "/var/lib/grafana", - "type": "bind" - } - ] + } } - }, - "version": "10.2.0" + } }, "homeassistant": { + "version": "2024.1", "image": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", "manifest": { "app": { + "id": "homeassistant", + "name": "Home Assistant", + "version": "2024.1.0", + "description": "Open source home automation platform. Control and monitor your smart home devices.", "container": { "image": "146.59.87.168:3000/lfg2025/home-assistant:2024.1", - "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": [ - { - "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", @@ -1843,40 +1815,82 @@ "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" }, - "version": "2024.1.0", + "ports": [ + { + "host": 8123, + "container": 8123, + "protocol": "tcp" + } + ], "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": "2024.1" + } }, "immich": { + "version": "release", "image": "146.59.87.168:3000/lfg2025/immich-server:release", "images": { + "immich_server": "146.59.87.168:3000/lfg2025/immich-server:release", "immich_postgres": "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", - "immich_redis": "146.59.87.168:3000/lfg2025/redis:7.4.8", - "immich_server": "146.59.87.168:3000/lfg2025/immich-server:release" + "immich_redis": "146.59.87.168:3000/lfg2025/redis:7.4.8" }, "manifest": { "app": { + "id": "immich", + "name": "Immich", + "version": "2.7.4", + "description": "Self-hosted photo and video backup with mobile apps and search.", + "container_name": "immich_server", "container": { "image": "146.59.87.168:3000/lfg2025/immich-server:release", - "network": "archy-net", "pull_policy": "if-not-present", + "network": "archy-net", "secret_env": [ { "key": "DB_PASSWORD", @@ -1884,7 +1898,6 @@ } ] }, - "container_name": "immich_server", "dependencies": [ { "app_id": "immich-postgres" @@ -1896,7 +1909,36 @@ "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/immich", + "target": "/usr/src/app/upload", + "options": [ + "rw" + ] + } + ], "environment": [ "DB_HOSTNAME=immich_postgres", "DB_USERNAME=postgres", @@ -1905,79 +1947,51 @@ "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": [ - { - "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 - }, - "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", + "description": "Postgres (pgvecto.rs / vectorchord) backend for Immich.", + "container_name": "immich_postgres", "container": { + "image": "146.59.87.168:3000/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": "146.59.87.168:3000/lfg2025/immich-postgres:14-vectorchord0.4.3-pgvectors0.2.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "POSTGRES_PASSWORD", @@ -1985,30 +1999,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": [ @@ -2018,46 +2016,49 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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": "146.59.87.168:3000/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", + "description": "Valkey (Redis-compatible) cache for Immich.", + "container_name": "immich_redis", + "container": { + "image": "146.59.87.168:3000/lfg2025/valkey:7-alpine", + "pull_policy": "if-not-present", + "network": "archy-net" + }, + "dependencies": [], "resources": { "memory_limit": "128Mi" }, @@ -2066,16 +2067,24 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0", "images": { "indeedhub": "146.59.87.168:3000/lfg2025/indeedhub:1.0.0", @@ -2084,13 +2093,17 @@ }, "manifest": { "app": { + "id": "indeedhub", + "name": "IndeeHub", + "version": "1.0.0", + "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": "146.59.87.168:3000/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" @@ -2099,16 +2112,50 @@ "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" + } + ], + "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": [ { @@ -2121,8 +2168,8 @@ }, { "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" } }, { @@ -2141,22 +2188,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", @@ -2164,75 +2219,34 @@ "media", "education", "nostr" - ], - "website": "https://indeedhub.com" - }, - "name": "IndeeHub", - "ports": [ - { - "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 - }, - "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": { + "version": "1.0.0", "manifest": { "app": { + "id": "indeedhub-api", + "name": "IndeedHub API", + "version": "1.0.0", + "description": "IndeedHub backend API (Nostr auth, media, payments).", "category": "community", + "container_name": "indeedhub-api", "container": { - "generated_secrets": [ - { - "kind": "hex32", - "name": "indeedhub-jwt" - } - ], "image": "146.59.87.168:3000/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" + } + ], "secret_env": [ { "key": "DATABASE_PASSWORD", @@ -2248,7 +2262,6 @@ } ] }, - "container_name": "indeedhub-api", "dependencies": [ { "app_id": "indeedhub-postgres" @@ -2260,7 +2273,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", @@ -2280,37 +2302,29 @@ "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 - }, - "version": "1.0.0", - "volumes": [] + "retries": 10 + } } - }, - "version": "1.0.0" + } }, "indeedhub-ffmpeg": { + "version": "1.0.0", "manifest": { "app": { + "id": "indeedhub-ffmpeg", + "name": "IndeedHub FFmpeg Worker", + "version": "1.0.0", + "description": "IndeedHub background media transcoding worker.", "category": "community", + "container_name": "indeedhub-ffmpeg", "container": { "image": "146.59.87.168:3000/lfg2025/indeedhub-ffmpeg:1.0.0", - "network": "indeedhub-net", "pull_policy": "if-not-present", + "network": "indeedhub-net", "secret_env": [ { "key": "DATABASE_PASSWORD", @@ -2322,13 +2336,21 @@ } ] }, - "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", @@ -2343,45 +2365,37 @@ "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 - }, - "version": "1.0.0", - "volumes": [] + ] } - }, - "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", + "description": "MinIO S3-compatible object storage for IndeedHub media.", "category": "community", + "container_name": "indeedhub-minio", "container": { + "image": "146.59.87.168:3000/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": "146.59.87.168:3000/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", @@ -2389,68 +2403,68 @@ } ] }, - "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 - }, - "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", + "description": "Postgres database backend for IndeedHub.", "category": "community", + "container_name": "indeedhub-postgres", "container": { - "generated_secrets": [ - { - "kind": "hex32", - "name": "indeedhub-db-password" - } - ], "image": "146.59.87.168:3000/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", @@ -2458,30 +2472,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": [ @@ -2491,54 +2489,57 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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", + "description": "Redis queue/cache backend for IndeedHub.", "category": "community", + "container_name": "indeedhub-redis", "container": { "image": "146.59.87.168:3000/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" }, @@ -2547,129 +2548,107 @@ "SETGID", "SETUID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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", + "description": "nostr-rs-relay backing IndeedHub's Nostr identity + comments.", "category": "community", + "container_name": "indeedhub-relay", "container": { "image": "146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.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" }, - "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.8.13", "image": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", "manifest": { "app": { + "id": "jellyfin", + "name": "Jellyfin", + "version": "10.8.13", + "description": "Free media server. Stream movies, music, and photos.", "container": { "image": "146.59.87.168:3000/lfg2025/jellyfin:10.8.13", - "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": [ - { - "container": 8096, - "host": 8096, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -2679,39 +2658,69 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "10.8.13", + "ports": [ + { + "host": 8096, + "container": 8096, + "protocol": "tcp" + } + ], "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.8.13" + } }, "lightning-stack": { + "version": "0.12.0", "manifest": { "app": { - "bitcoin_integration": { - "rpc_access": "admin", - "sync_required": true - }, + "id": "lightning-stack", + "name": "Lightning Stack", + "version": "0.12.0", + "description": "Complete Lightning Network implementation. Includes LND, CLN, and management tools.", "container": { "image": "lightninglabs/lightning-stack:v0.12.0", "image_signature": "cosign://...", @@ -2726,7 +2735,49 @@ "storage": "50Gi" } ], - "description": "Complete Lightning Network implementation. Includes LND, CLN, and management tools.", + "resources": { + "cpu_limit": 4, + "memory_limit": "4Gi", + "disk_limit": "50Gi" + }, + "security": { + "capabilities": [ + "NET_BIND_SERVICE" + ], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "lightning-stack" + }, + "ports": [ + { + "host": 9738, + "container": 9735, + "protocol": "tcp" + }, + { + "host": 10010, + "container": 10009, + "protocol": "tcp" + }, + { + "host": 8091, + "container": 8080, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/lightning-stack", + "target": "/root/.lightning", + "options": [ + "rw" + ] + } + ], "environment": [ "BITCOIND_HOST=bitcoin-core", "BITCOIND_RPCUSER=${BITCOIN_RPC_USER}", @@ -2734,92 +2785,50 @@ "NETWORK=mainnet" ], "health_check": { - "endpoint": "http://localhost:8087", - "interval": "30s", + "type": "http", + "endpoint": "http://127.0.0.1:8080", "path": "/v1/getinfo", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" + "retries": 3 }, - "id": "lightning-stack", - "lightning_integration": { - "channel_management": true, - "payment_routing": true - }, - "name": "Lightning Stack", - "ports": [ - { - "container": 9735, - "host": 9737, - "protocol": "tcp" - }, - { - "container": 10009, - "host": 10010, - "protocol": "tcp" - }, - { - "container": 8080, - "host": 8087, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 4, - "disk_limit": "50Gi", - "memory_limit": "4Gi" - }, - "security": { - "apparmor_profile": "lightning-stack", - "capabilities": [ - "NET_BIND_SERVICE" - ], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "version": "0.12.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/lightning-stack", - "target": "/root/.lightning", - "type": "bind" - } - ] - } - }, - "version": "0.12.0" - }, - "lnd": { - "image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", - "manifest": { - "app": { "bitcoin_integration": { "rpc_access": "admin", "sync_required": true }, + "lightning_integration": { + "channel_management": true, + "payment_routing": true + } + } + } + }, + "lnd": { + "version": "v0.18.4-beta", + "image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", + "manifest": { + "app": { + "id": "lnd", + "name": "LND", + "version": "0.18.4", + "description": "Lightning Network implementation by Lightning Labs. Enables instant, low-cost Bitcoin payments.", "container": { - "data_uid": "100000:100000", + "image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", + "pull_policy": "if-not-present", + "network": "archy-net", "derived_env": [ { "key": "BITCOIND_HOST", "template": "{{BITCOIN_HOST}}" } ], - "image": "146.59.87.168:3000/lfg2025/lnd:v0.18.4-beta", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "BITCOIND_RPCPASS", "secret_file": "bitcoin-rpc-password" } - ] + ], + "data_uid": "100000:100000" }, "dependencies": [ { @@ -2827,45 +2836,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": [ - { - "container": 9735, - "host": 9735, - "protocol": "tcp" - }, - { - "container": 10009, - "host": 10009, - "protocol": "tcp" - }, - { - "container": 8080, - "host": 18080, - "protocol": "tcp" - } - ], "resources": { "cpu_limit": 2, - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -2876,28 +2850,67 @@ "DAC_OVERRIDE", "NET_RAW" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "0.18.4", + "ports": [ + { + "host": 9735, + "container": 9735, + "protocol": "tcp" + }, + { + "host": 10009, + "container": 10009, + "protocol": "tcp" + }, + { + "host": 18080, + "container": 8080, + "protocol": "tcp" + } + ], "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.18.4-beta" + } }, "lnd-ui": { + "version": "latest", "image": "146.59.87.168:3000/lfg2025/lnd-ui:latest", "manifest": { "app": { + "id": "lnd-ui", + "name": "LND UI", + "version": "1.0.0", + "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", @@ -2910,51 +2923,47 @@ "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": [ - { - "container": 80, - "host": 18083, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "64Mi" }, "security": { - "network_policy": "bridge", - "readonly_root": false + "readonly_root": false, + "network_policy": "bridge" }, - "version": "1.0.0", - "volumes": [] + "ports": [ + { + "host": 18083, + "container": 80, + "protocol": "tcp" + } + ], + "volumes": [], + "environment": [], + "health_check": { + "type": "http", + "endpoint": "http://127.0.0.1:18083", + "path": "/", + "interval": "30s", + "timeout": "5s", + "retries": 3 + } } - }, - "version": "latest" + } }, "mempool": { + "version": "v3.0.1", "image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1", "images": { - "archy-mempool-db": "146.59.87.168:3000/lfg2025/mariadb:11.4.10", "archy-mempool-web": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1", - "mempool-api": "146.59.87.168:3000/lfg2025/mempool-backend:v3.0.0" + "mempool-api": "146.59.87.168:3000/lfg2025/mempool-backend:v3.0.0", + "archy-mempool-db": "146.59.87.168:3000/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", + "description": "Bitcoin mempool and blockchain explorer. Real-time transaction and block visualization.", "container": { "image": "146.59.87.168:3000/lfg2025/mempool-frontend:v3.0.1", "image_signature": "cosign://...", @@ -2970,7 +2979,37 @@ }, "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/mempool", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MEMPOOL_BACKEND=electrum", "MEMPOOL_BITCOIN_HOST=bitcoin-core", @@ -2979,69 +3018,38 @@ "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" + "retries": 3 }, - "id": "mempool", - "name": "Mempool Explorer", - "ports": [ - { - "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 - }, - "version": "3.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/mempool", - "target": "/data", - "type": "bind" - } - ] - } - }, - "version": "v3.0.1" - }, - "mempool-api": { - "manifest": { - "app": { "bitcoin_integration": { - "pruning_support": false, "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", + "description": "Backend API for mempool explorer.", "container": { + "image": "146.59.87.168:3000/lfg2025/mempool-backend:v3.0.0", + "pull_policy": "if-not-present", + "network": "archy-net", "derived_env": [ { "key": "CORE_RPC_HOST", "template": "{{BITCOIN_HOST}}" } ], - "image": "git.tx1138.com/lfg2025/mempool-backend:v3.0.0", - "network": "archy-net", - "pull_policy": "if-not-present", "secret_env": [ { "key": "CORE_RPC_PASSWORD", @@ -3068,7 +3076,32 @@ }, "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" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/mempool", + "target": "/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MEMPOOL_BACKEND=electrum", "ELECTRUM_HOST=electrumx", @@ -3082,49 +3115,29 @@ "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": [ - { - "container": 8999, - "host": 8999, - "protocol": "tcp" - } - ], - "resources": { - "disk_limit": "20Gi", - "memory_limit": "2Gi" - }, - "security": { - "capabilities": [], - "network_policy": "isolated", - "readonly_root": false - }, - "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" + } }, "morphos-server": { + "version": "1.0.0", "manifest": { "app": { + "id": "morphos-server", + "name": "MorphOS Server", + "version": "1.0.0", + "description": "MorphOS server platform. Decentralized application server.", "container": { "image": "archipelago/morphos-server:1.0.0", "image_signature": "cosign://...", @@ -3135,73 +3148,73 @@ "storage": "5Gi" } ], - "description": "MorphOS server platform. Decentralized application server.", + "resources": { + "cpu_limit": 2, + "memory_limit": "2Gi", + "disk_limit": "5Gi" + }, + "security": { + "capabilities": [], + "readonly_root": true, + "no_new_privileges": true, + "user": 1000, + "seccomp_profile": "default", + "network_policy": "isolated", + "apparmor_profile": "morphos-server" + }, + "ports": [ + { + "host": 8089, + "container": 8080, + "protocol": "tcp" + } + ], + "volumes": [ + { + "type": "bind", + "source": "/var/lib/archipelago/morphos-server", + "target": "/app/data", + "options": [ + "rw" + ] + } + ], "environment": [ "MORPHOS_ENV=production", "MORPHOS_DATA_DIR=/app/data" ], "health_check": { - "endpoint": "http://localhost:8086", - "interval": "30s", + "type": "http", + "endpoint": "http://127.0.0.1:8080", "path": "/health", - "retries": 3, + "interval": "30s", "timeout": "5s", - "type": "http" - }, - "id": "morphos-server", - "name": "MorphOS Server", - "ports": [ - { - "container": 8080, - "host": 8086, - "protocol": "tcp" - } - ], - "resources": { - "cpu_limit": 2, - "disk_limit": "5Gi", - "memory_limit": "2Gi" - }, - "security": { - "apparmor_profile": "morphos-server", - "capabilities": [], - "network_policy": "isolated", - "no_new_privileges": true, - "readonly_root": true, - "seccomp_profile": "default", - "user": 1000 - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/morphos-server", - "target": "/app/data", - "type": "bind" - } - ] + "retries": 3 + } } - }, - "version": "1.0.0" + } }, "netbird": { + "version": "2.38.0", "manifest": { "app": { + "id": "netbird", + "name": "NetBird", + "version": "2.38.0", + "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.27-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.27-alpine", - "network": "netbird-net", - "pull_policy": "if-not-present" + ] }, - "container_name": "netbird", "dependencies": [ { "app_id": "netbird-server" @@ -3213,55 +3226,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": [ - { - "container": 443, - "host": 8087, - "protocol": "tcp" - } - ], "resources": { "memory_limit": "256Mi" }, @@ -3273,45 +3237,101 @@ "SETUID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "2.38.0", + "ports": [ + { + "host": 8087, + "container": 443, + "protocol": "tcp" + } + ], "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", + "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", @@ -3325,21 +3345,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", @@ -3353,124 +3381,65 @@ "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 - }, - "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", + "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": [ - { - "container": 80, - "host": 8086, - "protocol": "tcp" - }, - { - "container": 3478, - "host": 3478, - "protocol": "udp" - } - ], "resources": { "memory_limit": "1Gi" }, @@ -3478,86 +3447,93 @@ "capabilities": [ "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "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" }, { + "host": 3478, + "container": 3478, + "protocol": "udp" + } + ], + "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": "146.59.87.168:3000/lfg2025/nextcloud:29", "manifest": { "app": { + "id": "nextcloud", + "name": "Nextcloud", + "version": "29", + "description": "Your own private cloud. File sync, calendars, contacts.", "container": { "image": "146.59.87.168:3000/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": [ - { - "container": 80, - "host": 8085, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -3567,44 +3543,111 @@ "DAC_OVERRIDE", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "29", + "ports": [ + { + "host": 8085, + "container": 80, + "protocol": "tcp" + } + ], "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": { - "image": "146.59.87.168:3000/lfg2025/nginx-proxy-manager:latest", - "version": "latest" + "version": "latest", + "image": "146.59.87.168:3000/lfg2025/nginx-proxy-manager:latest" }, "nostr-rs-relay": { + "version": "0.9.0", "image": "146.59.87.168:3000/lfg2025/nostr-rs-relay:0.9.0", "manifest": { "app": { + "id": "nostr-rs-relay", + "name": "Nostr Relay (Rust)", + "version": "0.8.0", + "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.8.9", "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" + } + ], + "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", @@ -3612,79 +3655,49 @@ "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": [ - { - "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 - }, - "version": "0.8.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/nostr-relay", - "target": "/usr/src/app/db", - "type": "bind" - } - ] + "event_storage": "sqlite" + } } - }, - "version": "0.9.0" + } }, "nostr-vpn": { - "image": "146.59.87.168:3000/lfg2025/nostr-vpn:v0.3.7", - "version": "v0.3.7" + "version": "v0.3.7", + "image": "146.59.87.168:3000/lfg2025/nostr-vpn:v0.3.7" }, "ollama": { - "image": "146.59.87.168:3000/lfg2025/ollama:latest", - "version": "latest" + "version": "latest", + "image": "146.59.87.168:3000/lfg2025/ollama:latest" }, "penpot": { + "version": "2.4", "image": "146.59.87.168:3000/lfg2025/penpot-frontend:2.4", "images": { + "penpot-frontend": "146.59.87.168:3000/lfg2025/penpot-frontend:2.4", "penpot-backend": "146.59.87.168:3000/lfg2025/penpot-backend:2.4", "penpot-exporter": "146.59.87.168:3000/lfg2025/penpot-exporter:2.4", - "penpot-frontend": "146.59.87.168:3000/lfg2025/penpot-frontend:2.4", "penpot-postgres": "146.59.87.168:3000/lfg2025/postgres:15", "penpot-valkey": "146.59.87.168:3000/lfg2025/valkey:8.1" - }, - "version": "2.4" + } }, "photoprism": { + "version": "240915", "image": "146.59.87.168:3000/lfg2025/photoprism:240915", "manifest": { "app": { + "id": "photoprism", + "name": "PhotoPrism", + "version": "240915", + "description": "AI-powered photo management with facial recognition.", "container": { "image": "146.59.87.168:3000/lfg2025/photoprism:240915", "pull_policy": "if-not-present" @@ -3694,49 +3707,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": [ - { - "container": 2342, - "host": 2342, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "10Gi", - "memory_limit": "1Gi" + "memory_limit": "1Gi", + "disk_limit": "10Gi" }, "security": { "capabilities": [ @@ -3744,75 +3717,82 @@ "SETUID", "SETGID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "240915", + "ports": [ + { + "host": 2342, + "container": 2342, + "protocol": "tcp" + } + ], "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" + } }, "portainer": { + "version": "2.19.4", "image": "146.59.87.168:3000/lfg2025/portainer:2.19.4", "manifest": { "app": { + "id": "portainer", + "name": "Portainer", + "version": "2.19.4", + "description": "Container management web UI for the local Podman socket.", "category": "development", "container": { - "data_uid": "1000:1000", "image": "146.59.87.168:3000/lfg2025/portainer:2.19.4", - "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": [ - { - "container": 9000, - "host": 9000, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -3821,44 +3801,77 @@ "SETGID", "DAC_OVERRIDE" ], - "network_policy": "isolated", + "readonly_root": false, "no_new_privileges": true, - "readonly_root": false + "network_policy": "isolated" }, - "version": "2.19.4", + "ports": [ + { + "host": 9000, + "container": 9000, + "protocol": "tcp" + } + ], "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.19.4" + } }, "router": { + "version": "1.0.0", "manifest": { "app": { + "id": "router", + "name": "Mesh Router", + "version": "1.0.0", + "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://...", @@ -3869,96 +3882,96 @@ "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" + }, + { + "host": 5353, + "container": 5353, + "protocol": "udp" + }, + { + "host": 1900, + "container": 1900, + "protocol": "udp" + } + ], + "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": [ - { - "container": 8080, - "host": 8084, - "protocol": "tcp" - }, - { - "container": 5353, - "host": 5353, - "protocol": "udp" - }, - { - "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 - }, - "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" + } }, "routstr": { - "image": "146.59.87.168:3000/lfg2025/routstr:v0.4.3", - "version": "v0.4.3" + "version": "v0.4.3", + "image": "146.59.87.168:3000/lfg2025/routstr:v0.4.3" }, "searxng": { + "version": "latest", "image": "146.59.87.168:3000/lfg2025/searxng:latest", "manifest": { "app": { + "id": "searxng", + "name": "SearXNG", + "version": "1.0.0", + "description": "Privacy-respecting metasearch engine. Search the web without tracking.", "container": { "image": "146.59.87.168:3000/lfg2025/searxng:latest", "pull_policy": "if-not-present" @@ -3968,60 +3981,60 @@ "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" + } + ], + "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": [ - { - "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 - }, - "version": "1.0.0", - "volumes": [ - { - "options": [ - "rw" - ], - "source": "/var/lib/archipelago/searxng", - "target": "/etc/searxng", - "type": "bind" - } - ] + "retries": 5 + } } - }, - "version": "latest" + } }, "strfry": { + "version": "0.9.0", "manifest": { "app": { + "id": "strfry", + "name": "Strfry Nostr Relay", + "version": "0.9.0", + "description": "Lightweight Nostr relay written in C++. Alternative to nostr-rs-relay with lower resource usage.", "container": { "image": "dockurr/strfry:1.0.4", "image_signature": "cosign://...", @@ -4032,128 +4045,97 @@ "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://localhost:8090", - "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": [ { - "container": 7777, "host": 8090, + "container": 7777, "protocol": "tcp" } ], - "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" - }, - "version": "0.9.0", "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": "0.9.0" + } }, "tailscale": { - "image": "146.59.87.168:3000/lfg2025/tailscale:stable", - "version": "stable" + "version": "stable", + "image": "146.59.87.168:3000/lfg2025/tailscale:stable" }, "uptime-kuma": { + "version": "1", "image": "146.59.87.168:3000/lfg2025/uptime-kuma:1", "manifest": { "app": { + "id": "uptime-kuma", + "name": "Uptime Kuma", + "version": "1.23.0", + "description": "Self-hosted uptime monitoring.", "container": { + "image": "146.59.87.168:3000/lfg2025/uptime-kuma:1", + "pull_policy": "if-not-present", + "network": "pasta", "custom_args": [ "--", "node", "server/server.js" - ], - "image": "146.59.87.168:3000/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": [ - { - "container": 3001, - "host": 3002, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4162,79 +4144,72 @@ "SETUID", "SETGID" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "1.23.0", + "ports": [ + { + "host": 3002, + "container": 3001, + "protocol": "tcp" + } + ], "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.30.0-alpine", "image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-alpine", "manifest": { "app": { + "id": "vaultwarden", + "name": "Vaultwarden", + "version": "1.30.0", + "description": "Self-hosted password vault with zero-knowledge encryption.", "container": { "image": "146.59.87.168:3000/lfg2025/vaultwarden:1.30.0-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": [ - { - "container": 80, - "host": 8082, - "protocol": "tcp" - } - ], "resources": { - "disk_limit": "1Gi", - "memory_limit": "256Mi" + "memory_limit": "256Mi", + "disk_limit": "1Gi" }, "security": { "capabilities": [ @@ -4243,27 +4218,56 @@ "SETGID", "NET_BIND_SERVICE" ], - "network_policy": "isolated", - "readonly_root": false + "readonly_root": false, + "network_policy": "isolated" }, - "version": "1.30.0", + "ports": [ + { + "host": 8082, + "container": 80, + "protocol": "tcp" + } + ], "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.30.0-alpine" + } } - }, - "schema": 1, - "signature": "bf1c813496074bb06cbd1b096cb3f8f5b72b5d31cfb651907712e54ffe746cc14a2ae6454a2ab266a8c3e8d187f997a7da447e0a4097d3cf0cb32a889bde9502", - "signed_by": "did:key:z6MkkidEnEpo6qHMCNSZoNKWtvQvxq3whnaME9wGgEFhq7ur", - "updated": "2026-07-09" + } } diff --git a/scripts/bootstrap-switchover.sh b/scripts/bootstrap-switchover.sh index 0018fb89..4f4543e4 100755 --- a/scripts/bootstrap-switchover.sh +++ b/scripts/bootstrap-switchover.sh @@ -78,7 +78,7 @@ if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^electrumx$'; then -e "DAEMON_URL=http://${RPC_USER}:${RPC_PASS}@bitcoin-knots:8332/" \ -e COIN=Bitcoin -e DB_DIRECTORY=/data \ -e "SERVICES=tcp://:50001,rpc://0.0.0.0:8000" \ - "${ELECTRUMX_IMAGE:-git.tx1138.com/lfg2025/electrumx:v1.18.0}" + "${ELECTRUMX_IMAGE:-146.59.87.168:3000/lfg2025/electrumx:v1.18.0}" fi # Mempool API @@ -98,7 +98,7 @@ if $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -q '^mempool-api$'; th -e "DATABASE_ENABLED=true" -e "DATABASE_HOST=archy-mempool-db" \ -e "DATABASE_DATABASE=mempool" -e "DATABASE_USERNAME=mempool" \ -e "DATABASE_PASSWORD=$(cat "$SECRETS_DIR/mempool-db-password" 2>/dev/null || echo mempoolpass)" \ - "${MEMPOOL_API_IMAGE:-git.tx1138.com/lfg2025/mempool-api:v3.2.0}" + "${MEMPOOL_API_IMAGE:-146.59.87.168:3000/lfg2025/mempool-api:v3.2.0}" fi # Stop Tor tunnel if it was active diff --git a/scripts/container-doctor.sh b/scripts/container-doctor.sh index 322c8e94..d7f2ada3 100755 --- a/scripts/container-doctor.sh +++ b/scripts/container-doctor.sh @@ -16,6 +16,9 @@ # 7. Containers stuck with exit code 127 (binary not found) # 8. Stopped core containers (rootless restart policy workaround) # 9. Missing rootless port listeners while Podman still shows published ports +# 10. Nginx Proxy Manager public hosts not mirrored into host nginx +# 11. BTCPay stores producing unpayable Lightning invoices (route hints off) +# 12. Missing catatonit (Podman init binary) — init-enabled deploys fail # # Safe to run multiple times (idempotent). Never blocks deploy (exit 0 always). # @@ -383,10 +386,22 @@ print(' '.join(['\"' + a + '\"' if ' ' in a else a for a in args[2:]])) # ── Fix 8: Rootless netns egress lost ──────────────────────── # Rootless podman uses pasta to give containers internet egress. If pasta's -# tap vanishes (host link flap, mount churn), the rootless-netns keeps inter- -# container traffic working but silently loses outbound. Bitcoin IBD stalls -# at 0 peers; package pulls fail. The only reliable repair is a stop-all/ -# start-all cycle so pasta + aardvark-dns rebuild the netns from scratch. +# tap vanishes (host link flap, mount churn, pasta dying during a boot-time +# restart storm), the rootless-netns keeps inter-container traffic working +# but silently loses outbound. Bitcoin IBD stalls at 0 peers; package pulls +# fail. The repair must rebuild the netns from scratch: merely cycling the +# containers reuses the existing (broken) netns because its holders +# (aardvark-dns, podman's pause process) survive — observed on shorty-s +# 2026-07-10, where the old stop/start-only cycle bounced all 35 containers +# every timer run for ~an hour without ever restoring egress. So: stop the +# containers, kill the netns holders, `podman system migrate`, clear the +# stale netns state, then start everything back up. +# +# Destructive-action latch: cycling the whole fleet is a last resort. After +# NETNS_CYCLE_MAX consecutive failed repairs we stop cycling (and log loudly) +# until a run observes egress healthy again, which resets the counter. +NETNS_CYCLE_STATE="/var/lib/archipelago/doctor-netns-cycle-failures" +NETNS_CYCLE_MAX=3 fix_rootless_netns_egress() { # Needs root for nsenter. When doctor runs as the rootless container owner, # a failed nsenter probe is a permissions artifact, not evidence of broken @@ -410,16 +425,28 @@ fix_rootless_netns_egress() { # Probe egress from inside the rootless-netns. One probe is noisy; # require two consecutive failures 10s apart to rule out transients. if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '/dev/null; then + rm -f "$NETNS_CYCLE_STATE" # healthy again — re-arm the latch return 1 # first probe succeeded fi sleep 10 aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1) [ -z "$aardvark_pid" ] && return 1 if timeout 3 nsenter -t "$aardvark_pid" -n bash -c '/dev/null; then + rm -f "$NETNS_CYCLE_STATE" return 1 # recovered on its own fi - log "Rootless-netns egress is broken (host online, container netns unreachable) — cycling" + # Latch: don't keep bouncing the fleet when the rebuild demonstrably + # isn't fixing it. + local failures + failures=$(cat "$NETNS_CYCLE_STATE" 2>/dev/null || echo 0) + case "$failures" in *[!0-9]*|"") failures=0;; esac + if [ "$failures" -ge "$NETNS_CYCLE_MAX" ]; then + log "Rootless-netns egress still broken but $failures rebuilds already failed — NOT cycling again (manual intervention needed; rm $NETNS_CYCLE_STATE to re-arm)" + return 1 + fi + + log "Rootless-netns egress is broken (host online, container netns unreachable) — rebuilding netns" local PODMANCMD="sudo -u archipelago XDG_RUNTIME_DIR=/run/user/$archi_uid podman" local running @@ -435,6 +462,19 @@ fix_rootless_netns_egress() { $PODMANCMD stop --all --time 30 >/dev/null 2>&1 sleep 5 + # Tear the broken netns down for real: kill its holders and drop the + # stale state so the first container start rebuilds pasta + aardvark-dns + # from scratch. Without this, podman re-enters the old netns and the + # missing pasta tap never comes back. + log " Rebuilding rootless netns (killing holders, clearing state)..." + pkill -U "$archi_uid" -x aardvark-dns 2>/dev/null + pkill -U "$archi_uid" -x pasta 2>/dev/null + pkill -U "$archi_uid" -x pasta.avx2 2>/dev/null + pkill -U "$archi_uid" -x slirp4netns 2>/dev/null + sleep 2 + $PODMANCMD system migrate >/dev/null 2>&1 + rm -rf "/run/user/$archi_uid/containers/networks" + log " Starting containers back up..." for c in $running; do $PODMANCMD start "$c" >/dev/null 2>&1 & @@ -445,8 +485,11 @@ fix_rootless_netns_egress() { aardvark_pid=$(pgrep -U "$archi_uid" -f '^/usr/lib/podman/aardvark-dns' 2>/dev/null | head -1) if [ -n "$aardvark_pid" ] && timeout 3 nsenter -t "$aardvark_pid" -n bash -c '/dev/null; then log " Rootless-netns egress restored ($count containers cycled)" + rm -f "$NETNS_CYCLE_STATE" else - log " WARN: egress still broken after cycle — may need manual intervention" + failures=$((failures + 1)) + echo "$failures" > "$NETNS_CYCLE_STATE" + log " WARN: egress still broken after rebuild (failure $failures/$NETNS_CYCLE_MAX) — may need manual intervention" fi return 0 } @@ -526,6 +569,51 @@ fix_npm_public_hosts() { return 1 } +# ── Fix 12: BTCPay Lightning route hints ───────────────────── +# A BTCPay store whose LND node has only private (unannounced) channels +# produces BOLT11 invoices that external wallets cannot route to unless the +# store's lightningPrivateRouteHints flag is on — payers see "no way to pay +# this invoice" (observed on shorty-s 2026-07-10 with a Blink payer). Route +# hints are a no-op with public channels and essential with private ones, so +# the doctor enforces the flag on every store. BTCPay reads store blobs from +# Postgres per request; no restart needed. +fix_btcpay_route_hints() { + local state + state=$(podman_rootless inspect archy-btcpay-db --format '{{.State.Status}}' 2>/dev/null || echo "missing") + [ "$state" = "running" ] || return 1 + + local count + count=$(podman_rootless exec archy-btcpay-db psql -U btcpay -d btcpay -t -A -c \ + "SELECT count(*) FROM \"Stores\" WHERE (\"StoreBlob\"->>'lightningPrivateRouteHints') = 'false';" 2>/dev/null) + [ -n "$count" ] && [ "$count" -gt 0 ] 2>/dev/null || return 1 + + if podman_rootless exec archy-btcpay-db psql -U btcpay -d btcpay -q -c \ + "UPDATE \"Stores\" SET \"StoreBlob\" = jsonb_set(\"StoreBlob\", '{lightningPrivateRouteHints}', 'true'::jsonb) WHERE (\"StoreBlob\"->>'lightningPrivateRouteHints') = 'false';" >/dev/null 2>&1; then + log "Enabled Lightning route hints on $count BTCPay store(s) (private-channel invoices were unpayable)" + return 0 + fi + return 1 +} + +# ── Fix 13: Missing catatonit (container init binary) ──────── +# Podman resolves `--init` (and any Portainer/compose deploy with +# "init: true") through catatonit; Debian's podman package only +# Recommends it, so a node installed or upgraded without it fails those +# deploys with a missing-init error (observed on shorty-s 2026-07-10 +# deploying sites via Portainer). install-podman.sh covers fresh ISO +# installs; this heals nodes that predate it. +fix_missing_catatonit() { + command -v catatonit >/dev/null 2>&1 && return 1 + command -v apt-get >/dev/null 2>&1 || return 1 + + if DEBIAN_FRONTEND=noninteractive apt-get install -y catatonit >/dev/null 2>&1; then + log "Installed catatonit (init-enabled container deploys were failing)" + return 0 + fi + log "WARNING: catatonit missing and apt-get install failed — init-enabled deploys will fail" + return 1 +} + # ── Main ───────────────────────────────────────────────────── # If remote host provided, run via SSH @@ -556,6 +644,8 @@ run_fix "netns-egress" fix_rootless_netns_egress run_fix "stopped-core" fix_stopped_core_containers run_fix "rootless-ports" fix_missing_rootless_ports run_fix "npm-public-hosts" fix_npm_public_hosts +run_fix "btcpay-route-hints" fix_btcpay_route_hints +run_fix "catatonit" fix_missing_catatonit echo "" if [ $FIXES_APPLIED -gt 0 ]; then diff --git a/scripts/deploy-bitcoin-knots.sh b/scripts/deploy-bitcoin-knots.sh index df651c1b..96c402d5 100644 --- a/scripts/deploy-bitcoin-knots.sh +++ b/scripts/deploy-bitcoin-knots.sh @@ -74,7 +74,7 @@ mkdir -p "$BUILD_DIR" # Create Dockerfile cat > "$BUILD_DIR/Dockerfile" << 'EOF' -FROM ${NGINX_ALPINE_IMAGE:-git.tx1138.com/lfg2025/nginx:1.29.6-alpine} +FROM ${NGINX_ALPINE_IMAGE:-146.59.87.168:3000/lfg2025/nginx:1.29.6-alpine} # Copy the static UI COPY index.html /usr/share/nginx/html/ diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index 51853135..28a2b506 100755 --- a/scripts/deploy-tailscale.sh +++ b/scripts/deploy-tailscale.sh @@ -425,6 +425,22 @@ deploy_node() { ' 2>/dev/null || true fi + # ── Deploy ALSA default-device config ──────────────────────────── + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + step "Deploying ALSA default-device config" + scp $SSH_OPTS "$ASOUND_CONF" "$TARGET:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$TARGET" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " Installed" + else + echo " Unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # ── Step 18: NTP + swap ────────────────────────────────────────── step "Ensuring NTP + swap" ssh $SSH_OPTS "$TARGET" ' diff --git a/scripts/deploy-to-target.sh b/scripts/deploy-to-target.sh index c93f3a50..69824a6d 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -463,6 +463,22 @@ deploy_secondary() { ' 2>/dev/null || true fi + # Deploy ALSA default-device config (routes ALSA "default" through PulseAudio/PipeWire) + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + echo " Syncing ALSA default-device config to .$SEC_LABEL..." + scp $SSH_OPTS "$ASOUND_CONF" "$SEC_TARGET:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$SEC_TARGET" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " ALSA default-device config installed" + else + echo " ALSA default-device config unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # Dev mode + FileBrowser ssh $SSH_OPTS "$SEC_TARGET" ' # Dev mode @@ -805,6 +821,23 @@ if [ "$LIVE" = true ]; then ' 2>/dev/null || true fi + # Deploy ALSA default-device config (routes ALSA "default" through + # PulseAudio/PipeWire — without it Chromium's raw ALSA fallback can't + # reach the HDMI sink and kiosk HDMI audio is silent). + ASOUND_CONF="$PROJECT_DIR/image-recipe/configs/asound.conf" + if [ -f "$ASOUND_CONF" ]; then + scp $SSH_OPTS "$ASOUND_CONF" "$TARGET_HOST:/tmp/asound.conf" 2>/dev/null || true + ssh $SSH_OPTS "$TARGET_HOST" ' + if ! diff -q /tmp/asound.conf /etc/asound.conf >/dev/null 2>&1; then + sudo cp /tmp/asound.conf /etc/asound.conf + echo " ALSA default-device config installed" + else + echo " ALSA default-device config unchanged" + fi + rm -f /tmp/asound.conf + ' 2>/dev/null || true + fi + # Deploy Claude API proxy (auto-install if missing) progress "Setting up Claude API proxy" ssh $SSH_OPTS "$TARGET_HOST" ' diff --git a/scripts/dev-container-test.sh b/scripts/dev-container-test.sh index a1e4f512..7973e0a1 100755 --- a/scripts/dev-container-test.sh +++ b/scripts/dev-container-test.sh @@ -146,7 +146,7 @@ run_smoke_tests() { # Test 3: Install a lightweight container (filebrowser — small, fast, no deps) TESTS=$((TESTS + 1)) - local install_img="git.tx1138.com/lfg2025/filebrowser:v2.27.0" + local install_img="146.59.87.168:3000/lfg2025/filebrowser:v2.27.0" # Check if already installed local fb_state fb_state=$(ssh $SSH_OPTS "$SSH_HOST" "podman inspect filebrowser --format '{{.State.Status}}' 2>/dev/null || echo 'none'") diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index d7446f3e..3d51c005 100755 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -9,7 +9,7 @@ # # Image versions: sourced from /opt/archipelago/image-versions.sh (single source of truth). # All container image references use the $*_IMAGE variables defined there. -# Images pull from the Archipelago app registry (git.tx1138.com/lfg2025/). +# Images pull from the Archipelago app registry (146.59.87.168:3000/lfg2025/). # # --- PLANNED REFACTOR (post-beta) --- # This script is ~995 lines and should be split into a modular library. diff --git a/scripts/image-versions.sh b/scripts/image-versions.sh index 77d8da39..a2f17e22 100644 --- a/scripts/image-versions.sh +++ b/scripts/image-versions.sh @@ -11,7 +11,8 @@ # Archipelago app registries (primary + fallback) ARCHY_REGISTRY="146.59.87.168:3000/lfg2025" -ARCHY_REGISTRY_FALLBACK="git.tx1138.com/lfg2025" +# No fallback registry: the old tx1138 registry host was retired (2026-06-13); empty disables the fallback path. +ARCHY_REGISTRY_FALLBACK="" # Bitcoin stack BITCOIN_KNOTS_IMAGE="$ARCHY_REGISTRY/bitcoin-knots:latest" diff --git a/scripts/reconcile-containers.sh b/scripts/reconcile-containers.sh index 5ac09c76..8d50c486 100755 --- a/scripts/reconcile-containers.sh +++ b/scripts/reconcile-containers.sh @@ -304,7 +304,7 @@ resolve_spec_image() { "${ARCHY_REGISTRY_FALLBACK:-}/${image_path}" \ "80.71.235.15:3000/archipelago/${image_name}:${image_tag}" \ "80.71.235.15:3000/lfg2025/${image_name}:${image_tag}"; do - [ "$candidate" = "/" ] && continue + case "$candidate" in /*) continue;; esac if image_exists "$candidate"; then info "$SPEC_NAME — using local image alias $candidate" SPEC_IMAGE="$candidate" diff --git a/scripts/self-update.sh b/scripts/self-update.sh index 3aa95c4d..c947ed71 100755 --- a/scripts/self-update.sh +++ b/scripts/self-update.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Self-update: pull latest code from git.tx1138.com and apply +# Self-update: pull latest code from the OVH Gitea (146.59.87.168:3000) and apply # Designed to run on installed Archipelago nodes (as archipelago user) # # Usage: @@ -8,7 +8,7 @@ # ./self-update.sh --force # Apply even if already up to date # # The script: -# 1. Pulls latest code from origin (git.tx1138.com) +# 1. Pulls latest code from origin (146.59.87.168:3000) # 2. Builds the Rust backend (release mode) # 3. Builds the Vue frontend (production mode) # 4. Installs the new binary and web UI @@ -69,7 +69,7 @@ done # Ensure repo exists if [ ! -d "$REPO_DIR/.git" ]; then err "Repo not found at $REPO_DIR" - err "Clone it first: git clone https://git.tx1138.com/lfg2025/archy ~/archy" + err "Clone it first: git clone http://146.59.87.168:3000/lfg2025/archy ~/archy" exit 1 fi diff --git a/scripts/validate-app-manifest.sh b/scripts/validate-app-manifest.sh index bc66de5b..9831d735 100755 --- a/scripts/validate-app-manifest.sh +++ b/scripts/validate-app-manifest.sh @@ -86,7 +86,7 @@ else # Check trusted registry TRUSTED=false - for reg in "docker.io" "ghcr.io" "quay.io" "registry.hub.docker.com" "git.tx1138.com"; do + for reg in "docker.io" "ghcr.io" "quay.io" "registry.hub.docker.com" "146.59.87.168:3000"; do if echo "$IMAGE" | grep -q "$reg"; then TRUSTED=true break