From ad1dd01484a205463c7e8e46ebd152ebf9207286 Mon Sep 17 00:00:00 2001 From: Jozsef Gtta Date: Sun, 24 May 2026 11:49:44 +0000 Subject: [PATCH] =?UTF-8?q?docs:=20update=20jellyfin-services.md=20?= =?UTF-8?q?=E2=80=94=20cron=20run-once=20Watchtower=20approach?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- jellyfin/jellyfin-services.md | 93 ++++++++++++++++------------------- 1 file changed, 43 insertions(+), 50 deletions(-) diff --git a/jellyfin/jellyfin-services.md b/jellyfin/jellyfin-services.md index 308c3fe..559bb05 100644 --- a/jellyfin/jellyfin-services.md +++ b/jellyfin/jellyfin-services.md @@ -5,13 +5,12 @@ - **IP**: `192.168.88.10` - **SSH**: `jgitta@192.168.88.10` (alias: `jellyfin`) - **Portainer endpoint ID**: 4 -- **Compose files**: managed via Portainer (stack IDs below); source of truth is Gitea `jgitta/homelab-configs` --- -## Stacks +## Stacks (Portainer) -| Stack | Portainer ID | Gitea Path | +| Stack | ID | Gitea Path | |---|---|---| | arr-stack | 35 | `jellyfin/arr-stack/docker-compose.yml` | | portainer-agent | 36 | `jellyfin/portainer-agent/` | @@ -27,67 +26,61 @@ | sonarr | lscr.io/linuxserver/sonarr:latest | 8989 | /srv/docker/sonarr/config | | prowlarr | lscr.io/linuxserver/prowlarr:latest | 9696 | /srv/docker/prowlarr/config | | flaresolverr | ghcr.io/flaresolverr/flaresolverr:latest | 8191 | — | -| watchtower | containrrr/watchtower | — | see below | -| watchtower-prowlarr | containrrr/watchtower | — | see below | -Media volume: `/mnt/media` on jellyfin host → mounted as `/data` in radarr/sonarr. +Media volume: `/mnt/media` → mounted as `/data` in radarr/sonarr. --- -## Watchtower — Split-Instance Setup +## Auto-Updates — Cron + Watchtower run-once -Two Watchtower instances run in the arr-stack to stagger registry checks and avoid -rate limiting lscr.io/ghcr.io with too many concurrent requests. +Watchtower is NOT run as a persistent container. Instead, cron jobs on the jellyfin +host invoke Watchtower in `--run-once` mode, one container at a time, at staggered +3-minute intervals. This serializes all lscr.io registry checks and eliminates the +CDN burst rate limit errors that occurred with concurrent requests. -| Instance | Schedule | Scope | Manages | -|---|---|---|---| -| `watchtower` | 4:00 AM | (none) | radarr, sonarr, flaresolverr, portainer-agent | -| `watchtower-prowlarr` | 4:05 AM | `prowlarr` | prowlarr only | +### Schedule (jgitta crontab on jellyfin) -The scope works via Docker labels: prowlarr has -`com.centurylinklabs.watchtower.scope=prowlarr` — this tells the main `watchtower` -to skip it, and tells `watchtower-prowlarr` to manage it. +| Time | Container | Command | +|---|---|---| +| 4:00 AM | radarr | `watchtower-update.sh radarr` | +| 4:03 AM | sonarr | `watchtower-update.sh sonarr` | +| 4:06 AM | prowlarr | `watchtower-update.sh prowlarr` | +| 4:09 AM | flaresolverr + others | `watchtower-update.sh` (no scope) | -Both instances mount `/home/jgitta/.docker/config.json:/config.json:ro` for -authenticated registry pulls. +### Update script + +Location: `/usr/local/bin/watchtower-update.sh` on jellyfin host. + +To view: `ssh jellyfin cat /usr/local/bin/watchtower-update.sh` +To edit cron: `ssh jellyfin crontab -e` + +Each run spins up a temporary `containrrr/watchtower` container with `--run-once`, +checks for updates, sends an email notification if anything changed, and exits. +The container is automatically removed after each run (`--rm`). + +### Scope labels + +radarr, sonarr, and prowlarr each carry a `com.centurylinklabs.watchtower.scope=` +label in the compose file. This tells Watchtower to only check the matching container +when that scope is set. flaresolverr has no scope label and is handled by the +unscoped (4:09 AM) run. --- -## Registry Authentication (fixed 2026-05-20) +## Registry Authentication -**Problem**: Watchtower was hitting lscr.io/ghcr.io rate limits and timeouts with -unauthenticated requests. lscr.io is a proxy that redirects to ghcr.io for the actual -registry API — credentials for lscr.io alone were not enough; ghcr.io also needed auth. -Two separate issues were compounding: -1. Unauthenticated requests shared a small anonymous quota -2. Two concurrent Watchtower instances were doubling all requests at 4 AM +Credentials are stored in `/home/jgitta/.docker/config.json` on the jellyfin host. +The update script mounts this file into each Watchtower container. -**Fix**: +Both `lscr.io` and `ghcr.io` are authenticated (lscr.io proxies to ghcr.io for the +actual registry API, so both need credentials). -Three sets of credentials were configured — both as the `jgitta` user (for Watchtower's -own registry digest checks) and as `root` (for the Docker daemon's actual image pulls): +Token: no-expiry `read:packages` GitHub PAT — see credentials.md. +To re-authenticate (e.g. after rotating token): ```bash -# As jgitta (Watchtower reads /home/jgitta/.docker/config.json via volume mount) -echo | docker login lscr.io -u jgitta --password-stdin -echo | docker login ghcr.io -u jgitta --password-stdin - -# As root (Docker daemon uses /root/.docker/config.json for docker pull) -echo | sudo docker login lscr.io -u jgitta --password-stdin -echo | sudo docker login ghcr.io -u jgitta --password-stdin +ssh jellyfin +echo | docker login lscr.io -u jgitta --password-stdin +echo | docker login ghcr.io -u jgitta --password-stdin ``` - -Token used: the no-expiry `read:packages` GitHub PAT in credentials.md. - -**If token ever needs rotating**: run the four `docker login` commands above on jellyfin -with the new token. No compose changes needed — both config files are read live. - ---- - -## Notes -- Do NOT run `docker restart watchtower` manually while `watchtower-prowlarr` is running — - Watchtower's multi-instance detection will cause one to kill the other. Use - `docker restart watchtower watchtower-prowlarr` to restart both together, or let - Portainer manage redeployment. -- Portainer stack 35 is the authoritative runtime config. If it drifts from Gitea, - update via Portainer API with `prune: true` to remove orphaned services. +No script or compose changes needed — the config.json is read at runtime.