From 780422315263d4cb591e112b81b1c39acf04d4bb Mon Sep 17 00:00:00 2001 From: archipelago Date: Sun, 17 May 2026 17:30:04 -0400 Subject: [PATCH] chore: release v1.7.57-alpha --- CHANGELOG.md | 8 +++ core/Cargo.lock | 2 +- core/archipelago/Cargo.toml | 2 +- .../archipelago/src/api/rpc/package/config.rs | 46 ++++++++---- .../src/api/rpc/package/install.rs | 42 ++++++++--- .../src/api/rpc/package/runtime.rs | 8 ++- core/archipelago/src/api/rpc/tor/mod.rs | 2 +- .../src/container/docker_packages.rs | 22 +++++- core/archipelago/src/port_allocator.rs | 7 +- core/container/src/podman_client.rs | 2 +- .../_archived/build-auto-installer-iso.sh | 1 + .../archipelago-scripts/install-to-disk.sh | 2 +- image-recipe/configs/nginx-archipelago.conf | 19 ++++- .../archipelago-https-app-proxies.conf | 2 +- neode-ui/package-lock.json | 4 +- neode-ui/package.json | 2 +- .../src/stores/__tests__/appLauncher.test.ts | 8 +-- neode-ui/src/stores/appLauncher.ts | 5 +- neode-ui/src/views/AppDetails.vue | 13 ++++ neode-ui/src/views/Apps.vue | 21 ++++-- neode-ui/src/views/Discover.vue | 2 +- neode-ui/src/views/Marketplace.vue | 2 +- .../src/views/appDetails/appDetailsData.ts | 2 +- .../src/views/appSession/appSessionConfig.ts | 2 +- neode-ui/src/views/apps/AppIconGrid.vue | 11 ++- neode-ui/src/views/apps/appsConfig.ts | 43 ++++++++++-- .../views/dashboard/DashboardMobileNav.vue | 8 +-- .../views/dashboard/useRouteTransitions.ts | 4 +- neode-ui/vite.config.ts | 1 + release-manifest.json | 41 +++++------ releases/manifest.json | 41 +++++------ scripts/container-specs.sh | 12 +++- scripts/deploy-tailscale.sh | 2 +- scripts/first-boot-containers.sh | 38 +++++++++- scripts/nginx-https-app-proxies.conf | 2 +- scripts/reconcile-containers.sh | 70 ++++++++++++++++++- tests/lifecycle/remote-lifecycle.sh | 2 +- 37 files changed, 382 insertions(+), 119 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a8a51f2..35b7d0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## v1.7.57-alpha (2026-05-17) + +- Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons. +- App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied. +- Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs. +- Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI. +- Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory. + ## v1.7.56-alpha (2026-05-15) - Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer. diff --git a/core/Cargo.lock b/core/Cargo.lock index 12472d0c..a1a84fb4 100644 --- a/core/Cargo.lock +++ b/core/Cargo.lock @@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" [[package]] name = "archipelago" -version = "1.7.56-alpha" +version = "1.7.57-alpha" dependencies = [ "anyhow", "archipelago-container", diff --git a/core/archipelago/Cargo.toml b/core/archipelago/Cargo.toml index 206b8398..79868ad4 100644 --- a/core/archipelago/Cargo.toml +++ b/core/archipelago/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "archipelago" -version = "1.7.56-alpha" +version = "1.7.57-alpha" edition = "2021" description = "Archipelago Bitcoin Node OS - Native backend" authors = ["Archipelago Team"] diff --git a/core/archipelago/src/api/rpc/package/config.rs b/core/archipelago/src/api/rpc/package/config.rs index 375ec7f0..8262818e 100644 --- a/core/archipelago/src/api/rpc/package/config.rs +++ b/core/archipelago/src/api/rpc/package/config.rs @@ -153,11 +153,13 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec { "--cap-add=SETGID".to_string(), "--cap-add=DAC_OVERRIDE".to_string(), ], - // Nginx Proxy Manager needs to bind low ports + // Nginx Proxy Manager initializes/chowns mounted state on first boot. "nginx-proxy-manager" => vec![ "--cap-add=CHOWN".to_string(), + "--cap-add=FOWNER".to_string(), "--cap-add=SETUID".to_string(), "--cap-add=SETGID".to_string(), + "--cap-add=DAC_OVERRIDE".to_string(), "--cap-add=NET_BIND_SERVICE".to_string(), ], // Bitcoin needs only file-ownership ops + NET_BIND_SERVICE for the @@ -924,20 +926,34 @@ pub(super) async fn get_app_config( ]), ) } - "nginx-proxy-manager" => ( - vec![ - "81:81".to_string(), - "8084:80".to_string(), - "8444: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, - ), + "nginx-proxy-manager" => { + let admin_port = allocator + .allocate_or_get(app_id, 8081, 81) + .await + .unwrap_or(8081); + let http_port = allocator + .allocate_or_get("nginx-proxy-manager-http", 8084, 80) + .await + .unwrap_or(8084); + let https_port = allocator + .allocate_or_get("nginx-proxy-manager-https", 8444, 443) + .await + .unwrap_or(8444); + ( + vec![ + format!("{}:81", admin_port), + format!("{}:80", http_port), + format!("{}:443", https_port), + ], + 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![ diff --git a/core/archipelago/src/api/rpc/package/install.rs b/core/archipelago/src/api/rpc/package/install.rs index 9c540730..fc2d2880 100644 --- a/core/archipelago/src/api/rpc/package/install.rs +++ b/core/archipelago/src/api/rpc/package/install.rs @@ -376,7 +376,7 @@ impl RpcHandler { package_id )) .await; - if let Err(e) = ensure_host_port_listener(package_id, package_id).await { + if let Err(e) = ensure_host_port_listener(package_id, package_id, &[]).await { install_log(&format!( "INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}", package_id, e @@ -402,7 +402,7 @@ impl RpcHandler { package_id )) .await; - if let Err(e) = ensure_host_port_listener(package_id, package_id).await { + if let Err(e) = ensure_host_port_listener(package_id, package_id, &[]).await { install_log(&format!( "INSTALL ADOPT RECREATE: {} — host listener repair failed, removing wedged container: {}", package_id, e @@ -880,7 +880,7 @@ impl RpcHandler { repair_nextcloud_permissions().await; } - ensure_host_port_listener(package_id, container_name).await?; + ensure_host_port_listener(package_id, container_name, &ports).await?; install_log(&format!( "INSTALL OK: {} (container: {})", @@ -1866,7 +1866,7 @@ async fn cleanup_stale_package_ports(package_id: &str) { cleanup_stale_pasta_port("3000").await; } "nginx-proxy-manager" => { - cleanup_stale_pasta_port("81").await; + cleanup_stale_pasta_port("8081").await; cleanup_stale_pasta_port("8084").await; cleanup_stale_pasta_port("8444").await; } @@ -1914,7 +1914,7 @@ async fn cleanup_start_conflict(package_id: &str, stderr: &str) -> bool { "nginx-proxy-manager" if stderr.contains("pasta failed") || stderr.contains("address already in use") => { - cleanup_stale_pasta_port("81").await; + cleanup_stale_pasta_port("8081").await; cleanup_stale_pasta_port("8084").await; cleanup_stale_pasta_port("8444").await; true @@ -1968,8 +1968,18 @@ async fn repair_nextcloud_permissions() { } } -async fn ensure_host_port_listener(package_id: &str, container_name: &str) -> Result<()> { - let Some(port) = required_host_port(package_id) else { +async fn ensure_host_port_listener( + package_id: &str, + container_name: &str, + runtime_ports: &[String], +) -> Result<()> { + let Some(port) = runtime_ports + .first() + .and_then(|p| p.split(':').next()) + .and_then(|p| p.parse::().ok()) + .or_else(|| published_host_port(container_name)) + .or_else(|| required_host_port(package_id)) + else { return Ok(()); }; @@ -2015,6 +2025,22 @@ async fn ensure_host_port_listener(package_id: &str, container_name: &str) -> Re )) } +fn published_host_port(container_name: &str) -> Option { + let output = std::process::Command::new("podman") + .args(["port", container_name]) + .output() + .ok()?; + if !output.status.success() { + return None; + } + let stdout = String::from_utf8_lossy(&output.stdout); + stdout.lines().find_map(|line| { + line.rsplit(':') + .next() + .and_then(|p| p.trim().parse::().ok()) + }) +} + async fn ensure_user_podman_socket() -> Result<()> { let socket_path = "/run/user/1000/podman/podman.sock"; if tokio::fs::try_exists(socket_path).await.unwrap_or(false) { @@ -2048,7 +2074,7 @@ fn required_host_port(package_id: &str) -> Option { "uptime-kuma" => Some(3002), "gitea" => Some(3001), "nextcloud" => Some(8085), - "nginx-proxy-manager" => Some(81), + "nginx-proxy-manager" => Some(8081), _ => None, } } diff --git a/core/archipelago/src/api/rpc/package/runtime.rs b/core/archipelago/src/api/rpc/package/runtime.rs index d86b46f4..b23aed3f 100644 --- a/core/archipelago/src/api/rpc/package/runtime.rs +++ b/core/archipelago/src/api/rpc/package/runtime.rs @@ -366,6 +366,10 @@ impl RpcHandler { { let mut allocator = self.port_allocator.lock().await; let _ = allocator.release(package_id).await; + if package_id == "nginx-proxy-manager" { + let _ = allocator.release("nginx-proxy-manager-http").await; + let _ = allocator.release("nginx-proxy-manager-https").await; + } } // Clean data directories unless preserve_data @@ -916,7 +920,7 @@ fn runtime_required_host_port(container_name: &str) -> Option { "vaultwarden" => Some(8082), "gitea" => Some(3001), "nextcloud" => Some(8085), - "nginx-proxy-manager" => Some(81), + "nginx-proxy-manager" => Some(8081), _ => None, } } @@ -1072,7 +1076,7 @@ async fn cleanup_start_conflict(container_name: &str, stderr: &str) { "vaultwarden" => cleanup_stale_pasta_port("8082").await, "nextcloud" => cleanup_stale_pasta_port("8085").await, "nginx-proxy-manager" => { - cleanup_stale_pasta_port("81").await; + cleanup_stale_pasta_port("8081").await; cleanup_stale_pasta_port("8084").await; cleanup_stale_pasta_port("8444").await; } diff --git a/core/archipelago/src/api/rpc/tor/mod.rs b/core/archipelago/src/api/rpc/tor/mod.rs index 77bed2e3..7b596ae2 100644 --- a/core/archipelago/src/api/rpc/tor/mod.rs +++ b/core/archipelago/src/api/rpc/tor/mod.rs @@ -353,7 +353,7 @@ pub(in crate::api::rpc) fn known_service_port(name: &str) -> u16 { "immich" => 2283, "photoprism" => 2342, "penpot" => 9001, - "nginx-proxy-manager" => 81, + "nginx-proxy-manager" => 8081, "vaultwarden" => 8343, "indeedhub" => 7778, _ => 0, diff --git a/core/archipelago/src/container/docker_packages.rs b/core/archipelago/src/container/docker_packages.rs index 4330a52a..3f7c6618 100644 --- a/core/archipelago/src/container/docker_packages.rs +++ b/core/archipelago/src/container/docker_packages.rs @@ -61,6 +61,7 @@ impl DockerPackageScanner { "indeedhub-build_minio-init_1", "indeedhub-build_relay_1", "indeedhub-build_ffmpeg-worker_1", + "buildx_buildkit_default", ]; // First pass: collect running UI containers. Custom UI-backed apps must @@ -123,6 +124,11 @@ impl DockerPackageScanner { continue; } + if app_id.starts_with("buildx_buildkit") { + debug!("Skipping BuildKit helper container: {}", app_id); + continue; + } + // Skip UI containers (they're merged with their parent apps) if app_id.ends_with("-ui") { debug!("Skipping UI container: {}", app_id); @@ -140,8 +146,13 @@ impl DockerPackageScanner { } else { // Prefer the known web UI port over arbitrary first binding // (for example Gitea exposes SSH on 2222 before web on 3001). - let candidate = PodmanClient::lan_address_for(&app_id) - .or_else(|| extract_lan_address(&container.ports)); + let candidate = if uses_allocated_launch_port(&app_id) { + extract_lan_address(&container.ports) + .or_else(|| PodmanClient::lan_address_for(&app_id)) + } else { + PodmanClient::lan_address_for(&app_id) + .or_else(|| extract_lan_address(&container.ports)) + }; reachable_lan_address(&app_id, candidate).await }; @@ -673,6 +684,13 @@ fn companion_lan_address(app_id: &str) -> Option { } } +fn uses_allocated_launch_port(app_id: &str) -> bool { + matches!( + app_id, + "filebrowser" | "nextcloud" | "nginx-proxy-manager" | "vaultwarden" + ) +} + fn convert_state(container_state: &ContainerState) -> (PackageState, ServiceStatus) { match container_state { ContainerState::Running => (PackageState::Running, ServiceStatus::Running), diff --git a/core/archipelago/src/port_allocator.rs b/core/archipelago/src/port_allocator.rs index c4581b37..39200ec0 100644 --- a/core/archipelago/src/port_allocator.rs +++ b/core/archipelago/src/port_allocator.rs @@ -28,7 +28,6 @@ const RESERVED_PORTS: &[u16] = &[ 8888, // SearXNG 8096, 2342, 2283, // Jellyfin, Photoprism, Immich 8443, // FIPS TCP fallback - 8444, 8084, // NPM ]; /// Start of range for allocating web app ports when preferred is taken. @@ -94,8 +93,12 @@ impl PortAllocator { .any(|m| m.host_port == port) } + fn can_bind(port: u16) -> bool { + std::net::TcpListener::bind(("0.0.0.0", port)).is_ok() + } + fn is_available(&self, port: u16) -> bool { - !self.is_reserved(port) && !self.is_allocated(port) + !self.is_reserved(port) && !self.is_allocated(port) && Self::can_bind(port) } /// Allocate a host port for an app. Uses preferred_port if available, else finds next free. diff --git a/core/container/src/podman_client.rs b/core/container/src/podman_client.rs index f4417697..ce674232 100644 --- a/core/container/src/podman_client.rs +++ b/core/container/src/podman_client.rs @@ -127,7 +127,7 @@ impl PodmanClient { "photoprism" => "http://localhost:2342", "immich_server" | "immich" => "http://localhost:2283", "filebrowser" => "http://localhost:8083", - "nginx-proxy-manager" => "http://localhost:81", + "nginx-proxy-manager" => "http://localhost:8081", "portainer" => "http://localhost:9000", "uptime-kuma" => "http://localhost:3002", "fedimint" | "fedimintd" => "http://localhost:8175", diff --git a/image-recipe/_archived/build-auto-installer-iso.sh b/image-recipe/_archived/build-auto-installer-iso.sh index cc196642..f1e8efcf 100755 --- a/image-recipe/_archived/build-auto-installer-iso.sh +++ b/image-recipe/_archived/build-auto-installer-iso.sh @@ -324,6 +324,7 @@ RUN apt-get update && apt-get -y full-upgrade && apt-get install -y --no-install openssh-server \ nginx \ podman \ + catatonit \ uidmap \ slirp4netns \ passt \ diff --git a/image-recipe/archipelago-scripts/install-to-disk.sh b/image-recipe/archipelago-scripts/install-to-disk.sh index 951e6d2b..0aef2524 100755 --- a/image-recipe/archipelago-scripts/install-to-disk.sh +++ b/image-recipe/archipelago-scripts/install-to-disk.sh @@ -183,7 +183,7 @@ chroot /mnt/archipelago apt-get install -y \ chrony echo "📦 Installing container tools..." -chroot /mnt/archipelago apt-get install -y podman || echo "⚠️ Podman not available in base repos, will use containers.io later" +chroot /mnt/archipelago apt-get install -y podman catatonit || echo "⚠️ Podman/catatonit not available in base repos, will use containers.io later" echo "🔧 Installing GRUB bootloader..." # Need to run grub-install inside chroot with proper environment diff --git a/image-recipe/configs/nginx-archipelago.conf b/image-recipe/configs/nginx-archipelago.conf index 139ec083..c631bf8d 100644 --- a/image-recipe/configs/nginx-archipelago.conf +++ b/image-recipe/configs/nginx-archipelago.conf @@ -741,7 +741,7 @@ server { sub_filter '' ''; } location /app/nginx-proxy-manager/ { - proxy_pass http://127.0.0.1:81/; + proxy_pass http://127.0.0.1:8081/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -873,6 +873,23 @@ server { } } +# Compatibility proxy for cached PWA bundles that still launch Nginx Proxy +# Manager on :81. Rootless Podman cannot bind host ports below 1024, so the +# container admin UI runs on :8081 and host nginx owns the old :81 entrypoint. +server { + listen 81; + server_name _; + + location / { + proxy_pass http://127.0.0.1:8081/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} + # HTTPS - required for PWA install (Add to Home Screen) from dev servers server { listen 443 ssl; diff --git a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf index 08198625..8f26b571 100644 --- a/image-recipe/configs/snippets/archipelago-https-app-proxies.conf +++ b/image-recipe/configs/snippets/archipelago-https-app-proxies.conf @@ -358,7 +358,7 @@ location /app/indeedhub/ { sub_filter '' ''; } location /app/nginx-proxy-manager/ { - proxy_pass http://127.0.0.1:81/; + proxy_pass http://127.0.0.1:8081/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/neode-ui/package-lock.json b/neode-ui/package-lock.json index d1ac4f83..07ead57d 100644 --- a/neode-ui/package-lock.json +++ b/neode-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "neode-ui", - "version": "1.7.56-alpha", + "version": "1.7.57-alpha", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "neode-ui", - "version": "1.7.56-alpha", + "version": "1.7.57-alpha", "dependencies": { "@types/dompurify": "^3.0.5", "@vue-leaflet/vue-leaflet": "^0.10.1", diff --git a/neode-ui/package.json b/neode-ui/package.json index db884a00..9639f80a 100644 --- a/neode-ui/package.json +++ b/neode-ui/package.json @@ -1,7 +1,7 @@ { "name": "neode-ui", "private": true, - "version": "1.7.56-alpha", + "version": "1.7.57-alpha", "type": "module", "scripts": { "start": "./start-dev.sh", diff --git a/neode-ui/src/stores/__tests__/appLauncher.test.ts b/neode-ui/src/stores/__tests__/appLauncher.test.ts index 9afd9cf8..baf36914 100644 --- a/neode-ui/src/stores/__tests__/appLauncher.test.ts +++ b/neode-ui/src/stores/__tests__/appLauncher.test.ts @@ -98,7 +98,7 @@ describe('useAppLauncherStore', () => { expect(store.panelAppId).toBe('btcpay-server') }) - it('opens Nginx Proxy Manager in new tab even when URL resolves', () => { + it('normalizes old Nginx Proxy Manager port 81 to 8081', () => { const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:81', title: 'Nginx Proxy Manager' }) @@ -106,7 +106,7 @@ describe('useAppLauncherStore', () => { expect(store.isOpen).toBe(false) expect(store.panelAppId).toBe(null) expect(mockWindowOpen).toHaveBeenCalledWith( - 'http://192.168.1.228:81', + 'http://192.168.1.228:8081', '_blank', 'noopener,noreferrer', ) @@ -125,13 +125,13 @@ describe('useAppLauncherStore', () => { expect(store.panelAppId).toBe(null) }) - it('normalizes legacy Nginx Proxy Manager port 8181 to 81', () => { + it('normalizes legacy Nginx Proxy Manager ports to 8081', () => { const store = useAppLauncherStore() store.open({ url: 'http://192.168.1.228:8181', title: 'Nginx Proxy Manager' }) expect(mockWindowOpen).toHaveBeenCalledWith( - 'http://192.168.1.228:81', + 'http://192.168.1.228:8081', '_blank', 'noopener,noreferrer', ) diff --git a/neode-ui/src/stores/appLauncher.ts b/neode-ui/src/stores/appLauncher.ts index 2fa7dd9d..e476ff34 100644 --- a/neode-ui/src/stores/appLauncher.ts +++ b/neode-ui/src/stores/appLauncher.ts @@ -61,8 +61,8 @@ function normalizeLaunchUrl(urlStr: string, appIdHint?: string | null): string { return rebuilt('3002') } - if (sameHost && appIdHint === 'nginx-proxy-manager' && u.port === '8181') { - return rebuilt('81') + if (sameHost && appIdHint === 'nginx-proxy-manager' && (u.port === '81' || u.port === '8181')) { + return rebuilt('8081') } return rewrittenLocalhost ? u.toString() : urlStr @@ -74,6 +74,7 @@ function normalizeLaunchUrl(urlStr: string, appIdHint?: string | null): string { /** Port → app ID for resolving URLs to AppSession routes */ const PORT_TO_APP_ID: Record = { '81': 'nginx-proxy-manager', + '8081': 'nginx-proxy-manager', '8181': 'nginx-proxy-manager', '3000': 'grafana', '3002': 'uptime-kuma', diff --git a/neode-ui/src/views/AppDetails.vue b/neode-ui/src/views/AppDetails.vue index 4c0192cb..dfa9c81d 100644 --- a/neode-ui/src/views/AppDetails.vue +++ b/neode-ui/src/views/AppDetails.vue @@ -141,6 +141,7 @@ import AppHeroSection from './appDetails/AppHeroSection.vue' import AppContentSection from './appDetails/AppContentSection.vue' import AppSidebar from './appDetails/AppSidebar.vue' import { resolveAppUrl } from './appSession/appSessionConfig' +import { isWebsitePackage, resolveRuntimeLaunchUrl } from './apps/appsConfig' import { WEB_ONLY_APP_URLS, PACKAGE_ALIASES, @@ -294,6 +295,18 @@ function launchApp() { return } + if (isWebsitePackage(id, pkg.value)) { + const url = resolveRuntimeLaunchUrl(pkg.value) + if (url) window.open(url, '_blank', 'noopener,noreferrer') + return + } + + const runtimeUrl = resolveRuntimeLaunchUrl(pkg.value) + if (runtimeUrl) { + useAppLauncherStore().open({ url: runtimeUrl, title: pkg.value.manifest.title }) + return + } + // Container apps should launch through session routing so protocol/path // handling stays centralized in appSessionConfig. useAppLauncherStore().openSession(id) diff --git a/neode-ui/src/views/Apps.vue b/neode-ui/src/views/Apps.vue index f2de426a..d13599be 100644 --- a/neode-ui/src/views/Apps.vue +++ b/neode-ui/src/views/Apps.vue @@ -7,7 +7,7 @@
App Store - +
diff --git a/neode-ui/src/views/dashboard/useRouteTransitions.ts b/neode-ui/src/views/dashboard/useRouteTransitions.ts index fd6fae63..84703acd 100644 --- a/neode-ui/src/views/dashboard/useRouteTransitions.ts +++ b/neode-ui/src/views/dashboard/useRouteTransitions.ts @@ -96,8 +96,8 @@ export function useRouteTransitions() { // Mobile: Horizontal slide transitions between sub-tabs if (typeof window !== 'undefined' && window.innerWidth < 768) { - const isServices = currentPath === '/dashboard/apps' && currentRoute.query.tab === 'services' - const wasServices = previousTab === 'services' + const isServices = currentPath === '/dashboard/apps' && (currentRoute.query.tab === 'services' || currentRoute.query.tab === 'websites') + const wasServices = previousTab === 'services' || previousTab === 'websites' const currentAppsIdx = isServices ? 2 : currentPath === '/dashboard/marketplace' ? 1 : currentPath === '/dashboard/apps' ? 0 : -1 diff --git a/neode-ui/vite.config.ts b/neode-ui/vite.config.ts index 6c84b093..43f26366 100644 --- a/neode-ui/vite.config.ts +++ b/neode-ui/vite.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ }, workbox: { navigateFallbackDenylist: [/^\/app\//, /^\/rpc\//, /^\/ws/, /^\/aiui\//], + cleanupOutdatedCaches: true, globPatterns: ['**/*.{js,css,html,ico,png,svg,jpg,jpeg,mp4,webp}'], globIgnores: [ '**/*-backup-*.mp4', diff --git a/release-manifest.json b/release-manifest.json index c4873bc6..8047cfac 100644 --- a/release-manifest.json +++ b/release-manifest.json @@ -1,34 +1,29 @@ { - "version": "1.7.56-alpha", - "release_date": "2026-05-15", + "version": "1.7.57-alpha", + "release_date": "2026-05-17", "changelog": [ - "Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.", - "Fresh installs now include the full Wi-Fi userspace stack (`wpasupplicant`, `wireless-regdb`, `iw`, `rfkill`, `polkitd`, `pciutils`, and `usbutils`) so NetworkManager can scan and connect with Intel Wi-Fi cards out of the box.", - "The installed system now grants the `archipelago` service user explicit NetworkManager PolicyKit access for web-triggered Wi-Fi scans and connection changes.", - "Wi-Fi connect now replaces stale/partial NetworkManager profiles and creates an explicit WPA-PSK profile with the supplied password, avoiding no-secret retry failures after a failed attempt.", - "Settings password changes now update the Linux/SSH password through non-interactive sudo, so the web password and SSH password stay in sync when the checkbox is enabled.", - "Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.", - "Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.", - "Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates.", - "Container reconcile force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.", - "Container health reporting is honest for running containers: Archipelago surfaces Podman's actual health state instead of marking every running container healthy." + "Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons.", + "App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied.", + "Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs.", + "Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI.", + "Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory." ], "components": [ { "name": "archipelago", - "current_version": "1.7.56-alpha", - "new_version": "1.7.56-alpha", - "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago", - "sha256": "f6c54cc7fbaac3dde97b1a719a6d380ce734a4d52366d4579770effadef92c9c", - "size_bytes": 42862944 + "current_version": "1.7.57-alpha", + "new_version": "1.7.57-alpha", + "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago", + "sha256": "e96d657c28f84c72c1358fdbbc014184530763341a2d02242b3a479f36dc2aef", + "size_bytes": 42647224 }, { - "name": "archipelago-frontend-1.7.56-alpha.tar.gz", - "current_version": "1.7.56-alpha", - "new_version": "1.7.56-alpha", - "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz", - "sha256": "91bfa10085696921c929ab7d216a8e617eb02e7105f21f32cdc3a2ba15158cd4", - "size_bytes": 166465069 + "name": "archipelago-frontend-1.7.57-alpha.tar.gz", + "current_version": "1.7.57-alpha", + "new_version": "1.7.57-alpha", + "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago-frontend-1.7.57-alpha.tar.gz", + "sha256": "a09c1507afb4b7e9f5279b92215e92ebc0b86155ae53ac2c0baaecd07522c7ee", + "size_bytes": 78322316 } ] } diff --git a/releases/manifest.json b/releases/manifest.json index c4873bc6..8047cfac 100644 --- a/releases/manifest.json +++ b/releases/manifest.json @@ -1,34 +1,29 @@ { - "version": "1.7.56-alpha", - "release_date": "2026-05-15", + "version": "1.7.57-alpha", + "release_date": "2026-05-17", "changelog": [ - "Health notifications now clear when an app is no longer unhealthy, including stale alerts for removed containers such as Portainer.", - "Fresh installs now include the full Wi-Fi userspace stack (`wpasupplicant`, `wireless-regdb`, `iw`, `rfkill`, `polkitd`, `pciutils`, and `usbutils`) so NetworkManager can scan and connect with Intel Wi-Fi cards out of the box.", - "The installed system now grants the `archipelago` service user explicit NetworkManager PolicyKit access for web-triggered Wi-Fi scans and connection changes.", - "Wi-Fi connect now replaces stale/partial NetworkManager profiles and creates an explicit WPA-PSK profile with the supplied password, avoiding no-secret retry failures after a failed attempt.", - "Settings password changes now update the Linux/SSH password through non-interactive sudo, so the web password and SSH password stay in sync when the checkbox is enabled.", - "Quadlet environment values with spaces or shell metacharacters are quoted consistently, preventing env drift recreate loops for apps like nostr-rs-relay and Grafana.", - "Boot/bootstrap reconcile avoids restarting running Bitcoin containers while repairing RPC config, preserving IBD progress on active nodes.", - "Exit code 137 is labeled as SIGKILL instead of assuming OOM, avoiding false OOM alerts for orchestrator-managed recreates.", - "Container reconcile force-recreates Podman records stuck in `Stopping`, preserving bind-mounted app data while recovering wedged containers automatically.", - "Container health reporting is honest for running containers: Archipelago surfaces Podman's actual health state instead of marking every running container healthy." + "Nginx Proxy Manager now avoids privileged rootless Podman host port `81`, preferring `8081:81` while host nginx keeps a compatibility proxy on `:81` for stale cached launch buttons.", + "App installs now allocate ports by checking live host bind availability, falling back to a free high port when preferred ports are already occupied.", + "Portainer-created launchable containers are separated into a `Websites` tab and launch through their discovered published host port instead of hard-coded app URLs.", + "Internal BuildKit helper containers such as `buildx_buildkit_default` are hidden from the Apps UI.", + "Portainer works out of the box on Debian 13/Podman installs by including `catatonit` and by preserving the Podman socket mount as a socket rather than creating it as a directory." ], "components": [ { "name": "archipelago", - "current_version": "1.7.56-alpha", - "new_version": "1.7.56-alpha", - "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago", - "sha256": "f6c54cc7fbaac3dde97b1a719a6d380ce734a4d52366d4579770effadef92c9c", - "size_bytes": 42862944 + "current_version": "1.7.57-alpha", + "new_version": "1.7.57-alpha", + "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago", + "sha256": "e96d657c28f84c72c1358fdbbc014184530763341a2d02242b3a479f36dc2aef", + "size_bytes": 42647224 }, { - "name": "archipelago-frontend-1.7.56-alpha.tar.gz", - "current_version": "1.7.56-alpha", - "new_version": "1.7.56-alpha", - "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.56-alpha/archipelago-frontend-1.7.56-alpha.tar.gz", - "sha256": "91bfa10085696921c929ab7d216a8e617eb02e7105f21f32cdc3a2ba15158cd4", - "size_bytes": 166465069 + "name": "archipelago-frontend-1.7.57-alpha.tar.gz", + "current_version": "1.7.57-alpha", + "new_version": "1.7.57-alpha", + "download_url": "http://146.59.87.168:3000/lfg2025/archy/releases/download/v1.7.57-alpha/archipelago-frontend-1.7.57-alpha.tar.gz", + "sha256": "a09c1507afb4b7e9f5279b92215e92ebc0b86155ae53ac2c0baaecd07522c7ee", + "size_bytes": 78322316 } ] } diff --git a/scripts/container-specs.sh b/scripts/container-specs.sh index 95537f21..aa6e22a5 100755 --- a/scripts/container-specs.sh +++ b/scripts/container-specs.sh @@ -99,6 +99,10 @@ reset_spec() { SPEC_ENTRYPOINT="" } +if ! declare -F alloc_port >/dev/null 2>&1; then + alloc_port() { printf '%s' "$2"; } +fi + # ── Tier 0: Databases ──────────────────────────────────────────────── load_spec_archy-mempool-db() { @@ -493,13 +497,17 @@ load_spec_nginx-proxy-manager() { reset_spec SPEC_NAME="nginx-proxy-manager" SPEC_IMAGE="${NPM_IMAGE}" - SPEC_PORTS="81:81 8084:80 8444:443" + local admin_port http_port https_port + admin_port=$(alloc_port nginx-proxy-manager 8081 81) + http_port=$(alloc_port nginx-proxy-manager-http 8084 80) + https_port=$(alloc_port nginx-proxy-manager-https 8444 443) + SPEC_PORTS="$admin_port:81 $http_port:80 $https_port:443" SPEC_VOLUMES="/var/lib/archipelago/nginx-proxy-manager/data:/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt" SPEC_MEMORY="$(mem_limit nginx-proxy-manager)" SPEC_HEALTH_CMD="curl -sf http://localhost:81/ || exit 1" SPEC_TIER="3" SPEC_DATA_DIR="/var/lib/archipelago/nginx-proxy-manager" - SPEC_CAPS="CHOWN SETUID SETGID NET_BIND_SERVICE" + SPEC_CAPS="CHOWN FOWNER SETUID SETGID DAC_OVERRIDE NET_BIND_SERVICE" SPEC_OPTIONAL="true" } diff --git a/scripts/deploy-tailscale.sh b/scripts/deploy-tailscale.sh index d3eeaa8e..26e901c2 100755 --- a/scripts/deploy-tailscale.sh +++ b/scripts/deploy-tailscale.sh @@ -914,7 +914,7 @@ LNDCONF --health-cmd 'curl -sf http://localhost:81/' --health-interval=30s --health-timeout=5s --health-retries=3 \ --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ - -p 81:81 -p 8084:80 -p 8444:443 \ + -p 8081:81 -p 8084:80 -p 8444:443 \ -v /var/lib/archipelago/nginx-proxy-manager/data:/data \ -v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \ $NPM_IMAGE diff --git a/scripts/first-boot-containers.sh b/scripts/first-boot-containers.sh index 3a6ddb80..217f63c9 100755 --- a/scripts/first-boot-containers.sh +++ b/scripts/first-boot-containers.sh @@ -48,6 +48,37 @@ SCRIPT_DIR_FBC="$(cd "$(dirname "$0")" && pwd)" # as root (rootful podman), the backend can't see them at all. DOCKER="runuser -u archipelago -- env XDG_RUNTIME_DIR=/run/user/$(id -u archipelago) podman" +PORT_ALLOC_FILE="/var/lib/archipelago/port-allocations.env" +mkdir -p /var/lib/archipelago 2>/dev/null || true +[ -f "$PORT_ALLOC_FILE" ] && . "$PORT_ALLOC_FILE" + +port_available() { + local port="$1" + ss -ltn 2>/dev/null | awk -v p=":$port" '$4 == p || $4 ~ p "$" { found=1 } END { exit found ? 1 : 0 }' +} + +alloc_port() { + local key="$1" preferred="$2" var="PORT_${key//[^A-Za-z0-9]/_}" cur="" + eval "cur=\${$var:-}" + if [ -n "$cur" ] && port_available "$cur"; then + printf '%s' "$cur" + return + fi + if port_available "$preferred"; then + cur="$preferred" + else + cur="" + for p in $(seq 8085 9999); do + if port_available "$p"; then cur="$p"; break; fi + done + fi + [ -n "$cur" ] || cur="$preferred" + if ! grep -q "^$var=" "$PORT_ALLOC_FILE" 2>/dev/null; then + printf '%s=%s\n' "$var" "$cur" >> "$PORT_ALLOC_FILE" + fi + printf '%s' "$cur" +} + # UNBUNDLED mode: only create FileBrowser, skip all other containers. # Users install apps on-demand from the Marketplace. UNBUNDLED_MARKER="/opt/archipelago/.unbundled" @@ -1146,12 +1177,15 @@ track_container "filebrowser" if ! $DOCKER ps --format '{{.Names}}' 2>/dev/null | grep -q nginx-proxy-manager; then log "Creating Nginx Proxy Manager..." mkdir -p /var/lib/archipelago/nginx-proxy-manager/data /var/lib/archipelago/nginx-proxy-manager/letsencrypt + NPM_ADMIN_PORT=$(alloc_port nginx-proxy-manager 8081) + NPM_HTTP_PORT=$(alloc_port nginx-proxy-manager-http 8084) + NPM_HTTPS_PORT=$(alloc_port nginx-proxy-manager-https 8444) $DOCKER run -d --name nginx-proxy-manager --restart unless-stopped \ --health-cmd="curl -sf http://localhost:81/ || exit 1" --health-interval=120s --health-timeout=5s --health-retries=3 \ --memory=$(mem_limit nginx-proxy-manager) \ - --cap-drop ALL --cap-add CHOWN --cap-add SETUID --cap-add SETGID --cap-add NET_BIND_SERVICE \ + --cap-drop ALL --cap-add CHOWN --cap-add FOWNER --cap-add SETUID --cap-add SETGID --cap-add DAC_OVERRIDE --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ - -p 81:81 -p 8084:80 -p 8444:443 \ + -p ${NPM_ADMIN_PORT}:81 -p ${NPM_HTTP_PORT}:80 -p ${NPM_HTTPS_PORT}:443 \ -v /var/lib/archipelago/nginx-proxy-manager/data:/data \ -v /var/lib/archipelago/nginx-proxy-manager/letsencrypt:/etc/letsencrypt \ "${NPM_IMAGE}" 2>>"$LOG" || true diff --git a/scripts/nginx-https-app-proxies.conf b/scripts/nginx-https-app-proxies.conf index a2dae9d7..20336c44 100644 --- a/scripts/nginx-https-app-proxies.conf +++ b/scripts/nginx-https-app-proxies.conf @@ -204,7 +204,7 @@ location /app/electrs-ui/ { proxy_hide_header Content-Security-Policy; } location /app/nginx-proxy-manager/ { - proxy_pass http://127.0.0.1:81/; + proxy_pass http://127.0.0.1:8081/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; diff --git a/scripts/reconcile-containers.sh b/scripts/reconcile-containers.sh index a60e93b5..73a58f68 100755 --- a/scripts/reconcile-containers.sh +++ b/scripts/reconcile-containers.sh @@ -63,6 +63,37 @@ header(){ echo -e "\n${BOLD}$*${NC}"; } source "$SCRIPT_DIR/container-specs.sh" || { echo "Cannot source container-specs.sh"; exit 1; } detect_environment +PORT_ALLOC_FILE="/var/lib/archipelago/port-allocations.env" +[ -f "$PORT_ALLOC_FILE" ] && . "$PORT_ALLOC_FILE" + +port_available() { + local port="$1" + ss -ltn 2>/dev/null | awk -v p=":$port" '$4 == p || $4 ~ p "$" { found=1 } END { exit found ? 1 : 0 }' +} + +alloc_port() { + local key="$1" preferred="$2" var="PORT_${key//[^A-Za-z0-9]/_}" cur="" + eval "cur=\${$var:-}" + if [ -n "$cur" ] && port_available "$cur"; then + printf '%s' "$cur" + return + fi + if port_available "$preferred"; then + cur="$preferred" + else + cur="" + for p in $(seq 8085 9999); do + if port_available "$p"; then cur="$p"; break; fi + done + fi + [ -n "$cur" ] || cur="$preferred" + sudo mkdir -p "$(dirname "$PORT_ALLOC_FILE")" 2>/dev/null || true + if ! grep -q "^$var=" "$PORT_ALLOC_FILE" 2>/dev/null; then + printf '%s=%s\n' "$var" "$cur" | sudo tee -a "$PORT_ALLOC_FILE" >/dev/null + fi + printf '%s' "$cur" +} + # ── Podman command ─────────────────────────────────────────────────── # Run as archipelago user — podman sees rootless containers directly. # Use sudo only for chown/mkdir operations. @@ -154,6 +185,39 @@ host_port_listening() { ' } +prepare_bind_source() { + local source="$1" + [ -n "$source" ] || return 0 + + case "$source" in + /run/user/*/podman/podman.sock) + if [ ! -S "$source" ]; then + local runtime_dir="${source%/podman/podman.sock}" + XDG_RUNTIME_DIR="$runtime_dir" systemctl --user start podman.socket 2>/dev/null || true + for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -S "$source" ] && return 0 + sleep 0.25 + done + fi + ;; + esac + + case "$source" in + /var/lib/archipelago/*) + sudo mkdir -p "$source" 2>/dev/null + ;; + *) + # Non-data bind mounts can be files/sockets/devices. Creating the full + # path would turn e.g. podman.sock into a directory and break Portainer. + if [ -e "$source" ]; then + return 0 + fi + fail "bind source missing: $source" + return 1 + ;; + esac +} + container_has_mount() { local name="$1" source="$2" target="$3" $PODMAN inspect "$name" --format '{{range .Mounts}}{{println .Source "|" .Destination}}{{end}}' 2>/dev/null \ @@ -536,7 +600,11 @@ reconcile() { else for v in $SPEC_VOLUMES; do local host_dir="${v%%:*}" - [ -n "$host_dir" ] && sudo mkdir -p "$host_dir" 2>/dev/null + prepare_bind_source "$host_dir" || { + COUNT_FAILED=$((COUNT_FAILED + 1)) + FAILED_LIST+=" $name" + return + } done if eval "$(build_run_cmd)" >/dev/null 2>&1; then fixed "$name — created" diff --git a/tests/lifecycle/remote-lifecycle.sh b/tests/lifecycle/remote-lifecycle.sh index de55dc57..55bbeb16 100755 --- a/tests/lifecycle/remote-lifecycle.sh +++ b/tests/lifecycle/remote-lifecycle.sh @@ -178,7 +178,7 @@ launch_url_for() { ollama) echo "http://${ARCHY_HOST}:11434/" ;; immich|immich_server) echo "http://${ARCHY_HOST}:2283/" ;; portainer) echo "http://${ARCHY_HOST}:9000/" ;; - nginx-proxy-manager) echo "http://${ARCHY_HOST}:81/" ;; + nginx-proxy-manager) echo "http://${ARCHY_HOST}:8081/" ;; tailscale) echo "http://${ARCHY_HOST}:8240/" ;; uptime-kuma) echo "http://${ARCHY_HOST}:3002/" ;; homeassistant) echo "http://${ARCHY_HOST}:8123/" ;;