chore: release v1.7.57-alpha
This commit is contained in:
parent
a322b04021
commit
7804223152
@ -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.
|
||||
|
||||
2
core/Cargo.lock
generated
2
core/Cargo.lock
generated
@ -80,7 +80,7 @@ checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "archipelago"
|
||||
version = "1.7.56-alpha"
|
||||
version = "1.7.57-alpha"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"archipelago-container",
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -153,11 +153,13 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
|
||||
"--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![
|
||||
|
||||
@ -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::<u16>().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<u16> {
|
||||
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::<u16>().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<u16> {
|
||||
"uptime-kuma" => Some(3002),
|
||||
"gitea" => Some(3001),
|
||||
"nextcloud" => Some(8085),
|
||||
"nginx-proxy-manager" => Some(81),
|
||||
"nginx-proxy-manager" => Some(8081),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<u16> {
|
||||
"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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<String> {
|
||||
}
|
||||
}
|
||||
|
||||
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),
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 \
|
||||
|
||||
@ -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
|
||||
|
||||
@ -741,7 +741,7 @@ server {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
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;
|
||||
|
||||
@ -358,7 +358,7 @@ location /app/indeedhub/ {
|
||||
sub_filter '</head>' '<script src="/nostr-provider.js"></script></head>';
|
||||
}
|
||||
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;
|
||||
|
||||
4
neode-ui/package-lock.json
generated
4
neode-ui/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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',
|
||||
)
|
||||
|
||||
@ -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<string, string> = {
|
||||
'81': 'nginx-proxy-manager',
|
||||
'8081': 'nginx-proxy-manager',
|
||||
'8181': 'nginx-proxy-manager',
|
||||
'3000': 'grafana',
|
||||
'3002': 'uptime-kuma',
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'apps' }" @click="activeTab = 'apps'; router.replace({ query: {} })">My Apps</button>
|
||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn">App Store</RouterLink>
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'services' }" @click="activeTab = 'services'; router.replace({ query: { tab: 'services' } })">Services</button>
|
||||
<button class="mode-switcher-btn" :class="{ 'mode-switcher-btn-active': activeTab === 'websites' }" @click="activeTab = 'websites'; router.replace({ query: { tab: 'websites' } })">Websites</button>
|
||||
</div>
|
||||
<div v-if="activeTab === 'apps' && categoriesWithApps.length > 1" class="mode-switcher flex-shrink-0">
|
||||
<button
|
||||
@ -155,7 +155,7 @@ import AppIconGrid from './apps/AppIconGrid.vue'
|
||||
import AppsUninstallModal from './apps/AppsUninstallModal.vue'
|
||||
import { useAppsActions } from './apps/useAppsActions'
|
||||
import {
|
||||
filterEntriesForTab, isWebOnlyApp,
|
||||
type AppsTab, filterEntriesForTab, isWebOnlyApp, isWebsitePackage, opensInTab, resolveRuntimeLaunchUrl,
|
||||
WEB_ONLY_APPS, buildAllCategories, useCategoriesWithApps,
|
||||
} from './apps/appsConfig'
|
||||
|
||||
@ -170,8 +170,8 @@ const actions = useAppsActions()
|
||||
const showStagger = !appsAnimationDone
|
||||
|
||||
// Tabs
|
||||
const activeTab = ref<'apps' | 'services'>(
|
||||
route.query.tab === 'services' ? 'services' : 'apps'
|
||||
const activeTab = ref<AppsTab>(
|
||||
route.query.tab === 'websites' || route.query.tab === 'services' ? 'websites' : 'apps'
|
||||
)
|
||||
|
||||
// Search (debounced)
|
||||
@ -285,6 +285,19 @@ function goToApp(id: string) {
|
||||
}
|
||||
|
||||
function launchApp(id: string) {
|
||||
const pkg = packages.value[id]
|
||||
if (pkg && isWebsitePackage(id, pkg)) {
|
||||
const url = resolveRuntimeLaunchUrl(pkg)
|
||||
if (url) window.open(url, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
if (pkg && opensInTab(id)) {
|
||||
const url = resolveRuntimeLaunchUrl(pkg)
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
}
|
||||
useAppLauncherStore().openSession(id)
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
||||
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
|
||||
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
|
||||
</div>
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink to="/dashboard/apps" class="mode-switcher-btn">My Apps</RouterLink>
|
||||
<RouterLink to="/dashboard/discover" class="mode-switcher-btn mode-switcher-btn-active">App Store</RouterLink>
|
||||
<RouterLink to="/dashboard/apps?tab=services" class="mode-switcher-btn">Services</RouterLink>
|
||||
<RouterLink to="/dashboard/apps?tab=websites" class="mode-switcher-btn">Websites</RouterLink>
|
||||
</div>
|
||||
<div class="mode-switcher flex-shrink-0">
|
||||
<RouterLink
|
||||
|
||||
@ -83,7 +83,7 @@ export const APP_URLS: Record<string, { dev: string; prod: string }> = {
|
||||
'photoprism': { dev: 'http://localhost:2342', prod: 'http://localhost:2342' },
|
||||
'immich': { dev: 'http://localhost:2283', prod: 'http://localhost:2283' },
|
||||
'filebrowser': { dev: 'http://localhost:8083', prod: 'http://localhost:8083' },
|
||||
'nginx-proxy-manager': { dev: 'http://localhost:81', prod: 'http://localhost:81' },
|
||||
'nginx-proxy-manager': { dev: 'http://localhost:8081', prod: 'http://localhost:8081' },
|
||||
'gitea': { dev: 'http://localhost:3001', prod: 'http://localhost:3001' },
|
||||
'portainer': { dev: 'http://localhost:9000', prod: 'http://localhost:9000' },
|
||||
'uptime-kuma': { dev: 'http://localhost:3002', prod: 'http://localhost:3002' },
|
||||
|
||||
@ -31,7 +31,7 @@ export const APP_PORTS: Record<string, number> = {
|
||||
'immich': 2283,
|
||||
'immich_server': 2283,
|
||||
'filebrowser': 8083,
|
||||
'nginx-proxy-manager': 81,
|
||||
'nginx-proxy-manager': 8081,
|
||||
'gitea': 3001,
|
||||
'portainer': 9000,
|
||||
'tailscale': 8240,
|
||||
|
||||
@ -79,7 +79,7 @@ import { useServerStore } from '@/stores/server'
|
||||
import { useAppLauncherStore } from '@/stores/appLauncher'
|
||||
import type { PackageDataEntry } from '@/types/api'
|
||||
import { resolveAppUrl } from '@/views/appSession/appSessionConfig'
|
||||
import { canLaunch, handleImageError, opensInTab, resolveAppIcon } from './appsConfig'
|
||||
import { canLaunch, handleImageError, isWebsitePackage, opensInTab, resolveAppIcon, resolveRuntimeLaunchUrl } from './appsConfig'
|
||||
import { getCuratedAppList } from '../discover/curatedApps'
|
||||
|
||||
const ITEMS_PER_PAGE = 16 // 4 columns x 4 rows
|
||||
@ -120,8 +120,15 @@ function getIcon(id: string, pkg: PackageDataEntry): string {
|
||||
|
||||
function handleTap(id: string, pkg: PackageDataEntry) {
|
||||
if (canLaunch(pkg)) {
|
||||
if (isWebsitePackage(id, pkg)) {
|
||||
const url = resolveRuntimeLaunchUrl(pkg)
|
||||
if (url) {
|
||||
window.open(url, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
}
|
||||
}
|
||||
if (opensInTab(id)) {
|
||||
const appUrl = resolveAppUrl(id)
|
||||
const appUrl = resolveRuntimeLaunchUrl(pkg) || resolveAppUrl(id)
|
||||
if (appUrl) {
|
||||
window.open(appUrl, '_blank', 'noopener,noreferrer')
|
||||
return
|
||||
|
||||
@ -5,6 +5,8 @@ import { computed } from 'vue'
|
||||
import { PackageState, type PackageDataEntry } from '@/types/api'
|
||||
import { resolveAppUrl } from '../appSession/appSessionConfig'
|
||||
|
||||
export type AppsTab = 'apps' | 'websites' | 'services'
|
||||
|
||||
// Service container name patterns (backend/infra, not user-facing)
|
||||
export const SERVICE_NAMES = new Set([
|
||||
'dwn', 'archy-mempool-db', 'archy-btcpay-db', 'archy-nbxplorer', 'archy-tor',
|
||||
@ -22,6 +24,15 @@ export const SERVICE_NAMES = new Set([
|
||||
'syntropy-institute', 't-zero', 'arch-presentation',
|
||||
])
|
||||
|
||||
const INTERNAL_TOOLING_NAMES = new Set([
|
||||
'buildx_buildkit_default',
|
||||
])
|
||||
|
||||
export function isInternalToolingPackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||
const manifestId = pkg?.manifest?.id || ''
|
||||
return INTERNAL_TOOLING_NAMES.has(id) || INTERNAL_TOOLING_NAMES.has(manifestId) || id.startsWith('buildx_buildkit') || manifestId.startsWith('buildx_buildkit')
|
||||
}
|
||||
|
||||
export function isServiceContainer(id: string): boolean {
|
||||
if (SERVICE_NAMES.has(id)) return true
|
||||
if (id.startsWith('indeedhub-build_')) return true
|
||||
@ -58,14 +69,32 @@ export function getAppCategory(id: string, pkg: PackageDataEntry): string {
|
||||
return cat || 'other'
|
||||
}
|
||||
|
||||
export function runtimeLanAddress(pkg: PackageDataEntry): string {
|
||||
return pkg.installed?.['interface-addresses']?.main?.['lan-address'] || ''
|
||||
}
|
||||
|
||||
export function isKnownApp(id: string, pkg?: PackageDataEntry): boolean {
|
||||
const manifestId = pkg?.manifest?.id
|
||||
return !!(APP_CATEGORY_MAP[id] || (manifestId && APP_CATEGORY_MAP[manifestId]) || isWebOnlyApp(id))
|
||||
}
|
||||
|
||||
export function isWebsitePackage(id: string, pkg?: PackageDataEntry): boolean {
|
||||
if (isInternalToolingPackage(id, pkg)) return false
|
||||
if (isServicePackage(id, pkg)) return true
|
||||
if (isKnownApp(id, pkg)) return false
|
||||
return !!pkg && !!runtimeLanAddress(pkg)
|
||||
}
|
||||
|
||||
export function filterEntriesForTab(
|
||||
entries: Array<[string, PackageDataEntry]>,
|
||||
activeTab: 'apps' | 'services',
|
||||
activeTab: AppsTab,
|
||||
selectedCategory: string,
|
||||
): Array<[string, PackageDataEntry]> {
|
||||
return entries.filter(([id, pkg]) => {
|
||||
const isSvc = isServicePackage(id, pkg)
|
||||
if (activeTab === 'services' ? !isSvc : isSvc) return false
|
||||
if (isInternalToolingPackage(id, pkg)) return false
|
||||
const wantsWebsites = activeTab === 'websites' || activeTab === 'services'
|
||||
const isWebsite = isWebsitePackage(id, pkg)
|
||||
if (wantsWebsites ? !isWebsite : isWebsite) return false
|
||||
if (activeTab === 'apps' && selectedCategory !== 'all') {
|
||||
return getAppCategory(id, pkg) === selectedCategory
|
||||
}
|
||||
@ -151,6 +180,12 @@ export function canLaunch(pkg: PackageDataEntry): boolean {
|
||||
return !!hasUI && pkg.state === 'running' && pkg.health !== 'starting' && pkg.health !== 'unhealthy'
|
||||
}
|
||||
|
||||
export function resolveRuntimeLaunchUrl(pkg: PackageDataEntry): string {
|
||||
const addr = runtimeLanAddress(pkg)
|
||||
if (!addr || typeof window === 'undefined') return addr
|
||||
return addr.replace(/^http:\/\/(localhost|127\.0\.0\.1)(?=[:/]|$)/, `http://${window.location.hostname}`)
|
||||
}
|
||||
|
||||
export function getStatusClass(state: PackageState, health?: string | null, exitCode?: number | null): string {
|
||||
if (state === PackageState.Running && health === 'starting') return 'bg-yellow-500/20 text-yellow-200'
|
||||
if (state === PackageState.Running && health === 'unhealthy') return 'bg-orange-500/20 text-orange-200'
|
||||
@ -212,7 +247,7 @@ export function useCategoriesWithApps(
|
||||
allCategories: Ref<Array<{ id: string; name: string }>>,
|
||||
) {
|
||||
return computed(() => {
|
||||
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isServicePackage(id, pkg))
|
||||
const entries = Object.entries(packages.value).filter(([id, pkg]) => !isWebsitePackage(id, pkg) && !isInternalToolingPackage(id, pkg))
|
||||
return allCategories.value.filter(cat => {
|
||||
if (cat.id === 'all') return true
|
||||
return entries.some(([id, pkg]) => getAppCategory(id, pkg) === cat.id)
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<RouterLink
|
||||
to="/dashboard/apps"
|
||||
class="mode-switcher-btn"
|
||||
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' }"
|
||||
:class="{ 'mode-switcher-btn-active': (route.path === '/dashboard/apps' || route.path.startsWith('/dashboard/apps/')) && route.query.tab !== 'services' && route.query.tab !== 'websites' }"
|
||||
@click.prevent="router.push({ path: '/dashboard/apps', query: {} })"
|
||||
>My Apps</RouterLink>
|
||||
<RouterLink
|
||||
@ -19,10 +19,10 @@
|
||||
:class="{ 'mode-switcher-btn-active': route.path === '/dashboard/marketplace' || route.path.startsWith('/dashboard/marketplace/') || route.path === '/dashboard/discover' }"
|
||||
>App Store</RouterLink>
|
||||
<RouterLink
|
||||
to="/dashboard/apps?tab=services"
|
||||
to="/dashboard/apps?tab=websites"
|
||||
class="mode-switcher-btn"
|
||||
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' }"
|
||||
>Services</RouterLink>
|
||||
:class="{ 'mode-switcher-btn-active': route.query.tab === 'services' || route.query.tab === 'websites' }"
|
||||
>Websites</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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/" ;;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user