fix(apps): self-host netbird and stabilize app sessions
This commit is contained in:
@@ -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)],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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![],
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user