Fix: display dynamic version on login and use subdomain for customer directories instead of kunde{id}

This commit is contained in:
Sascha Lustenberger | techlan gmbh
2026-02-23 12:58:39 +01:00
parent c3ab7a5a67
commit cda916f2af
9 changed files with 47 additions and 34 deletions

View File

@@ -91,7 +91,7 @@ netbird-msp-appliance/
1. Validate inputs (subdomain unique, email valid) 1. Validate inputs (subdomain unique, email valid)
2. Allocate ports (Management internal, Relay UDP public) 2. Allocate ports (Management internal, Relay UDP public)
3. Generate configs from Jinja2 templates 3. Generate configs from Jinja2 templates
4. Create instance directory: `/opt/netbird-instances/kunde{id}/` 4. Create instance directory: `/opt/netbird-instances/{subdomain}/`
5. Write `docker-compose.yml`, `management.json`, `relay.env` 5. Write `docker-compose.yml`, `management.json`, `relay.env`
6. Start Docker containers via Docker SDK 6. Start Docker containers via Docker SDK
7. Wait for health checks (max 60s) 7. Wait for health checks (max 60s)
@@ -113,7 +113,7 @@ No manual config file editing required!
### 4. Nginx Proxy Manager Integration ### 4. Nginx Proxy Manager Integration
**Per customer, create proxy host:** **Per customer, create proxy host:**
- Domain: `{subdomain}.{base_domain}` - Domain: `{subdomain}.{base_domain}`
- Forward to: `netbird-kunde{id}-dashboard:80` - Forward to: `netbird-{subdomain}-dashboard:80`
- SSL: Automatic Let's Encrypt - SSL: Automatic Let's Encrypt
- Advanced config: Route `/api/*` to management, `/signalexchange.*` to signal, `/relay` to relay - Advanced config: Route `/api/*` to management, `/signalexchange.*` to signal, `/relay` to relay
@@ -272,7 +272,7 @@ networks:
services: services:
netbird-management: netbird-management:
image: {{ netbird_management_image }} image: {{ netbird_management_image }}
container_name: netbird-kunde{{ customer_id }}-management container_name: netbird-{{ subdomain }}-management
restart: unless-stopped restart: unless-stopped
networks: networks:
- npm-network - npm-network
@@ -285,7 +285,7 @@ services:
netbird-signal: netbird-signal:
image: {{ netbird_signal_image }} image: {{ netbird_signal_image }}
container_name: netbird-kunde{{ customer_id }}-signal container_name: netbird-{{ subdomain }}-signal
restart: unless-stopped restart: unless-stopped
networks: networks:
- npm-network - npm-network
@@ -294,7 +294,7 @@ services:
netbird-relay: netbird-relay:
image: {{ netbird_relay_image }} image: {{ netbird_relay_image }}
container_name: netbird-kunde{{ customer_id }}-relay container_name: netbird-{{ subdomain }}-relay
restart: unless-stopped restart: unless-stopped
networks: networks:
- npm-network - npm-network
@@ -311,7 +311,7 @@ services:
netbird-dashboard: netbird-dashboard:
image: {{ netbird_dashboard_image }} image: {{ netbird_dashboard_image }}
container_name: netbird-kunde{{ customer_id }}-dashboard container_name: netbird-{{ subdomain }}-dashboard
restart: unless-stopped restart: unless-stopped
networks: networks:
- npm-network - npm-network

View File

