Add multi-instance support to 13 more services

Retrofits the standard multi-instance pattern (documented in CLAUDE.md)
onto actualbudget, filebrowser, fmd, homebox, immich, jellyfin, joplin,
lyrion, meshcentral, ntfy, rustdesk, unifi, and vaultwarden. First
instance of each keeps its original name/paths/ports unchanged; adding a
second instance prompts for a short name and auto-scans for free ports.

Service-specific handling beyond the base pattern:
- joplin, immich, unifi: dedicated Postgres/Mongo container per instance
  (not shared), matching the backup-isolation reasoning in CLAUDE.md.
- meshcentral, unifi: multiple fixed ports scanned/shifted together so
  they stay paired per instance.
- rustdesk: 6-port block shifted by a fixed offset per instance, since
  the image hardcodes its internal ports with no per-port env override.
- jellyfin: DLNA/discovery UDP ports only published for the first
  instance to avoid a host-wide fixed-port conflict.
- lyrion: first instance keeps network_mode: host (required for
  Chromecast/Squeezebox broadcast discovery); additional instances fall
  back to bridge networking with auto-scanned ports, trading away
  zero-config discovery since a second container can't also bind host
  networking's fixed ports.
- magicmirror.sh already had its own working multi-instance pattern
  (upfront instance count, numbered subdirs) and was left as-is.

