host naming, and chain-in from filebrowser/audiobookshelf/emby
Reported live: "mount error(79): Can not access a needed shared library"
on the local CIFS mount step. That message is misleadingly worded — errno
79 is ENOKEY, not a real missing-library problem, and a plain `guest`
mount with no explicit `sec=` hitting it against a real Samba server is a
known cifs-utils/kernel-cifs rough edge in the anonymous-session keyring
path. Fixed as a side effect of switching away from guest access per
direct request (real per-share Samba accounts, not root/guest, matching
"user accounts for data directories"): each mount now gets a dedicated
Samba account (reusing the SSH username — that Unix account already
exists on the home box) with a generated password, remotely provisioned
via smbpasswd over the same SSH trust, and mounted locally via a
root-only credentials file (same convention tools/mount-network-drive.sh
already uses) plus an explicit sec=ntlmssp instead of guest.
Host naming: entering a raw IP now offers to name it in /etc/hosts, then
uses that name for everything from then on (SSH commands, the CIFS mount
address, and re-runs against the same IP). Deliberately /etc/hosts, not
~/.ssh/config — an SSH Host alias only helps the `ssh` command resolve a
name, mount.cifs never consults ~/.ssh/config at all, so an alias alone
wouldn't get the actual mount using a name. Still offers to also add a
matching SSH Host alias on top (pure convenience — skips typing the
username for interactive ssh use) when services/ssh-config.sh's helpers
are available.
Chain-in: filebrowser/audiobookshelf/emby now offer to run
vpn-data-mount first if their data is on a home box that isn't mounted
yet, and default their own directory prompt to whatever was just mounted
(VDM_LAST_MOUNT_POINT, explicitly unset before each chain call so an
unrelated earlier vpn-data-mount run in the same setup.sh session can't
leak its mount point in as a stale default).
Confirmed from gtstef/filebrowser's own Dockerfile (_docker/Dockerfile):
the image runs as a fixed non-root user (adduser -u 1000 filebrowser;
USER filebrowser), not root and not remappable via PUID/PGID. The
installer's broad `chown -R $ACTUAL_USER:$ACTUAL_USER "$FB_DIR"` left the
bind-mounted ./data owned by $ACTUAL_USER (root, on a box where the
installer itself runs as root) — UID 1000 inside the container then had no
write access to it, so every start failed with "could not open database:
open /home/filebrowser/data/database.db: permission denied" and the
container crash-looped indefinitely (restart: unless-stopped kept retrying
every ~60s, matching the log timestamps this was diagnosed from).
Re-chown ./data to 1000:1000 specifically, after the broad chown so it
isn't clobbered back to $ACTUAL_USER.
fmd.sh pointed at nulide/findmydevice, which no longer exists on Docker
Hub — the project has moved twice (nulide/findmydevice ->
gitlab.com/Nulide/findmydeviceserver -> gitlab.com/fmd-foss/fmd-server) and
was rewritten from Node.js to Go+React along the way, confirmed against the
current upstream repo and its GitLab container registry. This means the
service never actually started for anyone who installed it before this fix
("pull access denied for nulide/findmydevice, repository does not exist").
Switch to registry.gitlab.com/fmd-foss/fmd-server:0 (GitLab's own registry
has no "latest" tag; ":0" tracks the current major release the same way
this repo's other services use a floating tag). The old FMD_ADMIN_PASSWORD
model is gone from the app too — replaced with FMD_REGISTRATIONTOKEN
(self-registration gated by a token instead of one shared admin login), and
the database path moved from /fmd/data to /var/lib/fmd-server/db.
Also: filebrowser.sh and fmd.sh both showed "Manage that install (update /
full reinstall / cancel)" when re-run against an existing install, but
choosing "1) Manage" fell straight through into the same unconditional
fresh-install flow every time — no way to actually cancel or update in
place, contradicting both the banner text and the documented
prompt_reinstall_mode contract (CLAUDE.md's "Update vs. fresh reinstall on
rerun"). Wired both up to prompt_reinstall_mode, matching the reference
pattern in services/mattermost.sh. The same gap exists in 15 other
multi-instance services (vaultwarden, immich, audiobookshelf, homebox,
rustdesk, emby, meshcentral, traccar, lyrion, actualbudget, mealie, joplin,
jellyfin, unifi, ntfy) — not fixed here, flagged for a follow-up pass.
With 70+ services sharing a handful of common default ports (emby and
jellyfin both default to 8096, changedetection and frigate both default
to 5000, arm and nextcloud both default to 8080...), nothing previously
checked whether a service's default port was actually free on the host.
Whichever service installed second would silently write a compose file
claiming an already-held port, only failing at `docker compose up` time.
Adds two shared helpers to lib/common.sh:
- port_in_use PORT [PROTO] — true if something's already listening
- find_free_port VARNAME START [PROTO] — scans upward, writes back the
first free port
Every service that publishes a fixed host port now scans before writing
docker-compose.yml, on every install (not just when adding an explicit
additional instance). On a normal single-install host this is a silent
no-op; it only changes behavior when something else already holds the
port.
- The 19 services already given multi-instance support this session had
their port scan moved out of the "add instance" branch to run
unconditionally, since the same collision risk exists on a plain first
install.
- 20 more services with previously-hardcoded ports gained scanning for
the first time: archivebox, arm, calibre-web, changedetection,
drum-rhythm-game, gatus, n8n, nextcloud, onlyoffice, stirling-pdf,
uptimekuma, portainer, iopaint (both GPU/CPU compose branches), koha
(paired), syncthing (paired), wg-easy (paired, plus WG_PORT env so
generated peer configs keep the right Endpoint), homeassistant
(bridge-mode only — host mode can only warn), frigate and
frigate-audio (multi-port stacks, moved together).
- caddy.sh is the deliberate exception: 80/443 stay fixed and only warn
on collision, since silently moving Caddy itself would leave nothing
listening where any client actually looks.
- authelia.sh needs no change — it has no published host port at all.
- Every service's standalone bootstrap fallback (sudo bash services/x.sh
with no sibling files) got the same two helpers duplicated into its
stub block, matching how every other shared helper is already handled
there.
Documents the full pattern in CLAUDE.md's new "Port collision avoidance"
section, including the quoted-heredoc/backtick-escaping gotcha and the
network_mode:host limitation (can only scan ports the app takes as a
configurable env var).
Verified via bash -n on every changed file, plus functional runs seeding
occupied ports for each collision shape used here (single, paired,
multi-port stacks) and confirming the scan/shift and generated
compose/README output are correct — including the emby/jellyfin,
nextcloud/arm, and frigate/changedetection collision scenarios that
originally motivated this.
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.
traccar.sh's caddy_net wiring was fixed to mirror configure_caddy_for_service's
own mode resolution (CADDY_MODE from site config, then a local ~/docker/caddy,
then the legacy CADDY_REMOTE_HOST var) instead of only checking for the local
directory. That same bare directory check was copy-pasted into the caddy_net
wiring of every other Docker service in the repo, so a site with Caddy on a
different box would silently fail to join any of their containers to caddy_net
during setup (or, for homeassistant/koha, only get half the wiring right).
Applied the same fix mechanically across all 37 services using the standard
_CADDY_NET_BLOCK/_CADDY_NET_SECTION pattern (verified identical text via
scripted diff before touching any of them), plus by hand for:
- homeassistant.sh and koha.sh, which use their own differently-shaped
variables (HA_CADDY_NET_LINES / _CADDY_NET_ENTRY) for the same decision
- paintplus.sh and ai-stack.sh, which do a live `docker network connect`
instead of a compose network block
- watchyourlan.sh, whose Caddy note was worded for local-only setups
sms-inbound.sh got more than a mode swap: its Caddy wiring was hand-rolled
(not routed through configure_caddy_for_service) and had no remote-Caddy
path at all — a remote Caddy box would get a misleading "Caddy isn't
installed here" message instead of a snippet. Added
_sms_write_caddy_snippet(), mirroring the snippet-file pattern
configure_caddy_for_service uses everywhere else, and pointed the firewall
gate at the same three-way mode instead of a two-way dir check.
Verified: bash -n across all of services/*.sh, a scripted check that every
touched file has exactly one _CADDY_MODE resolution and no leftover bare
`[ -d "$DOCKER_DIR/caddy" ]` feeding a caddy_net decision, and spot-checked
docker compose config renders (traccar, mattermost) confirming the ${VAR}
interpolation and multi-service usage sites still resolve correctly.
New site config key: CADDY_REMOTE_HOST (set via 'sudo ./setup.sh configure').
When set, configure_caddy_for_service operates in "remote" mode instead of
writing to a local Caddyfile:
- Upstream uses CADDY_REMOTE_HOST:PORT (host IP, not container name)
- Snippet saved to ~/docker/caddy-snippets/<subdomain>.caddy
- User is shown scp/rsync commands to copy it to the Caddy machine
Three modes in configure_caddy_for_service (lib/common.sh and inline stubs):
local: ~/docker/caddy/ exists → write Caddyfile + reload (existing behavior)
remote: CADDY_REMOTE_HOST set → save snippet, print copy instructions
none: neither configured → silent return (unchanged)
All 31 service standalone bootstrap stubs updated with the new logic.
CADDY_REMOTE_HOST global added to all 42 standalone bootstrap sections.
setup.sh configure now prompts for CADDY_REMOTE_HOST with a clear explanation.
wolf.sh: add missing stubs (configure_caddy_for_service, write_readme,
prompt_yn, ensure_docker_dir_ownership) and the Authelia/Caddy/start calls
that were missing from the install function.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
- Swaps image from filebrowser/filebrowser to gtstef/filebrowser:stable
- Uses ./data:/home/filebrowser/data volume layout (Quantum convention)
- Generates data/config.yaml instead of settings.json + database.db
- Mounts primary path as /files with defaultEnabled: true
- Deploys fbq-add-source.sh for adding extra directories post-install
- Updates README with Quantum-specific instructions
docs: save standalone bootstrap as docs/standalone-template.sh
Preserves the full standalone pattern from filebrowser.sh as the
reference template for adding standalone support to other services.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
Symlinks don't work for giving scoped FileBrowser users access to extra
folders — FileBrowser's afero.BasePathFs blocks symlinks that resolve
outside the user's scope directory.
Switch to bind-mount approach: additional_directories.sh edits
docker-compose.yml to add real bind-mount entries for each extra folder,
so FileBrowser sees them as actual subdirectories within the user's root.
No symlinks, no scope-boundary issues.
Features:
- Reads FB_PATH from .env to list available source folders on the host
- Parses docker-compose.yml to show what's already configured per user
- Adds/removes volume entries with a timestamped backup before each edit
- Prompts to restart the container after changes
- Normalises scope paths from the API (handles missing leading slash)
manage_users.sh removed — user CRUD is handled by the FileBrowser web UI.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
docker-compose now uses two separate mounts:
fb_users named volume → /srv (user home dirs + shortcuts, Docker only)
FB_PATH bind mount → /srv/data (actual files, unchanged on host)
User dirs and their shortcuts live entirely in the Docker named volume —
they persist across reboots but never appear on the host filesystem.
Full-access scope is /data; per-user scopes are /alice etc.
manage_users.sh:
- Detect layout: get_data_root() returns /srv/data (new) or /srv (legacy)
so the script works with both old and new installs automatically
- "Linked directories" renamed to "additional directories" throughout
(menu labels, prompts, error messages, usage text)
- prompt_add_links → prompt_add_dirs, menu_links → menu_add_dirs
- Available-folder listing and symlink targets use get_data_root()
- Scope examples updated to show /data for full access (new layout)
- Don't offer additional directories when scope is /data (full access)
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
Replace the "configure manually" stub with a real implementation that
matches common.sh's configure_caddy_for_service behaviour:
- Prompts to configure (skippable)
- Asks for domain with example hint
- Backs up Caddyfile before writing
- Checks for duplicate domain entry, offers overwrite
- Appends site block with reverse_proxy, security headers, JSON log
- fmt + reload via docker exec caddy caddy fmt/reload
Used when filebrowser.sh is run standalone without the repo present.
When the repo is present (common.sh sourced), the real function is used.
https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
DOCKER_DIR was computed before ACTUAL_HOME, so $HOME resolved to
/root (the root user's home) instead of the invoking user's home.
Files were created under /root/docker/ rather than ~/docker/.
Fix: compute ACTUAL_USER and ACTUAL_HOME first, then DOCKER_DIR.
https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
Add standalone bootstrap block at the top (BASH_SOURCE[0] == $0 guard):
- If lib/common.sh is present (repo cloned), source it — gets real helpers
and picks up any existing ~/docker/.config site settings automatically
- If not (one-off copy), inline minimal stubs: logging, require_docker
(checks Docker is installed, gives install hint if not), prompt_text/yn
(match common.sh eval pattern so install_filebrowser locals work),
configure_caddy_for_service (no-op with manual hint), write_readme
- Sets DOCKER_DIR, ACTUAL_USER, SITE_TZ etc. with sensible defaults
- register_service becomes a no-op (no wizard menu to register into)
- Execution deferred via _RUN_STANDALONE flag to after function definition
Still works identically when sourced by setup.sh — the bootstrap block
is skipped entirely in that path.
https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
Undo the host.docker.internal approach from the previous commit — proper
Docker networking routes Caddy to services by container name on the shared
caddy_net, not via the host gateway.
- lib/common.sh: configure_caddy_for_service now accepts either a plain
port number (localhost:PORT fallback) or container:port (preferred).
The Caddyfile entry uses the container name for direct Docker DNS routing.
- services/caddy.sh: remove extra_hosts hack; update Caddyfile template
comments to show container_name:port format
- All service files: update configure_caddy_for_service calls to pass
container_name:internal_port (e.g. "filebrowser:80", "mealie:9000").
Services using network_mode:host keep plain port numbers.
- tools/manage_users.sh: new FileBrowser user-management script (deployed
to ~/docker/filebrowser/ during installation). Manages users via the
FileBrowser REST API: list, add, delete, passwd, scope, info commands.
Documents username format (letters/numbers/hyphens/underscores),
password rules (min 8 chars, letter + number required), and scope path
convention relative to /srv (= FB_PATH on the host).
https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
- lib/common.sh: fix broken reverse_proxy target — localhost inside Caddy's
container is the container's own loopback, not the host; change to
host.docker.internal so proxied ports are actually reachable
- services/caddy.sh: add extra_hosts host.docker.internal:host-gateway so
the above resolves correctly; create caddy_net bridge network in Caddy's
own compose so other services can reference it as external; update all
Caddyfile template comments and README examples to match
- services/filebrowser.sh: update image tag from deprecated :s6 to :latest;
remove non-functional PUID/PGID env vars (filebrowser/filebrowser does not
honour them); add configure_caddy_for_service call; add caddy_net
- services/ntfy.sh: add configure_caddy_for_service call; add caddy_net
- services/portainer.sh: add configure_caddy_for_service call; add caddy_net
- services/frigate-notify.sh, watchtower.sh: add caddy_net for
container-to-container comms (frigate, ntfy) without a Caddy call
- All remaining web-facing Docker services: add caddy_net network block to
docker-compose and CADDY_NET to .env where applicable; services using
network_mode: host (wolf-pair, lyrion) have the top-level block only
https://claude.ai/code/session_01UZus2Q9gNTfUdqSMrhuX29
Introduces a one-time configuration wizard (sudo ./setup.sh configure)
that stores SITE_TZ, SITE_DOMAIN, and SITE_CADDY_NET in ~/docker/.config.
Every service now uses these as prompt defaults so the user types common
values once instead of re-answering the same questions for each service.
- lib/common.sh: load_site_config / save_site_config; auto-loads on source;
backward-compat BASE_DOMAIN alias kept for old .config files
- setup.sh: run_site_configure wizard; first-run offer after base install;
`sudo ./setup.sh configure` command to update defaults at any time
- 14 services: TZ_VAL now honours SITE_TZ, falling back to /etc/timezone
- 3 inline-heredoc services (filebrowser, homeassistant, ntfy): same fix
- authelia: SITE_TZ/SITE_DOMAIN as prompt defaults; SITE_CADDY_NET replaces
hardcoded caddy_net throughout (env, compose patch, network creation)
- minecraft, frigate-audio: simplify BASE_DOMAIN read to use SITE_DOMAIN
- sky-cam: SITE_TZ as default for timezone prompt
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
- lib/common.sh: add write_readme helper. Every module now writes a README.md
into its ~/docker/<service>/ folder (self-documenting service folders).
- services/authelia.sh: SSO + 2FA portal, ported from the authelia-setup repo +
the monolith's working block (secrets + Argon2 hash generation, caddy_net,
Caddyfile forward-auth snippet + portal block, README). Guards against
clobbering an existing install.
- services/{filebrowser,ntfy,uptimekuma,portainer,watchtower}.sh: mechanical
migrations from the monolith, each with a README. Ports 8085/8090/3001/9443/—.
All pass bash -n; ./setup.sh --list shows them under homelab; dry-run run-one
exits 0 for each with real commands guarded.
https://claude.ai/code/session_017eA2qqq9jfF2tNtpUYL8vK