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/.
7.2 KiB
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
- Open
https://dev.arrflix.s8n.rufrom any LAN/tailnet box. First visit hits the first-run wizard — create an admin user (use any throwaway name; nothing shared with prod). - Add libraries pointing at the same paths prod uses:
/media/movies/media/tvThe library ROOTS are shared (read-only); dev will rescrape independently into its ownlibrary.db. That's intentional — dev is a clean slate.
- 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). - Test, watch, break. Prod remains untouched on
arrflix.s8n.ru.
Theme workflow (dev → prod)
When a dev theme is "shipped":
- Export branding from dev:
curl -k -H "X-Emby-Token: $DEV_TOKEN" \ https://dev.arrflix.s8n.ru/Branding/Configuration > /tmp/branding.json - POST to prod:
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 - If the theme involves SPA-shim files (custom JS/CSS),
rsyncthem fromdev:/opt/docker/jellyfin-dev/web-overrides/toprod:/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:
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/8192.168.0.0/24(LAN)100.64.0.1/32onyx,100.64.0.2/32nullstone,100.64.0.4/32office (tailnet)82.22.5.233/32YOU500 home IP172.20.0.0/24docker 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)vsHost(arrflix.s8n.ru)— disjoint. Sanity-check after any compose edit withcurl -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.