added jellyfin
This commit is contained in:
+18
-9
@@ -1,12 +1,21 @@
|
|||||||
# Ignore all contents of the folder
|
# ─── Deny by default ────────────────────────────────────────────────
|
||||||
|
# Everything inside a service directory is ignored. This means a new
|
||||||
|
# service is safe automatically — you don't have to remember to add a
|
||||||
|
# rule for it before the first `docker compose up`.
|
||||||
|
*/*
|
||||||
|
|
||||||
openwebui/*
|
# ─── …except the files that define a stack ──────────────────────────
|
||||||
!openwebui/docker-compose.yaml
|
!*/compose.yaml
|
||||||
|
!*/docker-compose.yaml
|
||||||
overleaf/*
|
!*/.env.example
|
||||||
!overleaf/docker-compose.yaml
|
!*/README.md
|
||||||
|
|
||||||
owui-ollama/*
|
|
||||||
!owui-ollama/docker-compose.yaml
|
|
||||||
|
|
||||||
|
# Docs are not service data.
|
||||||
|
!docs/**
|
||||||
|
|
||||||
|
# ─── Never, anywhere ────────────────────────────────────────────────
|
||||||
|
.env
|
||||||
|
*.key
|
||||||
|
*.pem
|
||||||
|
*.crt
|
||||||
|
secrets/
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 goyban
|
Copyright (c) 2025 Goyban
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
|
||||||
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
associated documentation files (the "Software"), to deal in the Software without restriction, including
|
||||||
|
|||||||
@@ -1,3 +1,105 @@
|
|||||||
# docker-compose-repo
|
# Self-hosted Compose Stacks
|
||||||
|
|
||||||
All used docker compose files
|
Docker Compose files for the services I actually run, with notes on **why each
|
||||||
|
one looks the way it does** — the trade-offs, the things that broke once, the
|
||||||
|
settings that aren't obvious from the upstream docs.
|
||||||
|
|
||||||
|
Every stack is standalone. You need Docker and nothing else: no OMV, no
|
||||||
|
Kubernetes, no particular distro.
|
||||||
|
|
||||||
|
> Maintained by **Goyban** — I make videos about self-hosting, NAS, Linux and
|
||||||
|
> the terminal. TODO: YouTube link
|
||||||
|
>
|
||||||
|
> These stacks run on the setup documented in TODO: link to Homelab repo.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone TODO:_repo_url && cd <repo>
|
||||||
|
cd jellyfin
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env # set paths, ports, timezone
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Each service directory is self-contained: a `compose.yaml`, a `.env.example`,
|
||||||
|
and a `README.md` explaining the choices.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Services
|
||||||
|
|
||||||
|
Every service publishes a plain HTTP port. TLS, domains and authentication are
|
||||||
|
handled separately — see [Exposing services](#exposing-services).
|
||||||
|
|
||||||
|
| Service | Port | Notes |
|
||||||
|
|---------|------|-------|
|
||||||
|
| [jellyfin](jellyfin/) | 8096 | Media streaming. Also 7359/udp for auto-discovery |
|
||||||
|
| [openwebui](openwebui/) | 3033 | LLM chat UI, talks to an external model backend |
|
||||||
|
| [owui-ollama](owui-ollama/) | 3034 | Open WebUI bundled with Ollama |
|
||||||
|
| [overleaf](overleaf/) | 8020 | LaTeX editor. Needs Mongo + Redis |
|
||||||
|
|
||||||
|
<!-- One row per service. Add as you go — the table is the index people scan. -->
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Exposing services
|
||||||
|
|
||||||
|
Nothing in a compose file here is proxy-aware, so you can put whatever you like
|
||||||
|
in front:
|
||||||
|
|
||||||
|
- **[Reverse proxy with Caddy](docs/reverse-proxy.md)** — subdomain + automatic
|
||||||
|
TLS. The default choice.
|
||||||
|
- **[Cloudflare Tunnel](docs/cloudflare-tunnel.md)** — no open ports, no VPS.
|
||||||
|
Good for light services; **not** for media streaming or large uploads —
|
||||||
|
the doc explains why.
|
||||||
|
- **[Single sign-on with Authentik](docs/authentik-sso.md)** — OIDC where the
|
||||||
|
app supports it, forward auth where it doesn't.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
A few rules that keep the stacks consistent and safe to publish:
|
||||||
|
|
||||||
|
- **Data lives outside the repo.** Paths come from `${DATA_PATH}` in `.env`, so
|
||||||
|
a clone never fills up with application state and `git status` stays clean.
|
||||||
|
- **Secrets live in `.env`**, which is git-ignored. Only `.env.example` is
|
||||||
|
committed, with placeholders.
|
||||||
|
- **Shared settings have defaults; host-specific ones fail loudly.** `PUID`,
|
||||||
|
`PGID`, `TZ` and ports are written `${PUID:-1000}` in the compose file, so a
|
||||||
|
fresh clone runs with no `.env` at all. Three layers can set them, in
|
||||||
|
increasing priority: the default in `compose.yaml`, the service's `.env`, then
|
||||||
|
anything exported in your shell — so a global `export PUID=…` in `~/.profile`
|
||||||
|
keeps working and overrides both. Values that *cannot* have a sensible
|
||||||
|
default — a media path, a `render` group GID — use `${VAR:?message}` instead
|
||||||
|
and abort with an explanation rather than silently interpolating an empty
|
||||||
|
string.
|
||||||
|
- **The repo ignores by default** — everything inside a service directory is
|
||||||
|
ignored except `compose.yaml`, `.env.example` and `README.md`. A new service
|
||||||
|
can't leak a database by accident.
|
||||||
|
- **No `version:` key** — obsolete in Compose v2.
|
||||||
|
- **LinuxServer.io images where they fit.** `lscr.io/linuxserver/*` images all
|
||||||
|
speak the same dialect: `PUID`/`PGID`/`TZ` for ownership and time, `/config`
|
||||||
|
for state, s6 as init, a shared base rebuilt on a regular cadence. Learn one
|
||||||
|
and you've learned the family — worth more across a dozen stacks than tuning
|
||||||
|
each image separately. Where a project ships a strong first-party image
|
||||||
|
(Open WebUI, Overleaf), I use that instead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding a service
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r _template <service>
|
||||||
|
```
|
||||||
|
|
||||||
|
Then follow the checklist at the top of `_template/README.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT — see [`LICENSE`](LICENSE).
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# Copy to .env and adjust. .env is git-ignored; this file is not.
|
||||||
|
# Never put a real secret in this file.
|
||||||
|
|
||||||
|
# ─── Shared settings ────────────────────────────────────────────────
|
||||||
|
# compose.yaml already defaults these to 1000/1000/Etc/UTC. Three places
|
||||||
|
# can set them, in increasing priority:
|
||||||
|
#
|
||||||
|
# 1. the defaults in compose.yaml (a fresh clone just works)
|
||||||
|
# 2. this file (per-service)
|
||||||
|
# 3. exported in your shell (global, wins over this file)
|
||||||
|
#
|
||||||
|
# If you keep a global `export PUID=...` in ~/.profile, leave these commented.
|
||||||
|
#PUID=1000
|
||||||
|
#PGID=1000
|
||||||
|
#TZ=Europe/Paris
|
||||||
|
|
||||||
|
# ─── Paths ──────────────────────────────────────────────────────────
|
||||||
|
# Where persistent data lives. Keep it OUTSIDE the repo so a clone stays
|
||||||
|
# clean and a `git status` never shows application state.
|
||||||
|
#DATA_PATH=./data
|
||||||
|
|
||||||
|
# ─── Ports ──────────────────────────────────────────────────────────
|
||||||
|
# Host port for the web UI. Defaulted in compose.yaml.
|
||||||
|
#PORT=xxxx
|
||||||
|
|
||||||
|
# ─── Required ───────────────────────────────────────────────────────
|
||||||
|
# Anything written ${VAR:?...} in compose.yaml belongs here, uncommented,
|
||||||
|
# with a note on how to find the right value for this host.
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<!--
|
||||||
|
DAILY CHECKLIST — copy this directory, then:
|
||||||
|
1. Paste your real compose file into compose.yaml
|
||||||
|
2. Replace host paths with ${DATA_PATH} etc. and list them in .env.example
|
||||||
|
3. Fill "Why it looks like this" — this is the part worth reading
|
||||||
|
4. Add a row to the service table in the root README.md
|
||||||
|
5. Commit: "add <service>"
|
||||||
|
Delete this comment when you're done.
|
||||||
|
-->
|
||||||
|
|
||||||
|
# <Service>
|
||||||
|
|
||||||
|
One line: what it is and why you run it.
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp .env.example .env
|
||||||
|
$EDITOR .env
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open `http://<host>:<port>`.
|
||||||
|
|
||||||
|
## Port
|
||||||
|
|
||||||
|
| Port | Protocol | Purpose |
|
||||||
|
|------|----------|---------|
|
||||||
|
| xxxx | http | Web UI |
|
||||||
|
|
||||||
|
## Why it looks like this
|
||||||
|
|
||||||
|
<!--
|
||||||
|
The differentiator. Not "what the options do" — the docs already say that.
|
||||||
|
Why *these* choices, on a real machine, after living with it.
|
||||||
|
|
||||||
|
Prompts, delete what doesn't apply:
|
||||||
|
- Why this image? (linuxserver vs. official vs. a fork — what made you switch?)
|
||||||
|
- Why these mounts? What actually needs to persist, and what surprised you?
|
||||||
|
- PUID/PGID — what breaks when they're wrong?
|
||||||
|
- Anything here because something failed once? Those are the best notes.
|
||||||
|
- Anything you tried first that didn't work?
|
||||||
|
- Resource limits, hardware access (GPU, /dev/dri), device passthrough
|
||||||
|
- Why this restart policy
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
<!-- Things that cost you time. Empty is fine on day one — add as you hit them. -->
|
||||||
|
|
||||||
|
## Exposing it
|
||||||
|
|
||||||
|
Publishes a plain HTTP port; nothing here is proxy-aware.
|
||||||
|
|
||||||
|
- [Reverse proxy with Caddy](../docs/reverse-proxy.md)
|
||||||
|
- [Cloudflare Tunnel](../docs/cloudflare-tunnel.md)
|
||||||
|
- [Single sign-on with Authentik](../docs/authentik-sso.md)
|
||||||
|
|
||||||
|
<!-- If this service DOES need proxy-specific config (Nextcloud, Gitea), say so here. -->
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
- Upstream docs:
|
||||||
|
- Image:
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
# Paste your real compose file here, then adjust:
|
||||||
|
# - drop any `version:` key (obsolete in Compose v2)
|
||||||
|
# - replace hardcoded host paths with ${DATA_PATH}
|
||||||
|
# - remove `volumes:` blocks nothing references
|
||||||
|
# - publish the port; leave TLS and auth to the reverse proxy
|
||||||
|
# - give shared settings a default: ${PUID:-1000}, not ${PUID}
|
||||||
|
# - make host-specific values fail loudly: ${VAR:?how to find it}
|
||||||
|
# (a bare ${VAR} that isn't set becomes an empty string and starts anyway)
|
||||||
|
|
||||||
|
services:
|
||||||
|
<service>:
|
||||||
|
image: <image>
|
||||||
|
container_name: <service>
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Same on every host, so they're defaulted here. A service .env overrides
|
||||||
|
# them, and a global `export PUID=...` in your shell overrides that.
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID:-1000}
|
||||||
|
- PGID=${PGID:-1000}
|
||||||
|
- TZ=${TZ:-Etc/UTC}
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- ${DATA_PATH:-./data}/config:/config
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8080}:8080"
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Single sign-on with Authentik
|
||||||
|
|
||||||
|
TODO: fill in as you go. Outline below so the shape is decided in advance.
|
||||||
|
|
||||||
|
## Two ways to protect a service
|
||||||
|
|
||||||
|
1. **Native OIDC** — the app speaks OIDC itself (Gitea, Nextcloud,
|
||||||
|
Audiobookshelf, Immich). Better experience: real accounts, real logout,
|
||||||
|
group mapping.
|
||||||
|
2. **Forward auth** — the proxy asks Authentik before passing the request
|
||||||
|
through. Works for anything, including apps with no auth at all, but the
|
||||||
|
app has no idea who the user is.
|
||||||
|
|
||||||
|
Prefer native OIDC where the app supports it; use forward auth as the
|
||||||
|
fallback.
|
||||||
|
|
||||||
|
## Native OIDC — the shape
|
||||||
|
|
||||||
|
TODO: provider + application setup in Authentik, then the four values every
|
||||||
|
app asks for:
|
||||||
|
|
||||||
|
- Issuer / discovery URL
|
||||||
|
- Client ID
|
||||||
|
- Client secret → `.env`, never committed
|
||||||
|
- Redirect URI
|
||||||
|
|
||||||
|
## Forward auth — the shape
|
||||||
|
|
||||||
|
TODO: the Caddy snippet, and which services you apply it to.
|
||||||
|
|
||||||
|
## Groups and roles
|
||||||
|
|
||||||
|
TODO: how you map Authentik groups to per-app roles, and the admin/user
|
||||||
|
split.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
TODO: collect these as you hit them. Known ones worth writing down:
|
||||||
|
|
||||||
|
- Locking yourself out of an app whose only admin is now behind SSO — keep a
|
||||||
|
local fallback admin until the flow is proven.
|
||||||
|
- Redirect URI mismatches, which usually surface as a generic error.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Reverse proxy with Caddy](reverse-proxy.md)
|
||||||
|
- [Cloudflare Tunnel](cloudflare-tunnel.md)
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Cloudflare Tunnel
|
||||||
|
|
||||||
|
A tunnel exposes a service to the internet **without opening a port** on your
|
||||||
|
router and without a VPS. `cloudflared` makes an outbound connection to
|
||||||
|
Cloudflare, and Cloudflare routes traffic back down it.
|
||||||
|
|
||||||
|
## When to use it (and when not to)
|
||||||
|
|
||||||
|
This is the part most guides skip. A tunnel is **not** a drop-in replacement
|
||||||
|
for a reverse proxy on a VPS.
|
||||||
|
|
||||||
|
| Use a tunnel for | Use Caddy on a VPS for |
|
||||||
|
|---|---|
|
||||||
|
| Admin UIs, dashboards | Jellyfin, Plex — any video streaming |
|
||||||
|
| Gitea, small web apps | Nextcloud, Immich — large uploads |
|
||||||
|
| Anything low-bandwidth | Anything you want unmetered |
|
||||||
|
|
||||||
|
Two hard limits drive that split:
|
||||||
|
|
||||||
|
- **Upload size.** The free plan caps request bodies at roughly 100 MB. File
|
||||||
|
sync and photo backup break on this, often silently or with a confusing
|
||||||
|
413.
|
||||||
|
- **Terms of service.** Cloudflare's terms restrict serving large amounts of
|
||||||
|
non-HTML content — video in particular — through the proxy. Streaming a
|
||||||
|
media library through a tunnel is the single most common way people get
|
||||||
|
their account flagged.
|
||||||
|
|
||||||
|
For those services, expose them through a reverse proxy you control.
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
TODO: your preferred flow — dashboard-created tunnel vs. `cloudflared tunnel
|
||||||
|
create`. The dashboard route is easier to show on video; the CLI route is
|
||||||
|
easier to keep in version control.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
cloudflared:
|
||||||
|
image: cloudflare/cloudflared:latest
|
||||||
|
container_name: cloudflared
|
||||||
|
restart: unless-stopped
|
||||||
|
command: tunnel --no-autoupdate run
|
||||||
|
environment:
|
||||||
|
- TUNNEL_TOKEN=${CLOUDFLARE_TUNNEL_TOKEN}
|
||||||
|
```
|
||||||
|
|
||||||
|
The tunnel token is a **real secret** — it grants the ability to route
|
||||||
|
traffic into your network. It belongs in `.env`, never in a committed file.
|
||||||
|
|
||||||
|
## Routing to a service
|
||||||
|
|
||||||
|
Public hostname → service, configured per tunnel:
|
||||||
|
|
||||||
|
```
|
||||||
|
app.example.com → http://<host-or-container>:<port>
|
||||||
|
```
|
||||||
|
|
||||||
|
TODO: note whether you attach `cloudflared` to each service's compose network
|
||||||
|
or run one shared tunnel that reaches services by host IP. The second is
|
||||||
|
simpler to maintain; the first is better isolated.
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
Cloudflare Access can sit in front of a tunnel and handle authentication
|
||||||
|
before traffic ever reaches the service — useful for apps with weak or no
|
||||||
|
built-in auth.
|
||||||
|
|
||||||
|
TODO: whether you use Access, or terminate auth at Authentik instead. Note
|
||||||
|
that running both is usually redundant.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Reverse proxy with Caddy](reverse-proxy.md)
|
||||||
|
- [Single sign-on with Authentik](authentik-sso.md)
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Reverse proxy with Caddy
|
||||||
|
|
||||||
|
Every service in this repo publishes a plain HTTP port on the host. Nothing
|
||||||
|
in a service's compose file knows or cares about TLS, domains, or
|
||||||
|
authentication — that is all handled here, in one place.
|
||||||
|
|
||||||
|
This is deliberate: it means you can run any stack in this repo standalone,
|
||||||
|
and add a proxy later without touching the compose file.
|
||||||
|
|
||||||
|
## The basic pattern
|
||||||
|
|
||||||
|
Caddy gets a certificate automatically. For most services, one block is the
|
||||||
|
entire configuration:
|
||||||
|
|
||||||
|
```caddyfile
|
||||||
|
app.example.com {
|
||||||
|
reverse_proxy <host>:<port>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Look up `<port>` in the [service table](../README.md).
|
||||||
|
|
||||||
|
TODO: note where your Caddyfile lives and whether Caddy runs on the NAS or
|
||||||
|
on the VPS.
|
||||||
|
|
||||||
|
## Services that need more
|
||||||
|
|
||||||
|
Most apps don't care what's in front of them. A few build absolute URLs or
|
||||||
|
make security decisions based on the request, and those need to be told.
|
||||||
|
|
||||||
|
**Nextcloud** is the main one — it needs all of:
|
||||||
|
|
||||||
|
- `trusted_domains` — the public hostname, or it refuses the request
|
||||||
|
- `overwrite.cli.url` — so generated links use the public URL
|
||||||
|
- `overwriteprotocol=https` — or it builds `http://` links behind TLS and
|
||||||
|
breaks mixed content
|
||||||
|
- `TRUSTED_PROXIES` — the proxy's IP, or every client appears to come from
|
||||||
|
the proxy and rate limiting misfires
|
||||||
|
|
||||||
|
Milder cases:
|
||||||
|
|
||||||
|
- **Gitea** — `ROOT_URL`, or clone URLs point at the wrong host
|
||||||
|
- **Authentik** — knows its own external URL by configuration
|
||||||
|
- **Jellyfin** — only if you serve it from a subpath rather than a subdomain
|
||||||
|
|
||||||
|
TODO: your `header_up` defaults, and whether you set a shared snippet for
|
||||||
|
`X-Forwarded-*`.
|
||||||
|
|
||||||
|
## Large uploads
|
||||||
|
|
||||||
|
TODO: Caddy's defaults are usually fine, but note any `request_body`
|
||||||
|
`max_size` you set for Nextcloud/Immich, plus the matching app-side limit.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [Cloudflare Tunnel](cloudflare-tunnel.md) — when you don't want to open a port
|
||||||
|
- [Single sign-on with Authentik](authentik-sso.md)
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Copy to .env and adjust. .env is git-ignored; this file is not.
|
||||||
|
# Nothing here is secret, but keep it that way.
|
||||||
|
|
||||||
|
# ─── Identity ───────────────────────────────────────────────────────
|
||||||
|
# These three are the same for every service on the machine, so compose.yaml
|
||||||
|
# already defaults them to 1000/1000/Etc/UTC. Three places can set them, in
|
||||||
|
# increasing priority:
|
||||||
|
#
|
||||||
|
# 1. the defaults in compose.yaml (a fresh clone just works)
|
||||||
|
# 2. this file (per-service)
|
||||||
|
# 3. exported in your shell (global, wins over this file)
|
||||||
|
#
|
||||||
|
# If you keep a global `export PUID=...` in ~/.profile, leave these commented.
|
||||||
|
#
|
||||||
|
# The host user that should own everything Jellyfin writes: `id -u` / `id -g`.
|
||||||
|
# This is the setting that bites hardest when it's wrong: the container starts
|
||||||
|
# fine and then can't write its database, or writes it as root and you can no
|
||||||
|
# longer touch the files from the host.
|
||||||
|
#PUID=1000
|
||||||
|
#PGID=1000
|
||||||
|
|
||||||
|
# Affects timestamps in logs and scheduled-task times.
|
||||||
|
#TZ=Europe/Paris
|
||||||
|
|
||||||
|
# ─── Paths ──────────────────────────────────────────────────────────
|
||||||
|
# Jellyfin's own state: database, metadata, artwork, subtitles, logs.
|
||||||
|
# Small enough to live next to the compose file — see the README. The
|
||||||
|
# repo's .gitignore keeps it out of git either way. Defaults to ./config.
|
||||||
|
#CONFIG_PATH=./config
|
||||||
|
|
||||||
|
# Your main media library. REQUIRED — compose refuses to start without it,
|
||||||
|
# rather than silently mounting nothing.
|
||||||
|
MEDIA_PATH=/srv/media
|
||||||
|
|
||||||
|
# Extra libraries on other disks. Uncomment the matching lines in
|
||||||
|
# compose.yaml too, otherwise these do nothing.
|
||||||
|
#MEDIA_PATH_2=/mnt/disk2/tv
|
||||||
|
#MEDIA_PATH_3=/mnt/scratch/downloads
|
||||||
|
|
||||||
|
# ─── Ports ──────────────────────────────────────────────────────────
|
||||||
|
# Defaulted in compose.yaml; set here only to move them.
|
||||||
|
#PORT=8096
|
||||||
|
#DISCOVERY_PORT=7359
|
||||||
|
|
||||||
|
# ─── Hardware transcoding ───────────────────────────────────────────
|
||||||
|
# GID of the host group that owns /dev/dri/renderD128. Host-specific:
|
||||||
|
#
|
||||||
|
# getent group render | cut -d: -f3
|
||||||
|
#
|
||||||
|
# It varies wildly between distros and installs (105, 992, 993, 989 are
|
||||||
|
# all real answers I've seen). Do not copy the number below — look it up.
|
||||||
|
# REQUIRED while the hardware-transcoding block is present in compose.yaml;
|
||||||
|
# delete that block and this line together if you transcode on the CPU.
|
||||||
|
RENDER_GID=105
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# 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/>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
services:
|
||||||
|
jellyfin:
|
||||||
|
image: lscr.io/linuxserver/jellyfin:latest
|
||||||
|
container_name: jellyfin
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Defaults make a fresh clone work with no .env at all. Anything you set
|
||||||
|
# -- in this service's .env, or exported globally in your shell -- wins.
|
||||||
|
environment:
|
||||||
|
- PUID=${PUID:-1000}
|
||||||
|
- PGID=${PGID:-1000}
|
||||||
|
- TZ=${TZ:-Etc/UTC}
|
||||||
|
# The address Jellyfin advertises to clients during auto-discovery. Worth
|
||||||
|
# setting behind a reverse proxy, where clients otherwise get handed the
|
||||||
|
# container's internal address and fail to connect.
|
||||||
|
#- JELLYFIN_PublishedServerUrl=https://jellyfin.example.com
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
# Jellyfin's own state. Stays next to this file on purpose — see README.
|
||||||
|
- ${CONFIG_PATH:-./config}:/config
|
||||||
|
- "${MEDIA_PATH:?set MEDIA_PATH in .env to your media library}:/media"
|
||||||
|
# Extra libraries on other disks. One line per library; the path after
|
||||||
|
# the colon is what you browse to when adding the library in the UI.
|
||||||
|
#- ${MEDIA_PATH_2}:/media2
|
||||||
|
#- ${MEDIA_PATH_3}:/media3
|
||||||
|
|
||||||
|
ports:
|
||||||
|
- "${PORT:-8096}:8096"
|
||||||
|
# LAN client auto-discovery. Drop it if you always reach Jellyfin by
|
||||||
|
# name or through a reverse proxy.
|
||||||
|
- "${DISCOVERY_PORT:-7359}:7359/udp"
|
||||||
|
# Jellyfin's own HTTPS listener. Off here: TLS terminates at the proxy.
|
||||||
|
#- "8920:8920"
|
||||||
|
# DLNA / SSDP. Only for devices that discover servers that way.
|
||||||
|
#- "1900:1900/udp"
|
||||||
|
|
||||||
|
# ─── Hardware transcoding: Intel Quick Sync / VAAPI ──────────────────
|
||||||
|
# Delete both keys below if you transcode on the CPU, or replace them
|
||||||
|
# with the NVIDIA/AMD equivalent. RENDER_GID is host-specific — the
|
||||||
|
# .env.example explains how to find yours.
|
||||||
|
devices:
|
||||||
|
- /dev/dri/renderD128:/dev/dri/renderD128
|
||||||
|
group_add:
|
||||||
|
- "${RENDER_GID:?find yours with: getent group render | cut -d: -f3}"
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
version: "3.7"
|
|
||||||
|
|
||||||
services:
|
|
||||||
|
|
||||||
jellyfin:
|
|
||||||
image: linuxserver/jellyfin
|
|
||||||
restart: unless-stopped
|
|
||||||
#hostname: "${DEVICE_HOSTNAME}"
|
|
||||||
environment:
|
|
||||||
- PUID=1000
|
|
||||||
- PGID=1000
|
|
||||||
- TZ=Europe/Paris
|
|
||||||
volumes:
|
|
||||||
- ./config:/config
|
|
||||||
- ../media:/media
|
|
||||||
# - /media/umbrl/GPass_Serie:/hard_Drive
|
|
||||||
# - /media/umbrl/Videos:/hdd_movie_all
|
|
||||||
ports:
|
|
||||||
# Service auto-discovery
|
|
||||||
- 7359:7359/udp
|
|
||||||
- 8096:8096
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user