fix(apps): self-host netbird and stabilize app sessions

This commit is contained in:
archipelago 2026-05-19 15:52:50 -04:00
parent 881779005a
commit ab96c97cb9
17 changed files with 272 additions and 47 deletions

View File

@ -1,5 +1,13 @@
# Changelog
## v1.7.70-alpha (2026-05-19)
- NetBird is being corrected from the peer/client daemon image to the self-hosted NetBird control-plane stack with a launchable dashboard on port `8087`, a combined management/signal/relay server on `8086`, and STUN on UDP `3478`.
- App sessions now always launch local apps through direct host ports and carry an explicit dashboard return target, so closing an iframe returns to the launching dashboard screen instead of falling through to browser history or a 404.
- Mobile app launches ignore stale desktop panel state and route into the full app-session webview consistently.
- The desktop sidebar now pins the logo/version at the top and controller/online/mode controls at the bottom, with only the navigation section scrolling on shorter screens.
- Validation passed with catalog JSON checks, `scripts/image-versions.sh` syntax check, `npm run type-check`, `cargo fmt --all --check --manifest-path core/Cargo.toml`, and `cargo check -p archipelago --manifest-path core/Cargo.toml`.
## v1.7.69-alpha (2026-05-19)
- App installs now allow up to 10 minutes for the initial `package.install` RPC to return, matching slow container image pulls and preventing apps from disappearing from My Apps while the backend is still pulling or retrying mirrors.

View File

@ -279,17 +279,17 @@
"id": "netbird",
"title": "NetBird",
"version": "0.71.2",
"description": "WireGuard mesh VPN client for secure remote access through NetBird Cloud or a self-hosted management server.",
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN service.",
"icon": "/assets/img/app-icons/netbird.svg",
"author": "NetBird",
"category": "networking",
"tier": "recommended",
"dockerImage": "docker.io/netbirdio/netbird:0.71.2",
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
"repoUrl": "https://github.com/netbirdio/netbird",
"containerConfig": {
"ports": ["8087:80", "8086:80", "3478:3478/udp"],
"volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
"env": ["NB_SETUP_KEY=", "NB_MANAGEMENT_URL="],
"args": ["up"]
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
}
},
{

View File

@ -219,7 +219,7 @@ pub(super) fn get_app_capabilities(app_id: &str) -> Vec<String> {
],
// VPN/mesh daemons need TUN + NET_ADMIN.
// Note: --device=/dev/net/tun is added separately in install.rs
"nostr-vpn" | "fips" | "netbird" => vec![
"nostr-vpn" | "fips" => vec![
"--cap-add=NET_ADMIN".to_string(),
"--cap-add=NET_RAW".to_string(),
],
@ -329,7 +329,6 @@ pub(super) fn get_health_check_args(app_id: &str, _rpc_pass: &str) -> Vec<String
"3",
),
"nostr-vpn" => ("nvpn status || exit 1", "30s", "3"),
"netbird" => ("netbird status || exit 1", "30s", "3"),
"fips" => ("fipsctl status || exit 1", "30s", "3"),
_ => return vec![],
};
@ -390,7 +389,7 @@ pub(super) fn get_memory_limit(app_id: &str) -> &'static str {
"nostr-rs-relay" | "nostr-relay" => "256m",
"routstr" => "512m",
"nostr-vpn" => "256m",
"netbird" => "256m",
"netbird" => "1g",
"fips" => "256m",
"nginx-proxy-manager" => "256m",
// Databases
@ -496,6 +495,7 @@ pub(super) fn all_container_names(package_id: &str) -> Vec<String> {
"indeedhub-ffmpeg".into(),
"indeedhub".into(),
],
"netbird" => vec!["netbird".into(), "netbird-server".into()],
"nostr-vpn" => vec![
"nostr-vpn".into(),
"archy-nostr-vpn".into(),
@ -584,6 +584,7 @@ pub(super) fn get_data_dirs_for_app(package_id: &str) -> Vec<String> {
format!("{}/penpot-assets", base),
format!("{}/penpot-postgres", base),
],
"netbird" => vec![format!("{}/netbird", base)],
_ => vec![format!("{}/{}", base, package_id)],
}
}

View File