Verified via bash -n on every changed file, plus scripted functional
runs (fake docker/ss) exercising first + second instance installs for
every port-scanning shape used here (single, dual-paired, quad-paired,
block-offset) and confirming dedicated per-instance DB naming and the
lyrion host->bridge compose output.
This commit is contained in:
Claude
2026-08-09 22:53:40 +00:00
parent 3fc20238af
commit b860a8b174
13 changed files with 923 additions and 224 deletions
+90 -27
View File
@@ -98,16 +98,72 @@ register_service unifi utilities "Ubiquiti network controller (UniFi)" 8443
install_unifi() {
require_docker || return 1
log_info "Installing UniFi Network Application..."
# ── Instance selection ───────────────────────────────────────────────────
# First instance keeps the plain "unifi-db"/"unifi-app" names/paths/ports
# exactly as before (zero behavior change for anyone with a single
# instance). Only asking to add a second one introduces suffixed naming —
# same pattern as services/mattermost.sh and services/wordpress.sh.
# Each instance gets its own dedicated MongoDB container (not shared) —
# see CLAUDE.md's "Multi-instance services" section for why: Kopia's
# generic backup stops the container to snapshot it, so a shared DB would
# back up/restore every instance's site data as one unit instead of
# per-instance. All 4 published ports move together per instance.
local UNIFI_DIR="$DOCKER_DIR/unifi"
local INSTANCE_SUFFIX="" PROJECT="unifi" DB_CONTAINER="unifi-db" APP_CONTAINER="unifi-app"
local WEB_PORT="8443" INFORM_PORT="8080" STUN_PORT="3478" DISCOVERY_PORT="10001"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Would create $UNIFI_DIR (mongo_db_data/, unifi_data/)"
echo "[DRY-RUN] Would offer to add a new, separate instance if one already exists"
echo "[DRY-RUN] Would create \$DOCKER_DIR/unifi(-<name>) (mongo_db_data/, unifi_data/)"
echo "[DRY-RUN] Would deploy mongo:4 + linuxserver/unifi-network-application:latest"
echo "[DRY-RUN] Ports: 8443 (HTTPS web UI), 8080 (device inform), 3478/udp (STUN), 10001/udp (discovery)"
echo "[DRY-RUN] — all 4 auto-scanned/shifted together for additional instances"
echo "[DRY-RUN] Would generate MongoDB credentials"
return 0
fi
if [ -d "$UNIFI_DIR" ]; then
echo ""
echo " UniFi is already installed at $UNIFI_DIR."
echo " 1) Manage that install (update / full reinstall / cancel)"
echo " 2) Add a NEW, separate UniFi controller instance alongside it (its own"
echo " database, sites, and ports — full isolation)"
echo ""
local _TOP_CHOICE=""
prompt_text " Choice [1/2]:" "1" _TOP_CHOICE
if [ "$_TOP_CHOICE" = "2" ]; then
local _suffix=""
while true; do
prompt_text " Short name for the new instance (letters/numbers/hyphens, e.g. 'guest-site'):" "" _suffix
_suffix="$(echo "$_suffix" | tr -cs 'a-zA-Z0-9-' '-' | sed 's/^-*//;s/-*$//')"
if [ -z "$_suffix" ]; then
log_warning "Name can't be empty."; continue
fi
if [ -d "$DOCKER_DIR/unifi-$_suffix" ]; then
log_warning "unifi-$_suffix already exists — pick another name."; continue
fi
break
done
INSTANCE_SUFFIX="$_suffix"
UNIFI_DIR="$DOCKER_DIR/unifi-$_suffix"
PROJECT="unifi-$_suffix"
DB_CONTAINER="unifi-db-$_suffix"
APP_CONTAINER="unifi-app-$_suffix"
while ss -tlnH "sport = :${WEB_PORT}" 2>/dev/null | grep -q . \
|| ss -tlnH "sport = :${INFORM_PORT}" 2>/dev/null | grep -q . \
|| ss -ulnH "sport = :${STUN_PORT}" 2>/dev/null | grep -q . \
|| ss -ulnH "sport = :${DISCOVERY_PORT}" 2>/dev/null | grep -q .; do
WEB_PORT=$((WEB_PORT + 1))
INFORM_PORT=$((INFORM_PORT + 1))
STUN_PORT=$((STUN_PORT + 1))
DISCOVERY_PORT=$((DISCOVERY_PORT + 1))
done
log_info "New instance: $UNIFI_DIR (web $WEB_PORT, inform $INFORM_PORT, STUN $STUN_PORT, discovery $DISCOVERY_PORT)"
fi
fi
mkdir -p "$UNIFI_DIR"
ensure_docker_dir_ownership "$UNIFI_DIR"
cd "$UNIFI_DIR" || return 1
@@ -143,13 +199,13 @@ networks:
# Unquoted heredoc; ${...} used for caddy_net vars; all Docker Compose vars escaped with \$
cat > docker-compose.yml << UNIFI_COMPOSE
name: unifi
name: $PROJECT
services:
unifi-db:
image: mongo:4
container_name: unifi-db
hostname: unifi-db
container_name: $DB_CONTAINER
hostname: $DB_CONTAINER
restart: unless-stopped
env_file: .env
volumes:
@@ -162,8 +218,8 @@ services:
unifi-app:
image: lscr.io/linuxserver/unifi-network-application:latest
container_name: unifi-app
hostname: unifi-app
container_name: $APP_CONTAINER
hostname: $APP_CONTAINER
restart: unless-stopped
env_file: .env
depends_on:
@@ -171,10 +227,10 @@ services:
volumes:
- ./unifi_data:/config
ports:
- "8443:8443"
- "8080:8080"
- "3478:3478/udp"
- "10001:10001/udp"
- "${WEB_PORT}:8443"
- "${INFORM_PORT}:8080"
- "${STUN_PORT}:3478/udp"
- "${DISCOVERY_PORT}:10001/udp"
# Optional — uncomment as needed:
# - "1900:1900/udp" # L2 discovery (may conflict with UPnP)
# - "8843:8843" # guest portal HTTPS
@@ -216,7 +272,7 @@ UNIFI_ENV
mkdir -p mongo_db_data unifi_data
ensure_docker_dir_ownership "$UNIFI_DIR"
log_success "UniFi configured at $UNIFI_DIR"
log_success "UniFi${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} configured at $UNIFI_DIR (web port $WEB_PORT)"
# ── Optional Caddy reverse proxy (HTTPS backend requires tls_insecure_skip_verify) ──
local _caddy_mode="none"
@@ -232,15 +288,18 @@ UNIFI_ENV
fi
echo ""
local CADDY_UNIFI=""
prompt_yn "Configure Caddy reverse proxy for UniFi? (y/n):" "n" CADDY_UNIFI
prompt_yn "Configure Caddy reverse proxy for UniFi${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}? (y/n):" "n" CADDY_UNIFI
if [ "$CADDY_UNIFI" = "y" ] || [ "$CADDY_UNIFI" = "Y" ]; then
local UNIFI_DOMAIN=""
local _def_domain="unifi.${SITE_DOMAIN:-example.com}"
local _def_domain="unifi${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}.${SITE_DOMAIN:-example.com}"
prompt_text "UniFi domain [${_def_domain}]:" "$_def_domain" UNIFI_DOMAIN
if [ -n "$UNIFI_DOMAIN" ]; then
# UniFi uses HTTPS internally — upstream must use https:// + skip verify
local _upstream="https://unifi-app:8443"
[ "$_caddy_mode" = "remote" ] && _upstream="https://${CADDY_REMOTE_HOST}:8443"
# UniFi uses HTTPS internally — upstream must use https:// + skip verify.
# Local mode reaches the container over Docker networking (internal port
# never changes); remote mode reaches this host's published port, which
# is WEB_PORT (auto-scanned for additional instances).
local _upstream="https://${APP_CONTAINER}:8443"
[ "$_caddy_mode" = "remote" ] && _upstream="https://${CADDY_REMOTE_HOST}:${WEB_PORT}"
local _site_block
_site_block="$(cat << CBLOCK
@@ -277,7 +336,7 @@ CBLOCK
|| log_warning "Caddy reload failed — check: docker logs caddy"
else
local _snippet_dir="$DOCKER_DIR/caddy-snippets"
local _snippet_file="$_snippet_dir/unifi.caddy"
local _snippet_file="$_snippet_dir/unifi${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}.caddy"
mkdir -p "$_snippet_dir"
printf '%s\n' "$_site_block" > "$_snippet_file"
chown "$ACTUAL_USER:$ACTUAL_USER" "$_snippet_file" 2>/dev/null || true
@@ -290,25 +349,29 @@ CBLOCK
fi
write_readme "$UNIFI_DIR" << MD
# UniFi Network Application
# UniFi Network Application${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
Ubiquiti network controller. Manages UniFi APs, switches, and gateways.
$( [ -n "$INSTANCE_SUFFIX" ] && echo "
This is a separate, fully isolated instance (own dedicated database, own
sites/devices, own ports) — not shared adoption with another UniFi instance.
Devices can only be adopted by one controller at a time.")
## Access
- Web UI: **https://localhost:8443** (HTTPS, self-signed cert — accept the warning)
- Web UI: **https://localhost:${WEB_PORT}** (HTTPS, self-signed cert — accept the warning)
- First run: complete the setup wizard and adopt your devices.
## Device adoption
Make sure devices can reach **http://<server-ip>:8080/inform** as the inform URL.
Make sure devices can reach **http://<server-ip>:${INFORM_PORT}/inform** as the inform URL.
In the controller: Settings → System → Application Configuration → Override inform host.
## Ports
| Port | Protocol | Purpose |
|------|----------|---------|
| 8443 | TCP | HTTPS web UI |
| 8080 | TCP | Device inform / HTTP redirect |
| 3478 | UDP | STUN |
| 10001 | UDP | AP discovery |
| ${WEB_PORT} | TCP | HTTPS web UI |
| ${INFORM_PORT} | TCP | Device inform / HTTP redirect |
| ${STUN_PORT} | UDP | STUN |
| ${DISCOVERY_PORT} | UDP | AP discovery |
## Manage
\`\`\`bash
@@ -327,15 +390,15 @@ docker compose pull && docker compose up -d # update (wait for DB first)
MD
local START_UNIFI=""
prompt_yn "Start UniFi now? (y/n):" "y" START_UNIFI
prompt_yn "Start UniFi${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} now? (y/n):" "y" START_UNIFI
if [ "$START_UNIFI" = "y" ] || [ "$START_UNIFI" = "Y" ]; then
docker compose up -d \
&& log_success "UniFi started (first startup takes ~60 s while DB initializes)" \
&& log_success "UniFi${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} started (first startup takes ~60 s while DB initializes)" \
|| log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Web UI: https://localhost:8443 (accept the self-signed cert warning)"
echo " Web UI: https://localhost:${WEB_PORT} (accept the self-signed cert warning)"
echo " MongoDB credentials saved to: $UNIFI_DIR/.env"
echo ""
}