Jellyfin is a fully open-source media server. It indexes your movies, TV shows, music, and photos, then streams them to web browsers, phones, smart TVs, and dedicated clients. Unlike Plex, there are no paid tiers, no account requirement, and no telemetry phoning home — the server runs entirely on your hardware and your media never leaves your control.
This guide gets a working Jellyfin instance running in Docker Compose, sets up your media libraries, enables hardware transcoding so a low-power machine can handle multiple streams, and covers putting it behind a reverse proxy for access from outside your network.
Prerequisites
- Docker and Docker Compose installed (see our first self-hosted stack guide if you’re new to Compose)
- A directory containing your media, organized into folders (Movies, Shows, Music)
- For hardware transcoding: a Linux host with an Intel, AMD, or NVIDIA GPU
A modest mini PC with an Intel N100 or similar is more than enough for a few simultaneous streams once hardware transcoding is enabled.
How Jellyfin Uses Ports
The official Jellyfin container uses two ports:
- 8096/TCP — the web interface and API. This is the one you connect to.
- 7359/UDP — client auto-discovery on the local network (optional).
You don’t need to expose 7359 if you’ll always reach Jellyfin by hostname or IP.
Step 1: Create the Directory Structure
Keep configuration, cache, and media cleanly separated:
mkdir -p ~/services/jellyfin/config
mkdir -p ~/services/jellyfin/cache
cd ~/services/jellyfin
Your media can live anywhere — a separate mount point, a NAS share, or an attached drive. This guide assumes it’s at /mnt/media.
Step 2: Write the Compose File
Create ~/services/jellyfin/docker-compose.yml. This follows the official Jellyfin Docker documentation:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
user: 1000:1000
ports:
- "8096:8096/tcp"
- "7359:7359/udp"
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/media:/media:ro
environment:
- JELLYFIN_PublishedServerUrl=http://your-server-ip:8096
A few notes on the choices here:
user: 1000:1000runs the container as your normal user (runidto confirm your UID/GID). This keeps file ownership sane on the mounted media and config directories./mnt/media:/media:romounts your library read-only (:ro). Jellyfin never needs to write to your media files, and read-only mounting prevents an accident or a compromise from touching your originals.JELLYFIN_PublishedServerUrlsets the alternative address advertised during auto-discovery. Use an address that the discovering clients can reach.
The official docs use the bare jellyfin/jellyfin image; pinning to :latest (or a specific version tag for reproducibility) is the common homelab practice.
JELLYFIN_PublishedServerUrl: what it does and when you need it
The official container example makes this environment variable optional: it overrides the URL advertised to clients during auto-discovery. It does not create a DNS record, open a router port, enable HTTPS, or change the address Jellyfin listens on. If clients already connect using a manually entered URL, changing this variable is not the first fix for a failed connection.
Choose the advertised address from the client’s perspective:
| Access pattern | Example value | What to check |
|---|---|---|
| LAN only, published web port | http://192.168.1.20:8096 | Reserve that host address and confirm it opens from the client |
| LAN clients using a reverse proxy | https://media.example.com | Local DNS must resolve the hostname to a reachable proxy |
| Remote clients using HTTPS or a VPN | Enter the reachable server URL in the client | Discovery does not cross the internet; a public URL does not make it do so |
The addresses above are examples to replace. If one hostname serves both LAN and WAN clients, check it from both networks. Split DNS can resolve that hostname to the proxy’s LAN address internally; otherwise your router may need NAT loopback for local clients to use its public address. A LAN IP advertised through discovery is unsuitable for a phone after it leaves that LAN unless a VPN provides a route.
For a reverse proxy, configure WebSockets and add its address under Jellyfin’s Known Proxies setting, as described in the reverse-proxy documentation. The published URL is the client-facing address, while the proxy still forwards to Jellyfin on port 8096. After editing the environment variable, recreate the service with docker compose up -d jellyfin and check the advertised address from a LAN client.
Choosing network_mode for DLNA discovery
Jellyfin’s UDP 7359 client discovery and DLNA’s UDP 1900 SSDP discovery are separate protocols. Keep bridge networking for ordinary web or app playback using an explicit server URL. Publishing UDP 7359 supports Jellyfin discovery but does not provide the DLNA multicast path.
Since Jellyfin 10.9, DLNA is an official plugin installed from the dashboard catalog; restart Jellyfin after installing it. The DLNA documentation requires host networking for Docker, UDP 1900 access on the LAN, and clients on the same subnet. A reverse proxy does not relay SSDP between VLANs or across a WAN.
For DLNA on a Linux Docker host, replace the service’s bridge configuration with network_mode: host and remove both its ports: and service-level networks: entries. Keep the existing config, cache, media, and device mounts. Jellyfin now listens directly on the host’s ports; ensure another service is not using 8096 or 1900. A containerized proxy must use a reachable host address instead of the bridge service name. The Docker Compose host networking guide explains the port and DNS consequences before you make that change.
Step 3: Enable Hardware Transcoding
Transcoding is the act of converting media on the fly — for example, when a client can’t play a file’s codec, or when you’re streaming a 4K file to a device on a slow connection. Software transcoding pegs your CPU. Hardware transcoding offloads it to the GPU and is the difference between one stream and six on a small machine.
For Intel and AMD GPUs on Linux, pass the render device into the container with a devices block. Because the container runs as UID 1000 rather than root, it also needs the host’s render group ID through group_add, or the process cannot open the render node. Find that ID first:
getent group render | cut -d: -f3
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
user: 1000:1000
group_add:
- "993" # replace with your render GID from the getent command above
ports:
- "8096:8096/tcp"
- "7359:7359/udp"
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/media:/media:ro
devices:
- /dev/dri:/dev/dri
environment:
- JELLYFIN_PublishedServerUrl=http://your-server-ip:8096
/dev/dri is the Direct Rendering Infrastructure device — passing it in gives the container access to the GPU’s video engine.
Hardware acceleration in Docker is only supported on Linux. The Jellyfin documentation explicitly notes it is known to be broken when running in Docker on platforms other than Linux, so a Linux host is the right choice if transcoding matters to you.
After starting the container, you still need to turn the feature on inside Jellyfin: go to Dashboard → Playback → Transcoding and select your hardware acceleration method (for recent Intel chips, choose Intel QuickSync (QSV)). Vendor-specific setup steps vary, so consult the Jellyfin hardware-acceleration docs for your exact GPU. The per-vendor Compose blocks, including the NVIDIA Container Toolkit step, are in the Jellyfin hardware transcoding guide.
Step 4: Start Jellyfin
From the directory containing your Compose file:
docker compose up -d
Docker pulls the image and starts the container. Watch the logs on first start:
docker compose logs -f
When you see the startup complete, open http://your-server-ip:8096 in a browser. You’ll be greeted by the first-run setup wizard.
Step 5: Configure Libraries
The setup wizard walks you through creating an admin user and adding your first library. When you add a library:
- Choose the content type (Movies, Shows, Music, etc.) — this controls which metadata provider Jellyfin uses.
- Point the folder at the path inside the container, which is
/media/...(not the host path). For example, if your movies are at/mnt/media/Movieson the host, the library folder in Jellyfin is/media/Movies.
Jellyfin scans the folder, fetches metadata and artwork, and builds the library. Good file naming dramatically improves metadata matching — Movie Name (2024).mkv and Show Name/Season 01/Show Name S01E01.mkv are the conventions Jellyfin’s scanners expect.
Step 6: Reverse Proxy for Remote Access
To reach Jellyfin from outside your home network with a real HTTPS certificate, put it behind a reverse proxy rather than forwarding port 8096 directly. Our Traefik guide covers the full setup; here’s the Jellyfin-specific piece.
Remove the published 8096 port (Traefik will reach the container over the shared Docker network) and add labels:
services:
jellyfin:
image: jellyfin/jellyfin:latest
container_name: jellyfin
restart: unless-stopped
user: 1000:1000
group_add:
- "993" # your render GID, as in Step 3
volumes:
- ./config:/config
- ./cache:/cache
- /mnt/media:/media:ro
devices:
- /dev/dri:/dev/dri
environment:
- JELLYFIN_PublishedServerUrl=https://media.yourdomain.com
labels:
- "traefik.enable=true"
- "traefik.http.routers.jellyfin.rule=Host(`media.yourdomain.com`)"
- "traefik.http.routers.jellyfin.entrypoints=websecure"
- "traefik.http.routers.jellyfin.tls.certresolver=letsencrypt"
- "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
networks:
- traefik-proxy
networks:
traefik-proxy:
external: true
The key label is loadbalancer.server.port=8096 — it tells Traefik that the Jellyfin container listens on 8096 internally, even though there’s no published port on the host.
If you prefer a GUI over YAML, Nginx Proxy Manager achieves the same result with a web form: create a proxy host pointing at jellyfin:8096, enable the SSL tab, and request a Let’s Encrypt certificate.
A note on exposure: a media server is a tempting target. If you only need access for yourself and a few people, consider keeping Jellyfin on your LAN and reaching it over a VPN such as WireGuard or Tailscale instead of publishing it to the open internet. That removes an entire class of attack surface.
Step 7: Keeping It Updated
Update Jellyfin like any other Compose service:
docker compose pull
docker compose up -d
This pulls the newest image and recreates the container. Your config and metadata live in the ./config volume, so they survive the update. Back that directory up — it holds your users, library settings, watch history, and metadata, none of which you want to rebuild from scratch.
Troubleshooting Common Issues
A few problems come up often enough to call out:
- “Hardware transcoding isn’t working.” First confirm
/dev/driexists on the host (ls -l /dev/dri). Then check that the render GID is in the service’sgroup_addlist. A container started with a numericuser:does not inherit that user’s group membership on the host, so adding the user torenderorvideoon the host changes nothing inside the container; the GID has to be passed in withgroup_add. Compare it againstgetent group render | cut -d: -f3(some distributions usevideofor the render node, so checkls -l /dev/dri). Finally, make sure you selected the right acceleration method under Dashboard → Playback → Transcoding; the device being present isn’t enough on its own. - “Clients can’t find the server automatically.” Auto-discovery uses UDP 7359 and only works on the same local subnet. If your clients are on a different VLAN or you’re connecting remotely, just enter the server URL manually — discovery is a convenience, not a requirement.
- “A file won’t play / constantly buffers.” That’s usually transcoding. Check the Dashboard’s active streams to see whether the server is transcoding (CPU-heavy) or direct-playing. Direct play is best; if a particular client always transcodes, the issue is a codec or container the client can’t handle natively.
- “Permissions errors in the logs.” The
configandcachedirectories must be writable by the UID the container runs as. If you changeduser:, fix ownership on those host directories to match.
Most Jellyfin problems trace back to one of these three causes: device/permission issues, a codec mismatch forcing a transcode, or a path that points at the host instead of the in-container /media path.
Where to Go Next
Once Jellyfin is running, the natural additions are an automated download stack (Sonarr, Radarr, and a download client) feeding into your media folder, and the Jellyfin mobile and TV apps for playback. Pair it with Vaultwarden and the rest of a homelab and you’ve replaced a meaningful chunk of paid streaming and storage subscriptions with software you fully control. The home server container roundup covers the rest of that stack service by service.
The other thing a media library outgrows is the disk it started on. Once the collection is larger than the host’s NVMe, media belongs on network storage with the container mounting it — the best NAS for Docker containers guide covers which platforms handle that well and which CPU tiers can transcode at all.
For a TV using DLNA, apply the plugin and host-network requirements above; ordinary Jellyfin clients can connect directly by URL without DLNA.
The pattern here — official image, named volumes, a reverse proxy, hardware passthrough where it helps — is the same one you’ll reuse for every other service on the box.
Related across the network
- Jellyfin vs Plex for Home Server: Which to Run in 2026? — selfhostrealm.com
- The Beginner Homelab Guide: First 5 Services Worth Running — selfhostrealm.com