Adds an opt-in prompt to store Immich-managed data (thumbnails, encoded
video, new uploads) in S3-compatible object storage instead of local
disk, using Immich's native IMMICH_STORAGE_ENGINE=s3 — deliberately NOT
a FUSE-mounted bucket. Checked this against real reported issues before
implementing: Immich uses symlinks internally that S3 doesn't support
under FUSE (ENOSYS errors), and its startup does thousands of stat()/
read() calls that FUSE-over-network handles badly enough to crash the
mount under latency spikes as small as 100ms. Native S3 mode talks to
the bucket over the S3 API directly, sidestepping both problems.
Independent of the existing external-library strategy — an external
library (existing photos indexed read-only, e.g. over a VPN mount) is
a separate mount either way and works the same regardless of where
Immich's own managed data lives, since S3 mode only replaces
UPLOAD_LOCATION.
- New prompts: bucket, region, endpoint (for non-AWS S3-compatible
providers — auto-sets S3_FORCE_PATH_STYLE when given), prefix, access
key ID, and secret key (read via `read -rs` so it doesn't echo; left
blank with a warning under UNATTENDED, since there's no sane default).
- Refactored the docker-compose.yml generation from two near-duplicate
heredocs (with/without external library) into one with composable
volume-line variables, to avoid quadrupling the duplication once S3
was added as a second axis.
- Skips creating local upload-location subdirectories entirely in S3
mode (thumbs/upload/backups/library/profile/encoded-video) — Immich
manages that structure inside the bucket itself.
- .env now gets chmod 600 (previously ungated) — more pointed now that
it can hold an S3 secret key, not just the DB password.
- Generated README documents the S3 setup and carries the FUSE-mount
warning forward so a future reader doesn't try that route instead.
Verified both the non-S3 baseline (unchanged output) and S3 mode
end-to-end via non-interactive dry runs — correct .env, correct
compose volumes, no local upload dirs created, 0600 permissions.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TBtExJcqxnokyZZKmphdug
Same bug class just fixed in mattermost.sh: immich, joplin, koha,
mail-archiver, and nextcloud all generated a fresh random
DB/admin password on every single run with no check for an
existing one. Each backs its database with a persistent volume, so
Postgres/MariaDB keeps the password from its first init while the
freshly overwritten .env (or config-main.env for koha) no longer
matches it — any rerun would have locked the app out of its own
database. koha, mail-archiver, and nextcloud also regenerated an
app-level admin login password the same way.
Found by cross-referencing every service with a DB password against
which ones actually guard reuse on rerun (only traccar.sh did,
already correctly) rather than waiting to be told about each one
individually.
Fix mirrors traccar.sh's existing pattern: read the password back out
of the existing .env/config file if present, only generate fresh when
there's genuinely nothing there yet.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NQkdAn3iG5A4WoqU9FHMaN
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
Each service can now be run directly with sudo bash <service>.sh on any
machine with Docker installed, without needing the full post-install repo.
Uses the shared bootstrap pattern from docs/standalone-template.sh.
https://claude.ai/code/session_014CCYqVwW6d6f5dw1qRokYt
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
Both silent-send and immich already use NodeSource (not Ubuntu repos).
Node 24 is the current active LTS; 22 moves to maintenance in 2025.
Minimum version checks (>=18 and >=20) are unchanged — both services
accept any sufficiently recent Node.
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG
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
Migrate the full media batch from the monolith:
- jellyfin: auto-VAAPI on /dev/dri/renderD128 + render GID; DLNA/discovery UDP ports
- emby: UID/GID baked at install; HW transcoding block commented for manual opt-in
- audiobookshelf: separate audiobooks + podcasts paths; port 13378
- arm: optical drive detection; privileged:true; split movies/music output; port 8080
- lyrion: network_mode:host for Chromecast/Squeezebox UDP discovery; port 9000
- immich: multi-container stack (server+ML+valkey+postgres); two library strategies
(unified with import-photos.sh helper, or external read-only); port 2283
https://claude.ai/code/session_01Y4dMKtkqkpvmgDKoRdzhTG