fix: container orchestration stability, AIUI inclusion, lnd-ui port, version 1.3.0
Container stability: - Merge scan results instead of full replacement (prevents UI flapping) - Absence threshold: 3 consecutive missed scans before removing from state - container-list RPC uses cached scanner state for consistency - Increased Podman API timeout 30s → 60s (scanner + health monitor) - Keep crashed containers visible as "exited" instead of podman rm -f - Resolve host-gateway IP via ip route (podman 4.3.x compatibility) ISO build fixes: - AIUI web app inclusion: searches 5 paths + CI step to copy from build server - Claude API proxy: systemctl enable with symlink fallback - AIUI nginx: try_files =404 (was /aiui/index.html redirect loop) - Build version set to 1.3.0 Container fixes: - lnd-ui: nginx listens on 8080 (was 80, Permission denied in rootless) - first-boot: image-versions.sh sourced from correct path with validation - first-boot: host-gateway resolved to actual gateway IP Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
134de9fe3f
commit
c62d7f77b5
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -131,7 +131,37 @@ impl RpcHandler {
|
||||
}
|
||||
|
||||
pub(super) async fn handle_container_list(&self) -> Result<serde_json::Value> {
|
||||
// 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<serde_json::Value> = 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::Value> = serde_json::from_str(&stdout)
|
||||
.unwrap_or_else(|_| Vec::new());
|
||||
|
||||
// Convert to our ContainerStatus format
|
||||
let containers: Vec<serde_json::Value> = 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<String> = 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();
|
||||
|
||||
@ -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<String> = 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::<String>())).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::<String>())).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()
|
||||
}
|
||||
|
||||
@ -362,7 +362,7 @@ fn parse_memory_string(s: &str) -> Option<u64> {
|
||||
/// Query all containers and their health status.
|
||||
async fn check_containers() -> Vec<ContainerHealth> {
|
||||
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<ContainerHealth> {
|
||||
return Vec::new();
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("podman ps timed out (30s)");
|
||||
debug!("podman ps timed out (60s)");
|
||||
return Vec::new();
|
||||
}
|
||||
_ => return Vec::new(),
|
||||
|
||||
@ -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<String, u32> = 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<String, u32>,
|
||||
) -> 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 {
|
||||
// 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;
|
||||
if !packages.is_empty() {
|
||||
data.package_data = packages;
|
||||
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<String> = 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(())
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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;"]
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen 8080;
|
||||
server_name _;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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";
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user