DockerHomeLab
Rows of server racks inside a data center, the kind of infrastructure a self-hosted Jellyfin media server runs on
guides

Self-Host Jellyfin with Docker Compose

Run your own media server with Jellyfin and Docker Compose. A working Compose file, hardware transcoding setup, library configuration, and reverse-proxy notes for streaming your media anywhere.

By Editorial · · 8 min read

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:1000 runs the container as your normal user (run id to confirm your UID/GID). This keeps file ownership sane on the mounted media and config directories.
  • /mnt/media:/media:ro mounts 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_PublishedServerUrl tells clients which URL to use. Set it to your reverse-proxy domain once you have one.

The official docs use the bare jellyfin/jellyfin image; pinning to :latest (or a specific version tag for reproducibility) is the common homelab practice.

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 by adding a devices block:

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
    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.

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/Movies on 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
    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/dri exists on the host (ls -l /dev/dri). Then check that the container user (the UID you set) belongs to the render and video groups that own those devices — if not, either add the user to those groups or pass the group IDs into the container. 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 config and cache directories must be writable by the UID the container runs as. If you changed user:, 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 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.

Sources

  1. Jellyfin — Install with Docker (official docs)
  2. Jellyfin — Hardware Acceleration

Related

Comments