From 07808a95c4c4d8c12b243e3b9946cf0a822c49ee Mon Sep 17 00:00:00 2001 From: Dorian Date: Thu, 2 Apr 2026 10:34:58 +0100 Subject: [PATCH] fix: first-boot container creation, remote input relay, ISO packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical first-boot fixes (root cause: ALL 25 containers failed on install): - Fix image-versions.sh sourcing: multi-path fallback for /opt/archipelago/scripts/ - Fix --add-host host-gateway: resolve actual gateway IP (podman 4.3 compat) - Fix disk size detection: check /var/lib/archipelago not / (was forcing prune on 428GB disk) - Fix Bitcoin health check: expand $RPC vars at creation, not inside container - Add --network-alias to all containers (aardvark-dns reliability) - Add --network-alias to backend RPC install handler ISO build: - Add apache2-utils for htpasswd (Fedimint gateway password hashing) Remote input: - Add broadcast relay channel for companion app → browser input forwarding - Add /ws/remote-relay WebSocket endpoint - Android: NES controller improvements, server connect flow updates Container images: - Fix lnd-ui Dockerfile: listen on 8080, run as root user (rootless compat) - Fix bitcoin-ui, electrs-ui Dockerfiles: root user for rootless podman Co-Authored-By: Claude Opus 4.6 (1M context) --- .../archipelago/app/ui/components/NESMenu.kt | 8 +- .../ui/components/NESPortraitController.kt | 11 +- .../app/ui/screens/RemoteInputScreen.kt | 8 +- .../app/ui/screens/ServerConnectScreen.kt | 122 ++++++++++++------ core/Cargo.lock | 2 +- core/archipelago/src/api/handler/mod.rs | 16 ++- .../src/api/handler/remote_input.rs | 89 +++++++++++++ .../src/api/rpc/package/install.rs | 3 + docker/bitcoin-ui/Dockerfile | 6 + docker/electrs-ui/Dockerfile | 6 + docker/lnd-ui/Dockerfile | 7 +- image-recipe/build-auto-installer-iso.sh | 1 + scripts/first-boot-containers.sh | 30 +++-- 13 files changed, 238 insertions(+), 71 deletions(-) diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt index ab324052..d30805fd 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESMenu.kt @@ -128,16 +128,16 @@ private fun MenuPanel( OutlinedTextField( value = addr, onValueChange = { addr = it.trim() }, placeholder = { Text("192.168.1.100", color = NES.MenuMuted, fontSize = 11.sp) }, - modifier = Modifier.fillMaxWidth().height(40.dp), singleLine = true, + modifier = Modifier.fillMaxWidth().height(48.dp), singleLine = true, textStyle = androidx.compose.ui.text.TextStyle(color = NES.MenuText, fontSize = 12.sp), colors = nesFieldColors(), shape = RoundedCornerShape(2.dp), ) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { OutlinedTextField( value = pwd, onValueChange = { pwd = it }, placeholder = { Text("PASSWORD", color = NES.MenuMuted, fontSize = 11.sp) }, - modifier = Modifier.weight(1f).height(40.dp), singleLine = true, + modifier = Modifier.weight(1f).height(48.dp), singleLine = true, visualTransformation = PasswordVisualTransformation(), keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = ImeAction.Go), keyboardActions = KeyboardActions(onGo = { @@ -148,7 +148,7 @@ private fun MenuPanel( shape = RoundedCornerShape(2.dp), ) Box( - Modifier.size(40.dp).clip(RoundedCornerShape(2.dp)).background(NES.MenuSelected) + Modifier.size(48.dp).clip(RoundedCornerShape(2.dp)).background(NES.MenuSelected) .clickable { if (addr.isNotBlank()) { onAddServer(ServerEntry(addr, false, password = pwd)); addr = ""; pwd = ""; showAdd = false } }, diff --git a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt index 4249ad29..476b9be8 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/components/NESPortraitController.kt @@ -37,6 +37,9 @@ import com.archipelago.app.ui.theme.NES fun NESPortraitController( style: ControllerStyle = ControllerStyle.CLASSIC, onKey: (String) -> Unit, + onMouseMove: (Int, Int) -> Unit = { _, _ -> }, + onMouseClick: (Int) -> Unit = { _ -> }, + onMouseScroll: (Int) -> Unit = { _ -> }, onMenu: () -> Unit, ) { val c = paletteFor(style) @@ -80,11 +83,9 @@ fun NESPortraitController( ) { // Trackpad area (touch surface for mouse) Trackpad( - onMove = { _, _ -> }, // Not used in gamepad, but keeps the visual - onClick = { onKey("Return") }, - onScroll = { dy -> - if (dy > 0) onKey("Down") else onKey("Up") - }, + onMove = { dx, dy -> onMouseMove(dx, dy) }, + onClick = { onMouseClick(it) }, + onScroll = { dy -> onMouseScroll(dy) }, onTwoFingerHold = onMenu, modifier = Modifier .fillMaxWidth() diff --git a/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt index 7fdee9cb..102f14a0 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/screens/RemoteInputScreen.kt @@ -64,11 +64,6 @@ fun RemoteInputScreen(onBack: () -> Unit) { BackHandler { onBack() } DisposableEffect(Unit) { onDispose { ws.disconnect() } } LaunchedEffect(activeServer) { activeServer?.let { ws.connect(it.toUrl(), it.password) } } - LaunchedEffect(connectionState) { - if (connectionState == ConnectionState.ERROR) { - kotlinx.coroutines.delay(3000); activeServer?.let { ws.connect(it.toUrl(), it.password) } - } - } Box( Modifier @@ -85,6 +80,9 @@ fun RemoteInputScreen(onBack: () -> Unit) { isGamepadMode && !isLandscape -> NESPortraitController( style = controllerStyle, onKey = { ws.sendKey(it) }, + onMouseMove = { dx, dy -> ws.sendMouseMove(dx, dy) }, + onMouseClick = { ws.sendClick(it) }, + onMouseScroll = { ws.sendScroll(it) }, onMenu = { showModal = true }, ) else -> { diff --git a/Android/app/src/main/java/com/archipelago/app/ui/screens/ServerConnectScreen.kt b/Android/app/src/main/java/com/archipelago/app/ui/screens/ServerConnectScreen.kt index d7c02ca6..0712f825 100644 --- a/Android/app/src/main/java/com/archipelago/app/ui/screens/ServerConnectScreen.kt +++ b/Android/app/src/main/java/com/archipelago/app/ui/screens/ServerConnectScreen.kt @@ -25,6 +25,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close @@ -59,7 +61,10 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.archipelago.app.R @@ -94,6 +99,8 @@ fun ServerConnectScreen( var address by remember { mutableStateOf("") } var port by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var passwordVisible by remember { mutableStateOf(false) } var useHttps by remember { mutableStateOf(false) } var isConnecting by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } @@ -195,13 +202,7 @@ fun ServerConnectScreen( singleLine = true, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Uri, - imeAction = ImeAction.Go, - ), - keyboardActions = KeyboardActions( - onGo = { - keyboard?.hide() - connect(ServerEntry(address, useHttps, port)) - }, + imeAction = ImeAction.Next, ), colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = Color.White.copy(alpha = 0.3f), @@ -217,37 +218,78 @@ fun ServerConnectScreen( Spacer(modifier = Modifier.height(12.dp)) - OutlinedTextField( - value = port, - onValueChange = { - port = it.filter { c -> c.isDigit() }.take(5) - errorMessage = null - }, - label = { Text(stringResource(R.string.port_label)) }, - placeholder = { Text("80") }, - modifier = Modifier.width(140.dp), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Number, - imeAction = ImeAction.Go, - ), - keyboardActions = KeyboardActions( - onGo = { - keyboard?.hide() - connect(ServerEntry(address, useHttps, port)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlinedTextField( + value = port, + onValueChange = { + port = it.filter { c -> c.isDigit() }.take(5) + errorMessage = null }, - ), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = Color.White.copy(alpha = 0.3f), - unfocusedBorderColor = Color.White.copy(alpha = 0.12f), - cursorColor = Color.White, - focusedLabelColor = Color.White.copy(alpha = 0.7f), - unfocusedLabelColor = TextMuted, - focusedTextColor = TextPrimary, - unfocusedTextColor = TextPrimary, - ), - shape = RoundedCornerShape(12.dp), - ) + label = { Text(stringResource(R.string.port_label)) }, + placeholder = { Text("80") }, + modifier = Modifier.weight(1f), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Next, + ), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.White.copy(alpha = 0.3f), + unfocusedBorderColor = Color.White.copy(alpha = 0.12f), + cursorColor = Color.White, + focusedLabelColor = Color.White.copy(alpha = 0.7f), + unfocusedLabelColor = TextMuted, + focusedTextColor = TextPrimary, + unfocusedTextColor = TextPrimary, + ), + shape = RoundedCornerShape(12.dp), + ) + + OutlinedTextField( + value = password, + onValueChange = { + password = it + errorMessage = null + }, + label = { Text("Password") }, + modifier = Modifier.weight(2f), + singleLine = true, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) Icons.Default.VisibilityOff else Icons.Default.Visibility, + contentDescription = if (passwordVisible) "Hide password" else "Show password", + tint = TextMuted, + modifier = Modifier.size(20.dp), + ) + } + }, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Password, + imeAction = ImeAction.Go, + ), + keyboardActions = KeyboardActions( + onGo = { + keyboard?.hide() + connect(ServerEntry(address, useHttps, port, password)) + }, + ), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.White.copy(alpha = 0.3f), + unfocusedBorderColor = Color.White.copy(alpha = 0.12f), + cursorColor = Color.White, + focusedLabelColor = Color.White.copy(alpha = 0.7f), + unfocusedLabelColor = TextMuted, + focusedTextColor = TextPrimary, + unfocusedTextColor = TextPrimary, + ), + shape = RoundedCornerShape(12.dp), + ) + } Spacer(modifier = Modifier.height(16.dp)) @@ -303,7 +345,7 @@ fun ServerConnectScreen( text = if (isConnecting) stringResource(R.string.connecting) else stringResource(R.string.connect), onClick = { keyboard?.hide() - connect(ServerEntry(address, useHttps, port)) + connect(ServerEntry(address, useHttps, port, password)) }, modifier = Modifier.fillMaxWidth().height(56.dp), ) @@ -363,7 +405,7 @@ private fun SavedServerItem( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { - Row(verticalAlignment = Alignment.CenterVertically) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) { Icon( imageVector = if (server.useHttps) Icons.Default.Lock else Icons.Default.LockOpen, contentDescription = null, @@ -372,7 +414,7 @@ private fun SavedServerItem( ) Spacer(modifier = Modifier.width(12.dp)) Column { - Text(text = server.address, style = MaterialTheme.typography.bodyMedium, color = TextPrimary) + Text(text = server.address, style = MaterialTheme.typography.bodyMedium, color = TextPrimary, maxLines = 1, overflow = TextOverflow.Ellipsis) if (server.port.isNotBlank()) { Text(text = "Port ${server.port}", style = MaterialTheme.typography.labelMedium, color = TextMuted) } diff --git a/core/Cargo.lock b/core/Cargo.lock index 574292da..46281f5d 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.2.0-alpha" +version = "1.3.1" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/src/api/handler/mod.rs b/core/archipelago/src/api/handler/mod.rs index 2e380895..1d670f47 100644 --- a/core/archipelago/src/api/handler/mod.rs +++ b/core/archipelago/src/api/handler/mod.rs @@ -13,6 +13,7 @@ use crate::state::StateManager; use anyhow::Result; use hyper::{Method, Request, Response, StatusCode}; use std::sync::Arc; +use tokio::sync::broadcast; use tracing::debug; /// Build an HTTP response without unwrap. Falls back to a plain 500 if builder fails. @@ -32,6 +33,8 @@ pub struct ApiHandler { state_manager: Arc, metrics_store: Arc, session_store: SessionStore, + /// Broadcast channel for relaying companion app input to remote browsers. + input_relay_tx: broadcast::Sender, } impl ApiHandler { @@ -50,6 +53,7 @@ impl ApiHandler { ) .await?, ); + let (input_relay_tx, _) = broadcast::channel(64); Ok(Self { config, @@ -57,6 +61,7 @@ impl ApiHandler { state_manager, metrics_store, session_store, + input_relay_tx, }) } @@ -147,7 +152,16 @@ impl ApiHandler { tracing::warn!("401 WebSocket /ws/remote-input — session invalid or missing"); return Ok(Self::unauthorized()); } - return Self::handle_remote_input(req).await; + return Self::handle_remote_input(req, self.input_relay_tx.clone()).await; + } + + // Remote relay WebSocket — browser receives companion input events + if method == Method::GET && path == "/ws/remote-relay" { + if !self.is_authenticated(req.headers()).await { + tracing::warn!("401 WebSocket /ws/remote-relay — session invalid or missing"); + return Ok(Self::unauthorized()); + } + return Self::handle_remote_relay(req, self.input_relay_tx.subscribe()).await; } // Convert body to bytes for non-WS routes diff --git a/core/archipelago/src/api/handler/remote_input.rs b/core/archipelago/src/api/handler/remote_input.rs index c9e8187d..9ce50ac5 100644 --- a/core/archipelago/src/api/handler/remote_input.rs +++ b/core/archipelago/src/api/handler/remote_input.rs @@ -5,6 +5,7 @@ use hyper_ws_listener::WsStream; use serde::Deserialize; use std::time::Instant; use tokio::process::Command; +use tokio::sync::broadcast; use tokio_tungstenite::tungstenite::Message; use tracing::{debug, info, warn}; @@ -121,6 +122,7 @@ async fn handle_input(msg: &str) -> Result> { impl ApiHandler { pub(super) async fn handle_remote_input( req: Request, + relay_tx: broadcast::Sender, ) -> Result> { let (response, ws_fut_opt) = hyper_ws_listener::create_ws(req) .map_err(|e| anyhow::anyhow!("WebSocket upgrade failed: {}", e))?; @@ -183,6 +185,9 @@ impl ApiHandler { continue; // silently drop } + // Relay to connected browsers (best-effort, ignore if no receivers) + let _ = relay_tx.send(text.clone()); + match handle_input(&text).await { Ok(Some(reply)) => { let _ = tx.send(Message::Text(reply)).await; @@ -220,4 +225,88 @@ impl ApiHandler { Ok(response) } + + /// Browser relay — receives input events from the broadcast channel and forwards to the browser. + pub(super) async fn handle_remote_relay( + req: Request, + mut relay_rx: broadcast::Receiver, + ) -> Result> { + let (response, ws_fut_opt) = hyper_ws_listener::create_ws(req) + .map_err(|e| anyhow::anyhow!("WebSocket upgrade failed: {}", e))?; + + if let Some(ws_fut) = ws_fut_opt { + tokio::spawn(async move { + let ws_stream: WsStream = match ws_fut.await { + Ok(Ok(s)) => s, + Ok(Err(e)) => { + debug!("Remote relay WS handshake failed: {}", e); + return; + } + Err(e) => { + debug!("Remote relay WS task join failed: {}", e); + return; + } + }; + + info!("Remote relay browser connected"); + let (mut tx, mut rx) = ws_stream.split(); + + let _ = tx.send(Message::Text(r#"{"t":"ok"}"#.to_string())).await; + + let ping_interval = tokio::time::interval(tokio::time::Duration::from_secs(30)); + tokio::pin!(ping_interval); + let mut last_activity = Instant::now(); + const INACTIVITY_TIMEOUT: u64 = 300; + + loop { + tokio::select! { + _ = ping_interval.tick() => { + if last_activity.elapsed().as_secs() >= INACTIVITY_TIMEOUT { + info!("Remote relay inactive, closing"); + let _ = tx.send(Message::Close(None)).await; + break; + } + if tx.send(Message::Ping(vec![])).await.is_err() { + break; + } + } + input = relay_rx.recv() => { + match input { + Ok(text) => { + if tx.send(Message::Text(text)).await.is_err() { + break; + } + } + Err(broadcast::error::RecvError::Lagged(n)) => { + debug!("Remote relay lagged, skipped {} messages", n); + } + Err(broadcast::error::RecvError::Closed) => break, + } + } + msg = rx.next() => { + match msg { + Some(Ok(Message::Pong(_))) | Some(Ok(Message::Text(_))) => { + last_activity = Instant::now(); + } + Some(Ok(Message::Ping(data))) => { + last_activity = Instant::now(); + let _ = tx.send(Message::Pong(data)).await; + } + Some(Ok(Message::Close(_))) | None => break, + Some(Ok(_)) => { last_activity = Instant::now(); } + Some(Err(e)) => { + debug!("Remote relay stream error: {}", e); + break; + } + } + } + } + } + + info!("Remote relay browser disconnected"); + }); + } + + Ok(response) + } } diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 2c177315..1b4781e0 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -149,6 +149,8 @@ impl RpcHandler { ]; let is_tailscale = package_id == "tailscale"; + // Explicit DNS alias for aardvark-dns (must outlive run_args) + let network_alias_flag = format!("--network-alias={}", container_name); // Network mode if is_tailscale { @@ -182,6 +184,7 @@ impl RpcHandler { .await; if net_check.map(|s| s.success()).unwrap_or(false) { run_args.push("--network=archy-net"); + run_args.push(&network_alias_flag); } else { tracing::error!( "archy-net network does not exist — {} will use default network. \ diff --git a/docker/bitcoin-ui/Dockerfile b/docker/bitcoin-ui/Dockerfile index 0caa225e..71bb91c7 100644 --- a/docker/bitcoin-ui/Dockerfile +++ b/docker/bitcoin-ui/Dockerfile @@ -2,5 +2,11 @@ FROM 80.71.235.15:3000/archipelago/nginx:1.27.4-alpine COPY index.html /usr/share/nginx/html/ COPY 50x.html /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf +# Run nginx as root to avoid chown failures in rootless Podman user namespaces +RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \ + mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp EXPOSE 8334 +ENTRYPOINT [] CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/electrs-ui/Dockerfile b/docker/electrs-ui/Dockerfile index 464fb7a6..b3fad404 100644 --- a/docker/electrs-ui/Dockerfile +++ b/docker/electrs-ui/Dockerfile @@ -3,5 +3,11 @@ COPY index.html /usr/share/nginx/html/ COPY 50x.html /usr/share/nginx/html/ COPY qrcode.js /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf +# Run nginx as root to avoid chown failures in rootless Podman user namespaces +RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \ + mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp EXPOSE 50002 +ENTRYPOINT [] CMD ["nginx", "-g", "daemon off;"] diff --git a/docker/lnd-ui/Dockerfile b/docker/lnd-ui/Dockerfile index 31a1cc29..babc6f48 100644 --- a/docker/lnd-ui/Dockerfile +++ b/docker/lnd-ui/Dockerfile @@ -17,6 +17,11 @@ COPY bg-intro.jpg /usr/share/nginx/html/assets/img/ # Copy nginx config COPY nginx.conf /etc/nginx/conf.d/default.conf +# Run nginx as root to avoid chown failures in rootless Podman user namespaces +RUN sed -i 's/^user nginx;/user root;/' /etc/nginx/nginx.conf && \ + mkdir -p /var/cache/nginx/client_temp /var/cache/nginx/proxy_temp \ + /var/cache/nginx/fastcgi_temp /var/cache/nginx/uwsgi_temp \ + /var/cache/nginx/scgi_temp EXPOSE 8080 - +ENTRYPOINT [] CMD ["nginx", "-g", "daemon off;"] diff --git a/image-recipe/build-auto-installer-iso.sh b/image-recipe/build-auto-installer-iso.sh index a2227a2a..9e845c0a 100755 --- a/image-recipe/build-auto-installer-iso.sh +++ b/image-recipe/build-auto-installer-iso.sh @@ -301,6 +301,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ plymouth-themes \ zstd \ python3 \ + apache2-utils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index 4bead34e..e21d09ca 100644 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -374,7 +374,9 @@ log "=== Tier 1: Databases & Core Infrastructure ===" if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|archy-bitcoin-knots'; then log "Creating Bitcoin Knots..." mkdir -p /var/lib/archipelago/bitcoin - DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9') + # Check the DATA partition size, not root — Bitcoin data goes to /var/lib/archipelago + DISK_GB=$(df --output=size -BG /var/lib/archipelago 2>/dev/null | tail -1 | tr -dc '0-9') + [ -z "$DISK_GB" ] && DISK_GB=$(df --output=size -BG / 2>/dev/null | tail -1 | tr -dc '0-9') if [ "${DISK_GB:-0}" -lt 1000 ]; then BTC_EXTRA_ARGS="-prune=550" BTC_DBCACHE=512 @@ -385,8 +387,8 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'bitcoin-knots|arch log " Large disk (${DISK_GB}GB) — enabling txindex" fi 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 \ + --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 --network-alias bitcoin-knots \ $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 \ @@ -433,7 +435,7 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-d mkdir -p /var/lib/archipelago/mysql-mempool $DOCKER run -d --name archy-mempool-db --restart unless-stopped \ --health-cmd="mariadb -uroot -e 'SELECT 1' || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit archy-mempool-db) --network archy-net \ + --memory=$(mem_limit archy-mempool-db) --network archy-net --network-alias archy-mempool-db \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -v /var/lib/archipelago/mysql-mempool:/var/lib/mysql \ @@ -455,7 +457,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q electrumx; then mkdir -p /var/lib/archipelago/electrumx $DOCKER run -d --name electrumx --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8000/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit electrumx) --network archy-net \ + --memory=$(mem_limit electrumx) --network archy-net --network-alias electrumx \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 50001:50001 -v /var/lib/archipelago/electrumx:/data \ @@ -472,7 +474,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q mempool-api; then mkdir -p /var/lib/archipelago/mempool $DOCKER run -d --name mempool-api --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8999/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit mempool-api) --network archy-net \ + --memory=$(mem_limit mempool-api) --network archy-net --network-alias mempool-api \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 8999:8999 -v /var/lib/archipelago/mempool:/data \ @@ -489,7 +491,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-mempool-web| log "Creating mempool frontend..." $DOCKER run -d --name archy-mempool-web --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8080/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit archy-mempool-web) --network archy-net \ + --memory=$(mem_limit archy-mempool-web) --network archy-net --network-alias archy-mempool-web \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 4080:8080 -e FRONTEND_HTTP_PORT=8080 -e BACKEND_MAINNET_HTTP_HOST=mempool-api \ @@ -530,7 +532,7 @@ if ! $DOCKER ps -a --format '{{.Names}}' 2>/dev/null | grep -qE 'archy-btcpay-db mkdir -p /var/lib/archipelago/postgres-btcpay $DOCKER run -d --name archy-btcpay-db --restart unless-stopped \ --health-cmd="pg_isready -U postgres || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit archy-btcpay-db) --network archy-net \ + --memory=$(mem_limit archy-btcpay-db) --network archy-net --network-alias archy-btcpay-db \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -v /var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data \ @@ -553,7 +555,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q archy-nbxplorer; the mkdir -p /var/lib/archipelago/nbxplorer $DOCKER run -d --name archy-nbxplorer --restart unless-stopped \ --health-cmd="curl -sf http://localhost:32838/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit archy-nbxplorer) --network archy-net \ + --memory=$(mem_limit archy-nbxplorer) --network archy-net --network-alias archy-nbxplorer \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID \ --security-opt no-new-privileges:true \ -p 32838:32838 -v /var/lib/archipelago/nbxplorer:/data \ @@ -571,7 +573,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q btcpay-server; then mkdir -p /var/lib/archipelago/btcpay $DOCKER run -d --name btcpay-server --restart unless-stopped \ --health-cmd="curl -sf http://localhost:49392/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit btcpay-server) --network archy-net \ + --memory=$(mem_limit btcpay-server) --network archy-net --network-alias btcpay-server \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE \ --security-opt no-new-privileges:true \ -p 23000:49392 -v /var/lib/archipelago/btcpay:/datadir \ @@ -626,7 +628,7 @@ LNDCONF fi $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 \ + --memory=$(mem_limit lnd) --network archy-net --network-alias lnd \ $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 \ @@ -642,7 +644,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint; then mkdir -p /var/lib/archipelago/fedimint $DOCKER run -d --name fedimint --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8174/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit fedimint) --network archy-net \ + --memory=$(mem_limit fedimint) --network archy-net --network-alias fedimint \ --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 8173:8173 -p 8174:8174 -p 8175:8175 \ @@ -667,7 +669,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th log " LND detected — using lnd mode" $DOCKER run -d --name fedimint-gateway --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit fedimint-gateway) --network archy-net \ + --memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \ --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 8176:8176 \ @@ -684,7 +686,7 @@ if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q fedimint-gateway; th log " No LND found — using ldk (built-in Lightning)" $DOCKER run -d --name fedimint-gateway --restart unless-stopped \ --health-cmd="curl -sf http://localhost:8175/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ - --memory=$(mem_limit fedimint-gateway) --network archy-net \ + --memory=$(mem_limit fedimint-gateway) --network archy-net --network-alias fedimint-gateway \ --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 8176:8176 -p 9737:9737 \