Files
docker-compose-repo/jellyfin/README.md
T
Goyban aaef0796a3 added jellyfin
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 15:03:27 +02:00

111 lines
5.1 KiB
Markdown

# Jellyfin
Media server. Reads films and TV off disk and streams them to whatever is in
the room — no account, no subscription, nothing phoning home.
## Quick start
```bash
cp .env.example .env
$EDITOR .env # MEDIA_PATH and RENDER_GID are required
docker compose up -d
```
Open `http://<host>:8096` and point your libraries at `/media` — the path
*inside* the container, whatever `MEDIA_PATH` is on the host.
## Ports
| Port | Proto | Purpose | |
|------|-------|---------|---|
| 8096 | http | Web UI and client API | published |
| 7359 | udp | LAN auto-discovery | published |
| 8920 | https | Jellyfin's own TLS listener | off — TLS belongs at the proxy |
| 1900 | udp | DLNA / SSDP | off — uncomment if you use DLNA |
## Why it looks like this
**The LinuxServer image.** I started with it years ago, it has worked every day
since, and I'm not changing a base image to fix nothing. The better reason: I
run several of their images and they all speak the same dialect —
`PUID`/`PGID`/`TZ`, `/config`, s6, one rebuild cadence. Learn one, learn the
family. Jellyfin's official image is also fine; it just doesn't do the
`PUID`/`PGID` dance, so ownership works differently.
**`/config` lives next to the compose file**, breaking this repo's own
"data outside the repo" rule on purpose. It's Jellyfin's database, metadata and
artwork — 988M after three years here. Under a gigabyte, and keeping it beside
the compose file means the whole service is one `tar` from being moved.
`.gitignore` keeps it out of git. Note that transcode scratch files land under
`/config` and *can* spike several GB — move that path in **Dashboard → Playback**
if the disk is small.
**`PUID`/`PGID`/`TZ` carry defaults** (`${PUID:-1000}`) because they're the same
for every service on a box. Compose resolves them from the default, then `.env`,
then the ambient environment — so a global `export PUID=…`, or OpenMediaVault's
compose plugin, wins. That last one is why my own copy has bare `${PUID}`: OMV
supplies those three, so they're never empty *on my machine*. Without OMV they
would be, and Compose interpolates an empty string and starts anyway rather than
failing. Hence the defaults.
Values that can't be guessed use `${VAR:?message}` instead and abort:
```console
$ docker compose up -d
error while interpolating services.jellyfin.group_add.[]: required variable
RENDER_GID is missing a value: find yours with: getent group render | cut -d: -f3
```
**One `/media` mount.** My real server has three libraries on separate disks;
that's my risk appetite, not yours. Add a line per library if you need more.
Upstream's example splits `/data/tvshows` and `/data/movies` — fine on separate
disks, but if they share a filesystem, mounting the common parent is the better
habit: hardlinks only survive within one mount point inside the container.
Mount media `:ro` if you like — Jellyfin never writes to it.
**Hardware transcoding needs `devices` *and* `group_add`.** Passing
`/dev/dri/renderD128` in is half the job; the node is owned by a host group and
the container user has to be in it or the device opens and does nothing. The
catch is that `group_add` wants a numeric GID and it differs per host — mine is
`105`, on the laptop I wrote this on it's `992`. Copying someone's compose file
verbatim is exactly how you get silently broken acceleration.
```bash
getent group render | cut -d: -f3
```
Not on Intel? Delete both keys — CPU transcoding works, it just costs cores.
NVIDIA and AMD variants:
[LinuxServer's hardware acceleration docs](https://docs.linuxserver.io/images/docker-jellyfin/#hardware-acceleration).
**`restart: unless-stopped`, not `always`** — if I stop it deliberately, it
should stay stopped across a reboot.
## Gotchas
- **`:latest` is a choice.** Pin a tag if that makes you nervous. Back up
`/config` either way — that directory *is* your server.
- **Auto-discovery is LAN-only.** 7359/udp is broadcast; behind a proxy it does
nothing for you. Set `JELLYFIN_PublishedServerUrl` there instead.
- **On a subpath, set the base URL** under **Dashboard → Networking** first, or
the UI loads and every asset 404s. A subdomain avoids the question.
- **Verify hardware transcoding actually engaged.** Enabling VAAPI and it
*working* are different states — force a transcode and check
**Dashboard → Playback**. A wrong GID falls back to software silently.
## Exposing it
Publishes plain HTTP; nothing here is proxy-aware.
- [Caddy reverse proxy](../docs/reverse-proxy.md) — what I do.
- [Cloudflare Tunnel](../docs/cloudflare-tunnel.md) — **not for this one.**
Streaming video through a tunnel breaks Cloudflare's terms and performs badly.
- [Authentik SSO](../docs/authentik-sso.md) — works for the web UI, but native
clients can't handle a forward-auth login page.
## Links
- Jellyfin docs: <https://jellyfin.org/docs/>
- Hardware acceleration: [upstream](https://jellyfin.org/docs/general/administration/hardware-acceleration/) · [this image](https://docs.linuxserver.io/images/docker-jellyfin/#hardware-acceleration)
- Image: <https://docs.linuxserver.io/images/docker-jellyfin/>