docs: update jellyfin-services.md — cron run-once Watchtower approach
This commit is contained in:
@@ -5,13 +5,12 @@
|
|||||||
- **IP**: `192.168.88.10`
|
- **IP**: `192.168.88.10`
|
||||||
- **SSH**: `jgitta@192.168.88.10` (alias: `jellyfin`)
|
- **SSH**: `jgitta@192.168.88.10` (alias: `jellyfin`)
|
||||||
- **Portainer endpoint ID**: 4
|
- **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` |
|
| arr-stack | 35 | `jellyfin/arr-stack/docker-compose.yml` |
|
||||||
| portainer-agent | 36 | `jellyfin/portainer-agent/` |
|
| portainer-agent | 36 | `jellyfin/portainer-agent/` |
|
||||||
@@ -27,67 +26,61 @@
|
|||||||
| sonarr | lscr.io/linuxserver/sonarr:latest | 8989 | /srv/docker/sonarr/config |
|
| sonarr | lscr.io/linuxserver/sonarr:latest | 8989 | /srv/docker/sonarr/config |
|
||||||
| prowlarr | lscr.io/linuxserver/prowlarr:latest | 9696 | /srv/docker/prowlarr/config |
|
| prowlarr | lscr.io/linuxserver/prowlarr:latest | 9696 | /srv/docker/prowlarr/config |
|
||||||
| flaresolverr | ghcr.io/flaresolverr/flaresolverr:latest | 8191 | — |
|
| 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
|
Watchtower is NOT run as a persistent container. Instead, cron jobs on the jellyfin
|
||||||
rate limiting lscr.io/ghcr.io with too many concurrent requests.
|
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 |
|
### Schedule (jgitta crontab on jellyfin)
|
||||||
|---|---|---|---|
|
|
||||||
| `watchtower` | 4:00 AM | (none) | radarr, sonarr, flaresolverr, portainer-agent |
|
|
||||||
| `watchtower-prowlarr` | 4:05 AM | `prowlarr` | prowlarr only |
|
|
||||||
|
|
||||||
The scope works via Docker labels: prowlarr has
|
| Time | Container | Command |
|
||||||
`com.centurylinklabs.watchtower.scope=prowlarr` — this tells the main `watchtower`
|
|---|---|---|
|
||||||
to skip it, and tells `watchtower-prowlarr` to manage it.
|
| 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
|
### Update script
|
||||||
authenticated registry pulls.
|
|
||||||
|
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=<name>`
|
||||||
|
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
|
Credentials are stored in `/home/jgitta/.docker/config.json` on the jellyfin host.
|
||||||
unauthenticated requests. lscr.io is a proxy that redirects to ghcr.io for the actual
|
The update script mounts this file into each Watchtower container.
|
||||||
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
|
|
||||||
|
|
||||||
**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
|
Token: no-expiry `read:packages` GitHub PAT — see credentials.md.
|
||||||
own registry digest checks) and as `root` (for the Docker daemon's actual image pulls):
|
|
||||||
|
|
||||||
|
To re-authenticate (e.g. after rotating token):
|
||||||
```bash
|
```bash
|
||||||
# As jgitta (Watchtower reads /home/jgitta/.docker/config.json via volume mount)
|
ssh jellyfin
|
||||||
echo <token> | docker login lscr.io -u jgitta --password-stdin
|
echo <new_token> | docker login lscr.io -u jgitta --password-stdin
|
||||||
echo <token> | docker login ghcr.io -u jgitta --password-stdin
|
echo <new_token> | docker login ghcr.io -u jgitta --password-stdin
|
||||||
|
|
||||||
# As root (Docker daemon uses /root/.docker/config.json for docker pull)
|
|
||||||
echo <token> | sudo docker login lscr.io -u jgitta --password-stdin
|
|
||||||
echo <token> | sudo docker login ghcr.io -u jgitta --password-stdin
|
|
||||||
```
|
```
|
||||||
|
No script or compose changes needed — the config.json is read at runtime.
|
||||||
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.
|
|
||||||
|
|||||||
Reference in New Issue
Block a user