@ -241,6 +241,9 @@ impl RpcHandler {
if package_id == "indeedhub" {
return self.install_indeedhub_stack().await;
}
if package_id == "netbird" {
return self.install_netbird_stack().await;
}
// Dependency checks. Prefer the scanner's cached package state so a
// congested Podman API does not turn an already-running dependency into
@ -552,7 +555,6 @@ impl RpcHandler {
"uptime-kuma"
| "gitea"
| "tailscale"
| "netbird"
| "vaultwarden"
| "homeassistant"
| "home-assistant"
@ -627,10 +629,6 @@ impl RpcHandler {
run_args.push("--tmpfs=/tmp:rw,exec,size=256m");
}
if package_id == "netbird" {
run_args.push("--device=/dev/net/tun:/dev/net/tun");
}
// Create data directories (mkdir only — chown happens AFTER config files are written)
for volume in &volumes {
if let Some(host_path) = volume.split(':').next() {

View File

@ -6,6 +6,7 @@
use crate::api::rpc::RpcHandler;
use crate::data_model::InstallPhase;
use anyhow::{Context, Result};
use base64::Engine;
use tracing::info;
use super::install::{install_log, patch_indeedhub_nostr_provider};
@ -310,6 +311,9 @@ fn mempool_stack_app_ids() -> &'static [&'static str] {
const REGISTRY: &str = "146.59.87.168:3000/lfg2025";
const NETBIRD_DASHBOARD_IMAGE: &str = "docker.io/netbirdio/dashboard:v2.38.0";
const NETBIRD_SERVER_IMAGE: &str = "docker.io/netbirdio/netbird-server:0.71.2";
/// Pull an image with retry and exponential backoff (3 attempts).
async fn pull_image_with_retry(image: &str) -> Result<()> {
const MAX_ATTEMPTS: u32 = 3;
@ -1357,6 +1361,187 @@ impl RpcHandler {
"message": "IndeedHub stack installed (7 containers)",
}))
}
/// Install self-hosted NetBird (dashboard + combined management/signal/relay server).
pub(super) async fn install_netbird_stack(&self) -> Result<serde_json::Value> {
if let Some(adopted) =
adopt_stack_if_exists("netbird", "netbird", &["netbird", "netbird-server"]).await?
{
return Ok(adopted);
}
install_log("INSTALL START: netbird stack (dashboard + server)").await;
info!("Installing self-hosted NetBird stack");
self.set_install_phase("netbird", InstallPhase::PullingImage)
.await;
for (i, image) in [NETBIRD_DASHBOARD_IMAGE, NETBIRD_SERVER_IMAGE]
.iter()
.enumerate()
{
self.set_install_progress("netbird", i as u64, 2).await;
pull_image_with_retry(image)
.await
.with_context(|| format!("Failed to pull NetBird image: {}", image))?;
}
self.set_install_progress("netbird", 2, 2).await;
for name in ["netbird", "netbird-server"] {
let _ = tokio::process::Command::new("podman")
.args(["rm", "-f", name])
.status()
.await;
}
let _ = tokio::process::Command::new("podman")
.args(["network", "rm", "-f", "netbird-net"])
.status()
.await;
self.set_install_phase("netbird", InstallPhase::CreatingContainer)
.await;
tokio::fs::create_dir_all("/var/lib/archipelago/netbird")
.await
.context("Failed to create NetBird data directory")?;
let host_ip = self.config.host_ip.clone();
let dashboard_origin = format!("http://{}:8087", host_ip);
let mgmt_origin = format!("http://{}:8086", host_ip);
let relay_secret = read_or_generate_b64_secret("netbird-relay-auth-secret").await;
let encryption_key = read_or_generate_b64_secret("netbird-store-encryption-key").await;
let config = format!(
r#"server:
listenAddress: ":80"
exposedAddress: "{mgmt_origin}"
stunPorts:
- 3478
metricsPort: 9090
healthcheckAddress: ":9000"
logLevel: "info"
logFile: "console"
authSecret: "{relay_secret}"
dataDir: "/var/lib/netbird"
auth:
issuer: "{mgmt_origin}/oauth2"
localAuthDisabled: false
signKeyRefreshEnabled: true
dashboardRedirectURIs:
- "{dashboard_origin}/nb-auth"
- "{dashboard_origin}/nb-silent-auth"
cliRedirectURIs:
- "http://localhost:53000/"
store:
engine: "sqlite"
encryptionKey: "{encryption_key}"
"#
);
tokio::fs::write("/var/lib/archipelago/netbird/config.yaml", config)
.await
.context("Failed to write NetBird config.yaml")?;
let dashboard_env = format!(
r#"NETBIRD_MGMT_API_ENDPOINT={mgmt_origin}
NETBIRD_MGMT_GRPC_API_ENDPOINT={mgmt_origin}
AUTH_AUDIENCE=netbird-dashboard
AUTH_CLIENT_ID=netbird-dashboard
AUTH_CLIENT_SECRET=
AUTH_AUTHORITY={mgmt_origin}/oauth2
USE_AUTH0=false
AUTH_SUPPORTED_SCOPES=openid profile email groups
AUTH_REDIRECT_URI=/nb-auth
AUTH_SILENT_REDIRECT_URI=/nb-silent-auth
NGINX_SSL_PORT=443
LETSENCRYPT_DOMAIN=none
"#
);
tokio::fs::write("/var/lib/archipelago/netbird/dashboard.env", dashboard_env)
.await
.context("Failed to write NetBird dashboard.env")?;
let _ = tokio::process::Command::new("podman")
.args(["network", "create", "netbird-net"])
.status()
.await;
let mut server_cmd = tokio::process::Command::new("podman");
server_cmd.args([
"run",
"-d",
"--name",
"netbird-server",
"--network",
"netbird-net",
"--network-alias",
"netbird-server",
"--restart=unless-stopped",
"-p",
"8086:80",
"-p",
"3478:3478/udp",
"-v",
"/var/lib/archipelago/netbird/data:/var/lib/netbird",
"-v",
"/var/lib/archipelago/netbird/config.yaml:/etc/netbird/config.yaml:ro",
NETBIRD_SERVER_IMAGE,
"--config",
"/etc/netbird/config.yaml",
]);
run_required_stack_command("netbird", "create server", &mut server_cmd).await?;
self.set_install_phase("netbird", InstallPhase::StartingContainer)
.await;
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
let mut dashboard_cmd = tokio::process::Command::new("podman");
dashboard_cmd.args([
"run",
"-d",
"--name",
"netbird",
"--network",
"netbird-net",
"--restart=unless-stopped",
"-p",
"8087:80",
"--env-file",
"/var/lib/archipelago/netbird/dashboard.env",
NETBIRD_DASHBOARD_IMAGE,
]);
run_required_stack_command("netbird", "create dashboard", &mut dashboard_cmd).await?;
wait_for_stack_containers("netbird", &["netbird-server", "netbird"], 60).await?;
self.set_install_phase("netbird", InstallPhase::WaitingHealthy)
.await;
self.set_install_phase("netbird", InstallPhase::PostInstall)
.await;
self.set_install_phase("netbird", InstallPhase::Done).await;
self.clear_install_progress("netbird").await;
install_log("INSTALL OK: netbird stack").await;
info!("NetBird stack installed");
Ok(serde_json::json!({
"success": true,
"package_id": "netbird",
"message": "NetBird self-hosted stack installed",
}))
}
}
async fn read_or_generate_b64_secret(name: &str) -> String {
let path = format!("/var/lib/archipelago/secrets/{}", name);
if let Ok(val) = tokio::fs::read_to_string(&path).await {
let trimmed = val.trim().to_string();
if !trimmed.is_empty() {
return trimmed;
}
}
let mut buf = [0u8; 32];
rand::RngCore::fill_bytes(&mut rand::rngs::OsRng, &mut buf);
let secret = base64::engine::general_purpose::STANDARD.encode(buf);
let _ = tokio::fs::create_dir_all("/var/lib/archipelago/secrets").await;
let _ = tokio::fs::write(&path, &secret).await;
secret
}
#[cfg(test)]

View File

@ -61,6 +61,7 @@ impl DockerPackageScanner {
"indeedhub-build_minio-init_1",
"indeedhub-build_relay_1",
"indeedhub-build_ffmpeg-worker_1",
"netbird-server",
"buildx_buildkit_default",
];
@ -481,7 +482,7 @@ fn get_app_metadata(app_id: &str) -> AppMetadata {
},
"netbird" => AppMetadata {
title: "NetBird".to_string(),
description: "WireGuard mesh VPN client for secure remote access".to_string(),
description: "Self-hosted WireGuard mesh VPN control plane and dashboard".to_string(),
icon: "/assets/img/app-icons/netbird.svg".to_string(),
repo: "https://github.com/netbirdio/netbird".to_string(),
tier: "",

View File

@ -168,7 +168,8 @@ fn image_var_for_app(app_id: &str) -> Option<&'static str> {
"nginx-proxy-manager" => Some("NPM_IMAGE"),
"portainer" => Some("PORTAINER_IMAGE"),
"tailscale" => Some("TAILSCALE_IMAGE"),
"netbird" => Some("NETBIRD_IMAGE"),
"netbird" => Some("NETBIRD_DASHBOARD_IMAGE"),
"netbird-server" => Some("NETBIRD_SERVER_IMAGE"),
// Fedimint
"fedimint" | "fedimintd" => Some("FEDIMINT_IMAGE"),
@ -300,6 +301,10 @@ pub fn containers_for_stack(app_id: &str) -> Vec<(&'static str, &'static str)> {
("penpot-exporter", "PENPOT_EXPORTER_IMAGE"),
("penpot-frontend", "PENPOT_FRONTEND_IMAGE"),
],
"netbird" => vec![
("netbird", "NETBIRD_DASHBOARD_IMAGE"),
("netbird-server", "NETBIRD_SERVER_IMAGE"),
],
_ => vec![],
}
}

