diff --git a/.gitea/workflows/build-iso.yml b/.gitea/workflows/build-iso.yml index f023f064..ba2f800f 100644 --- a/.gitea/workflows/build-iso.yml +++ b/.gitea/workflows/build-iso.yml @@ -49,6 +49,17 @@ jobs: location = "80.71.235.15:3000" insecure = true' | sudo tee /etc/containers/registries.conf.d/archipelago.conf + - name: Include AIUI if available + run: | + # Copy AIUI from the deployed system (build server has it at /opt/archipelago/web-ui/aiui/) + if [ -d "/opt/archipelago/web-ui/aiui" ] && [ -f "/opt/archipelago/web-ui/aiui/index.html" ]; then + mkdir -p web/dist/neode-ui/aiui + cp -r /opt/archipelago/web-ui/aiui/* web/dist/neode-ui/aiui/ + echo "AIUI included from /opt/archipelago/web-ui/aiui/" + else + echo "WARNING: AIUI not found on build server" + fi + - name: Build unbundled ISO run: | cd image-recipe @@ -100,6 +111,8 @@ jobs: echo " logind lid: $(sudo tar tf "$ROOTFS" ./etc/systemd/logind.conf.d/lid-ignore.conf 2>/dev/null && echo 'PRESENT' || echo 'MISSING')" echo " backend binary: $(sudo tar tf "$ROOTFS" ./usr/local/bin/archipelago 2>/dev/null && echo 'PRESENT' || echo 'MISSING')" echo " web-ui index: $(sudo tar tf "$ROOTFS" ./opt/archipelago/web-ui/index.html 2>/dev/null && echo 'PRESENT' || echo 'MISSING')" + echo " AIUI: $(sudo tar tf "$ROOTFS" ./opt/archipelago/web-ui/aiui/index.html 2>/dev/null && echo 'PRESENT' || echo 'MISSING')" + echo " claude-api-proxy: $(sudo tar tf "$ROOTFS" ./opt/archipelago/claude-api-proxy.py 2>/dev/null && echo 'PRESENT' || echo 'MISSING')" else echo " rootfs.tar not found in workspace" fi diff --git a/CLAUDE.md b/CLAUDE.md index 0b2ba055..5ccd0fa8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,7 @@ Archipelago is a **Bitcoin Node OS** — bootable, self-sovereign personal server. Flash to USB, install on hardware, manage via web UI. **Stack**: Rust backend + Vue 3 + TypeScript (strict) + Vite 7 + Tailwind + Pinia + Podman on Debian 12 -**Version**: 0.1.0 | **Target**: x86_64 and ARM64 +**Version**: 1.3.0 | **Target**: x86_64 and ARM64 --- diff --git a/core/archipelago/src/api/rpc/container.rs b/core/archipelago/src/api/rpc/container.rs index 5f75ba57..36471069 100644 --- a/core/archipelago/src/api/rpc/container.rs +++ b/core/archipelago/src/api/rpc/container.rs @@ -131,7 +131,37 @@ impl RpcHandler { } pub(super) async fn handle_container_list(&self) -> Result { - // Try to get containers from orchestrator first + // Use the scanner's cached state for consistency with WebSocket updates. + // This prevents the container-list RPC from returning different results + // than the WebSocket-delivered package_data, which caused apps to flicker + // between "installed" and "not-installed" in the UI. + let (data, _) = self.state_manager.get_snapshot().await; + if data.server_info.status_info.containers_scanned && !data.package_data.is_empty() { + let containers: Vec = data.package_data.iter().map(|(id, pkg)| { + let state = match &pkg.state { + crate::data_model::PackageState::Running => "running", + crate::data_model::PackageState::Stopped => "stopped", + crate::data_model::PackageState::Exited => "exited", + crate::data_model::PackageState::Starting => "created", + _ => "unknown", + }; + let lan = pkg.installed.as_ref() + .and_then(|i| i.interface_addresses.get("main")) + .and_then(|a| a.lan_address.as_deref()); + serde_json::json!({ + "id": id, + "name": id, + "state": state, + "image": "", + "created": "", + "ports": [], + "lan_address": lan, + }) + }).collect(); + return Ok(serde_json::json!(containers)); + } + + // Fallback: scanner hasn't run yet, query podman directly if let Some(orchestrator) = &self.orchestrator { if let Ok(containers) = orchestrator.list_containers().await { if !containers.is_empty() { @@ -140,7 +170,6 @@ impl RpcHandler { } } - // Fallback: list containers directly via podman (for bundled apps) let output = tokio::process::Command::new("podman") .args(["ps", "-a", "--format", "json"]) .output() @@ -156,11 +185,9 @@ impl RpcHandler { return Ok(serde_json::json!([])); } - // Parse podman JSON output let podman_containers: Vec = serde_json::from_str(&stdout) .unwrap_or_else(|_| Vec::new()); - // Convert to our ContainerStatus format let containers: Vec = podman_containers .iter() .map(|c| { @@ -173,42 +200,7 @@ impl RpcHandler { "paused" => "paused", _ => "unknown", }; - let name = c.get("Names").and_then(|v| v.as_array()).and_then(|a| a.first()).and_then(|v| v.as_str()).unwrap_or(""); - - // Map container name to its UI port (lan_address) - let lan_address = match name { - "bitcoin-knots" | "bitcoin-ui" => Some("http://localhost:8334"), - "lnd" | "archy-lnd-ui" => Some("http://localhost:8081"), - "tailscale" => Some("http://localhost:8240"), - "homeassistant" => Some("http://localhost:8123"), - "archy-mempool-web" | "mempool" => Some("http://localhost:4080"), - "btcpay-server" => Some("http://localhost:23000"), - "grafana" => Some("http://localhost:3000"), - "searxng" => Some("http://localhost:8888"), - "ollama" => Some("http://localhost:11434"), - "onlyoffice" => Some("http://localhost:9980"), - "penpot" => Some("http://localhost:9001"), - "nextcloud" => Some("http://localhost:8085"), - "vaultwarden" => Some("http://localhost:8082"), - "jellyfin" => Some("http://localhost:8096"), - "photoprism" => Some("http://localhost:2342"), - "immich_server" | "immich" => Some("http://localhost:2283"), - "filebrowser" => Some("http://localhost:8083"), - "nginx-proxy-manager" => Some("http://localhost:81"), - "portainer" => Some("http://localhost:9000"), - "uptime-kuma" => Some("http://localhost:3001"), - "fedimint" => Some("http://localhost:8175"), - "fedimint-gateway" => Some("http://localhost:8176"), - "nostr-rs-relay" => Some("http://localhost:18081"), - "indeedhub" => Some("http://localhost:7777"), - "dwn" => Some("http://localhost:3100"), - "endurain" => Some("http://localhost:8080"), - "electrs" | "archy-electrs-ui" => Some("http://localhost:50002"), - _ => None, - }; - - // Parse ports from podman JSON (field is "host_port" in snake_case) let ports: Vec = c.get("Ports") .and_then(|v| v.as_array()) .map(|a| { @@ -220,7 +212,6 @@ impl RpcHandler { }).collect() }) .unwrap_or_default(); - serde_json::json!({ "id": c.get("Id").and_then(|v| v.as_str()).unwrap_or(""), "name": name, @@ -228,7 +219,7 @@ impl RpcHandler { "image": c.get("Image").and_then(|v| v.as_str()).unwrap_or(""), "created": c.get("Created").and_then(|v| v.as_str()).unwrap_or(""), "ports": ports, - "lan_address": lan_address, + "lan_address": serde_json::Value::Null, }) }) .collect(); diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 042cc02e..2c177315 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -192,7 +192,9 @@ impl RpcHandler { } // DNS: ensure host.containers.internal resolves (needed for Tor proxy, inter-service calls) - run_args.push("--add-host=host.containers.internal:host-gateway"); + // Rootless podman 4.3.x doesn't support "host-gateway" — resolve to actual gateway IP + let host_gateway_flag = resolve_host_gateway().await; + run_args.push(&host_gateway_flag); // Security hardening (skip for privileged containers) let security_caps: Vec = if !is_tailscale { @@ -340,6 +342,8 @@ impl RpcHandler { } if state == "exited" { // Container crashed immediately — get logs for diagnosis + // Keep the container (don't rm) so it shows as "exited" in My Apps + // instead of vanishing completely. User can retry or remove manually. let logs = tokio::process::Command::new("podman") .args(["logs", "--tail", "20", container_name]) .output() @@ -351,11 +355,7 @@ impl RpcHandler { format!("{}{}", stdout, stderr) }) .unwrap_or_default(); - install_log(&format!("INSTALL CRASH: {} — container exited. Logs:\n{}", package_id, &log_output.chars().take(1000).collect::())).await; - let _ = tokio::process::Command::new("podman") - .args(["rm", "-f", container_name]) - .output() - .await; + install_log(&format!("INSTALL CRASH: {} — container exited (kept for visibility). Logs:\n{}", package_id, &log_output.chars().take(1000).collect::())).await; return Err(anyhow::anyhow!( "Container {} exited immediately after start. Logs: {}", container_name, @@ -936,3 +936,39 @@ autopilot.active=false\n", Ok(serde_json::json!({ "token": token })) } } + +/// Resolve the host gateway IP for --add-host flag. +/// Podman 4.3.x (Debian 12) doesn't support "host-gateway" in rootless mode, +/// so we resolve the default gateway IP from the routing table. +async fn resolve_host_gateway() -> String { + // Try `ip route` to get the default gateway + if let Ok(output) = tokio::process::Command::new("ip") + .args(["route", "show", "default"]) + .output() + .await + { + let stdout = String::from_utf8_lossy(&output.stdout); + for line in stdout.lines() { + if line.starts_with("default") { + if let Some(gw) = line.split_whitespace().nth(2) { + if !gw.is_empty() { + return format!("--add-host=host.containers.internal:{}", gw); + } + } + } + } + } + // Fallback: try hostname -I (first IP) + if let Ok(output) = tokio::process::Command::new("hostname") + .args(["-I"]) + .output() + .await + { + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(ip) = stdout.split_whitespace().next() { + return format!("--add-host=host.containers.internal:{}", ip); + } + } + // Last resort + "--add-host=host.containers.internal:10.0.2.2".to_string() +} diff --git a/core/archipelago/src/health_monitor.rs b/core/archipelago/src/health_monitor.rs index 48586a3f..b13f235d 100644 --- a/core/archipelago/src/health_monitor.rs +++ b/core/archipelago/src/health_monitor.rs @@ -362,7 +362,7 @@ fn parse_memory_string(s: &str) -> Option { /// Query all containers and their health status. async fn check_containers() -> Vec { let output = match tokio::time::timeout( - std::time::Duration::from_secs(30), + std::time::Duration::from_secs(60), tokio::process::Command::new("podman") .args(["ps", "-a", "--format", "json"]) .output(), @@ -375,7 +375,7 @@ async fn check_containers() -> Vec { return Vec::new(); } Err(_) => { - debug!("podman ps timed out (30s)"); + debug!("podman ps timed out (60s)"); return Vec::new(); } _ => return Vec::new(), diff --git a/core/archipelago/src/server.rs b/core/archipelago/src/server.rs index ab90355e..9e561416 100644 --- a/core/archipelago/src/server.rs +++ b/core/archipelago/src/server.rs @@ -11,6 +11,7 @@ use crate::state::StateManager; use anyhow::Result; use hyper::server::conn::Http; use hyper::service::service_fn; +use std::collections::HashMap; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -269,7 +270,10 @@ impl Server { // Brief delay for containers to stabilize after boot tokio::time::sleep(Duration::from_secs(3)).await; info!("🐳 Scanning containers..."); - if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref()).await { + // Tracks how many consecutive scans each container has been absent from. + // Prevents UI flapping when podman intermittently returns incomplete results. + let mut absence_tracker: HashMap = HashMap::new(); + if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref(), &mut absence_tracker).await { error!("Failed to scan containers: {}", e); } @@ -284,7 +288,7 @@ impl Server { continue; } scanning.store(true, std::sync::atomic::Ordering::Relaxed); - if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref()).await { + if let Err(e) = scan_and_update_packages(&scanner, &state, identity_clone.as_ref(), &mut absence_tracker).await { error!("Failed to update containers: {}", e); } scanning.store(false, std::sync::atomic::Ordering::Relaxed); @@ -458,15 +462,19 @@ async fn refresh_tor_address(state: &StateManager, identity: &NodeIdentity) -> R Ok(()) } +/// Number of consecutive absent scans before removing a container from state. +/// 3 scans × 30s = 90 seconds of absence before removal. +const CONTAINER_ABSENCE_THRESHOLD: u32 = 3; + async fn scan_and_update_packages( scanner: &DockerPackageScanner, state: &StateManager, identity: &NodeIdentity, + absence_tracker: &mut HashMap, ) -> Result<()> { let packages = scanner.scan_containers().await?; - + let (current_data, _) = state.get_snapshot().await; - let packages_changed = !packages.is_empty() && current_data.package_data != packages; let tor_addr = docker_packages::read_tor_address("archipelago").await; let tor_changed = tor_addr != current_data.server_info.tor_address; let first_scan = !current_data.server_info.status_info.containers_scanned; @@ -478,17 +486,58 @@ async fn scan_and_update_packages( .unwrap_or(false); let update_changed = update_available != current_data.server_info.status_info.updated; - if packages_changed || tor_changed || first_scan || update_changed { - let mut data = current_data; - if !packages.is_empty() { - data.package_data = packages; + // Empty scan result = podman failure or timeout, preserve existing state + if packages.is_empty() && !first_scan { + if tor_changed || update_changed { + let mut data = current_data; + data.server_info.tor_address = tor_addr.clone(); + data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t)); + data.server_info.status_info.updated = update_available; + state.update_data(data).await; } + return Ok(()); + } + + // Merge scan results with current state instead of full replacement. + // This prevents containers from vanishing when podman intermittently + // returns incomplete results under heavy load. + let mut merged = current_data.package_data.clone(); + let mut changed = false; + + // Update/add containers found in this scan + for (id, pkg) in &packages { + absence_tracker.remove(id); + if merged.get(id) != Some(pkg) { + merged.insert(id.clone(), pkg.clone()); + changed = true; + } + } + + // Track containers in state but missing from this scan. + // Only remove after CONTAINER_ABSENCE_THRESHOLD consecutive absent scans. + let current_ids: Vec = merged.keys().cloned().collect(); + for id in current_ids { + if !packages.contains_key(&id) { + let count = absence_tracker.entry(id.clone()).or_insert(0); + *count += 1; + if *count >= CONTAINER_ABSENCE_THRESHOLD { + debug!("Removing {} from state after {} consecutive absent scans", id, count); + merged.remove(&id); + absence_tracker.remove(&id); + changed = true; + } + } + } + + if changed || tor_changed || first_scan || update_changed { + let mut data = current_data; + data.package_data = merged; data.server_info.tor_address = tor_addr.clone(); data.server_info.node_address = tor_addr.as_ref().map(|t| identity.node_address(t)); data.server_info.status_info.containers_scanned = true; data.server_info.status_info.updated = update_available; state.update_data(data).await; - debug!("📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update", packages_changed, tor_changed, first_scan, update_changed); + debug!("📦 State changed (packages={}, tor={}, first_scan={}, update={}), broadcasting update", changed, tor_changed, first_scan, update_changed); } Ok(()) diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index 3f005b95..7ecdd66e 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -13,7 +13,7 @@ use thiserror::Error; use tokio::net::UnixStream; const API_VERSION: &str = "v4.0.0"; -const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30); +const DEFAULT_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(60); const LONG_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(120); #[derive(Debug, Error)] diff --git a/docker/lnd-ui/Dockerfile b/docker/lnd-ui/Dockerfile index 0afade2b..31a1cc29 100644 --- a/docker/lnd-ui/Dockerfile +++ b/docker/lnd-ui/Dockerfile @@ -17,6 +17,6 @@ COPY bg-intro.jpg /usr/share/nginx/html/assets/img/ # Copy nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf -EXPOSE 80 +EXPOSE 8080 CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/lnd-ui/nginx.conf b/docker/lnd-ui/nginx.conf index a755c4b0..27d382ef 100644 --- a/docker/lnd-ui/nginx.conf +++ b/docker/lnd-ui/nginx.conf @@ -1,5 +1,5 @@ server { - listen 80; + listen 8080; server_name _; root /usr/share/nginx/html; diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index 5e53ab63..0d410c1f 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -65,8 +65,8 @@ else fi echo "$BUILD_NUM" | sudo tee "$BUILD_COUNTER_FILE" > /dev/null 2>/dev/null || BUILD_NUM=1 GIT_SHORT=$(cd "$SCRIPT_DIR/.." && git rev-parse --short HEAD 2>/dev/null || echo "dev") -# Version format: 0.1.0-beta.BUILD_NUM (semver pre-release) -BUILD_VERSION="0.1.0-beta.${BUILD_NUM}" +# Version format: major.minor.patch (semver) +BUILD_VERSION="1.3.0" echo "Build #${BUILD_NUM} (${BUILD_VERSION}, commit ${GIT_SHORT})" # Architecture-dependent variables @@ -972,6 +972,30 @@ if [ "$WEBUI_CAPTURED" = "0" ]; then cd "$SCRIPT_DIR" fi +# Include AIUI web app (Claude chat interface) +AIUI_INCLUDED=0 +# Search multiple locations for a pre-built AIUI app +for AIUI_DIR in \ + "$SCRIPT_DIR/../../AIUI/packages/app/dist" \ + "$HOME/AIUI/packages/app/dist" \ + "/home/archipelago/AIUI/packages/app/dist" \ + "/opt/archipelago/web-ui/aiui" \ + "/home/archipelago/archy/AIUI/packages/app/dist"; do + if [ -d "$AIUI_DIR" ] && [ -f "$AIUI_DIR/index.html" ]; then + echo " Including AIUI from $AIUI_DIR..." + mkdir -p "$ARCH_DIR/web-ui/aiui" + cp -r "$AIUI_DIR/"* "$ARCH_DIR/web-ui/aiui/" + echo " ✅ AIUI included ($(du -sh "$ARCH_DIR/web-ui/aiui" | cut -f1))" + AIUI_INCLUDED=1 + break + fi +done +if [ "$AIUI_INCLUDED" = "0" ]; then + echo " ⚠️ AIUI not found — build it first:" + echo " cd ~/AIUI/packages/app && VITE_BASE_PATH=/aiui/ npx vite build" + echo " Searched: ~/AIUI, /home/archipelago/AIUI, /opt/archipelago/web-ui/aiui" +fi + # Copy app manifests if [ -d "$SCRIPT_DIR/../apps" ]; then echo " Including app manifests..." @@ -2560,6 +2584,9 @@ chroot /mnt/target systemctl enable archipelago-load-images.service 2>/dev/null chroot /mnt/target systemctl enable archipelago-setup-tor.service 2>/dev/null || true chroot /mnt/target systemctl enable archipelago-first-boot-containers.service 2>/dev/null || true chroot /mnt/target systemctl enable archipelago-kiosk.service 2>/dev/null || true +# Enable claude-api-proxy (create symlink manually — chroot systemctl can fail) +chroot /mnt/target systemctl enable claude-api-proxy.service 2>/dev/null || \ + ln -sf /etc/systemd/system/claude-api-proxy.service /mnt/target/etc/systemd/system/multi-user.target.wants/claude-api-proxy.service 2>/dev/null || true # Fix console-setup: setupcon needs /tmp writable, add ordering dependency mkdir -p /mnt/target/etc/systemd/system/console-setup.service.d diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 316baa26..f501d7f5 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -25,7 +25,7 @@ server { location /aiui/ { alias /opt/archipelago/web-ui/aiui/; index index.html; - try_files $uri $uri/ /aiui/index.html; + try_files $uri $uri/ =404; add_header Cache-Control "no-cache, no-store, must-revalidate"; } diff --git a/neode-ui/docker/nginx-demo.conf b/neode-ui/docker/nginx-demo.conf index 973fafc9..a80acba5 100644 --- a/neode-ui/docker/nginx-demo.conf +++ b/neode-ui/docker/nginx-demo.conf @@ -75,7 +75,7 @@ http { # Serve AIUI SPA location /aiui/ { alias /usr/share/nginx/html/aiui/; - try_files $uri $uri/ /aiui/index.html; + try_files $uri $uri/ =404; location ~* /aiui/assets/ { expires 1y; diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index ae50c29f..4bead34e 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -18,7 +18,20 @@ LOG="/var/log/archipelago-first-boot.log" # Source pinned image versions (single source of truth) -source /opt/archipelago/image-versions.sh 2>/dev/null || true +# ISO copies to scripts/ subdir; also check the direct path for manual installs +source /opt/archipelago/scripts/image-versions.sh 2>/dev/null \ + || source /opt/archipelago/image-versions.sh 2>/dev/null \ + || source /home/archipelago/archy/scripts/image-versions.sh 2>/dev/null \ + || true + +# Verify image-versions loaded — fail loudly if not +if [ -z "$ARCHY_REGISTRY" ] || [ -z "$BITCOIN_KNOTS_IMAGE" ]; then + log "FATAL: image-versions.sh not loaded — checked:" + log " /opt/archipelago/scripts/image-versions.sh" + log " /opt/archipelago/image-versions.sh" + log " /home/archipelago/archy/scripts/image-versions.sh" + log "Container creation will fail. Check ISO build." +fi # Source shared utility library SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)" @@ -38,6 +51,12 @@ DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/1000 podman" TARGET_IP=$(hostname -I 2>/dev/null | awk '{print $1}') [ -z "$TARGET_IP" ] && TARGET_IP="127.0.0.1" +# Resolve host-gateway for --add-host (podman 4.3.x doesn't support "host-gateway") +# Use the default gateway IP from the podman network, falling back to host LAN IP +HOST_GATEWAY=$(ip route show default 2>/dev/null | awk '/default/ {print $3}' | head -1) +[ -z "$HOST_GATEWAY" ] && HOST_GATEWAY="$TARGET_IP" +ADD_HOST_FLAG="--add-host=host.containers.internal:${HOST_GATEWAY}" + log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a "$LOG"; } # Ensure Tor is running for hidden services (LND connect, Electrumx, etc.) @@ -368,7 +387,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch if $DOCKER run -d --name bitcoin-knots --restart unless-stopped \ --health-cmd="bitcoin-cli -rpcuser=\$BITCOIN_RPC_USER -rpcpassword=\$BITCOIN_RPC_PASS getblockchaininfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --memory=$(mem_limit bitcoin-knots) --network archy-net \ - --add-host host.containers.internal:host-gateway \ + $ADD_HOST_FLAG \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 8332:8332 -p 8333:8333 -p 28332:28332 -p 28333:28333 \ @@ -608,7 +627,7 @@ LNDCONF $DOCKER run -d --name lnd --restart unless-stopped \ --health-cmd="curl -sf --insecure https://localhost:8080/v1/getinfo || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --memory=$(mem_limit lnd) --network archy-net \ - --add-host host.containers.internal:host-gateway \ + $ADD_HOST_FLAG \ --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_RAW \ --security-opt no-new-privileges:true \ -p 9735:9735 -p 10009:10009 -p 8080:8080 \ @@ -990,9 +1009,9 @@ for ui in bitcoin-ui lnd-ui; do case $ui in # UI containers use --network host so they can proxy to localhost services # (Bitcoin RPC at 127.0.0.1:8332, backend at 127.0.0.1:5678) - # Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=80 (mapped via nginx to 8081) + # Internal nginx ports: bitcoin-ui=8334, electrs-ui=50002, lnd-ui=8080 (host 8081) bitcoin-ui) PORT_ARG=""; NET_ARG="--network host" ;; - lnd-ui) PORT_ARG="-p 8081:80"; NET_ARG="" ;; # exception: port 80 conflicts with host nginx on host network + lnd-ui) PORT_ARG="-p 8081:8080"; NET_ARG="" ;; # nginx inside listens on 8080 (no NET_BIND_SERVICE needed) electrs-ui) PORT_ARG=""; NET_ARG="--network host" ;; esac CONTAINER_NAME="archy-$ui"