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
+79 -16
View File
@@ -188,19 +188,67 @@ register_service jellyfin media "Free media server — movies, TV, music (Jellyf
install_jellyfin() {
require_docker || return 1
# ── Instance selection ───────────────────────────────────────────────────
# First instance keeps the plain "jellyfin" name/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 (and services/emby.sh,
# the same idea for a sibling media server).
local JELLYFIN_DIR="$DOCKER_DIR/jellyfin"
local INSTANCE_SUFFIX="" CONTAINER="jellyfin"
local WEB_PORT="8096"
local DEFAULT_MEDIA="$ACTUAL_HOME/media"
if [ "$DRY_RUN" = true ]; then
echo "[DRY-RUN] Jellyfin would:"
echo " - Create $JELLYFIN_DIR with docker-compose.yml + .env (config/ cache/)"
echo " - Offer to add a new, separate instance if one already exists"
echo " - Create \$DOCKER_DIR/jellyfin(-<name>) with docker-compose.yml + .env (config/ cache/)"
echo " - Mount a media folder (default $DEFAULT_MEDIA) read-only at /media"
echo " - Auto-enable VAAPI hw transcoding if /dev/dri/renderD128 exists"
echo " - Expose port 8096 (+ DLNA 1900/udp, discovery 7359/udp)"
echo " - Expose port 8096, auto-scanned for additional instances"
echo " - First instance only: DLNA 1900/udp + discovery 7359/udp (host-wide;"
echo " additional instances skip these to avoid a fixed-port conflict)"
echo " - Offer a Caddy reverse proxy and to start the container"
return 0
fi
if [ -d "$JELLYFIN_DIR" ]; then
echo ""
echo " Jellyfin is already installed at $JELLYFIN_DIR."
echo " 1) Manage that install (update / full reinstall / cancel)"
echo " 2) Add a NEW, separate Jellyfin instance alongside it (its own"
echo " server, library, and port — 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. 'kids'):" "" _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/jellyfin-$_suffix" ]; then
log_warning "jellyfin-$_suffix already exists — pick another name."; continue
fi
break
done
INSTANCE_SUFFIX="$_suffix"
JELLYFIN_DIR="$DOCKER_DIR/jellyfin-$_suffix"
CONTAINER="jellyfin-$_suffix"
DEFAULT_MEDIA="$ACTUAL_HOME/media-$_suffix"
while ss -tlnH "sport = :${WEB_PORT}" 2>/dev/null | grep -q .; do
WEB_PORT=$((WEB_PORT + 1))
done
log_info "New instance: $JELLYFIN_DIR (port $WEB_PORT)"
log_warning "DLNA/discovery (1900/udp, 7359/udp) are fixed, host-wide ports already"
log_warning "claimed by the first instance — this instance skips them (web UI and"
log_warning "app-based streaming are unaffected; DLNA auto-discovery is not)."
fi
fi
local MEDIA_PATH=""
prompt_text "Path to media folder [$DEFAULT_MEDIA]:" "$DEFAULT_MEDIA" MEDIA_PATH
MEDIA_PATH="${MEDIA_PATH/#\~/$ACTUAL_HOME}"; MEDIA_PATH="${MEDIA_PATH%/}"
@@ -248,14 +296,25 @@ networks:
"
fi
# DLNA (1900/udp) and discovery (7359/udp) are fixed, host-wide UDP ports —
# only the first instance publishes them to avoid a bind conflict with an
# existing instance. Additional instances still work for the web UI and
# every app-based client; only DLNA/network auto-discovery is instance-1-only.
local _DISCOVERY_PORTS=""
if [ -z "$INSTANCE_SUFFIX" ]; then
_DISCOVERY_PORTS=' - "1900:1900/udp"
- "7359:7359/udp"
'
fi
cat > docker-compose.yml << JELLYFIN_COMPOSE
name: jellyfin
name: $CONTAINER
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
hostname: jellyfin
container_name: $CONTAINER
hostname: $CONTAINER
restart: unless-stopped
environment:
- TZ=$TZ_VAL
@@ -265,10 +324,8 @@ $HWACCEL_BLOCK
- ./cache:/cache
- \${MEDIA_PATH}:/media:ro
ports:
- "8096:8096"
- "1900:1900/udp"
- "7359:7359/udp"
${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION}
- "${WEB_PORT}:8096"
${_DISCOVERY_PORTS}${_CADDY_NET_BLOCK}${_CADDY_NET_SECTION}
JELLYFIN_COMPOSE
cat > .env << JELLYFIN_ENV
@@ -278,16 +335,22 @@ JELLYFIN_ENV
mkdir -p config cache
chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR"
log_success "Jellyfin configured at $JELLYFIN_DIR"
log_success "Jellyfin${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} configured at $JELLYFIN_DIR (port $WEB_PORT)"
configure_caddy_for_service "Jellyfin" "jellyfin:8096" "jellyfin"
configure_caddy_for_service "Jellyfin${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)}" "${CONTAINER}:8096" "jellyfin${INSTANCE_SUFFIX:+-$INSTANCE_SUFFIX}"
write_readme "$JELLYFIN_DIR" << MD
# Jellyfin
# Jellyfin${INSTANCE_SUFFIX:+ — $INSTANCE_SUFFIX}
Free media server (movies, TV, music) — a no-paywall alternative to Emby.
$( [ -n "$INSTANCE_SUFFIX" ] && echo "
This is a separate, fully isolated instance (own server, own library, own
port) — not a shared library with another Jellyfin instance. DLNA and
network auto-discovery are only published for the first instance (fixed,
host-wide ports); this instance still works for the web UI and every
app-based client.")
- Web UI: http://localhost:8096
- Web UI: http://localhost:${WEB_PORT}
- Media folder (read-only): \`$MEDIA_PATH\` → mounted at /media
- App data: \`config/\` and \`cache/\` in this folder
- Edit the media path in \`.env\` (\`MEDIA_PATH=\`), then \`docker compose up -d\`.
@@ -309,13 +372,13 @@ docker compose pull && docker compose up -d # update
MD
local START_JF=""
prompt_yn "Start Jellyfin now? (y/n):" "y" START_JF
prompt_yn "Start Jellyfin${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} now? (y/n):" "y" START_JF
if [ "$START_JF" = "y" ] || [ "$START_JF" = "Y" ]; then
docker compose up -d && log_success "Jellyfin started" || log_warning "Failed to start — check: docker compose logs"
docker compose up -d && log_success "Jellyfin${INSTANCE_SUFFIX:+ ($INSTANCE_SUFFIX)} started" || log_warning "Failed to start — check: docker compose logs"
fi
echo ""
echo " Access at: http://localhost:8096"
echo " Access at: http://localhost:${WEB_PORT}"
echo ""
}