View File

@ -279,17 +279,17 @@
"id": "netbird",
"title": "NetBird",
"version": "0.71.2",
"description": "WireGuard mesh VPN client for secure remote access through NetBird Cloud or a self-hosted management server.",
"description": "Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN service.",
"icon": "/assets/img/app-icons/netbird.svg",
"author": "NetBird",
"category": "networking",
"tier": "recommended",
"dockerImage": "docker.io/netbirdio/netbird:0.71.2",
"dockerImage": "docker.io/netbirdio/dashboard:v2.38.0",
"repoUrl": "https://github.com/netbirdio/netbird",
"containerConfig": {
"ports": ["8087:80", "8086:80", "3478:3478/udp"],
"volumes": ["/var/lib/archipelago/netbird:/var/lib/netbird"],
"env": ["NB_SETUP_KEY=", "NB_MANAGEMENT_URL="],
"args": ["up"]
"notes": "Installed as a two-container stack: netbird dashboard on 8087 and netbird-server control plane on 8086 plus UDP 3478. For production clients, publish a DNS name over HTTPS with gRPC/WebSocket routing."
}
},
{

View File

@ -93,6 +93,8 @@ const PORT_TO_APP_ID: Record<string, string> = {
'8334': 'bitcoin-knots',
'8888': 'searxng',
'9000': 'portainer',
'8087': 'netbird',
'8086': 'netbird',
'9980': 'onlyoffice',
'11434': 'ollama',
'2283': 'immich',
@ -151,13 +153,20 @@ export const useAppLauncherStore = defineStore('appLauncher', () => {
const panelAppId = ref<string | null>(null)
/** Open app in session view — panel mode uses store, overlay/fullscreen uses route */
function dashboardReturnPath(): string {
const current = router.currentRoute.value
const fullPath = current.fullPath || '/dashboard/apps'
if (!fullPath.startsWith('/dashboard') || current.name === 'app-session') return '/dashboard/apps'
return fullPath
}
function openSession(appId: string) {
const mode = localStorage.getItem(DISPLAY_MODE_KEY) || 'panel'
if (mode === 'panel' && !isMobileViewport()) {
panelAppId.value = appId
} else {
panelAppId.value = null
router.push({ name: 'app-session', params: { appId } })
router.push({ name: 'app-session', params: { appId }, query: { returnTo: dashboardReturnPath() } })
}
}

View File

@ -165,11 +165,6 @@ function closeRouteSession() {
const fallbackPath = typeof fallback === 'string' && fallback.startsWith('/dashboard')
? fallback
: '/dashboard/apps'
const previous = router.options.history.state.back
if (typeof previous === 'string' && previous.startsWith('/dashboard') && router.resolve(previous).name !== 'app-session') {
router.back()
return
}
router.replace(fallbackPath).catch(() => {})
}
@ -193,7 +188,8 @@ function setMode(mode: DisplayMode) {
if (isInlinePanel.value && mode !== 'panel') {
const id = appId.value
emit('close')
router.push({ name: 'app-session', params: { appId: id } })
const returnTo = route.fullPath.startsWith('/dashboard') ? route.fullPath : '/dashboard/apps'
router.push({ name: 'app-session', params: { appId: id }, query: { returnTo } })
return
}

View File

@ -34,6 +34,7 @@ export const APP_PORTS: Record<string, number> = {
'nginx-proxy-manager': 8081,
'gitea': 3001,
'portainer': 9000,
'netbird': 8087,
'tailscale': 8240,
'uptime-kuma': 3002,
'fedimint': 8175,

View File

@ -142,7 +142,7 @@ const mobileTabBar = ref<HTMLElement | null>(null)
// App sessions own their mobile controls. Normal mobile launches use the route
// session; keeping this guard also protects any desktop-panel state on resize.
const isAppSessionActive = computed(() => route.name === 'app-session' || !!appLauncher.panelAppId)
const isAppSessionActive = computed(() => route.name === 'app-session')
// Show persistent tabs for Apps/Marketplace on mobile
const showAppsTabs = computed(() => {

View File

@ -2,11 +2,11 @@
<aside
v-show="!chatFullscreen"
data-controller-zone="sidebar"
class="hidden md:flex w-[256px] flex-shrink-0 relative flex-col z-10"
class="hidden md:flex w-[256px] h-screen flex-shrink-0 sticky top-0 relative flex-col z-10"
:class="{ 'sidebar-animate': showZoomIn }"
>
<div class="sidebar-shell">
<div class="sidebar-inner flex flex-col min-h-full">
<div class="sidebar-inner flex flex-col h-full min-h-0">
<div class="sidebar-logo flex items-center gap-3 mb-8 p-6 pb-0 shrink-0">
<AnimatedLogo />
<div class="min-w-0 flex-1">
@ -15,7 +15,7 @@
</div>
</div>
<nav class="sidebar-nav flex-1 min-h-0 space-y-2 p-6 pt-4" :aria-label="t('dashboard.mainNav')">
<nav class="sidebar-nav flex-1 min-h-0 overflow-y-auto overscroll-contain space-y-2 px-6 py-4" :aria-label="t('dashboard.mainNav')">
<RouterLink
v-for="(item, idx) in desktopNavItems"
:key="item.path"
@ -74,21 +74,23 @@
</button>
</nav>
<div class="sidebar-controller px-6 pb-2 shrink-0">
<ControllerIndicator />
<CompanionIndicator />
</div>
<!-- Online status -->
<div class="px-6 pb-2 shrink-0">
<div class="rounded-lg bg-white/5 border border-white/10 px-4 py-2.5">
<OnlineStatusPill />
<div class="sidebar-bottom shrink-0">
<div class="sidebar-controller px-6 pb-2">
<ControllerIndicator />
<CompanionIndicator />
</div>
</div>
<!-- Mode switcher -->
<div class="px-6 pb-6 shrink-0">
<ModeSwitcher />
<!-- Online status -->
<div class="px-6 pb-2">
<div class="rounded-lg bg-white/5 border border-white/10 px-4 py-2.5">
<OnlineStatusPill />
</div>
</div>
<!-- Mode switcher -->
<div class="px-6 pb-6">
<ModeSwitcher />
</div>
</div>
</div>
</div>

View File

@ -47,7 +47,7 @@
.sidebar-shell {
width: 100%;
height: 100%;
min-height: 100vh;
min-height: 0;
background: rgba(0, 0, 0, 0.25);
backdrop-filter: blur(18px);
-webkit-backdrop-filter: blur(18px);
@ -85,6 +85,24 @@
overflow: hidden;
}
.sidebar-nav {
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.24) transparent;
}
.sidebar-nav::-webkit-scrollbar {
width: 6px;
}
.sidebar-nav::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.22);
border-radius: 999px;
}
.sidebar-bottom {
background: linear-gradient(to top, rgba(0, 0, 0, 0.18), transparent 100%);
}
/* Only hide sidebar content when doing the login entrance animation */
.sidebar-animate .sidebar-inner {
opacity: 0;

View File

@ -96,7 +96,7 @@ export function getCuratedAppList(): MarketplaceApp[] {
{ id: 'portainer', title: 'Portainer', version: '2.19.4', description: 'Container management UI. Manage your containerized services through the web.', icon: '/assets/img/app-icons/portainer.webp', author: 'Portainer', dockerImage: `${R}/portainer:latest`, repoUrl: 'https://github.com/portainer/portainer' },
{ id: 'uptime-kuma', title: 'Uptime Kuma', version: '1.23.0', description: 'Self-hosted uptime monitoring. Track HTTP, TCP, DNS, and more.', icon: '/assets/img/app-icons/uptime-kuma.webp', author: 'Uptime Kuma', dockerImage: `${R}/uptime-kuma:1`, repoUrl: 'https://github.com/louislam/uptime-kuma' },
{ id: 'tailscale', title: 'Tailscale', version: '1.78.0', description: 'Zero-config VPN. Secure remote access with WireGuard mesh networking.', icon: '/assets/img/app-icons/tailscale.webp', author: 'Tailscale', dockerImage: `${R}/tailscale:stable`, repoUrl: 'https://github.com/tailscale/tailscale' },
{ id: 'netbird', title: 'NetBird', version: '0.71.2', description: 'WireGuard mesh VPN client. Connect this node through NetBird Cloud or your own NetBird management server.', icon: '/assets/img/app-icons/netbird.svg', author: 'NetBird', dockerImage: 'docker.io/netbirdio/netbird:0.71.2', repoUrl: 'https://github.com/netbirdio/netbird' },
{ id: 'netbird', title: 'NetBird', version: '0.71.2', description: 'Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN.', icon: '/assets/img/app-icons/netbird.svg', author: 'NetBird', dockerImage: 'docker.io/netbirdio/dashboard:v2.38.0', repoUrl: 'https://github.com/netbirdio/netbird' },
{ id: 'electrumx', title: 'ElectrumX', version: '1.18.0', description: 'Electrum protocol server. Index the blockchain for fast wallet lookups, privately.', icon: '/assets/img/app-icons/electrumx.png', author: 'Luke Childs', dockerImage: `${R}/electrumx:v1.18.0`, repoUrl: 'https://github.com/spesmilo/electrumx' },
{ id: 'fedimint', title: 'Fedimint', version: '0.10.0', description: 'Federated Bitcoin mint. Private, scalable Bitcoin through federated guardians.', icon: '/assets/img/app-icons/fedimint.png', author: 'Fedimint', dockerImage: `${R}/fedimintd:v0.10.0`, repoUrl: 'https://github.com/fedimint/fedimint' },
{ id: 'indeedhub', title: 'Indeehub', version: '1.0.0', description: 'Bitcoin documentary streaming with Nostr identity. Stream sovereignty content.', icon: '/assets/img/app-icons/indeedhub.png', author: 'Indeehub Team', dockerImage: `${R}/indeedhub:1.0.0`, repoUrl: 'https://github.com/indeedhub/indeedhub' },

View File

@ -370,10 +370,10 @@ export function getCuratedAppList(): MarketplaceApp[] {
id: 'netbird',
title: 'NetBird',
version: '0.71.2',
description: 'WireGuard mesh VPN client. Connect this node through NetBird Cloud or a self-hosted management server.',
description: 'Self-hosted WireGuard mesh VPN control plane with dashboard, embedded identity provider, management API, signal, relay, and STUN.',
icon: '/assets/img/app-icons/netbird.svg',
author: 'NetBird',
dockerImage: 'docker.io/netbirdio/netbird:0.71.2',
dockerImage: 'docker.io/netbirdio/dashboard:v2.38.0',
manifestUrl: undefined,
repoUrl: 'https://github.com/netbirdio/netbird'
},

View File

@ -48,7 +48,8 @@ PORTAINER_IMAGE="$ARCHY_REGISTRY/portainer:latest"
# Networking
TAILSCALE_IMAGE="$ARCHY_REGISTRY/tailscale:stable"
NETBIRD_IMAGE="docker.io/netbirdio/netbird:0.71.2"
NETBIRD_DASHBOARD_IMAGE="docker.io/netbirdio/dashboard:v2.38.0"
NETBIRD_SERVER_IMAGE="docker.io/netbirdio/netbird-server:0.71.2"
ALPINE_TOR_IMAGE="$ARCHY_REGISTRY/alpine-tor:0.4.8.13"
ADGUARDHOME_IMAGE="$ARCHY_REGISTRY/adguardhome:v0.107.55"