use super::RpcHandler; use crate::port_allocator::PortAllocator; use anyhow::{Context, Result}; use tracing::{debug, info}; impl RpcHandler { /// Install a package from a Docker image /// Security: Image verification, resource limits, network isolation pub(super) async fn handle_package_install( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let package_id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing package id"))?; let docker_image = params .get("dockerImage") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing dockerImage"))?; debug!("Installing package {} from image {}", package_id, docker_image); // Security: Validate image name format (prevent injection) if !is_valid_docker_image(docker_image) { return Err(anyhow::anyhow!("Invalid Docker image format")); } // Virtual app: Indeehub (no container, opens external URL) if package_id == "indeedhub" { return Ok(serde_json::json!({ "success": true })); } // Multi-container apps: create full stack if package_id == "immich" { return self.install_immich_stack().await; } if package_id == "penpot" || package_id == "penpot-frontend" { return self.install_penpot_stack().await; } // Check if container already exists let check_output = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name=^{}$", package_id)]) .output() .await .context("Failed to check existing containers")?; if !String::from_utf8_lossy(&check_output.stdout).trim().is_empty() { return Err(anyhow::anyhow!("Container {} already exists. Stop and remove it first.", package_id)); } // Pull the image (skip for local images - must be built locally first) let is_local_image = docker_image.starts_with("localhost/"); if !is_local_image { debug!("Pulling image: {}", docker_image); let pull_output = tokio::process::Command::new("sudo") .args(["podman", "pull", docker_image]) .output() .await .context("Failed to pull image")?; if !pull_output.status.success() { let stderr = String::from_utf8_lossy(&pull_output.stderr); return Err(anyhow::anyhow!("Failed to pull image: {}", stderr)); } } else { // Verify local image exists let images_output = tokio::process::Command::new("sudo") .args(["podman", "images", "-q", docker_image]) .output() .await .context("Failed to check local image")?; if String::from_utf8_lossy(&images_output.stdout).trim().is_empty() { return Err(anyhow::anyhow!( "Local image {} not found. Run ./deploy-to-archipelago.sh from the Indeehub Prototype project on your Mac—it builds the image on this server and starts the app.", docker_image )); } debug!("Using local image: {}", docker_image); } // Create and start container with security constraints let mut run_args = vec![ "podman", "run", "-d", // Detached "--name", package_id, "--restart=unless-stopped", // Auto-restart policy ]; // App-specific configuration (should come from manifest) let (ports, volumes, env_vars, custom_command, custom_args) = { let mut allocator = self.port_allocator.lock().map_err(|e| { anyhow::anyhow!("Port allocator lock poisoned: {}", e) })?; get_app_config(package_id, &self.config.host_ip, &mut allocator) }; // Special handling: Tailscale needs host network; mempool stack needs archy-net let is_tailscale = package_id == "tailscale"; let needs_archy_net = matches!( package_id, "bitcoin-knots" | "bitcoin" | "bitcoin-core" | "mempool" | "mempool-web" | "mempool-api" | "mempool-electrs" | "mysql-mempool" | "archy-mempool-db" | "archy-mempool-web" | "btcpay-server" | "btcpayserver" | "archy-btcpay-db" ); if is_tailscale { run_args.push("--network=host"); run_args.push("--privileged"); run_args.push("--cap-add=NET_ADMIN"); run_args.push("--cap-add=NET_RAW"); run_args.push("--device=/dev/net/tun"); } else if needs_archy_net { let _ = tokio::process::Command::new("sudo") .args(["podman", "network", "create", "archy-net"]) .output() .await; run_args.push("--network=archy-net"); } // Create data directories if they don't exist for volume in &volumes { if let Some(host_path) = volume.split(':').next() { if host_path.starts_with("/var/lib/archipelago/") { debug!("Creating directory: {}", host_path); let create_dir = tokio::process::Command::new("sudo") .args(["mkdir", "-p", host_path]) .output() .await; if let Err(e) = create_dir { debug!("Failed to create directory {}: {}", host_path, e); } // Grafana runs as UID 472 - fix permissions so it can write if package_id == "grafana" && host_path.contains("grafana") { let _ = tokio::process::Command::new("sudo") .args(["chown", "-R", "472:472", host_path]) .output() .await; } } } } // Add port mappings (skip if host network mode like Tailscale) if !is_tailscale { for port in &ports { run_args.push("-p"); run_args.push(port); } } // Add volume mounts for volume in &volumes { run_args.push("-v"); run_args.push(volume); } // Add environment variables for env in &env_vars { run_args.push("-e"); run_args.push(env); } // Security: Resource limits (from manifest) let memory_limit = if package_id == "ollama" { "4g" } else { "2g" }; let mem_arg = format!("--memory={}", memory_limit); run_args.push(&mem_arg); run_args.push("--cpus=2"); // Finally, the image run_args.push(docker_image); debug!("Running container with args: {:?}", run_args); // Build command with optional custom command let mut cmd = tokio::process::Command::new("sudo"); cmd.args(&run_args); // Add custom command/args if specified if let Some(custom_cmd) = custom_command { cmd.arg(custom_cmd); } else if let Some(args) = custom_args { cmd.args(args); } let run_output = cmd .output() .await .context("Failed to run container")?; if !run_output.status.success() { let stderr = String::from_utf8_lossy(&run_output.stderr); return Err(anyhow::anyhow!("Failed to start container: {}", stderr)); } let container_id = String::from_utf8_lossy(&run_output.stdout).trim().to_string(); // Post-install: Nextcloud needs trusted domains configured for iframe embedding if package_id == "nextcloud" { let host_ip = self.config.host_ip.clone(); tokio::spawn(async move { // Wait for Nextcloud to finish first-run initialization tokio::time::sleep(std::time::Duration::from_secs(30)).await; for domain_idx in 1..=2u8 { let value = if domain_idx == 1 { host_ip.as_str() } else { "localhost" }; let _ = tokio::process::Command::new("sudo") .args([ "podman", "exec", "-u", "33", "nextcloud", "php", "occ", "config:system:set", "trusted_domains", &domain_idx.to_string(), "--value", value, ]) .output() .await; } info!("Nextcloud trusted domains configured for {}", host_ip); }); } Ok(serde_json::json!({ "success": true, "package_id": package_id, "container_id": container_id, "message": format!("Package {} installed and started", package_id) })) } /// Install Immich stack (postgres + redis + server) async fn install_immich_stack(&self) -> Result { let check = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "{{.Names}}"]) .output() .await .context("Failed to list containers")?; let stdout = String::from_utf8_lossy(&check.stdout); if stdout.contains("immich_server") { return Err(anyhow::anyhow!("Immich already installed. Stop and remove it first.")); } if stdout.contains("immich\n") || stdout.lines().any(|l| l.trim() == "immich") { let _ = tokio::process::Command::new("sudo") .args(["podman", "stop", "immich"]) .output() .await; let _ = tokio::process::Command::new("sudo") .args(["podman", "rm", "-f", "immich"]) .output() .await; } let images = [ "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0", "docker.io/valkey/valkey:7-alpine", "ghcr.io/immich-app/immich-server:release", ]; for img in &images { let _ = tokio::process::Command::new("sudo") .args(["podman", "pull", img]) .output() .await; } let _ = tokio::process::Command::new("sudo") .args(["mkdir", "-p", "/var/lib/archipelago/immich", "/var/lib/archipelago/immich-db"]) .output() .await; let _ = tokio::process::Command::new("sudo") .args(["podman", "network", "create", "immich-net"]) .output() .await; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "immich_postgres", "--restart", "unless-stopped", "--network", "immich-net", "-v", "/var/lib/archipelago/immich-db:/var/lib/postgresql/data", "-e", "POSTGRES_PASSWORD=immichpass", "-e", "POSTGRES_USER=postgres", "-e", "POSTGRES_DB=immich", "ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(5)).await; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "immich_redis", "--restart", "unless-stopped", "--network", "immich-net", "docker.io/valkey/valkey:7-alpine", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(2)).await; let run = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "immich_server", "--restart", "unless-stopped", "--network", "immich-net", "-p", "2283:2283", "-v", "/var/lib/archipelago/immich:/usr/src/app/upload", "-e", "DB_HOSTNAME=immich_postgres", "-e", "DB_USERNAME=postgres", "-e", "DB_PASSWORD=immichpass", "-e", "DB_DATABASE_NAME=immich", "-e", "REDIS_HOSTNAME=immich_redis", "-e", "UPLOAD_LOCATION=/usr/src/app/upload", "ghcr.io/immich-app/immich-server:release", ]) .output() .await .context("Failed to start immich_server")?; if !run.status.success() { let stderr = String::from_utf8_lossy(&run.stderr); return Err(anyhow::anyhow!("Failed to start Immich server: {}", stderr)); } Ok(serde_json::json!({ "success": true, "package_id": "immich", "message": "Immich stack installed and started" })) } /// Install Penpot stack (postgres + valkey + backend + exporter + frontend) async fn install_penpot_stack(&self) -> Result { let check = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "{{.Names}}"]) .output() .await .context("Failed to list containers")?; let stdout = String::from_utf8_lossy(&check.stdout); if stdout.contains("penpot-frontend") { return Err(anyhow::anyhow!("Penpot already installed. Stop and remove it first.")); } let images = [ "docker.io/postgres:15", "docker.io/valkey/valkey:8.1", "docker.io/penpotapp/backend:latest", "docker.io/penpotapp/exporter:latest", "docker.io/penpotapp/frontend:latest", ]; for img in &images { let _ = tokio::process::Command::new("sudo") .args(["podman", "pull", img]) .output() .await; } let _ = tokio::process::Command::new("sudo") .args(["mkdir", "-p", "/var/lib/archipelago/penpot-assets"]) .output() .await; let _ = tokio::process::Command::new("sudo") .args(["podman", "network", "create", "penpot-net"]) .output() .await; let secret = "archipelago-penpot-secret-key-change-in-production"; let host_ip = &self.config.host_ip; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "penpot-postgres", "--restart", "unless-stopped", "--network", "penpot-net", "-v", "/var/lib/archipelago/penpot-postgres:/var/lib/postgresql/data", "-e", "POSTGRES_DB=penpot", "-e", "POSTGRES_USER=penpot", "-e", "POSTGRES_PASSWORD=penpot", "docker.io/postgres:15", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(5)).await; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "penpot-valkey", "--restart", "unless-stopped", "--network", "penpot-net", "-e", "VALKEY_EXTRA_FLAGS=--maxmemory 128mb --maxmemory-policy volatile-lfu", "docker.io/valkey/valkey:8.1", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(3)).await; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "penpot-backend", "--restart", "unless-stopped", "--network", "penpot-net", "-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets", "-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip), "-e", &format!("PENPOT_SECRET_KEY={}", secret), "-e", "PENPOT_DATABASE_URI=postgresql://penpot-postgres/penpot", "-e", "PENPOT_DATABASE_USERNAME=penpot", "-e", "PENPOT_DATABASE_PASSWORD=penpot", "-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0", "-e", "PENPOT_OBJECTS_STORAGE_BACKEND=fs", "-e", "PENPOT_OBJECTS_STORAGE_FS_DIRECTORY=/opt/data/assets", "-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies", "docker.io/penpotapp/backend:latest", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(5)).await; let _ = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "penpot-exporter", "--restart", "unless-stopped", "--network", "penpot-net", "-e", &format!("PENPOT_SECRET_KEY={}", secret), "-e", "PENPOT_PUBLIC_URI=http://penpot-frontend:8080", "-e", "PENPOT_REDIS_URI=redis://penpot-valkey/0", "docker.io/penpotapp/exporter:latest", ]) .output() .await; tokio::time::sleep(std::time::Duration::from_secs(2)).await; let run = tokio::process::Command::new("sudo") .args([ "podman", "run", "-d", "--name", "penpot-frontend", "--restart", "unless-stopped", "--network", "penpot-net", "-p", "9001:8080", "-v", "/var/lib/archipelago/penpot-assets:/opt/data/assets", "-e", &format!("PENPOT_PUBLIC_URI=http://{}:9001", host_ip), "-e", "PENPOT_FLAGS=disable-email-verification enable-smtp enable-prepl-server disable-secure-session-cookies", "docker.io/penpotapp/frontend:latest", ]) .output() .await .context("Failed to start penpot-frontend")?; if !run.status.success() { let stderr = String::from_utf8_lossy(&run.stderr); return Err(anyhow::anyhow!("Failed to start Penpot frontend: {}", stderr)); } Ok(serde_json::json!({ "success": true, "package_id": "penpot", "message": "Penpot stack installed and started" })) } pub(super) async fn handle_package_start( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let package_id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing package id"))?; let containers = get_containers_for_app(package_id).await?; let to_start: Vec = if containers.is_empty() { vec![format!("archy-{}", package_id)] } else { let order: &[&str] = match package_id { "mempool" | "mempool-web" => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"], "immich" => &["immich_postgres", "immich_redis", "immich_server"], "penpot" | "penpot-frontend" => &["penpot-postgres", "penpot-valkey", "penpot-backend", "penpot-exporter", "penpot-frontend"], _ => &["archy-mempool-db", "mysql-mempool", "mempool-electrs", "mempool-api", "archy-mempool-api", "archy-mempool-web", "mempool"], }; let mut sorted = containers; sorted.sort_by_key(|c| order.iter().position(|o| *o == c).unwrap_or(99)); sorted }; for name in to_start { let _ = tokio::process::Command::new("sudo") .args(["podman", "start", &name]) .output() .await; } Ok(serde_json::Value::Null) } pub(super) async fn handle_package_stop( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let package_id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing package id"))?; let containers = get_containers_for_app(package_id).await?; if containers.is_empty() { let container_name = format!("archy-{}", package_id); let _ = tokio::process::Command::new("sudo") .args(["podman", "stop", &container_name]) .output() .await; return Ok(serde_json::Value::Null); } for name in containers { let _ = tokio::process::Command::new("sudo") .args(["podman", "stop", &name]) .output() .await; } Ok(serde_json::Value::Null) } pub(super) async fn handle_package_restart( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let package_id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing package id"))?; let containers = get_containers_for_app(package_id).await?; if containers.is_empty() { let container_name = format!("archy-{}", package_id); let _ = tokio::process::Command::new("sudo") .args(["podman", "restart", &container_name]) .output() .await; return Ok(serde_json::Value::Null); } for name in containers { let _ = tokio::process::Command::new("sudo") .args(["podman", "restart", &name]) .output() .await; } Ok(serde_json::Value::Null) } /// Uninstall a package: stop and remove all related containers, clean data. pub(super) async fn handle_package_uninstall( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let package_id = params .get("id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing package id"))?; let preserve_data = params .get("preserve_data") .and_then(|v| v.as_bool()) .unwrap_or(false); let containers_to_remove = get_containers_for_app(package_id).await?; for name in &containers_to_remove { let _ = tokio::process::Command::new("sudo") .args(["podman", "stop", name]) .output() .await; let _ = tokio::process::Command::new("sudo") .args(["podman", "rm", "-f", name]) .output() .await; } // Release port allocation if let Ok(mut allocator) = self.port_allocator.lock() { let _ = allocator.release(package_id); } // Clean data directories unless preserve_data if !preserve_data { let data_dirs = get_data_dirs_for_app(package_id); for dir in &data_dirs { let _ = tokio::process::Command::new("sudo") .args(["rm", "-rf", dir]) .output() .await; } } Ok(serde_json::json!({ "status": "uninstalled" })) } /// Start a bundled app (create container from pre-loaded image if needed, then start) pub(super) async fn handle_bundled_app_start( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; let image = params .get("image") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing image"))?; let ports = params .get("ports") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow::anyhow!("Missing ports"))?; let volumes = params .get("volumes") .and_then(|v| v.as_array()) .ok_or_else(|| anyhow::anyhow!("Missing volumes"))?; let check_output = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "{{.Names}}", "--filter", &format!("name={}", app_id)]) .output() .await .context("Failed to check container")?; let existing = String::from_utf8_lossy(&check_output.stdout); if existing.trim().is_empty() { let mut cmd = tokio::process::Command::new("sudo"); cmd.args(["podman", "run", "-d", "--name", app_id]); for port in ports { if let (Some(host), Some(container)) = ( port.get("host").and_then(|v| v.as_u64()), port.get("container").and_then(|v| v.as_u64()), ) { cmd.arg("-p").arg(format!("{}:{}", host, container)); } } for volume in volumes { if let (Some(host), Some(container)) = ( volume.get("host").and_then(|v| v.as_str()), volume.get("container").and_then(|v| v.as_str()), ) { let _ = tokio::process::Command::new("sudo") .args(["mkdir", "-p", host]) .output() .await; cmd.arg("-v").arg(format!("{}:{}", host, container)); } } cmd.arg(image); let output = cmd.output().await.context("Failed to create container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to create container: {}", stderr)); } } else { let output = tokio::process::Command::new("sudo") .args(["podman", "start", app_id]) .output() .await .context("Failed to start container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to start container: {}", stderr)); } } Ok(serde_json::json!({ "status": "started", "app_id": app_id })) } /// Stop a bundled app pub(super) async fn handle_bundled_app_stop( &self, params: Option, ) -> Result { let params = params.ok_or_else(|| anyhow::anyhow!("Missing params"))?; let app_id = params .get("app_id") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing app_id"))?; let output = tokio::process::Command::new("sudo") .args(["podman", "stop", app_id]) .output() .await .context("Failed to stop container")?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow::anyhow!("Failed to stop container: {}", stderr)); } Ok(serde_json::json!({ "status": "stopped", "app_id": app_id })) } } /// Get all container names for an app (handles multi-container apps like mempool) async fn get_containers_for_app(package_id: &str) -> Result> { let output = tokio::process::Command::new("sudo") .args(["podman", "ps", "-a", "--format", "{{.Names}}"]) .output() .await .context("Failed to list containers")?; let stdout = String::from_utf8_lossy(&output.stdout); let all: Vec<&str> = stdout.lines().filter(|s| !s.is_empty()).collect(); let patterns: Vec = match package_id { "mempool" | "mempool-web" => { vec![ "mempool-electrs".into(), "mempool-api".into(), "archy-mempool-api".into(), "archy-mempool-web".into(), "mempool".into(), "archy-mempool-db".into(), "mysql-mempool".into(), ] } "fedimint" => vec!["fedimint".into(), "fedimint-ui".into(), "archy-fedimint".into()], "immich" => vec![ "immich_postgres".into(), "immich_redis".into(), "immich_server".into(), ], "penpot" | "penpot-frontend" => vec![ "penpot-postgres".into(), "penpot-valkey".into(), "penpot-backend".into(), "penpot-exporter".into(), "penpot-frontend".into(), ], _ => vec![package_id.to_string(), format!("archy-{}", package_id)], }; let mut result = Vec::new(); for name in all { for pat in &patterns { if name == pat { result.push(name.to_string()); break; } } } Ok(result) } /// Get data directories to clean for an app fn get_data_dirs_for_app(package_id: &str) -> Vec { let base = "/var/lib/archipelago"; match package_id { "mempool" | "mempool-web" => vec![ format!("{}/mempool", base), format!("{}/mysql-mempool", base), format!("{}/mempool-electrs", base), ], "fedimint" => vec![format!("{}/fedimint", base)], "immich" => vec![ format!("{}/immich", base), format!("{}/immich-db", base), ], "penpot" | "penpot-frontend" => vec![ format!("{}/penpot-assets", base), format!("{}/penpot-postgres", base), ], _ => vec![format!("{}/{}", base, package_id)], } } /// Validate Docker image name format /// Prevents command injection via malicious image names fn is_valid_docker_image(image: &str) -> bool { let dangerous_chars = ['&', '|', ';', '`', '$', '(', ')', '<', '>', '\n', '\r']; if image.chars().any(|c| dangerous_chars.contains(&c)) { return false; } if !image.chars().any(|c| c.is_alphanumeric()) { return false; } if image.len() > 256 { return false; } true } /// Get app-specific configuration /// Returns: (ports, volumes, env_vars, custom_command, custom_args) fn get_app_config( app_id: &str, host_ip: &str, allocator: &mut PortAllocator, ) -> (Vec, Vec, Vec, Option, Option>) { match app_id { "homeassistant" | "home-assistant" => ( vec!["8123:8123".to_string()], vec!["/var/lib/archipelago/home-assistant:/config".to_string()], vec!["TZ=UTC".to_string()], None, None, ), "bitcoin" | "bitcoin-core" | "bitcoin-knots" => ( vec!["8332:8332".to_string(), "8333:8333".to_string()], vec!["/var/lib/archipelago/bitcoin:/home/bitcoin/.bitcoin".to_string()], vec![], None, None, ), "lnd" => ( vec!["9735:9735".to_string(), "10009:10009".to_string(), "8080:8080".to_string()], vec!["/var/lib/archipelago/lnd:/root/.lnd".to_string()], vec!["BITCOIN_ACTIVE=1".to_string()], None, None, ), "btcpay-server" | "btcpayserver" => ( vec!["23000:49392".to_string()], vec!["/var/lib/archipelago/btcpay:/datadir".to_string()], vec![ "ASPNETCORE_URLS=http://0.0.0.0:49392".to_string(), "BTCPAY_PROTOCOL=http".to_string(), format!("BTCPAY_HOST={}:23000", host_ip), "BTCPAY_CHAINS=btc".to_string(), format!("BTCPAY_BTCRPCURL=http://{}:8332", host_ip), "BTCPAY_BTCRPCUSER=archipelago".to_string(), "BTCPAY_BTCRPCPASSWORD=archipelago123".to_string(), "BTCPAY_POSTGRES=User ID=btcpay;Password=btcpaypass;Host=archy-btcpay-db;Port=5432;Database=btcpay;Include Error Detail=true".to_string(), ], None, None, ), "mempool" | "mempool-web" => ( vec!["4080:8080".to_string()], vec![], vec![format!("BACKEND_MAINNET_HTTP_HOST={}", host_ip)], None, None, ), "mempool-api" => ( vec!["8999:8999".to_string()], vec!["/var/lib/archipelago/mempool:/data".to_string()], vec![ "MEMPOOL_BACKEND=electrum".to_string(), "ELECTRUM_HOST=mempool-electrs".to_string(), "ELECTRUM_PORT=50001".to_string(), "ELECTRUM_TLS_ENABLED=false".to_string(), format!("CORE_RPC_HOST={}", host_ip), "CORE_RPC_PORT=8332".to_string(), "CORE_RPC_USERNAME=archipelago".to_string(), "CORE_RPC_PASSWORD=archipelago123".to_string(), "DATABASE_ENABLED=true".to_string(), "DATABASE_HOST=archy-mempool-db".to_string(), "DATABASE_DATABASE=mempool".to_string(), "DATABASE_USERNAME=mempool".to_string(), "DATABASE_PASSWORD=mempoolpass".to_string(), ], None, None, ), "mempool-electrs" => ( vec!["50001:50001".to_string()], vec!["/var/lib/archipelago/mempool-electrs:/data".to_string()], vec![], None, Some(vec![ "--daemon-rpc-addr".to_string(), format!("{}:8332", host_ip), "--cookie".to_string(), "archipelago:archipelago123".to_string(), "--jsonrpc-import".to_string(), "--electrum-rpc-addr".to_string(), "0.0.0.0:50001".to_string(), "--db-dir".to_string(), "/data".to_string(), "--lightmode".to_string(), ]), ), "mysql-mempool" => ( vec![], vec!["/var/lib/archipelago/mysql-mempool:/var/lib/mysql".to_string()], vec![ "MYSQL_DATABASE=mempool".to_string(), "MYSQL_USER=mempool".to_string(), "MYSQL_PASSWORD=mempoolpass".to_string(), "MYSQL_ROOT_PASSWORD=rootpass".to_string(), ], None, None, ), "grafana" => ( vec!["3000:3000".to_string()], vec!["/var/lib/archipelago/grafana:/var/lib/grafana".to_string()], vec!["GF_PATHS_DATA=/var/lib/grafana".to_string(), "GF_USERS_ALLOW_SIGN_UP=false".to_string()], None, None, ), "searxng" => ( vec!["8888:8080".to_string()], vec![], vec![], None, None, ), "ollama" => ( vec!["11434:11434".to_string()], vec!["/var/lib/archipelago/ollama:/root/.ollama".to_string()], vec![], None, None, ), "onlyoffice" | "onlyoffice-documentserver" => ( vec!["9980:80".to_string()], vec![], vec![], None, None, ), "penpot" | "penpot-frontend" => ( vec!["9001:80".to_string()], vec![], vec![], None, None, ), "nextcloud" => { let host_port = allocator .allocate_or_get(app_id, 8085, 80) .unwrap_or(8085); ( vec![format!("{}:80", host_port)], vec!["/var/lib/archipelago/nextcloud:/var/www/html".to_string()], vec![], None, None, ) } "vaultwarden" => { let host_port = allocator .allocate_or_get(app_id, 8082, 80) .unwrap_or(8082); ( vec![format!("{}:80", host_port)], vec!["/var/lib/archipelago/vaultwarden:/data".to_string()], vec![], None, None, ) } "jellyfin" => ( vec!["8096:8096".to_string()], vec!["/var/lib/archipelago/jellyfin/config:/config".to_string(), "/var/lib/archipelago/jellyfin/cache:/cache".to_string()], vec![], None, None, ), "photoprism" => ( vec!["2342:2342".to_string()], vec!["/var/lib/archipelago/photoprism:/photoprism/storage".to_string()], vec!["PHOTOPRISM_ADMIN_PASSWORD=archipelago".to_string(), "PHOTOPRISM_DEFAULT_LOCALE=en".to_string()], None, None, ), "immich" => ( vec!["2283:2283".to_string()], vec!["/var/lib/archipelago/immich:/usr/src/app/upload".to_string()], vec![ "DB_HOSTNAME=immich_postgres".to_string(), "DB_USERNAME=postgres".to_string(), "DB_PASSWORD=immichpass".to_string(), "DB_DATABASE_NAME=immich".to_string(), "REDIS_HOSTNAME=immich_redis".to_string(), "UPLOAD_LOCATION=/usr/src/app/upload".to_string(), ], None, None, ), "filebrowser" => { let host_port = allocator .allocate_or_get(app_id, 8083, 80) .unwrap_or(8083); ( vec![format!("{}:80", host_port)], vec!["/var/lib/archipelago/filebrowser:/srv".to_string()], vec![], None, None, ) } "nginx-proxy-manager" => ( vec!["81:81".to_string(), "8084:80".to_string(), "8443:443".to_string()], vec![ "/var/lib/archipelago/nginx-proxy-manager/data:/data".to_string(), "/var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt".to_string(), ], vec![], None, None, ), "portainer" => ( vec!["9000:9000".to_string()], vec!["/var/lib/archipelago/portainer:/data".to_string(), "/var/run/podman/podman.sock:/var/run/docker.sock".to_string()], vec![], None, None, ), "uptime-kuma" => ( vec!["3001:3001".to_string()], vec!["/var/lib/archipelago/uptime-kuma:/app/data".to_string()], vec!["TZ=UTC".to_string()], None, None, ), "tailscale" => ( vec!["8240:8240".to_string()], vec![ "/var/lib/archipelago/tailscale:/var/lib/tailscale".to_string(), ], vec![ "TS_STATE_DIR=/var/lib/tailscale".to_string(), ], Some("sh -c 'tailscale web --listen 0.0.0.0:8240 & exec tailscaled'".to_string()), None, ), "fedimint" => ( vec![ "8173:8173".to_string(), "8174:8174".to_string(), "8175:8175".to_string(), ], vec!["/var/lib/archipelago/fedimint:/data".to_string()], vec![ "FM_DATA_DIR=/data".to_string(), "FM_BITCOIND_USERNAME=archipelago".to_string(), "FM_BITCOIND_PASSWORD=archipelago123".to_string(), "FM_BITCOIN_NETWORK=bitcoin".to_string(), "FM_BIND_P2P=0.0.0.0:8173".to_string(), "FM_BIND_API=0.0.0.0:8174".to_string(), "FM_BIND_UI=0.0.0.0:8175".to_string(), format!("FM_P2P_URL=fedimint://{}:8173", host_ip), format!("FM_API_URL=ws://{}:8174", host_ip), format!("FM_BITCOIND_URL=http://{}:8332", host_ip), ], None, None, ), "indeedhub" => ( vec!["7777:7777".to_string()], vec![], vec!["NGINX_HOST=0.0.0.0".to_string(), "NGINX_PORT=7777".to_string()], None, None, ), _ => (vec![], vec![], vec![], None, None), } }