fix: first-boot container creation, remote input relay, ISO packages
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) <noreply@anthropic.com>
This commit is contained in:
@@ -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 }
|
||||
},
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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 -> {
|
||||
|
||||
@@ -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<String?>(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)
|
||||
}
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.2.0-alpha"
|
||||
version = "1.3.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@@ -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<StateManager>,
|
||||
metrics_store: Arc<MetricsStore>,
|
||||
session_store: SessionStore,
|
||||
/// Broadcast channel for relaying companion app input to remote browsers.
|
||||
input_relay_tx: broadcast::Sender<String>,
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -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<Option<String>> {
|
||||
impl ApiHandler {
|
||||
pub(super) async fn handle_remote_input(
|
||||
req: Request<hyper::Body>,
|
||||
relay_tx: broadcast::Sender<String>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
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<hyper::Body>,
|
||||
mut relay_rx: broadcast::Receiver<String>,
|
||||
) -> Result<Response<hyper::Body>> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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. \
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
@@ -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/*
|
||||
|
||||
|
||||
@@ -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 \
|
||||
|
||||
Reference in New Issue
Block a user