From 03c048e9584d471ab074258030b30d74a27d9cc2 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 16:22:45 +0000 Subject: [PATCH 01/22] fix(kiosk): pass XDG_RUNTIME_DIR so Chromium can reach PipeWire-Pulse The launcher's sudo -u archipelago invocation set DISPLAY/HOME but not XDG_RUNTIME_DIR, so Chromium's audio backend couldn't find the PipeWire-Pulse socket at /run/user//pulse/native. It silently fell back to raw ALSA "default", which also failed ("Connection refused"), producing no HDMI audio at all with no visible error since --noerrdialogs suppresses it. Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 2f34a33e..562d607c 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -93,8 +93,14 @@ 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 \ From 23e04b1859e1da45d325cc6e256ec811c02a8564 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 17:02:43 +0000 Subject: [PATCH 02/22] fix(kiosk): restore in-process-gpu + CPUQuota=200% to stop choppy HDMI audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both had regressed back to the pre-2026-06-28-incident state: GPU_FLAGS used --enable-gpu-rasterization on any node with a GPU, and CPUQuota was 75%. On archy-x250-exp (Intel HD 5500 under X11) this spins a dedicated GPU process at 55-92% CPU (falls back to software compositing anyway), and the 75% cgroup quota throttled the kiosk unit ~81% of the time (nr_throttled 4856/6005) — the exact CPU-starvation signature documented as the choppy- audio root cause. Restores the validated fix: --in-process-gpu, --num-raster-threads=1, GpuRasterization disabled via --disable-features, CPUQuota=200%. Verified on archy-x250-exp: no separate gpu-process, throttling dropped to ~3% (nr_throttled 14/503). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 14 ++++++++------ image-recipe/configs/archipelago-kiosk.service | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 562d607c..1e227760 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -83,12 +83,14 @@ 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 @@ -107,7 +109,7 @@ while true; do --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 5db41f3c..d05e425f 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% MemoryMax=1500M MemoryHigh=1200M From 81f1c7ed24a3cf7370c6fe6eea69076341ec6373 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 20:25:34 +0000 Subject: [PATCH 03/22] fix(kiosk): track /etc/asound.conf in image-recipe and sync it on deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes ALSA's "default" device through PulseAudio/PipeWire. Was a manual, untracked fix living only on archy-x250-exp — a reprovision or fresh image would silently lose it, same failure mode that already bit the GPU-flags and CPUQuota fixes. Wired into deploy-to-target.sh (both the primary --live path and the .198/.253 secondary-copy path) and deploy-tailscale.sh, mirroring the existing 99-mesh-radio.rules udev sync pattern (diff-check, copy if changed). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/asound.conf | 7 +++++++ scripts/deploy-tailscale.sh | 16 ++++++++++++++++ scripts/deploy-to-target.sh | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 image-recipe/configs/asound.conf 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/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index cc85eb77..bfb88346 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 c7533c89..7b133fda 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -456,6 +456,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 @@ -762,6 +778,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" ' From fca34270beb2a394a67f806772090c63f583345f Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 16:22:45 +0000 Subject: [PATCH 04/22] fix(kiosk): pass XDG_RUNTIME_DIR so Chromium can reach PipeWire-Pulse The launcher's sudo -u archipelago invocation set DISPLAY/HOME but not XDG_RUNTIME_DIR, so Chromium's audio backend couldn't find the PipeWire-Pulse socket at /run/user//pulse/native. It silently fell back to raw ALSA "default", which also failed ("Connection refused"), producing no HDMI audio at all with no visible error since --noerrdialogs suppresses it. Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 90d77e43..10cb435a 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -89,8 +89,14 @@ 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 \ From e559ccb76738e57240100961eedf8a224903a33b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 17:02:43 +0000 Subject: [PATCH 05/22] fix(kiosk): restore in-process-gpu + CPUQuota=200% to stop choppy HDMI audio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both had regressed back to the pre-2026-06-28-incident state: GPU_FLAGS used --enable-gpu-rasterization on any node with a GPU, and CPUQuota was 75%. On archy-x250-exp (Intel HD 5500 under X11) this spins a dedicated GPU process at 55-92% CPU (falls back to software compositing anyway), and the 75% cgroup quota throttled the kiosk unit ~81% of the time (nr_throttled 4856/6005) — the exact CPU-starvation signature documented as the choppy- audio root cause. Restores the validated fix: --in-process-gpu, --num-raster-threads=1, GpuRasterization disabled via --disable-features, CPUQuota=200%. Verified on archy-x250-exp: no separate gpu-process, throttling dropped to ~3% (nr_throttled 14/503). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/archipelago-kiosk-launcher.sh | 14 ++++++++------ image-recipe/configs/archipelago-kiosk.service | 5 ++++- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/image-recipe/configs/archipelago-kiosk-launcher.sh b/image-recipe/configs/archipelago-kiosk-launcher.sh index 10cb435a..3651fd3c 100644 --- a/image-recipe/configs/archipelago-kiosk-launcher.sh +++ b/image-recipe/configs/archipelago-kiosk-launcher.sh @@ -79,12 +79,14 @@ 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 @@ -103,7 +105,7 @@ while true; do --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 5db41f3c..d05e425f 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% MemoryMax=1500M MemoryHigh=1200M From 9b6ec0be97ef09d2ee344c6b8612309b5f0a6c2b Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 20:25:34 +0000 Subject: [PATCH 06/22] fix(kiosk): track /etc/asound.conf in image-recipe and sync it on deploy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Routes ALSA's "default" device through PulseAudio/PipeWire. Was a manual, untracked fix living only on archy-x250-exp — a reprovision or fresh image would silently lose it, same failure mode that already bit the GPU-flags and CPUQuota fixes. Wired into deploy-to-target.sh (both the primary --live path and the .198/.253 secondary-copy path) and deploy-tailscale.sh, mirroring the existing 99-mesh-radio.rules udev sync pattern (diff-check, copy if changed). Co-Authored-By: Claude Sonnet 5 --- image-recipe/configs/asound.conf | 7 +++++++ scripts/deploy-tailscale.sh | 16 ++++++++++++++++ scripts/deploy-to-target.sh | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) create mode 100644 image-recipe/configs/asound.conf 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/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index cc85eb77..bfb88346 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 c7533c89..7b133fda 100755 --- a/scripts/deploy-to-target.sh +++ b/scripts/deploy-to-target.sh @@ -456,6 +456,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 @@ -762,6 +778,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" ' From 95d6bf5dace35730c58068c7b2b02fcff95cb1db Mon Sep 17 00:00:00 2001 From: ssmithx Date: Wed, 1 Jul 2026 20:50:19 +0000 Subject: [PATCH 07/22] not sure --- core/Cargo.lock | 1 + core/archipelago/Cargo.toml | 1 + core/archipelago/src/wallet/cashu.rs | 178 +++++++++++++++++++++++---- 3 files changed, 159 insertions(+), 21 deletions(-) diff --git a/core/Cargo.lock b/core/Cargo.lock index 4596ff06..957a8cfd 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -139,6 +139,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 157c1d00..ddbf0964 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -109,6 +109,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/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()); From a252eb12fd366ca870c9c86c31a536587afefb21 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 17:23:12 +0000 Subject: [PATCH 08/22] fix(tollgate): install and configure NoDogSplash for real client gating MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tollgate-wrt has no firewall/netfilter code of its own (confirmed via strings on the binary and the upstream Go source) — it delegates all MAC authorization and gate open/close to NoDogSplash via ndsctl. Upstream's package declares +nodogsplash as a hard dependency, but neither of our install paths pull 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. Result: tollgate-wrt ran, accepted payments, and tracked balances, but never actually blocked unpaid clients — the static firewall zone just forwarded everyone to WAN unconditionally. Also fixes the config.json mismatch: tollgate-wrt reads /etc/tollgate/config.json exclusively, never the tollgate.main.* UCI keys we were writing — so mint/price changes through this project's UI silently had no effect on what the daemon actually advertised. - tollgate/nodogsplash.rs: install nodogsplash, configure it to gate the dedicated br-tollgate bridge, open the pre-auth walled-garden ports (2121 payment, 2050 portal). - tollgate/wifi.rs: bind network.tollgate to a stable br-tollgate bridge device (NoDogSplash needs a known interface name — the driver-assigned name of a bare wifi vif isn't guaranteed); disable IPv6 RA/DHCPv6 on it (NoDogSplash only manages IPv4 iptables, so IPv6 would let clients bypass the portal entirely). - tollgate/config.rs: apply_daemon_config() merges pricing/mint into the real config.json instead of (only) UCI. - opkg.rs: generic install_package() for standard feed packages under either PkgManager mode. - router.rs: upload_file() (SCP) for non-UCI config files. --- core/openwrt/src/opkg.rs | 27 +++++++++++++ core/openwrt/src/router.rs | 20 +++++++++- core/openwrt/src/tollgate/config.rs | 44 ++++++++++++++++++++- core/openwrt/src/tollgate/mod.rs | 28 +++++++++++-- core/openwrt/src/tollgate/nodogsplash.rs | 50 ++++++++++++++++++++++++ core/openwrt/src/tollgate/wifi.rs | 24 +++++++++++- 6 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 core/openwrt/src/tollgate/nodogsplash.rs 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..88d8db0a 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -1,21 +1,25 @@ 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 and configure NoDogSplash (client gating — tollgate-wrt has no +/// firewall/enforcement code of its own; see `nodogsplash::provision`) +/// 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. Restart affected services pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { info!("[{}] Starting TollGate provisioning", router.host); @@ -29,9 +33,25 @@ 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. + nodogsplash::provision(router, pkg_mgr, config) + .context("provision nodogsplash — tollgate-wrt cannot gate clients without it")?; + 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")?; restart_services(router, config.enabled)?; + nodogsplash::restart(router)?; info!("[{}] TollGate provisioning complete", router.host); Ok(()) diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs new file mode 100644 index 00000000..5742cee2 --- /dev/null +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -0,0 +1,50 @@ +use anyhow::{Context, Result}; + +use crate::opkg::PkgManager; +use crate::tollgate::TollGateConfig; +use crate::Router; + +/// Install and configure NoDogSplash — the captive-portal engine that +/// `tollgate-wrt` delegates all MAC authorization and gate open/close to via +/// `ndsctl`. `tollgate-wrt` contains no firewall/netfilter code of its own +/// (confirmed: its binary has no `nft`/`ipset`/`iptables` calls at all); the +/// upstream package therefore hard-depends on `+nodogsplash` and its postinst +/// restarts it. Without NoDogSplash actually running, nothing blocks +/// unauthenticated clients from reaching the internet, regardless of what +/// TollGate itself is configured to charge. +/// +/// Gates 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`. +pub fn provision(router: &Router, pkg_mgr: PkgManager, cfg: &TollGateConfig) -> Result<()> { + router + .install_package(pkg_mgr, "nodogsplash") + .context("install nodogsplash — required by tollgate-wrt for client gating")?; + + router.run_ok("touch /etc/config/nodogsplash")?; + 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": unauthenticated clients must still be able to + // reach the TollGate payment endpoint (2121) and the splash portal itself + // (2050) — everything else is blocked by NoDogSplash until ndsctl + // authorizes their MAC. Rebuild the list each run rather than trying to + // dedupe, so repeated provisioning stays idempotent. + let _ = router.uci_delete("nodogsplash.main.users_to_router"); + 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")?; + + 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( From 7439a1251c67b93c661861ed2b8dbab4e906d7a0 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 19:28:48 +0000 Subject: [PATCH 09/22] fix(tollgate): fix nodogsplash provisioning order (found live: gated br-lan) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deploying the previous commit's fix live exposed a real bug: nodogsplash's OpenWrt package postinst auto-enables and starts the service immediately on install, using its stock default config — gatewayinterface=br-lan. Since NoDogSplash only touches IPv4 iptables, this silently cut IPv4 (not IPv6) connectivity for anything on br-lan, including the admin management box plugged into this router's LAN port, for the window between install and our own configure step. Split nodogsplash provisioning into install_and_stop() (runs first, closes that window immediately) and configure() (runs after wifi::provision_ssid has created br-tollgate, so gatewayinterface is pointed at the isolated tollgate bridge instead of br-lan). --- core/openwrt/src/tollgate/mod.rs | 21 ++++++++---- core/openwrt/src/tollgate/nodogsplash.rs | 41 +++++++++++++++++------- 2 files changed, 44 insertions(+), 18 deletions(-) diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index 88d8db0a..9f9e3ab0 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -14,12 +14,14 @@ use crate::{opkg::PkgManager, Router}; /// Full TollGate provisioning sequence: /// 1. Install tollgate-module-basic-go -/// 2. Install and configure NoDogSplash (client gating — tollgate-wrt has no -/// firewall/enforcement code of its own; see `nodogsplash::provision`) +/// 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. Restart affected services +/// 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); @@ -40,9 +42,11 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { // 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. - nodogsplash::provision(router, pkg_mgr, config) - .context("provision nodogsplash — tollgate-wrt cannot gate clients without it")?; + // 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")?; config::apply(router, config)?; wifi::provision_ssid(router, config)?; @@ -50,6 +54,11 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { // 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)?; diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs index 5742cee2..06025bf4 100644 --- a/core/openwrt/src/tollgate/nodogsplash.rs +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -4,23 +4,40 @@ use crate::opkg::PkgManager; use crate::tollgate::TollGateConfig; use crate::Router; -/// Install and configure NoDogSplash — the captive-portal engine that -/// `tollgate-wrt` delegates all MAC authorization and gate open/close to via -/// `ndsctl`. `tollgate-wrt` contains no firewall/netfilter code of its own -/// (confirmed: its binary has no `nft`/`ipset`/`iptables` calls at all); the -/// upstream package therefore hard-depends on `+nodogsplash` and its postinst -/// restarts it. Without NoDogSplash actually running, nothing blocks -/// unauthenticated clients from reaching the internet, regardless of what -/// TollGate itself is configured to charge. +/// Install NoDogSplash and immediately stop it, before configuring anything. /// -/// Gates 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`. -pub fn provision(router: &Router, pkg_mgr: PkgManager, cfg: &TollGateConfig) -> Result<()> { +/// 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(()) +} +/// 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")?; router.uci_set("nodogsplash.main", "nodogsplash")?; router.uci_set("nodogsplash.main.enabled", "1")?; From abe03a57028543edc24bbd91ba230615d4803773 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 20:21:32 +0000 Subject: [PATCH 10/22] fix(tollgate): remove stray nodogsplash section, fix netifd claim race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two more issues found deploying the previous two commits live: 1. The nodogsplash package's own uci-defaults populate an anonymous @nodogsplash[0] section pointed at br-lan (see install_and_stop's doc comment). NoDogSplash runs one gateway instance per config section, so this ran alongside our own nodogsplash.main instead of being superseded by it — silently re-gating br-lan. configure() now deletes it. 2. After `network restart` + `wifi down/up`, netifd intermittently loses the race to claim br-tollgate as the wifi vif attaches (reports up:false, DEVICE_CLAIM_FAILED) even though the bridge device and member interface both exist correctly. NoDogSplash refuses to start against an interface netifd hasn't brought up. restart_services() now explicitly cycles just the tollgate interface (ifdown/ifup) after the wifi restart. --- core/openwrt/src/tollgate/mod.rs | 7 +++++++ core/openwrt/src/tollgate/nodogsplash.rs | 10 ++++++++++ 2 files changed, 17 insertions(+) diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index 9f9e3ab0..c05d96a4 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -86,5 +86,12 @@ 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: netifd can lose the race to claim br-tollgate as the + // wifi vif attaches to it during the restart above, leaving the + // interface reported "up: false, DEVICE_CLAIM_FAILED" even though the + // bridge device and its member both exist. An explicit down/up of just + // this logical interface resolves it — needed before NoDogSplash starts, + // since it refuses to start against an interface netifd hasn't brought up. + router.run_ok("sleep 2; ifdown tollgate 2>&1; sleep 1; ifup tollgate 2>&1; sleep 2")?; Ok(()) } diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs index 06025bf4..de040e32 100644 --- a/core/openwrt/src/tollgate/nodogsplash.rs +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -39,6 +39,16 @@ pub fn install_and_stop(router: &Router, pkg_mgr: PkgManager) -> Result<()> { /// 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")?; From c1f191128fab1d65a935ba0c2039bd8241831bf8 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 20:51:27 +0000 Subject: [PATCH 11/22] fix(tollgate): restore DHCP/DNS in nodogsplash's pre-auth walled garden MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found live: new clients on the archipelago SSID couldn't get an IP address at all. configure()'s users_to_router rebuild replaced the nodogsplash package's stock default list (DNS, DHCP, SSH/Telnet to the router) with only our two TollGate-specific ports (2121, 2050) — dropping `allow udp port 67`, so DHCP DISCOVER from an unauthenticated client hit ndsRTR's default REJECT before ever reaching dnsmasq. Carries over DNS (53) and DHCP (67/udp) from the stock default — without them a client can't get an IP or resolve anything before authenticating. Deliberately does not carry over SSH/Telnet (22/23): the stock default exposes router shell access to every unauthenticated device on the network, which isn't an appropriate default for a public pay-as-you-go hotspot. --- core/openwrt/src/tollgate/nodogsplash.rs | 25 +++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs index de040e32..887cb144 100644 --- a/core/openwrt/src/tollgate/nodogsplash.rs +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -56,12 +56,27 @@ pub fn configure(router: &Router, cfg: &TollGateConfig) -> Result<()> { router.uci_set("nodogsplash.main.gatewaydomainname", "TollGate.lan")?; router.uci_set("nodogsplash.main.gatewayport", "2050")?; - // Pre-auth "walled garden": unauthenticated clients must still be able to - // reach the TollGate payment endpoint (2121) and the splash portal itself - // (2050) — everything else is blocked by NoDogSplash until ndsctl - // authorizes their MAC. Rebuild the list each run rather than trying to - // dedupe, so repeated provisioning stays idempotent. + // 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")?; From 6060ad23b24020a55ec85ab89d27cbbe357e4166 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 21:54:43 +0000 Subject: [PATCH 12/22] fix(tollgate): verify br-tollgate's kernel-level IP after restart, retry if missing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found live again, in a different shape: after a later round of service restarts (dnsmasq restart while debugging), br-tollgate desynced a second time — but this time netifd's own status reported the interface up with 192.168.99.1 assigned, while `ip -4 addr show br-tollgate` was genuinely empty at the kernel level. dnsmasq logged "DHCP packet received on br-tollgate which has no address" and silently dropped every DISCOVER — clients associated to the archipelago SSID fine but never got an IP. A single blind ifdown/ifup (the previous fix) isn't trustworthy against this netifd race — replace it with a loop that checks the actual kernel address after each cycle and retries up to 5 times, failing loudly (rather than silently leaving DHCP broken) if it never converges. --- core/openwrt/src/tollgate/mod.rs | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index c05d96a4..8017f8f0 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -86,12 +86,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: netifd can lose the race to claim br-tollgate as the - // wifi vif attaches to it during the restart above, leaving the - // interface reported "up: false, DEVICE_CLAIM_FAILED" even though the - // bridge device and its member both exist. An explicit down/up of just - // this logical interface resolves it — needed before NoDogSplash starts, - // since it refuses to start against an interface netifd hasn't brought up. - router.run_ok("sleep 2; ifdown tollgate 2>&1; sleep 1; ifup tollgate 2>&1; sleep 2")?; + // 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(()) } From 9902ffd31d3914bca2932cb58a4ad100ae2733cd Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 22:56:53 +0000 Subject: [PATCH 13/22] fix(tollgate): wire up TollGate's real captive portal (was serving NDS's stock page) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found live: after fixing DHCP, the user could join the archipelago SSID, saw a splash page, clicked "Continue", and got straight to the internet — no payment step at all. NoDogSplash was serving its own bundled generic click-to-continue splash page, whose "Continue" button calls NDS's built-in auth handler directly and authorizes the client unconditionally. TollGate's actual payment UI — a React SPA with a Cashu/QR token entry flow — was already sitting on disk at /etc/tollgate/tollgate-captive-portal-site (staged by the .ipk's data payload during install), just never wired up as NoDogSplash's webroot. install_captive_portal_symlink() mirrors upstream's own 90-tollgate-captive-portal-symlink uci-defaults script exactly: swap /etc/nodogsplash/htdocs for a symlink to the real portal directory, backing up any existing real directory first. Confirmed live that setting `option webroot` directly instead (rather than the symlink swap) makes NoDogSplash 500 on every request for reasons not fully understood — the symlink approach is what's actually shipped/tested upstream, so that's what this uses. Also restores `authenticated_users 'allow all'` (the stock package default our from-scratch nodogsplash.main section never carried over) for correctness, even though this router's default-ACCEPT FORWARD policy happens to make an empty list behave the same. --- core/openwrt/src/tollgate/mod.rs | 9 +++++ core/openwrt/src/tollgate/nodogsplash.rs | 47 ++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/core/openwrt/src/tollgate/mod.rs b/core/openwrt/src/tollgate/mod.rs index 8017f8f0..d92b1c55 100644 --- a/core/openwrt/src/tollgate/mod.rs +++ b/core/openwrt/src/tollgate/mod.rs @@ -48,6 +48,15 @@ pub async fn provision(router: &Router, config: &TollGateConfig) -> Result<()> { 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 diff --git a/core/openwrt/src/tollgate/nodogsplash.rs b/core/openwrt/src/tollgate/nodogsplash.rs index 887cb144..82bafed4 100644 --- a/core/openwrt/src/tollgate/nodogsplash.rs +++ b/core/openwrt/src/tollgate/nodogsplash.rs @@ -29,6 +29,45 @@ pub fn install_and_stop(router: &Router, pkg_mgr: PkgManager) -> Result<()> { 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 @@ -80,6 +119,14 @@ pub fn configure(router: &Router, cfg: &TollGateConfig) -> Result<()> { 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(()) } From 8f47d6608a4368abea3ddc308751c170091d6bd0 Mon Sep 17 00:00:00 2001 From: ssmithx Date: Thu, 2 Jul 2026 23:55:15 +0000 Subject: [PATCH 14/22] feat(tollgate): periodically sweep TollGate's router wallet into the local wallet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tollgate-wrt keeps its own separate Cashu wallet on the router (/etc/tollgate/wallet.db) — customer payments land there, never in this node's own wallet. Its Lightning auto-payout is configured independently in /etc/tollgate/identities.json, which is easy to leave misconfigured or pointed at a placeholder address (found live: both "owner" and "developer" identities on this deployment pointed at the same unconfigured tollgate@minibits.cash default). Rather than depend on getting that Lightning payout config right, tollgate_sweep::sweep_once() periodically (every 5 min, via SSH) checks the router's `tollgate wallet balance`, and if nonzero, runs `tollgate wallet drain cashu` and receives the resulting token(s) straight into the local wallet via the same path the "Receive ecash" UI uses (wallet::ecash::receive_token) — bypassing Lightning payout entirely. A no-op (Ok(0)) if no router is configured or it doesn't have TollGate installed. --- core/archipelago/src/main.rs | 1 + core/archipelago/src/server.rs | 22 +++++++ core/archipelago/src/tollgate_sweep.rs | 81 ++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 core/archipelago/src/tollgate_sweep.rs diff --git a/core/archipelago/src/main.rs b/core/archipelago/src/main.rs index 23805fb9..a6567e65 100644 --- a/core/archipelago/src/main.rs +++ b/core/archipelago/src/main.rs @@ -71,6 +71,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 093557c4..445d4929 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -534,6 +534,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) +} From d99f4438d8e73749be152f6987c0d0b0db3cbdee Mon Sep 17 00:00:00 2001 From: ssmithx Date: Fri, 3 Jul 2026 02:46:48 +0000 Subject: [PATCH 15/22] fix(wallet): show Cashu/Fedimint receives in the Transactions modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found live: after receiving a TollGate Cashu payment into the wallet, the balance correctly showed the new sats (wallet.ecash-balance), but the Transactions modal said "no transactions yet." Home.vue's loadWeb5Status() only ever fetched lnd.gettransactions — ecash and Fedimint activity (wallet.ecash-history, which already unifies both) was never wired into walletTransactions at all, so no Cashu or Fedimint receive could ever show up there regardless of how long you waited. Maps EcashTransaction into the existing (LND-shaped) WalletTransaction interface rather than widening that type, and merges+sorts both lists by timestamp. --- neode-ui/src/views/Home.vue | 39 ++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) 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 From bb0875f7f2fc5bf951b37f153faed68343641e80 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 17:31:18 +0100 Subject: [PATCH 16/22] feat(manifest): optional secret_env entries A secret_env entry with optional: true is skipped when its secret file is missing, unreadable, or empty, instead of failing the whole resolution and taking the app down. Needed for per-node integrations like btcpay's internal-LND connection string: nodes without LND must still run btcpay. Co-Authored-By: Claude Fable 5 --- core/container/src/manifest.rs | 66 ++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) 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 = [ From ce6ec13e28217809f519c60ada1b8bf102ab7d87 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 17:31:18 +0100 Subject: [PATCH 17/22] feat(lnd): auto-generate btcpay internal-LND connection secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Materialise /var/lib/archipelago/secrets/btcpay-lnd-connection (type=lnd-rest;server=https://lnd:8080/;macaroon=;certthumbprint=) in ensure_app_secrets before btcpay-server env resolution. The macaroon travels inline as hex because LND's datadir is owned by its container subuid (100999) — bind-mounting it into btcpay's userns EACCESes. Reads the macaroon via the existing read_file_as_root sudo fallback; pins the TLS cert by DER SHA256 thumbprint and rewrites on cert rotation. No-op while LND is absent (btcpay's secret_env entry is optional), and generation errors log-and-continue so btcpay never fails to start over this. Co-Authored-By: Claude Fable 5 --- core/archipelago/src/container/lnd.rs | 67 +++++++++++++++++++ .../src/container/prod_orchestrator.rs | 11 +++ core/archipelago/src/container/secrets.rs | 11 +++ 3 files changed, 89 insertions(+) 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..6b7328a3 100644 --- a/core/archipelago/src/container/prod_orchestrator.rs +++ b/core/archipelago/src/container/prod_orchestrator.rs @@ -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. 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<()> { From 6f1d0695d08654b3771dbe13a01ad12e3b743fbf Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 17:48:27 +0100 Subject: [PATCH 18/22] fix(apps): persist btcpay plugins and wire the internal LND node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two btcpay-server manifest gaps found on shorty-s (2026-07-10): - BTCPAY_PLUGINDIR=/datadir/Plugins: the image default is container-local, so every container recreate silently wiped installed plugins (the reinstall-the-nostr-plugin-after-every-restart symptom, plus a soft-restart loop while an install was pending). - BTCPAY_BTCLIGHTNING via optional secret_env btcpay-lnd-connection: the manifest never wired the internal LND node. The secret is generated by the daemon (see previous commit); nodes without LND skip it. releases/app-catalog.json regenerated (embeds the manifest; origin-wins overlay means nodes only pick this up from the published catalog). Signature stripped by regeneration — run scripts/sign-catalog.sh before publishing. Co-Authored-By: Claude Fable 5 --- apps/btcpay-server/manifest.yml | 10 + releases/app-catalog.json | 4730 ++++++++++++++++--------------- 2 files changed, 2377 insertions(+), 2363 deletions(-) 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/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" + } } From 51aa40043161cf320ad6c7c0db56eeb725a3d0c3 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 17:48:27 +0100 Subject: [PATCH 19/22] fix(doctor): actually rebuild the rootless netns, and latch after repeated failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The netns-egress repair only stopped/started containers, which reuses the existing netns — its holders (aardvark-dns, podman pause) survive, so a missing pasta tap was never rebuilt. On shorty-s (2026-07-10) this cycled all 35 containers every timer run for ~an hour, bouncing bitcoind/LND/BTCPay the whole time, with 'egress still broken after cycle' after every pass. Now the cycle kills the netns holders, runs podman system migrate, and clears /run/user//containers/networks so the first container start recreates pasta + aardvark-dns from scratch (the sequence that actually fixed the node). A failure counter (/var/lib/archipelago/doctor-netns-cycle-failures) stops the fleet-wide cycling after 3 consecutive failed rebuilds until egress is observed healthy again. Co-Authored-By: Claude Fable 5 --- scripts/container-doctor.sh | 52 ++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 6 deletions(-) diff --git a/scripts/container-doctor.sh b/scripts/container-doctor.sh index 322c8e94..e16795af 100755 --- a/scripts/container-doctor.sh +++ b/scripts/container-doctor.sh @@ -383,10 +383,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 +422,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 +459,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 +482,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 } From 2d8606872ee8325e36a0c65d0872c77b940264ca Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 18:16:21 +0100 Subject: [PATCH 20/22] fix(doctor): enforce BTCPay Lightning route hints so private-channel invoices stay payable A store on a node whose LND has only unannounced channels mints BOLT11 invoices with no route hints when lightningPrivateRouteHints is off, and external wallets fail with "no way to pay this invoice" (hit on shorty-s 2026-07-10 with a Blink payer). Hints cost nothing on public channels, so the doctor now flips the flag on for every store on each run. Co-Authored-By: Claude Fable 5 --- scripts/container-doctor.sh | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/scripts/container-doctor.sh b/scripts/container-doctor.sh index e16795af..dc5dd35a 100755 --- a/scripts/container-doctor.sh +++ b/scripts/container-doctor.sh @@ -16,6 +16,8 @@ # 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) # # Safe to run multiple times (idempotent). Never blocks deploy (exit 0 always). # @@ -566,6 +568,32 @@ 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 +} + # ── Main ───────────────────────────────────────────────────── # If remote host provided, run via SSH @@ -596,6 +624,7 @@ 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 echo "" if [ $FIXES_APPLIED -gt 0 ]; then From 99529fcce590853c0d7bad237ae9d4090899dcd6 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 18:22:20 +0100 Subject: [PATCH 21/22] =?UTF-8?q?fix(doctor):=20install=20catatonit=20when?= =?UTF-8?q?=20missing=20=E2=80=94=20Portainer=20init=20deploys=20fail=20wi?= =?UTF-8?q?thout=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Debian's podman only Recommends catatonit; nodes installed before install-podman.sh gained the dependency fail any init-enabled container deploy (observed on shorty-s deploying sites via Portainer). Doctor Fix 13 installs it via apt when absent. Co-Authored-By: Claude Fable 5 --- scripts/container-doctor.sh | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/scripts/container-doctor.sh b/scripts/container-doctor.sh index dc5dd35a..d7f2ada3 100755 --- a/scripts/container-doctor.sh +++ b/scripts/container-doctor.sh @@ -18,6 +18,7 @@ # 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). # @@ -594,6 +595,25 @@ fix_btcpay_route_hints() { 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 @@ -625,6 +645,7 @@ 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 From 4b0e44fc6cada16f10d220bb06def78a345cb9a4 Mon Sep 17 00:00:00 2001 From: Dorian Date: Fri, 10 Jul 2026 18:55:32 +0100 Subject: [PATCH 22/22] chore: purge retired git.tx1138.com registry host from the codebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tx1138 Gitea was retired as a release server 2026-06-13 and its registry frontend is fully dead (500 on every /v2 manifest read, observed 2026-07-10). Nothing may reference it anymore: - registry.rs: no longer a default registry, no longer force-enabled on load; saved configs are stripped of it on load (same one-time migration treatment as the decommissioned Hetzner mirror), with a regression test. - image_policy.rs: removed from TRUSTED_REGISTRIES — refs through the dead host are now refused at the pull site (rejection test added). - api/handler: dropped the legacy catalog-proxy fallback URL. - .gitmodules: indeedhub submodule repointed to the OVH Gitea. - scripts, image-recipe, app-catalog data, neode-ui strings, docs, and all test fixtures repointed to 146.59.87.168:3000 (or neutral example hosts). - image-versions.sh: ARCHY_REGISTRY_FALLBACK emptied (guarded consumers skip it); reconcile-containers.sh candidate guard hardened. The only remaining occurrences of the host string are the strip/reject enforcement paths and their regression tests — the code that guarantees it is never used again. Co-Authored-By: Claude Fable 5 --- .gitmodules | 2 +- app-catalog/README.md | 2 +- app-catalog/catalog.json | 4 +- apps/indeedhub/push-to-registry.sh | 2 +- core/archipelago/src/api/handler/mod.rs | 14 +-- .../archipelago/src/container/image_policy.rs | 11 +- .../src/container/image_versions.rs | 18 ++-- .../src/container/prod_orchestrator.rs | 8 +- core/archipelago/src/container/registry.rs | 100 ++++++++++++------ core/archipelago/src/update.rs | 14 +-- core/container/src/podman_client.rs | 3 +- docs/1.8.0-RELEASE-HARDENING-PLAN.md | 2 +- docs/container-architecture.html | 2 +- docs/dht-distribution-design.md | 2 +- .../.gitea-workflows/build-iso-dev.yml | 2 +- .../_archived/build-auto-installer-iso.sh | 16 +-- image-recipe/scripts/install-podman.sh | 4 +- neode-ui/public/catalog.json | 4 +- neode-ui/src/views/Apps.vue | 2 +- neode-ui/src/views/discover/curatedApps.ts | 2 +- .../src/views/settings/AccountInfoSection.vue | 4 +- scripts/bootstrap-switchover.sh | 4 +- scripts/deploy-bitcoin-knots.sh | 2 +- scripts/dev-container-test.sh | 2 +- scripts/first-boot-containers.sh | 2 +- scripts/image-versions.sh | 3 +- scripts/reconcile-containers.sh | 2 +- scripts/self-update.sh | 6 +- scripts/validate-app-manifest.sh | 2 +- 29 files changed, 140 insertions(+), 101 deletions(-) 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/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/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/prod_orchestrator.rs b/core/archipelago/src/container/prod_orchestrator.rs index 6b7328a3..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. @@ -4848,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 @@ -4872,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 @@ -4959,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/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/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/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/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/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/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/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/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