From 19f2125a4d9d53be2cb1a107266682917a2dd22e Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 17 May 2026 22:38:04 -0400 Subject: [PATCH] fix(apps): repair stale nginx proxy manager ports --- CHANGELOG.md | 7 ++ .../src/api/rpc/package/runtime.rs | 104 +++++++++++++++++- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f23bc1..fd683626 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v1.7.62-alpha (2026-05-18) + +- Nginx Proxy Manager start and restart now repair stale Podman containers that still publish the admin UI on host port `81`, which conflicts with host nginx on updated nodes. +- The repair recreates only the stale Nginx Proxy Manager container metadata while preserving `/var/lib/archipelago/nginx-proxy-manager` data and using the current `8081:81`, `8084:80`, and `8444:443` mappings. +- Runtime stale-listener cleanup for Nginx Proxy Manager is shared across start and restart paths so rootless port helper leftovers are still cleared before lifecycle retries. +- Validation passed with `cargo fmt --all --check --manifest-path core/Cargo.toml` and `cargo check -p archipelago --manifest-path core/Cargo.toml`. + ## v1.7.61-alpha (2026-05-18) - Multi-container stack installs now keep their app card in the `Installing` state for up to 20 minutes while dependency containers are being pulled and prepared. diff --git a/core/archipelago/src/api/rpc/package/runtime.rs b/core/archipelago/src/api/rpc/package/runtime.rs index b23aed3f..ee770818 100644 --- a/core/archipelago/src/api/rpc/package/runtime.rs +++ b/core/archipelago/src/api/rpc/package/runtime.rs @@ -1,4 +1,7 @@ -use super::config::{get_containers_for_app, get_data_dirs_for_app, is_valid_docker_image}; +use super::config::{ + get_app_capabilities, get_containers_for_app, get_data_dirs_for_app, get_health_check_args, + get_memory_limit, is_valid_docker_image, +}; use super::dependencies::ordered_containers_for_start; use super::install::install_log; use super::validation::validate_app_id; @@ -863,11 +866,98 @@ async fn repair_before_package_start(container_name: &str) { repair_nextcloud_dirs().await; cleanup_stale_pasta_port("8085").await; } + "nginx-proxy-manager" => repair_nginx_proxy_manager_container().await, "gitea" => cleanup_gitea_stale_ports().await, _ => {} } } +async fn repair_nginx_proxy_manager_container() { + if !nginx_proxy_manager_has_legacy_admin_port().await { + cleanup_nginx_proxy_manager_ports().await; + return; + } + + install_log( + "START REPAIR: nginx-proxy-manager - recreating stale container using host port 8081", + ) + .await; + let _ = podman_control(&["rm", "-f", "nginx-proxy-manager"]).await; + cleanup_nginx_proxy_manager_ports().await; + if let Err(err) = recreate_nginx_proxy_manager_container().await { + tracing::warn!(error = %err, "failed to recreate stale nginx-proxy-manager container"); + } +} + +async fn nginx_proxy_manager_has_legacy_admin_port() -> bool { + let Ok(output) = podman_control(&["port", "nginx-proxy-manager", "81/tcp"]).await else { + return false; + }; + if !output.status.success() { + return false; + } + String::from_utf8_lossy(&output.stdout).lines().any(|line| { + line.rsplit(':') + .next() + .is_some_and(|port| port.trim() == "81") + }) +} + +async fn recreate_nginx_proxy_manager_container() -> Result<()> { + tokio::process::Command::new("sudo") + .args([ + "mkdir", + "-p", + "/var/lib/archipelago/nginx-proxy-manager/data", + "/var/lib/archipelago/nginx-proxy-manager/letsencrypt", + ]) + .output() + .await + .context("failed to create nginx-proxy-manager data directories")?; + + let image = crate::container::image_versions::pinned_image_for_app("nginx-proxy-manager") + .unwrap_or_else(|| "docker.io/jc21/nginx-proxy-manager:latest".to_string()); + let mut args = vec![ + "run".to_string(), + "-d".to_string(), + "--name".to_string(), + "nginx-proxy-manager".to_string(), + "--restart=unless-stopped".to_string(), + "--network=slirp4netns:allow_host_loopback=true".to_string(), + "--cap-drop=ALL".to_string(), + "--security-opt=no-new-privileges:true".to_string(), + "--pids-limit=4096".to_string(), + ]; + args.extend(get_app_capabilities("nginx-proxy-manager")); + args.extend([ + "-p".to_string(), + "8081:81".to_string(), + "-p".to_string(), + "8084:80".to_string(), + "-p".to_string(), + "8444:443".to_string(), + "-v".to_string(), + "/var/lib/archipelago/nginx-proxy-manager/data:/data".to_string(), + "-v".to_string(), + "/var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt".to_string(), + "--memory".to_string(), + get_memory_limit("nginx-proxy-manager").to_string(), + "--cpus=2".to_string(), + ]); + args.extend(get_health_check_args("nginx-proxy-manager", "")); + args.push(image); + + let refs = args.iter().map(String::as_str).collect::>(); + let output = podman_control(&refs).await?; + if !output.status.success() { + anyhow::bail!( + "podman run nginx-proxy-manager failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + ); + } + Ok(()) +} + async fn ensure_runtime_host_port_listener(container_name: &str) -> Result<()> { let Some(port) = runtime_required_host_port(container_name) else { return Ok(()); @@ -1075,15 +1165,17 @@ async fn cleanup_start_conflict(container_name: &str, stderr: &str) { "homeassistant" | "home-assistant" => cleanup_stale_pasta_port("8123").await, "vaultwarden" => cleanup_stale_pasta_port("8082").await, "nextcloud" => cleanup_stale_pasta_port("8085").await, - "nginx-proxy-manager" => { - cleanup_stale_pasta_port("8081").await; - cleanup_stale_pasta_port("8084").await; - cleanup_stale_pasta_port("8444").await; - } + "nginx-proxy-manager" => cleanup_nginx_proxy_manager_ports().await, _ => {} } } +async fn cleanup_nginx_proxy_manager_ports() { + cleanup_stale_pasta_port("8081").await; + cleanup_stale_pasta_port("8084").await; + cleanup_stale_pasta_port("8444").await; +} + async fn cleanup_stale_pasta_port(port: &str) { let kill_listener = format!( "ss -ltnp 'sport = :{}' 2>/dev/null | sed -n 's/.*pid=\\([0-9]*\\).*/\\1/p' | xargs -r kill 2>/dev/null || true",