@@ -95,8 +95,8 @@ A management solution for running isolated NetBird instances for your MSP busine
| | Caddy | | | | Caddy | | | | Caddy | | | | Caddy | |
| +------------+ | | +------------+ | | +------------+ | | +------------+ |
+------------------+ +------------------+ +------------------+ +------------------+
kunde1.domain.de kundeN.domain.de customer-a.domain.de customer-x.domain.de
UDP 3478 UDP 3478+N-1 | |3478+N-1
``` ```
### Components per Customer Instance (5 containers): ### Components per Customer Instance (5 containers):
@@ -140,9 +140,9 @@ Example for 3 customers:
| Customer | Dashboard (TCP) | Relay (UDP) | | Customer | Dashboard (TCP) | Relay (UDP) |
|----------|----------------|-------------| |----------|----------------|-------------|
| Kunde 1 | 9001 | 3478 | | Customer-A | 9001 | 3478 |
| Kunde 2 | 9002 | 3479 | | Customer-C | 9002 | 3479 |
| Kunde 3 | 9003 | 3480 | | Customer-X | 9003 | 3480 |
**Your firewall must allow both the TCP dashboard ports and the UDP relay ports!** **Your firewall must allow both the TCP dashboard ports and the UDP relay ports!**

View File

@@ -237,6 +237,10 @@ async def test_ldap(
@router.get("/branding") @router.get("/branding")
async def get_branding(db: Session = Depends(get_db)): async def get_branding(db: Session = Depends(get_db)):
"""Public endpoint — returns branding info for the login page (no auth required).""" """Public endpoint — returns branding info for the login page (no auth required)."""
current_version = update_service.get_current_version().get("tag", "alpha-1.1")
if current_version == "unknown":
current_version = "alpha-1.1"
row = db.query(SystemConfig).filter(SystemConfig.id == 1).first() row = db.query(SystemConfig).filter(SystemConfig.id == 1).first()
if not row: if not row:
return { return {
@@ -244,12 +248,14 @@ async def get_branding(db: Session = Depends(get_db)):
"branding_subtitle": "Multi-Tenant Management Platform", "branding_subtitle": "Multi-Tenant Management Platform",
"branding_logo_path": None, "branding_logo_path": None,
"default_language": "en", "default_language": "en",
"version": current_version
} }
return { return {
"branding_name": row.branding_name or "NetBird MSP Appliance", "branding_name": row.branding_name or "NetBird MSP Appliance",
"branding_subtitle": row.branding_subtitle or "Multi-Tenant Management Platform", "branding_subtitle": row.branding_subtitle or "Multi-Tenant Management Platform",
"branding_logo_path": row.branding_logo_path, "branding_logo_path": row.branding_logo_path,
"default_language": row.default_language or "en", "default_language": row.default_language or "en",
"version": current_version
} }

View File

@@ -118,7 +118,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
allocated_port = None allocated_port = None
instance_dir = None instance_dir = None
container_prefix = f"netbird-kunde{customer_id}" container_prefix = f"netbird-{customer.subdomain}"
local_mode = _is_local_domain(config.base_domain) local_mode = _is_local_domain(config.base_domain)
existing_deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first() existing_deployment = db.query(Deployment).filter(Deployment.customer_id == customer_id).first()
@@ -135,7 +135,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
# Step 2: Generate secrets (reuse existing key if instance data exists) # Step 2: Generate secrets (reuse existing key if instance data exists)
relay_secret = generate_relay_secret() relay_secret = generate_relay_secret()
datastore_key = _get_existing_datastore_key( datastore_key = _get_existing_datastore_key(
os.path.join(config.data_dir, f"kunde{customer_id}", "management.json") os.path.join(config.data_dir, customer.subdomain, "management.json")
) )
if datastore_key: if datastore_key:
_log_action(db, customer_id, "deploy", "info", _log_action(db, customer_id, "deploy", "info",
@@ -159,7 +159,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
relay_ws_protocol = "rels" relay_ws_protocol = "rels"
# Step 4: Create instance directory # Step 4: Create instance directory
instance_dir = os.path.join(config.data_dir, f"kunde{customer_id}") instance_dir = os.path.join(config.data_dir, customer.subdomain)
os.makedirs(instance_dir, exist_ok=True) os.makedirs(instance_dir, exist_ok=True)
os.makedirs(os.path.join(instance_dir, "data", "management"), exist_ok=True) os.makedirs(os.path.join(instance_dir, "data", "management"), exist_ok=True)
os.makedirs(os.path.join(instance_dir, "data", "signal"), exist_ok=True) os.makedirs(os.path.join(instance_dir, "data", "signal"), exist_ok=True)
@@ -225,7 +225,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
# Step 8: Auto-create admin user via NetBird setup API # Step 8: Auto-create admin user via NetBird setup API
admin_email = customer.email admin_email = customer.email
admin_password = secrets.token_urlsafe(16) admin_password = secrets.token_urlsafe(16)
management_container = f"netbird-kunde{customer_id}-management" management_container = f"netbird-{customer.subdomain}-management"
setup_api_url = f"http://{management_container}:80/api/setup" setup_api_url = f"http://{management_container}:80/api/setup"
setup_payload = json.dumps({ setup_payload = json.dumps({
"name": customer.name, "name": customer.name,
@@ -387,7 +387,7 @@ async def deploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
# Rollback: stop containers if they were started # Rollback: stop containers if they were started
try: try:
await docker_service.compose_down( await docker_service.compose_down(
instance_dir or os.path.join(config.data_dir, f"kunde{customer_id}"), instance_dir or os.path.join(config.data_dir, customer.subdomain),
container_prefix, container_prefix,
remove_volumes=True, remove_volumes=True,
) )
@@ -423,7 +423,7 @@ async def undeploy_customer(db: Session, customer_id: int) -> dict[str, Any]:
config = get_system_config(db) config = get_system_config(db)
if deployment and config: if deployment and config:
instance_dir = os.path.join(config.data_dir, f"kunde{customer_id}") instance_dir = os.path.join(config.data_dir, customer.subdomain)
# Stop and remove containers # Stop and remove containers
try: try:
@@ -488,7 +488,7 @@ async def stop_customer(db: Session, customer_id: int) -> dict[str, Any]:
if not deployment or not config: if not deployment or not config:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment or config not found."}
instance_dir = os.path.join(config.data_dir, f"kunde{customer_id}") instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_stop(instance_dir, deployment.container_prefix) ok = await docker_service.compose_stop(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "stopped" deployment.deployment_status = "stopped"
@@ -509,7 +509,7 @@ async def start_customer(db: Session, customer_id: int) -> dict[str, Any]:
if not deployment or not config: if not deployment or not config:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment or config not found."}
instance_dir = os.path.join(config.data_dir, f"kunde{customer_id}") instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_start(instance_dir, deployment.container_prefix) ok = await docker_service.compose_start(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "running" deployment.deployment_status = "running"
@@ -530,7 +530,7 @@ async def restart_customer(db: Session, customer_id: int) -> dict[str, Any]:
if not deployment or not config: if not deployment or not config:
return {"success": False, "error": "Deployment or config not found."} return {"success": False, "error": "Deployment or config not found."}
instance_dir = os.path.join(config.data_dir, f"kunde{customer_id}") instance_dir = os.path.join(config.data_dir, customer.subdomain)
ok = await docker_service.compose_restart(instance_dir, deployment.container_prefix) ok = await docker_service.compose_restart(instance_dir, deployment.container_prefix)
if ok: if ok:
deployment.deployment_status = "running" deployment.deployment_status = "running"

View File

@@ -21,7 +21,7 @@
<h3 class="mt-2" id="login-title">NetBird MSP Appliance</h3> <h3 class="mt-2" id="login-title">NetBird MSP Appliance</h3>
<p class="text-muted" id="login-subtitle" data-i18n="login.subtitle">Multi-Tenant Management <p class="text-muted" id="login-subtitle" data-i18n="login.subtitle">Multi-Tenant Management
Platform</p> Platform</p>
<p class="text-muted small mb-0" style="opacity:0.6;"><i class="bi bi-tag me-1"></i>alpha-1.1 <p class="text-muted small mb-0" style="opacity:0.6;"><i class="bi bi-tag me-1"></i>
</p> </p>
</div> </div>
<div id="login-error" class="alert alert-danger d-none"></div> <div id="login-error" class="alert alert-danger d-none"></div>

View File

@@ -12,7 +12,7 @@ let currentPage = 'dashboard';
let currentCustomerId = null; let currentCustomerId = null;
let currentCustomerData = null; let currentCustomerData = null;
let customersPage = 1; let customersPage = 1;
let brandingData = { branding_name: 'NetBird MSP Appliance', branding_logo_path: null }; let brandingData = { branding_name: 'NetBird MSP Appliance', branding_logo_path: null, version: 'alpha-1.1' };
let azureConfig = { azure_enabled: false }; let azureConfig = { azure_enabled: false };
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -127,12 +127,19 @@ function applyBranding() {
const name = brandingData.branding_name || 'NetBird MSP Appliance'; const name = brandingData.branding_name || 'NetBird MSP Appliance';
const subtitle = brandingData.branding_subtitle || t('login.subtitle'); const subtitle = brandingData.branding_subtitle || t('login.subtitle');
const logoPath = brandingData.branding_logo_path; const logoPath = brandingData.branding_logo_path;
const version = brandingData.version || 'alpha-1.1';
// Login page // Login page
document.getElementById('login-title').textContent = name; document.getElementById('login-title').textContent = name;
const subtitleEl = document.getElementById('login-subtitle'); const subtitleEl = document.getElementById('login-subtitle');
if (subtitleEl) subtitleEl.textContent = subtitle; if (subtitleEl) subtitleEl.textContent = subtitle;
document.title = name; document.title = name;
// Update version string in login page
const versionEl = document.querySelector('#login-page .text-muted.small.mb-0');
if (versionEl) {
versionEl.innerHTML = `<i class="bi bi-tag me-1"></i>${version}`;
}
if (logoPath) { if (logoPath) {
document.getElementById('login-logo').innerHTML = `<img src="${logoPath}" alt="Logo" style="max-height:64px;max-width:200px;" class="mb-1">`; document.getElementById('login-logo').innerHTML = `<img src="${logoPath}" alt="Logo" style="max-height:64px;max-width:200px;" class="mb-1">`;
} else { } else {

View File

@@ -108,7 +108,7 @@
"groupExternal": "Umsysteme", "groupExternal": "Umsysteme",
"baseDomain": "Basis-Domain", "baseDomain": "Basis-Domain",
"baseDomainPlaceholder": "ihredomain.com", "baseDomainPlaceholder": "ihredomain.com",
"baseDomainHint": "Kunden erhalten Subdomains: kunde.ihredomain.com", "baseDomainHint": "Kunden erhalten Subdomains: kundenname.ihredomain.com",
"adminEmail": "Admin E-Mail", "adminEmail": "Admin E-Mail",
"adminEmailPlaceholder": "admin@ihredomain.com", "adminEmailPlaceholder": "admin@ihredomain.com",
"dataDir": "Datenverzeichnis", "dataDir": "Datenverzeichnis",
@@ -118,7 +118,7 @@
"relayBasePort": "Relay-Basisport", "relayBasePort": "Relay-Basisport",
"relayBasePortHint": "Erster UDP-Port für Relay. Bereich: Basis bis Basis+99", "relayBasePortHint": "Erster UDP-Port für Relay. Bereich: Basis bis Basis+99",
"dashboardBasePort": "Dashboard-Basisport", "dashboardBasePort": "Dashboard-Basisport",
"dashboardBasePortHint": "Basisport für Kunden-Dashboards. Kunde N erhält Basis+N", "dashboardBasePortHint": "Basisport für Kunden-Dashboards. Der erste Kunde erhält Basis+1",
"saveSystemSettings": "Systemeinstellungen speichern", "saveSystemSettings": "Systemeinstellungen speichern",
"npmDescription": "NPM verwendet JWT-Authentifizierung. Geben Sie Ihre NPM-Zugangsdaten ein. Das System meldet sich automatisch an.", "npmDescription": "NPM verwendet JWT-Authentifizierung. Geben Sie Ihre NPM-Zugangsdaten ein. Das System meldet sich automatisch an.",
"npmApiUrl": "NPM API URL", "npmApiUrl": "NPM API URL",

View File

@@ -5,15 +5,15 @@
:80 { :80 {
# Embedded IdP OAuth2/OIDC endpoints # Embedded IdP OAuth2/OIDC endpoints
handle /oauth2/* { handle /oauth2/* {
reverse_proxy netbird-kunde{{ customer_id }}-management:80 reverse_proxy netbird-{{ subdomain }}-management:80
} }
# NetBird Management API + gRPC # NetBird Management API + gRPC
handle /api/* { handle /api/* {
reverse_proxy netbird-kunde{{ customer_id }}-management:80 reverse_proxy netbird-{{ subdomain }}-management:80
} }
handle /management.ManagementService/* { handle /management.ManagementService/* {
reverse_proxy netbird-kunde{{ customer_id }}-management:80 { reverse_proxy netbird-{{ subdomain }}-management:80 {
transport http { transport http {
versions h2c versions h2c
} }
@@ -22,7 +22,7 @@
# NetBird Signal gRPC # NetBird Signal gRPC
handle /signalexchange.SignalExchange/* { handle /signalexchange.SignalExchange/* {
reverse_proxy netbird-kunde{{ customer_id }}-signal:80 { reverse_proxy netbird-{{ subdomain }}-signal:80 {
transport http { transport http {
versions h2c versions h2c
} }
@@ -31,6 +31,6 @@
# Default: NetBird Dashboard # Default: NetBird Dashboard
handle { handle {
reverse_proxy netbird-kunde{{ customer_id }}-dashboard:80 reverse_proxy netbird-{{ subdomain }}-dashboard:80
} }
} }

View File

@@ -6,7 +6,7 @@ services:
# --- Caddy Reverse Proxy (entry point) --- # --- Caddy Reverse Proxy (entry point) ---
netbird-caddy: netbird-caddy:
image: caddy:2-alpine image: caddy:2-alpine
container_name: netbird-kunde{{ customer_id }}-caddy container_name: netbird-{{ subdomain }}-caddy
restart: unless-stopped restart: unless-stopped
networks: networks:
- {{ docker_network }} - {{ docker_network }}
@@ -18,7 +18,7 @@ services:
# --- NetBird Management (with embedded IdP) --- # --- NetBird Management (with embedded IdP) ---
netbird-management: netbird-management:
image: {{ netbird_management_image }} image: {{ netbird_management_image }}
container_name: netbird-kunde{{ customer_id }}-management container_name: netbird-{{ subdomain }}-management
restart: unless-stopped restart: unless-stopped
networks: networks:
- {{ docker_network }} - {{ docker_network }}
@@ -39,7 +39,7 @@ services:
# --- NetBird Signal --- # --- NetBird Signal ---
netbird-signal: netbird-signal:
image: {{ netbird_signal_image }} image: {{ netbird_signal_image }}
container_name: netbird-kunde{{ customer_id }}-signal container_name: netbird-{{ subdomain }}-signal
restart: unless-stopped restart: unless-stopped
networks: networks:
- {{ docker_network }} - {{ docker_network }}
@@ -49,7 +49,7 @@ services:
# --- NetBird Relay --- # --- NetBird Relay ---
netbird-relay: netbird-relay:
image: {{ netbird_relay_image }} image: {{ netbird_relay_image }}
container_name: netbird-kunde{{ customer_id }}-relay container_name: netbird-{{ subdomain }}-relay
restart: unless-stopped restart: unless-stopped
networks: networks:
- {{ docker_network }} - {{ docker_network }}
@@ -61,7 +61,7 @@ services:
# --- NetBird Dashboard --- # --- NetBird Dashboard ---
netbird-dashboard: netbird-dashboard:
image: {{ netbird_dashboard_image }} image: {{ netbird_dashboard_image }}
container_name: netbird-kunde{{ customer_id }}-dashboard container_name: netbird-{{ subdomain }}-dashboard
restart: unless-stopped restart: unless-stopped
networks: networks:
- {{ docker_network }} - {{ docker_network }}