fix: container stack installers, DNS resolution, uninstall cleanup
- Replace aardvark-dns container names with host.containers.internal for all cross-app connections (LND→Bitcoin, ElectrumX→Bitcoin, Mempool→ElectrumX, Fedimint→Bitcoin, NBXplorer→Bitcoin P2P+RPC) - Add BTCPay multi-container stack installer (postgres + nbxplorer + btcpay-server) with proper secrets, data dir ownership, NOAUTH - Add Mempool multi-container stack installer (mariadb + mempool-api + mempool-frontend) with host.containers.internal for RPC - Immediately remove apps from state on uninstall (no 3-min ghost delay) - Include archy-bitcoin-ui in bitcoin uninstall container list - Fix LND UI port 8081 (was 8080, conflicting with LND gRPC) - Fix ElectrumX UI: proxy /electrs-status to backend, cache-busting headers, graceful fallback when backend returns HTML - Add Tor hidden services for ElectrumX and LND in torrc template - Remove unused detect_bitcoin_container_name() (replaced by host.containers.internal) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
b30f41f3d7
commit
07dff3e4ca
@ -6,25 +6,6 @@ use anyhow::{Context, Result};
|
|||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
pub(super) const TRUSTED_REGISTRIES: &[&str] = &["docker.io/", "ghcr.io/", "localhost/", "80.71.235.15:3000/"];
|
pub(super) const TRUSTED_REGISTRIES: &[&str] = &["docker.io/", "ghcr.io/", "localhost/", "80.71.235.15:3000/"];
|
||||||
|
|
||||||
/// Detect which Bitcoin container is running on archy-net for DNS resolution.
|
|
||||||
/// Returns the container name to use as the RPC host (e.g., "bitcoin-knots").
|
|
||||||
pub(super) fn detect_bitcoin_container_name() -> String {
|
|
||||||
// Synchronous check — called from get_app_config which is sync
|
|
||||||
let output = std::process::Command::new("podman")
|
|
||||||
.args(["ps", "--format", "{{.Names}}"])
|
|
||||||
.output();
|
|
||||||
if let Ok(out) = output {
|
|
||||||
let names = String::from_utf8_lossy(&out.stdout);
|
|
||||||
for candidate in &["bitcoin-knots", "bitcoin-core", "bitcoin"] {
|
|
||||||
if names.lines().any(|l| l.trim() == *candidate) {
|
|
||||||
return candidate.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Default to bitcoin-knots (most common)
|
|
||||||
"bitcoin-knots".to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Validate Docker image against trusted registry allowlist.
|
/// Validate Docker image against trusted registry allowlist.
|
||||||
pub(super) fn is_valid_docker_image(image: &str) -> bool {
|
pub(super) fn is_valid_docker_image(image: &str) -> bool {
|
||||||
if image.is_empty() || image.len() > 256 {
|
if image.is_empty() || image.len() > 256 {
|
||||||
@ -318,7 +299,7 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
|
|||||||
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => vec![
|
"bitcoin" | "bitcoin-core" | "bitcoin-knots" => vec![
|
||||||
"bitcoin-knots".into(), "bitcoin".into(), "bitcoin-core".into(),
|
"bitcoin-knots".into(), "bitcoin".into(), "bitcoin-core".into(),
|
||||||
"archy-bitcoin-knots".into(), "archy-bitcoin".into(),
|
"archy-bitcoin-knots".into(), "archy-bitcoin".into(),
|
||||||
"bitcoin-ui".into(),
|
"bitcoin-ui".into(), "archy-bitcoin-ui".into(),
|
||||||
],
|
],
|
||||||
// LND + UI
|
// LND + UI
|
||||||
"lnd" => vec!["lnd".into(), "archy-lnd".into(), "archy-lnd-ui".into()],
|
"lnd" => vec!["lnd".into(), "archy-lnd".into(), "archy-lnd-ui".into()],
|
||||||
@ -426,6 +407,24 @@ fn read_secret(name: &str, default: &str) -> String {
|
|||||||
.unwrap_or_else(|_| default.to_string())
|
.unwrap_or_else(|_| default.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read a secret or generate and persist a random one if it doesn't exist.
|
||||||
|
pub(super) async fn read_or_generate_secret(name: &str) -> String {
|
||||||
|
let path = format!("/var/lib/archipelago/secrets/{}", name);
|
||||||
|
if let Ok(val) = tokio::fs::read_to_string(&path).await {
|
||||||
|
let trimmed = val.trim().to_string();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Generate a 24-byte random password (hex-encoded = 48 chars)
|
||||||
|
let mut buf = [0u8; 24];
|
||||||
|
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
|
||||||
|
let secret = hex::encode(buf);
|
||||||
|
let _ = tokio::fs::create_dir_all("/var/lib/archipelago/secrets").await;
|
||||||
|
let _ = tokio::fs::write(&path, &secret).await;
|
||||||
|
secret
|
||||||
|
}
|
||||||
|
|
||||||
/// Read the node-level Nostr secret key (hex) for identity-aware apps.
|
/// Read the node-level Nostr secret key (hex) for identity-aware apps.
|
||||||
/// Returns empty string if not yet generated.
|
/// Returns empty string if not yet generated.
|
||||||
fn read_nostr_secret_hex() -> String {
|
fn read_nostr_secret_hex() -> String {
|
||||||
@ -491,9 +490,9 @@ pub(super) async fn get_app_config(
|
|||||||
"--bitcoin.node=bitcoind".to_string(),
|
"--bitcoin.node=bitcoind".to_string(),
|
||||||
format!("--bitcoind.rpcuser={}", rpc_user),
|
format!("--bitcoind.rpcuser={}", rpc_user),
|
||||||
format!("--bitcoind.rpcpass={}", rpc_pass),
|
format!("--bitcoind.rpcpass={}", rpc_pass),
|
||||||
"--bitcoind.rpchost=bitcoin-knots:8332".to_string(),
|
"--bitcoind.rpchost=host.containers.internal:8332".to_string(),
|
||||||
"--bitcoind.zmqpubrawblock=tcp://bitcoin-knots:28332".to_string(),
|
"--bitcoind.zmqpubrawblock=tcp://host.containers.internal:28332".to_string(),
|
||||||
"--bitcoind.zmqpubrawtx=tcp://bitcoin-knots:28333".to_string(),
|
"--bitcoind.zmqpubrawtx=tcp://host.containers.internal:28333".to_string(),
|
||||||
"--rpclisten=0.0.0.0:10009".to_string(),
|
"--rpclisten=0.0.0.0:10009".to_string(),
|
||||||
"--restlisten=0.0.0.0:8080".to_string(),
|
"--restlisten=0.0.0.0:8080".to_string(),
|
||||||
"--listen=0.0.0.0:9735".to_string(),
|
"--listen=0.0.0.0:9735".to_string(),
|
||||||
@ -528,7 +527,7 @@ pub(super) async fn get_app_config(
|
|||||||
vec!["/var/lib/archipelago/mempool:/data".to_string()],
|
vec!["/var/lib/archipelago/mempool:/data".to_string()],
|
||||||
vec![
|
vec![
|
||||||
"MEMPOOL_BACKEND=electrum".to_string(),
|
"MEMPOOL_BACKEND=electrum".to_string(),
|
||||||
"ELECTRUM_HOST=electrumx".to_string(),
|
"ELECTRUM_HOST=host.containers.internal".to_string(),
|
||||||
"ELECTRUM_PORT=50001".to_string(),
|
"ELECTRUM_PORT=50001".to_string(),
|
||||||
"ELECTRUM_TLS_ENABLED=false".to_string(),
|
"ELECTRUM_TLS_ENABLED=false".to_string(),
|
||||||
format!("CORE_RPC_HOST={}", host_ip),
|
format!("CORE_RPC_HOST={}", host_ip),
|
||||||
@ -545,15 +544,13 @@ pub(super) async fn get_app_config(
|
|||||||
None,
|
None,
|
||||||
),
|
),
|
||||||
"electrumx" | "mempool-electrs" | "electrs" => {
|
"electrumx" | "mempool-electrs" | "electrs" => {
|
||||||
// Detect which bitcoin container is running for archy-net DNS resolution
|
|
||||||
let bitcoin_host = detect_bitcoin_container_name();
|
|
||||||
(
|
(
|
||||||
vec!["50001:50001".to_string()],
|
vec!["50001:50001".to_string()],
|
||||||
vec!["/var/lib/archipelago/electrumx:/data".to_string()],
|
vec!["/var/lib/archipelago/electrumx:/data".to_string()],
|
||||||
vec![
|
vec![
|
||||||
format!(
|
format!(
|
||||||
"DAEMON_URL=http://{}:{}@{}:8332/",
|
"DAEMON_URL=http://{}:{}@host.containers.internal:8332/",
|
||||||
rpc_user, rpc_pass, bitcoin_host
|
rpc_user, rpc_pass
|
||||||
),
|
),
|
||||||
"COIN=Bitcoin".to_string(),
|
"COIN=Bitcoin".to_string(),
|
||||||
"DB_DIRECTORY=/data".to_string(),
|
"DB_DIRECTORY=/data".to_string(),
|
||||||
@ -757,7 +754,7 @@ pub(super) async fn get_app_config(
|
|||||||
Some(vec![
|
Some(vec![
|
||||||
"--data-dir".to_string(),
|
"--data-dir".to_string(),
|
||||||
"/data".to_string(),
|
"/data".to_string(),
|
||||||
format!("--bitcoind-url=http://{}:{}@bitcoin-knots:8332", rpc_user, rpc_pass),
|
format!("--bitcoind-url=http://{}:{}@host.containers.internal:8332", rpc_user, rpc_pass),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
"fedimint-gateway" => (
|
"fedimint-gateway" => (
|
||||||
|
|||||||
@ -68,6 +68,12 @@ impl RpcHandler {
|
|||||||
if package_id == "penpot" || package_id == "penpot-frontend" {
|
if package_id == "penpot" || package_id == "penpot-frontend" {
|
||||||
return self.install_penpot_stack().await;
|
return self.install_penpot_stack().await;
|
||||||
}
|
}
|
||||||
|
if matches!(package_id, "btcpay-server" | "btcpayserver" | "btcpay") {
|
||||||
|
return self.install_btcpay_stack().await;
|
||||||
|
}
|
||||||
|
if matches!(package_id, "mempool" | "mempool-web") {
|
||||||
|
return self.install_mempool_stack().await;
|
||||||
|
}
|
||||||
|
|
||||||
// Dependency checks
|
// Dependency checks
|
||||||
let deps = detect_running_deps().await?;
|
let deps = detect_running_deps().await?;
|
||||||
@ -692,7 +698,7 @@ bitcoin.mainnet=true\n\
|
|||||||
bitcoin.node=bitcoind\n\
|
bitcoin.node=bitcoind\n\
|
||||||
\n\
|
\n\
|
||||||
[Bitcoind]\n\
|
[Bitcoind]\n\
|
||||||
bitcoind.rpchost=bitcoin-knots:8332\n\
|
bitcoind.rpchost=host.containers.internal:8332\n\
|
||||||
bitcoind.rpcuser={user}\n\
|
bitcoind.rpcuser={user}\n\
|
||||||
bitcoind.rpcpass={pass}\n\
|
bitcoind.rpcpass={pass}\n\
|
||||||
bitcoind.rpcpolling=true\n\
|
bitcoind.rpcpolling=true\n\
|
||||||
|
|||||||
@ -374,6 +374,25 @@ impl RpcHandler {
|
|||||||
removed
|
removed
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Immediately remove from in-memory state so the UI updates without
|
||||||
|
// waiting for the scanner's absence threshold (3 scans × 60s each).
|
||||||
|
{
|
||||||
|
let (mut data, _rev) = self.state_manager.get_snapshot().await;
|
||||||
|
let before = data.package_data.len();
|
||||||
|
data.package_data.remove(package_id);
|
||||||
|
// Also remove any alias keys (e.g. "bitcoin-knots" vs "bitcoin")
|
||||||
|
let aliases: Vec<String> = data.package_data.keys()
|
||||||
|
.filter(|k| super::config::all_container_names(package_id)
|
||||||
|
.iter().any(|c| c.strip_prefix("archy-").unwrap_or(c) == k.as_str()))
|
||||||
|
.cloned().collect();
|
||||||
|
for alias in &aliases {
|
||||||
|
data.package_data.remove(alias);
|
||||||
|
}
|
||||||
|
if data.package_data.len() < before {
|
||||||
|
self.state_manager.update_data(data).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(serde_json::json!({
|
Ok(serde_json::json!({
|
||||||
"status": "uninstalled",
|
"status": "uninstalled",
|
||||||
"stopped": stopped,
|
"stopped": stopped,
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
//! Multi-container app stack installers (Immich, Penpot).
|
//! Multi-container app stack installers (Immich, Penpot, BTCPay, Mempool).
|
||||||
//!
|
//!
|
||||||
//! Each stack pulls multiple images, creates a private network, and starts
|
//! Each stack pulls multiple images, creates a private network, and starts
|
||||||
//! containers in dependency order.
|
//! containers in dependency order.
|
||||||
@ -7,6 +7,8 @@ use crate::api::rpc::RpcHandler;
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use tracing::info;
|
use tracing::info;
|
||||||
|
|
||||||
|
const REGISTRY: &str = "80.71.235.15:3000/archipelago";
|
||||||
|
|
||||||
/// Pull an image with retry and exponential backoff (3 attempts).
|
/// Pull an image with retry and exponential backoff (3 attempts).
|
||||||
async fn pull_image_with_retry(image: &str) -> Result<()> {
|
async fn pull_image_with_retry(image: &str) -> Result<()> {
|
||||||
const MAX_ATTEMPTS: u32 = 3;
|
const MAX_ATTEMPTS: u32 = 3;
|
||||||
@ -361,4 +363,318 @@ impl RpcHandler {
|
|||||||
"message": "Penpot stack installed and started"
|
"message": "Penpot stack installed and started"
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install BTCPay stack (postgres + nbxplorer + btcpay-server).
|
||||||
|
pub(super) async fn install_btcpay_stack(&self) -> Result<serde_json::Value> {
|
||||||
|
use super::install::install_log;
|
||||||
|
|
||||||
|
let check = tokio::process::Command::new("podman")
|
||||||
|
.args(["ps", "-a", "--format", "{{.Names}}"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to list containers")?;
|
||||||
|
let stdout = String::from_utf8_lossy(&check.stdout);
|
||||||
|
if stdout.lines().any(|l| l.trim() == "btcpay-server") {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"BTCPay already installed. Stop and remove it first."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency check: Bitcoin must be running
|
||||||
|
let deps = super::dependencies::detect_running_deps().await?;
|
||||||
|
super::dependencies::check_install_deps("btcpay-server", &deps)?;
|
||||||
|
|
||||||
|
install_log("INSTALL START: btcpay-server (stack: postgres + nbxplorer + btcpay)").await;
|
||||||
|
|
||||||
|
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||||
|
let host_ip = &self.config.host_ip;
|
||||||
|
|
||||||
|
// Read or generate btcpay DB password
|
||||||
|
let db_pass = super::config::read_or_generate_secret("btcpay-db-password").await;
|
||||||
|
|
||||||
|
let images = [
|
||||||
|
&format!("{}/postgres:15.17", REGISTRY),
|
||||||
|
&format!("{}/nbxplorer:2.6.0", REGISTRY),
|
||||||
|
&format!("{}/btcpayserver:1.13.7", REGISTRY),
|
||||||
|
];
|
||||||
|
for img in &images {
|
||||||
|
pull_image_with_retry(img).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create data dirs (chown to current user so rootless podman can write)
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args([
|
||||||
|
"mkdir", "-p",
|
||||||
|
"/var/lib/archipelago/postgres-btcpay",
|
||||||
|
"/var/lib/archipelago/btcpay/Main",
|
||||||
|
"/var/lib/archipelago/nbxplorer/Main",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string());
|
||||||
|
for dir in &[
|
||||||
|
"/var/lib/archipelago/postgres-btcpay",
|
||||||
|
"/var/lib/archipelago/btcpay",
|
||||||
|
"/var/lib/archipelago/nbxplorer",
|
||||||
|
] {
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["chown", "-R", &format!("{}:{}", user, user), dir])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure archy-net exists
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["network", "create", "archy-net"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 1. PostgreSQL
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "archy-btcpay-db",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "archy-btcpay-db",
|
||||||
|
"--memory=512m",
|
||||||
|
"-v", "/var/lib/archipelago/postgres-btcpay:/var/lib/postgresql/data",
|
||||||
|
"-e", "POSTGRES_DB=btcpay",
|
||||||
|
"-e", "POSTGRES_USER=btcpay",
|
||||||
|
"-e", &format!("POSTGRES_PASSWORD={}", db_pass),
|
||||||
|
&format!("{}/postgres:15.17", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(8)).await;
|
||||||
|
|
||||||
|
// Create nbxplorer database (superuser is "btcpay", not "postgres")
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"exec", "archy-btcpay-db",
|
||||||
|
"psql", "-U", "btcpay", "-c", "CREATE DATABASE nbxplorer;",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 2. NBXplorer
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "archy-nbxplorer",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "archy-nbxplorer",
|
||||||
|
"--memory=512m",
|
||||||
|
"-p", "32838:32838",
|
||||||
|
"-v", "/var/lib/archipelago/nbxplorer:/data",
|
||||||
|
"-e", "NBXPLORER_DATADIR=/data",
|
||||||
|
"-e", "NBXPLORER_NETWORK=mainnet",
|
||||||
|
"-e", "NBXPLORER_CHAINS=btc",
|
||||||
|
"-e", "NBXPLORER_BIND=0.0.0.0:32838",
|
||||||
|
"-e", "NBXPLORER_BTCRPCURL=http://host.containers.internal:8332",
|
||||||
|
"-e", &format!("NBXPLORER_BTCRPCUSER={}", rpc_user),
|
||||||
|
"-e", &format!("NBXPLORER_BTCRPCPASSWORD={}", rpc_pass),
|
||||||
|
"-e", "NBXPLORER_BTCNODEENDPOINT=host.containers.internal:8333",
|
||||||
|
"-e", "NBXPLORER_NOAUTH=1",
|
||||||
|
"-e", &format!(
|
||||||
|
"NBXPLORER_POSTGRES=User ID=btcpay;Password={};Host=archy-btcpay-db;Port=5432;Database=nbxplorer;Include Error Detail=true",
|
||||||
|
db_pass
|
||||||
|
),
|
||||||
|
&format!("{}/nbxplorer:2.6.0", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||||
|
|
||||||
|
// 3. BTCPay Server
|
||||||
|
let run = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "btcpay-server",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "btcpay-server",
|
||||||
|
"--memory=1g",
|
||||||
|
"-p", "23000:49392",
|
||||||
|
"-v", "/var/lib/archipelago/btcpay:/datadir",
|
||||||
|
"-e", "ASPNETCORE_URLS=http://0.0.0.0:49392",
|
||||||
|
"-e", "BTCPAY_PROTOCOL=http",
|
||||||
|
"-e", &format!("BTCPAY_HOST={}:23000", host_ip),
|
||||||
|
"-e", "BTCPAY_CHAINS=btc",
|
||||||
|
"-e", "BTCPAY_BTCEXPLORERURL=http://archy-nbxplorer:32838",
|
||||||
|
"-e", &format!("BTCPAY_BTCRPCURL=http://host.containers.internal:8332"),
|
||||||
|
"-e", &format!("BTCPAY_BTCRPCUSER={}", rpc_user),
|
||||||
|
"-e", &format!("BTCPAY_BTCRPCPASSWORD={}", rpc_pass),
|
||||||
|
"-e", &format!(
|
||||||
|
"BTCPAY_POSTGRES=User ID=btcpay;Password={};Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true",
|
||||||
|
db_pass
|
||||||
|
),
|
||||||
|
&format!("{}/btcpayserver:1.13.7", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to start btcpay-server")?;
|
||||||
|
|
||||||
|
if !run.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&run.stderr);
|
||||||
|
install_log(&format!("INSTALL FAIL: btcpay-server — {}", stderr)).await;
|
||||||
|
return Err(anyhow::anyhow!("Failed to start BTCPay Server: {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
install_log("INSTALL OK: btcpay-server stack").await;
|
||||||
|
info!("BTCPay stack installed and started");
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"package_id": "btcpay-server",
|
||||||
|
"message": "BTCPay stack installed and started",
|
||||||
|
"container_id": String::from_utf8_lossy(&run.stdout).trim().to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install Mempool stack (mariadb + mempool-api + mempool-web).
|
||||||
|
pub(super) async fn install_mempool_stack(&self) -> Result<serde_json::Value> {
|
||||||
|
use super::install::install_log;
|
||||||
|
|
||||||
|
let check = tokio::process::Command::new("podman")
|
||||||
|
.args(["ps", "-a", "--format", "{{.Names}}"])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to list containers")?;
|
||||||
|
let stdout = String::from_utf8_lossy(&check.stdout);
|
||||||
|
if stdout.lines().any(|l| l.trim() == "mempool" || l.trim() == "archy-mempool-web") {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Mempool already installed. Stop and remove it first."
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dependency check: Bitcoin + ElectrumX must be running
|
||||||
|
let deps = super::dependencies::detect_running_deps().await?;
|
||||||
|
super::dependencies::check_install_deps("mempool", &deps)?;
|
||||||
|
|
||||||
|
install_log("INSTALL START: mempool (stack: mariadb + mempool-api + mempool-web)").await;
|
||||||
|
|
||||||
|
let (rpc_user, rpc_pass) = crate::bitcoin_rpc::bitcoin_rpc_credentials().await;
|
||||||
|
|
||||||
|
let db_pass = super::config::read_or_generate_secret("mempool-db-password").await;
|
||||||
|
let root_pass = super::config::read_or_generate_secret("mempool-db-root-password").await;
|
||||||
|
|
||||||
|
let images = [
|
||||||
|
&format!("{}/mariadb:11.4.10", REGISTRY),
|
||||||
|
&format!("{}/mempool-backend:v3.0.0", REGISTRY),
|
||||||
|
&format!("{}/mempool-frontend:v3.0.0", REGISTRY),
|
||||||
|
];
|
||||||
|
for img in &images {
|
||||||
|
pull_image_with_retry(img).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create data dirs (chown to current user so rootless podman can write)
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args([
|
||||||
|
"mkdir", "-p",
|
||||||
|
"/var/lib/archipelago/mysql-mempool",
|
||||||
|
"/var/lib/archipelago/mempool",
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
let user = std::env::var("USER").unwrap_or_else(|_| "archipelago".to_string());
|
||||||
|
for dir in &[
|
||||||
|
"/var/lib/archipelago/mysql-mempool",
|
||||||
|
"/var/lib/archipelago/mempool",
|
||||||
|
] {
|
||||||
|
let _ = tokio::process::Command::new("sudo")
|
||||||
|
.args(["chown", "-R", &format!("{}:{}", user, user), dir])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure archy-net exists
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args(["network", "create", "archy-net"])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// 1. MariaDB
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "archy-mempool-db",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "archy-mempool-db",
|
||||||
|
"--memory=512m",
|
||||||
|
"-v", "/var/lib/archipelago/mysql-mempool:/var/lib/mysql",
|
||||||
|
"-e", "MYSQL_DATABASE=mempool",
|
||||||
|
"-e", "MYSQL_USER=mempool",
|
||||||
|
"-e", &format!("MYSQL_PASSWORD={}", db_pass),
|
||||||
|
"-e", &format!("MYSQL_ROOT_PASSWORD={}", root_pass),
|
||||||
|
&format!("{}/mariadb:11.4.10", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(10)).await;
|
||||||
|
|
||||||
|
// 2. Mempool API backend
|
||||||
|
let _ = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "mempool-api",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "mempool-api",
|
||||||
|
"--memory=512m",
|
||||||
|
"-p", "8999:8999",
|
||||||
|
"-v", "/var/lib/archipelago/mempool:/data",
|
||||||
|
"-e", "MEMPOOL_BACKEND=electrum",
|
||||||
|
"-e", "ELECTRUM_HOST=host.containers.internal",
|
||||||
|
"-e", "ELECTRUM_PORT=50001",
|
||||||
|
"-e", "ELECTRUM_TLS_ENABLED=false",
|
||||||
|
"-e", "CORE_RPC_HOST=host.containers.internal",
|
||||||
|
"-e", "CORE_RPC_PORT=8332",
|
||||||
|
"-e", &format!("CORE_RPC_USERNAME={}", rpc_user),
|
||||||
|
"-e", &format!("CORE_RPC_PASSWORD={}", rpc_pass),
|
||||||
|
"-e", "DATABASE_ENABLED=true",
|
||||||
|
"-e", "DATABASE_HOST=archy-mempool-db",
|
||||||
|
"-e", "DATABASE_DATABASE=mempool",
|
||||||
|
"-e", "DATABASE_USERNAME=mempool",
|
||||||
|
"-e", &format!("DATABASE_PASSWORD={}", db_pass),
|
||||||
|
&format!("{}/mempool-backend:v3.0.0", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await;
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(3)).await;
|
||||||
|
|
||||||
|
// 3. Mempool frontend
|
||||||
|
let run = tokio::process::Command::new("podman")
|
||||||
|
.args([
|
||||||
|
"run", "-d",
|
||||||
|
"--name", "mempool",
|
||||||
|
"--restart", "unless-stopped",
|
||||||
|
"--network", "archy-net",
|
||||||
|
"--network-alias", "mempool",
|
||||||
|
"--memory=256m",
|
||||||
|
"-p", "4080:8080",
|
||||||
|
"-e", "FRONTEND_HTTP_PORT=8080",
|
||||||
|
"-e", "BACKEND_MAINNET_HTTP_HOST=mempool-api",
|
||||||
|
&format!("{}/mempool-frontend:v3.0.0", REGISTRY),
|
||||||
|
])
|
||||||
|
.output()
|
||||||
|
.await
|
||||||
|
.context("Failed to start mempool frontend")?;
|
||||||
|
|
||||||
|
if !run.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&run.stderr);
|
||||||
|
install_log(&format!("INSTALL FAIL: mempool — {}", stderr)).await;
|
||||||
|
return Err(anyhow::anyhow!("Failed to start Mempool: {}", stderr));
|
||||||
|
}
|
||||||
|
|
||||||
|
install_log("INSTALL OK: mempool stack").await;
|
||||||
|
info!("Mempool stack installed and started");
|
||||||
|
Ok(serde_json::json!({
|
||||||
|
"success": true,
|
||||||
|
"package_id": "mempool",
|
||||||
|
"message": "Mempool stack installed and started",
|
||||||
|
"container_id": String::from_utf8_lossy(&run.stdout).trim().to_string(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -355,11 +355,15 @@
|
|||||||
|
|
||||||
async function updateStatus() {
|
async function updateStatus() {
|
||||||
try {
|
try {
|
||||||
var resp = await fetch('/electrs-status');
|
var resp = await fetch('/electrs-status', { cache: 'no-store' });
|
||||||
if (!resp.ok) {
|
if (!resp.ok) {
|
||||||
throw new Error('Backend unavailable (HTTP ' + resp.status + ')');
|
throw new Error('Backend unavailable (HTTP ' + resp.status + ')');
|
||||||
}
|
}
|
||||||
var data = await resp.json();
|
var text = await resp.text();
|
||||||
|
if (text.trim().charAt(0) !== '{') {
|
||||||
|
throw new Error('Waiting for Archipelago backend...');
|
||||||
|
}
|
||||||
|
var data = JSON.parse(text);
|
||||||
|
|
||||||
// Extract Tor onion from status response
|
// Extract Tor onion from status response
|
||||||
if (data.tor_onion && !torOnion) {
|
if (data.tor_onion && !torOnion) {
|
||||||
|
|||||||
@ -5,9 +5,17 @@ server {
|
|||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html;
|
index index.html;
|
||||||
|
|
||||||
# electrs-status is fetched via absolute /electrs-status path,
|
location /electrs-status {
|
||||||
# handled by the host nginx → backend at :5678 directly
|
proxy_pass http://127.0.0.1:5678/electrs-status;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_connect_timeout 10s;
|
||||||
|
proxy_read_timeout 10s;
|
||||||
|
add_header Cache-Control "no-store, no-cache, must-revalidate";
|
||||||
|
}
|
||||||
|
|
||||||
location / {
|
location / {
|
||||||
|
add_header Cache-Control "no-cache";
|
||||||
try_files $uri $uri/ /index.html;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8081;
|
||||||
server_name _;
|
server_name _;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
|
|||||||
@ -12,9 +12,19 @@ DataDirectory /var/lib/archipelago/tor
|
|||||||
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_archipelago/
|
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_archipelago/
|
||||||
HiddenServicePort 80 127.0.0.1:80
|
HiddenServicePort 80 127.0.0.1:80
|
||||||
|
|
||||||
# LND UI
|
# Bitcoin P2P (protocol service)
|
||||||
|
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_bitcoin/
|
||||||
|
HiddenServicePort 8333 127.0.0.1:8333
|
||||||
|
|
||||||
|
# ElectrumX (protocol service — wallet connections)
|
||||||
|
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_electrumx/
|
||||||
|
HiddenServicePort 50001 127.0.0.1:50001
|
||||||
|
|
||||||
|
# LND (protocol service — Lightning Network)
|
||||||
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_lnd/
|
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_lnd/
|
||||||
HiddenServicePort 80 127.0.0.1:8081
|
HiddenServicePort 80 127.0.0.1:8081
|
||||||
|
HiddenServicePort 9735 127.0.0.1:9735
|
||||||
|
HiddenServicePort 10009 127.0.0.1:10009
|
||||||
|
|
||||||
# BTCPay Server
|
# BTCPay Server
|
||||||
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_btcpay/
|
HiddenServiceDir /var/lib/archipelago/tor/hidden_service_btcpay/
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user