Token 76858153...f8b1 was committed across 9 docs + 1 snapshot RESTORE.md and exposed via the brief public window of this repo. Replaced with `<JELLYFIN_API_TOKEN>` placeholder. WARNING: this commit only redacts HEAD — the token remains in git history. Anyone who cloned during the public window has the full value. Treat the old token as compromised and rotate at Jellyfin Dashboard > API Keys. Original value backed up to private s8n/secrets/ARRFLIX/.
174 lines
7.2 KiB
Markdown
174 lines
7.2 KiB
Markdown
# 12 — Jellyfin DEV instance for theme experimentation
|
|
|
|
A second Jellyfin container, `jellyfin-dev`, runs alongside prod on
|
|
nullstone. Same media library (read-only), separate config/cache/users,
|
|
separate domain. LAN-only by design — you can break it freely without
|
|
real users (marco, house, guest, 5) noticing.
|
|
|
|
---
|
|
|
|
## Architecture diff
|
|
|
|
| Aspect | Prod | Dev |
|
|
|-------------------|-------------------------------------|-------------------------------------------|
|
|
| Container | `jellyfin` | `jellyfin-dev` |
|
|
| Image | `jellyfin/jellyfin:10.10.3` | `jellyfin/jellyfin:10.10.3` (must match) |
|
|
| Compose path | `/opt/docker/jellyfin/` | `/opt/docker/jellyfin-dev/` |
|
|
| Config dir | `/home/docker/jellyfin/{config,cache}` | `/home/docker/jellyfin-dev/{config,cache}` |
|
|
| Media mount | `/home/user/media:/media:ro` | `/home/user/media:/media:ro` (SAME, RO) |
|
|
| Domain | `arrflix.s8n.ru` | `dev.arrflix.s8n.ru` |
|
|
| Pi-hole DNS | `dns.hosts` in pihole.toml | `dns.hosts` in pihole.toml (added 2026-05-08) |
|
|
| Traefik router | `Host(arrflix.s8n.ru)` | `Host(dev.arrflix.s8n.ru)` |
|
|
| Cert | LE DNS-01 (Gandi) | LE DNS-01 (auto-issued on first request) |
|
|
| Middleware | `security-headers@file` only | `security-headers@file,no-guest@file` |
|
|
| WAN exposure | Yes during WAN window (doc 09) | NEVER — LAN-only forever |
|
|
| Internal port | `8096` | `8096` |
|
|
| User | `1000:1000` | `1000:1000` |
|
|
| `userns_mode` | `host` | `host` |
|
|
| index.html shim | Bind-mounted (doc 10) | None (vanilla shell — clean theme canvas) |
|
|
| Branding/auth | Configured | Empty — first-run wizard required |
|
|
|
|
The compose file lives in this repo at `compose-dev/docker-compose.yml`
|
|
and is deployed to nullstone at `/opt/docker/jellyfin-dev/docker-compose.yml`.
|
|
|
|
---
|
|
|
|
## How to use
|
|
|
|
1. Open `https://dev.arrflix.s8n.ru` from any LAN/tailnet box. First visit hits the
|
|
first-run wizard — create an admin user (use any throwaway name; nothing
|
|
shared with prod).
|
|
2. Add libraries pointing at the same paths prod uses:
|
|
- `/media/movies`
|
|
- `/media/tv`
|
|
The library ROOTS are shared (read-only); dev will rescrape independently
|
|
into its own `library.db`. That's intentional — dev is a clean slate.
|
|
3. Apply a theme via Branding API or via the SPA shim (doc 10) by dropping
|
|
files into `/opt/docker/jellyfin-dev/web-overrides/` and adding the same
|
|
bind-mount pattern as prod (currently absent for a clean canvas).
|
|
4. Test, watch, break. Prod remains untouched on `arrflix.s8n.ru`.
|
|
|
|
---
|
|
|
|
## Theme workflow (dev → prod)
|
|
|
|
When a dev theme is "shipped":
|
|
|
|
1. **Export branding** from dev:
|
|
```bash
|
|
curl -k -H "X-Emby-Token: $DEV_TOKEN" \
|
|
https://dev.arrflix.s8n.ru/Branding/Configuration > /tmp/branding.json
|
|
```
|
|
2. **POST to prod**:
|
|
```bash
|
|
curl -k -X POST \
|
|
-H "X-Emby-Token: <JELLYFIN_API_TOKEN>" \
|
|
-H "Content-Type: application/json" \
|
|
--data @/tmp/branding.json \
|
|
https://arrflix.s8n.ru/System/Configuration/branding
|
|
```
|
|
3. If the theme involves SPA-shim files (custom JS/CSS), `rsync` them from
|
|
`dev:/opt/docker/jellyfin-dev/web-overrides/` to
|
|
`prod:/opt/docker/jellyfin/web-overrides/` and hot-reload prod via the
|
|
bind-mount (no container restart needed for read-only mounts on file
|
|
change — Jellyfin will serve the new file on next request).
|
|
|
|
Auth tokens for dev are local to the dev instance — they'll be issued by
|
|
the dev wizard. They DO NOT cross over.
|
|
|
|
---
|
|
|
|
## Reset / wipe dev
|
|
|
|
When experiments make a mess:
|
|
|
|
```bash
|
|
ssh user@192.168.0.100
|
|
cd /opt/docker/jellyfin-dev
|
|
docker compose down
|
|
sudo rm -rf /home/docker/jellyfin-dev/config/* /home/docker/jellyfin-dev/cache/*
|
|
# (use the privileged-userns-host bypass if no sudo:
|
|
# docker run --rm --privileged --userns=host -v /home/docker:/h alpine \
|
|
# sh -c 'rm -rf /h/jellyfin-dev/config/* /h/jellyfin-dev/cache/*')
|
|
docker compose up -d
|
|
```
|
|
|
|
First-run wizard reappears. The media library is intact (read-only mount,
|
|
unaffected).
|
|
|
|
---
|
|
|
|
## LAN-only enforcement
|
|
|
|
`no-guest@file` middleware (defined in `/opt/docker/traefik/config/dynamic.yml`)
|
|
restricts source IPs to:
|
|
- `127.0.0.0/8`
|
|
- `192.168.0.0/24` (LAN)
|
|
- `100.64.0.1/32` onyx, `100.64.0.2/32` nullstone, `100.64.0.4/32` office (tailnet)
|
|
- `82.22.5.233/32` YOU500 home IP
|
|
- `172.20.0.0/24` docker proxy gateway
|
|
|
|
Anyone outside that list trying `https://dev.arrflix.s8n.ru` from the WAN
|
|
gets a Traefik 403. Even if a guest tailnet node (100.64.0.3 friend GPU)
|
|
hits dev, no-guest blocks them — only `tag:admin` and `tag:infra` are
|
|
allowed.
|
|
|
|
There is **no plan** to expose dev publicly. If you need to test something
|
|
WAN-shaped, do it on prod inside the WAN window (doc 09) — never widen
|
|
dev's allowlist.
|
|
|
|
---
|
|
|
|
## Risks and non-risks
|
|
|
|
- **Read-only media mount.** Dev cannot write to `/home/user/media`.
|
|
Theme experiments cannot accidentally rename, delete or scramble files.
|
|
- **Separate library.db.** Dev rescrapes from scratch. If a metadata
|
|
experiment in dev produces bad results, it never touches prod metadata.
|
|
- **Same Traefik instance.** Both routers share the proxy network and the
|
|
one Traefik. A misconfigured label on dev could *theoretically* shadow
|
|
prod's router, but the rules are `Host(dev.arrflix.s8n.ru)` vs
|
|
`Host(arrflix.s8n.ru)` — disjoint. Sanity-check after any compose edit
|
|
with `curl -kI https://arrflix.s8n.ru/`.
|
|
- **Same image tag.** Bumping prod to a new Jellyfin version means
|
|
bumping dev too; do prod first, then sync dev. Never test a version
|
|
bump on dev and forget to mirror prod — the API surface might drift.
|
|
- **No shared sessions.** Tokens, users, watch progress, playlists are
|
|
100% isolated. A test admin in dev cannot act on prod, and vice versa.
|
|
|
|
---
|
|
|
|
## Quick reference
|
|
|
|
```
|
|
# Status
|
|
ssh user@192.168.0.100 'docker ps --filter name=jellyfin'
|
|
|
|
# Logs
|
|
ssh user@192.168.0.100 'docker logs jellyfin-dev --tail 100 -f'
|
|
|
|
# Restart
|
|
ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose restart'
|
|
|
|
# Stop / start
|
|
ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose down'
|
|
ssh user@192.168.0.100 'cd /opt/docker/jellyfin-dev && docker compose up -d'
|
|
|
|
# Health check from onyx
|
|
curl -kI https://dev.arrflix.s8n.ru
|
|
# expect HTTP/2 302, location: web/
|
|
```
|
|
|
|
---
|
|
|
|
## DNS pin path used
|
|
|
|
The dev hostname was added to Pi-hole's `dns.hosts` array in
|
|
`/opt/docker/pihole/etc-pihole/pihole.toml` (alongside the existing
|
|
LAN-only entries) and Pi-hole was restarted to pick up the change.
|
|
The legacy `custom.list` file is still present but is no longer the
|
|
authoritative source — `dns.hosts` in `pihole.toml` is what
|
|
`pihole-FTL` actually consults.
|
|
|
|
If `dev.arrflix.s8n.ru` ever fails to resolve, restart Pi-hole and
|
|
re-check the `dns.hosts` array.
|