From 031b3c34f40d8e331eb6ca5767ca367bbdf5e0f0 Mon Sep 17 00:00:00 2001 From: Dorian Date: Mon, 30 Mar 2026 16:35:06 +0100 Subject: [PATCH] fix: container installs, Tor, kiosk, GRUB, LUKS display, error messages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - fix: container installs fail with "statfs: no such file or directory" Root cause: NoNewPrivileges=yes in systemd blocks sudo inside backend. Fix: use std::fs::create_dir_all + podman unshare chown (no sudo needed) - fix: Tor services.json never written — \$ARCHY_TOR_DIR escaping bug - fix: kiosk white screen — increase health wait to 60s, add --disable-gpu Improvements: - feat: LUKS encryption badge in Server disk stats (backend detects dm-crypt) - fix: GRUB theme text scaling on 4:3 monitors — explicit fonts, wider menu - fix: suppress default Debian MOTD (custom profile.d welcome is enough) - fix: install error messages now show "Failed to pull/start" instead of generic "Operation failed" (middleware.rs allowlist expanded) - fix: container-tests CI — source cargo env before running tests - docs: interactive container architecture diagram (HTML) Co-Authored-By: Claude Opus 4.6 (1M context) --- .gitea/workflows/container-tests.yml | 5 +- core/archipelago/src/api/rpc/middleware.rs | 4 + .../src/api/rpc/package/install.rs | 31 +- .../src/api/rpc/system/handlers.rs | 13 +- core/archipelago/src/api/rpc/system/mod.rs | 7 +- docs/container-architecture.html | 428 ++++++++++++++++++ image-recipe/branding/grub-theme/theme.txt | 38 +- image-recipe/build-auto-installer-iso.sh | 18 +- neode-ui/src/views/Server.vue | 15 +- 9 files changed, 522 insertions(+), 37 deletions(-) create mode 100644 docs/container-architecture.html diff --git a/.gitea/workflows/container-tests.yml b/.gitea/workflows/container-tests.yml index f1b13036..f76afdfe 100644 --- a/.gitea/workflows/container-tests.yml +++ b/.gitea/workflows/container-tests.yml @@ -29,6 +29,7 @@ jobs: - name: Run orchestration unit tests working-directory: core run: | + source $HOME/.cargo/env 2>/dev/null || true echo "=== Container crate tests ===" cargo test -p archipelago-container --no-fail-fast 2>&1 @@ -38,7 +39,9 @@ jobs: - name: Verify cargo check (full crate) working-directory: core - run: cargo check --release 2>&1 + run: | + source $HOME/.cargo/env 2>/dev/null || true + cargo check --release 2>&1 smoke-tests: runs-on: ubuntu-latest diff --git a/core/archipelago/src/api/rpc/middleware.rs b/core/archipelago/src/api/rpc/middleware.rs index c30250ef..34090a15 100644 --- a/core/archipelago/src/api/rpc/middleware.rs +++ b/core/archipelago/src/api/rpc/middleware.rs @@ -56,6 +56,10 @@ pub(super) fn sanitize_error_message(msg: &str) -> String { "cannot", "Password", "Session", + "Failed to pull", + "Failed to start", + "Container", + "Image", ]; for prefix in &user_facing_prefixes { if msg.starts_with(prefix) { diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 0efa8f3d..76c30217 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -472,18 +472,31 @@ impl RpcHandler { if let Some(host_path) = volume.split(':').next() { if host_path.starts_with("/var/lib/archipelago/") { debug!("Creating directory: {} (owner: {})", host_path, uid_str); - let create_dir = tokio::process::Command::new("sudo") - .args(["mkdir", "-p", host_path]) - .output() - .await; - if let Err(e) = create_dir { - debug!("Failed to create directory {}: {}", host_path, e); + + // Create directory directly (service has ReadWritePaths access). + // sudo is blocked by NoNewPrivileges=yes in the systemd service. + if let Err(e) = std::fs::create_dir_all(host_path) { + tracing::warn!("Failed to create directory {}: {}", host_path, e); } - // Set ownership to the mapped UID for rootless podman - let _ = tokio::process::Command::new("sudo") - .args(["chown", "-R", &uid_str, host_path]) + + // Set ownership to the mapped UID for rootless podman. + // This needs elevated privileges — use podman unshare to run + // chown inside the user namespace where UIDs are mapped. + let chown_result = tokio::process::Command::new("podman") + .args(["unshare", "chown", "-R", &uid_str, host_path]) .output() .await; + match chown_result { + Ok(out) if !out.status.success() => { + tracing::warn!( + "podman unshare chown failed for {}: {}", + host_path, + String::from_utf8_lossy(&out.stderr) + ); + } + Err(e) => tracing::warn!("Failed to chown {}: {}", host_path, e), + _ => {} + } } } } diff --git a/core/archipelago/src/api/rpc/system/handlers.rs b/core/archipelago/src/api/rpc/system/handlers.rs index 66f413f2..86fa7906 100644 --- a/core/archipelago/src/api/rpc/system/handlers.rs +++ b/core/archipelago/src/api/rpc/system/handlers.rs @@ -98,7 +98,11 @@ impl RpcHandler { /// system.disk-status — Disk usage with warning/critical thresholds. pub(in crate::api::rpc) async fn handle_system_disk_status(&self) -> Result { - let (used, total) = read_disk_usage().await.unwrap_or((0, 0)); + // Prefer the encrypted data partition if it exists + let data_path = std::path::Path::new("/var/lib/archipelago"); + let df_target = if data_path.exists() { "/var/lib/archipelago" } else { "/" }; + + let (used, total) = read_disk_usage_path(df_target).await.unwrap_or((0, 0)); let percent = if total > 0 { (used as f64 / total as f64) * 100.0 } else { @@ -114,12 +118,19 @@ impl RpcHandler { "ok" }; + // Detect LUKS encryption (device name varies by install) + let encrypted = std::path::Path::new("/dev/mapper/archipelago_crypt").exists() + || std::path::Path::new("/dev/mapper/archipelago-data").exists() + || std::path::Path::new("/dev/mapper/archipelago_data").exists(); + Ok(serde_json::json!({ "used_bytes": used, "total_bytes": total, "free_bytes": total.saturating_sub(used), "used_percent": percent_rounded, "level": level, + "encrypted": encrypted, + "partition": df_target, })) } diff --git a/core/archipelago/src/api/rpc/system/mod.rs b/core/archipelago/src/api/rpc/system/mod.rs index 4bf99b97..5578b252 100644 --- a/core/archipelago/src/api/rpc/system/mod.rs +++ b/core/archipelago/src/api/rpc/system/mod.rs @@ -157,8 +157,13 @@ pub(super) fn parse_meminfo_kb(val: &str) -> Result { /// Read disk usage via `df` for the root filesystem. /// Returns (used_bytes, total_bytes). pub(super) async fn read_disk_usage() -> Result<(u64, u64)> { + read_disk_usage_path("/").await +} + +/// Read disk usage via `df` for a given path. +pub(super) async fn read_disk_usage_path(path: &str) -> Result<(u64, u64)> { let output = tokio::process::Command::new("df") - .args(["--block-size=1", "--output=used,size", "/"]) + .args(["--block-size=1", "--output=used,size", path]) .output() .await .context("Failed to run df")?; diff --git a/docs/container-architecture.html b/docs/container-architecture.html new file mode 100644 index 00000000..7ee65dd8 --- /dev/null +++ b/docs/container-architecture.html @@ -0,0 +1,428 @@ + + + + + +Archipelago Container Architecture + + + + +
+

Archipelago Container Architecture

+

Interactive map of all containers, dependencies, and networks · Click any container for details

+
+ +
+
28
Total Containers
+
4
Tier 0 · DB
+
2
Tier 1 · Core
+
8
Tier 2 · Service
+
14
Tier 3 · App
+
+ +
+ + + + + + + +
+ +
+ +
+
Tier 0 — Databases
+
+
+
+ archy-mempool-db + archy-net +
+
mariadb:11.4.10
+
No ports exposed
+
+
UID100999:100999
+
Healthmariadb -uroot -e 'SELECT 1'
+
UI TabServices
+
Data/var/lib/archipelago/mysql-mempool
+
DepsNone
+
Needed bymempool-api
+
+
+
+
+ archy-btcpay-db + archy-net +
+
postgres:15.17
+
No ports exposed
+
+
UID100070:100070
+
Healthpg_isready -U postgres
+
UI TabServices
+
Needed bynbxplorer btcpay
+
+
+
+
+ immich_postgres + bridge +
+
immich-postgres:14 (optional)
+
Needed byimmich
+
+
+
+ immich_redis + bridge +
+
valkey:8.1.6 (optional)
+
Needed byimmich
+
+
+
+ +
+
Tier 1 — Core Infrastructure
+
+
+
+ bitcoin-knots + archy-net +
+
bitcoin-knots:latest
+
Ports: 8332 8333
+
+
UID100101:100101
+
Healthbitcoin-cli getblockchaininfo
+
UI TabApps
+
Memory2g (1g low-mem)
+
DiskPrunes if <1TB, txindex if ≥1TB
+
DepsNone — ROOT DEPENDENCY
+
Needed byelectrumx lnd mempool nbxplorer fedimint
+
+
+
+
+ electrumx + archy-net +
+
electrumx:v1.18.0
+
Ports: 50001
+
+
Healthcurl localhost:8000
+
UI TabApps
+
Depsbitcoin-knots
+
Needed bymempool-api
+
+
+
+
+ +
+
Tier 2 — Services
+
+
+
lndarchy-net
+
lnd:v0.18.4-beta
+
Ports: 9735 10009 8080
+
+
Depsbitcoin-knots
+
UI TabApps
+
Needed byfedi-gateway (LND mode)
+
+
+
+
mempool-apiarchy-net
+
mempool-backend:v3.0.0
+
Ports: 8999
+
+
Depsbitcoin-knots electrumx mempool-db
+
UI TabServices
+
Needed bymempool-web
+
+
+
+
archy-mempool-webarchy-net
+
mempool-frontend:v3.0.0
+
Ports: 4080
+
+
Depsmempool-api
+
UI TabApps
+
+
+
+
archy-nbxplorerarchy-net
+
nbxplorer:2.6.0
+
Ports: 32838
+
+
Depsbitcoin-knots btcpay-db
+
UI TabServices
+
Needed bybtcpay
+
+
+
+
btcpay-serverarchy-net
+
btcpayserver:1.13.7
+
Ports: 23000
+
+
Depsnbxplorer btcpay-db
+
UI TabApps
+
+
+
+
fedimintarchy-net
+
fedimintd:v0.10.0
+
Ports: 8173 8174 8175
+
+
Depsbitcoin-knots
+
UI TabApps
+
Needed byfedi-gateway
+
+
+
+
fedimint-gatewayarchy-net
+
gatewayd:v0.10.0
+
Ports: 8176
+
+
Depsbitcoin-knots fedimint
+
UI TabApps
+
NoteUses LDK (built-in Lightning) if no LND
+
+
+
+
immich_serverbridge
+
immich-server:release (optional)
+
Ports: 2283
+
+
Depsimmich_postgres immich_redis
+
+
+
+
+ +
+
Tier 3 — Applications (independent, no dependencies)
+
+
+
homeassistantbridge
+
home-assistant:2024.1
+
Ports: 8123
+
+
+
grafanabridge
+
grafana:10.2.0
+
Ports: 3000
+
UID100472:100472
+
+
+
jellyfinbridge
+
jellyfin:10.8.13
+
Ports: 8096
+
+
+
photoprismbridge
+
photoprism:240915
+
Ports: 2342
+
+
+
vaultwardenbridge
+
vaultwarden:1.30.0-alpine
+
Ports: 8082
+
+
+
nextcloudbridge
+
nextcloud:28
+
Ports: 8085
+
+
+
searxngbridge
+
searxng:latest
+
Ports: 8888
+
+
+
uptime-kumabridge
+
uptime-kuma:1
+
Ports: 3001
+
+
+
filebrowserbridge
+
filebrowser:v2.27.0
+
Ports: 8083
+
NoteOnly container created by first-boot (unbundled ISO)
+
+
+
onlyofficebridge
+
onlyoffice:latest
+
Ports: 9980
+
+
+
ollamabridge
+
ollama:latest (optional)
+
Ports: 11434
+
+
+
nginx-proxy-managerbridge
+
nginx-proxy-manager:latest
+
Ports: 81 8084 8443
+
+
+
portainerbridge
+
portainer:latest
+
Ports: 9000
+
+
+
+ +
+ +
+

Dependency Chain

+
// Startup order: Tier 0 → 1 → 2 → 3 +// Health monitor restarts in this order too + +mempool-db ───┐ +btcpay-db ───┤ + + ├──→ bitcoin-knots ──→ electrumx + + ┌────┴────┬──────────┬──────────┐ + + lnd fedimint mempool-api nbxplorer + + fedi-gw mempool-web btcpay + +// Tier 3: All independent — start in any order +filebrowser grafana homeassist jellyfin photoprism +vaultwarden nextcloud searxng uptime-kuma ollama
+
+ +
+
archy-net (Bitcoin stack, inter-container DNS)
+
bridge (standalone apps, port-mapped)
+
Apps tab (user-facing)
+
Services tab (infrastructure)
+
+ + + + + diff --git a/image-recipe/branding/grub-theme/theme.txt b/image-recipe/branding/grub-theme/theme.txt index 67374784..be5029d4 100644 --- a/image-recipe/branding/grub-theme/theme.txt +++ b/image-recipe/branding/grub-theme/theme.txt @@ -1,6 +1,5 @@ # Archipelago GRUB Theme # Dark background with Bitcoin orange accents -# Font references removed — GRUB uses whatever fonts are loaded in grub.cfg title-text: "" desktop-color: "#0a0a0a" @@ -8,41 +7,46 @@ desktop-image: "background.png" desktop-image-scale-method: "stretch" + boot_menu { - left = 15% - top = 40% - width = 70% - height = 35% + left = 10% + top = 35% + width = 80% + height = 40% + item_font = "DejaVu Sans Bold 16" item_color = "#aaaaaa" + selected_item_font = "DejaVu Sans Bold 16" selected_item_color = "#f7931a" - item_height = 40 - item_spacing = 10 - item_padding = 20 + item_height = 36 + item_spacing = 8 + item_padding = 16 scrollbar = false } + label { - left = 25% - top = 20% - width = 50% + left = 10% + top = 18% + width = 80% + font = "DejaVu Sans Mono Bold 24" text = "a r c h i p e l a g o" color = "#f7931a" align = "center" } + label { - left = 25% - top = 28% - width = 50% + left = 10% + top = 26% + width = 80% + font = "DejaVu Sans Bold 14" text = "bitcoin node os" color = "#888888" align = "center" } + label { - left = 25% + left = 10% top = 90% - width = 50% - text = "use arrow keys to select, enter to boot" + width = 80% + font = "DejaVu Sans Bold 12" + text = "press tab to edit | use arrow keys to select | enter to boot" color = "#555555" align = "center" } diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 0c73450b..c61b806c 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -1158,7 +1158,7 @@ LOG="/var/log/archipelago-tor.log" mkdir -p "$ARCHY_TOR_DIR" # Write services.json for the backend to read -cat > "\$ARCHY_TOR_DIR/services.json" < "$ARCHY_TOR_DIR/services.json" </dev/null 2>&1; then @@ -1960,6 +1960,10 @@ fi PROFILE chmod +x /mnt/target/etc/profile.d/archipelago.sh +# Suppress default Debian MOTD (our profile.d script handles the welcome) +echo -n > /mnt/target/etc/motd +rm -f /mnt/target/etc/motd.d/* 2>/dev/null || true + # Ensure reboot/shutdown work without sudo for the archipelago user # profile.d only runs for login shells; .bashrc handles SSH interactive sessions if ! grep -q '/sbin' /mnt/target/home/archipelago/.bashrc 2>/dev/null; then @@ -2088,14 +2092,14 @@ while true; do --disable-translate \ --no-first-run \ --check-for-update-interval=31536000 \ - --disable-features=TranslateUI \ + --disable-features=TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \ --disable-session-crashed-bubble \ --disable-save-password-bubble \ --disable-suggestions-service \ --password-store=basic \ - --disable-features=TranslateUI,PasswordManagerOnboarding,AutofillServerCommunication,PasswordManagerEnabled \ --disable-component-update \ --credentials_enable_service=false \ + --disable-gpu \ --user-data-dir=/home/archipelago/.config/chromium-kiosk sleep 3 done @@ -2113,9 +2117,9 @@ ConditionPathExists=/usr/local/bin/archipelago-kiosk-launcher [Service] Type=simple -ExecStartPre=/bin/bash -c 'for i in $(seq 1 15); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0' +ExecStartPre=/bin/bash -c 'for i in $(seq 1 30); do curl -sf http://localhost/health >/dev/null 2>&1 && exit 0; sleep 2; done; exit 0' ExecStart=/usr/local/bin/archipelago-kiosk-launcher -TimeoutStartSec=60 +TimeoutStartSec=90 Restart=always RestartSec=5 @@ -2692,7 +2696,7 @@ set default=0 # Load font for graphical menu if loadfont ($root)/boot/grub/font.pf2; then - set gfxmode=1024x768,auto + set gfxmode=auto insmod gfxterm insmod png terminal_output gfxterm diff --git a/neode-ui/src/views/Server.vue b/neode-ui/src/views/Server.vue index 70f15f38..c8acab30 100644 --- a/neode-ui/src/views/Server.vue +++ b/neode-ui/src/views/Server.vue @@ -1,6 +1,12 @@