Media
Jellyfin logo

How to install Jellyfin with Docker (with Intel hardware transcoding)

Jellyfin guide cover
Difficulty: BeginnerTime: 30 minutesTested with: Jellyfin 10.11 (linuxserver.io image)

Jellyfin is a free, open-source media server. Point it at your films, TV shows and music and it gives you a polished, Netflix-style library you can watch from a browser, phone, TV or streaming stick. There are no accounts to sign up for, no subscriptions and nothing phoning home.

In this guide we will run Jellyfin in Docker using the excellent linuxserver.io image, pass through an Intel iGPU for hardware transcoding, and get it ready to sit behind a reverse proxy.

πŸ’‘
Note: This guide works with the current Jellyfin 10.11 release from the linuxserver.io image.

βœ… Before you start

  • A Linux host with Docker and Docker Compose v2 installed (docker compose version should work).
  • Your media on a local disk or mounted share, e.g. /path/to/media/movies and /path/to/media/tv.
  • Optional but recommended: an Intel CPU with integrated graphics (6th gen or newer) for hardware transcoding. If Docker runs inside a VM, pass the iGPU through to the VM first.
  • About 2 GB of free space for Jellyfin's config and metadata, plus space for transcodes.

STEP 1: Find your user and group IDs

Jellyfin should run as a normal user that can read your media, not as root. Log in to your Docker host over SSH and run:

id

You will see something like uid=1000(you) gid=1000(you). Make a note of the two numbers; they become PUID and PGID below.

If you want hardware transcoding, you also need the IDs of the video and render groups, as these own the GPU device files:

ls -l /dev/dri
getent group video render

You should see card0 (or card1) and renderD128 in /dev/dri, and two lines like video:x:44: and render:x:109:. The numbers vary between distributions, so use yours, not mine.

πŸ‘
Tip: No /dev/dri?
If the folder does not exist, the host cannot see a GPU. On bare metal, check the iGPU is enabled in the BIOS. On Proxmox, add the iGPU (or a virtual function of it) to the VM as a PCI device and reboot the VM.

STEP 2: Create the folders

I like to keep every app's data under one parent folder so backups are a single job. Create the folders for Jellyfin:

mkdir -p /path/to/appdata/jellyfin/{config,transcodes}
mkdir -p /path/to/backups/jellyfin

When you are done, the layout should look like this:

/path/to/appdata/jellyfin/
    docker-compose.yml
    config/                       # Jellyfin settings, database and metadata
    transcodes/                   # temporary files while transcoding
/path/to/media/
    movies/
    tv/
    music/
/path/to/backups/jellyfin/        # somewhere on a different disk
Folder layout
⚠️
Warning: Make sure the user from STEP 1 can read the media folders: ls -ln /path/to/media should show that user or a group they belong to.

STEP 3: Create a Docker network

I put all my media apps on one shared Docker network so they can talk to each other by name (for example, Jellyseerr reaching http://jellyfin:8096). Create it once:

docker network create media

If you already have a network for your media stack, use that name instead and change it at the bottom of the compose file.

STEP 4: The compose file

Create /path/to/appdata/jellyfin/docker-compose.yml and paste in the following. Use the copy button in the top right corner of the block.

services:
  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    hostname: jellyfin
    restart: unless-stopped
    environment:
      - PUID=1000            # your user ID from STEP 1
      - PGID=1000            # your group ID from STEP 1
      - TZ=Europe/London     # your time zone
      # - JELLYFIN_PublishedServerUrl=https://jellyfin.example.com
    volumes:
      - /path/to/appdata/jellyfin/config:/config
      - /path/to/appdata/jellyfin/transcodes:/transcodes
      - /path/to/media:/media
      - /path/to/backups/jellyfin:/backup
    ports:
      - 8096:8096        # web UI and API (HTTP)
      - 8920:8920        # HTTPS (only if you configure a certificate in Jellyfin)
      - 7359:7359/udp    # client auto-discovery on your LAN
      - 1900:1900/udp    # DLNA
    devices:
      - /dev/dri:/dev/dri    # Intel iGPU for hardware transcoding
    group_add:
      - "44"                 # video group ID (from STEP 1)
      - "109"                # render group ID (from STEP 1)
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8096/health"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 60s
    networks:
      - media

networks:
  media:
    external: true
docker-compose.yml
πŸ’‘
Note: No Intel GPU?
Delete the devices: and group_add: sections. Jellyfin will still work perfectly well; it will just use the CPU for any transcoding.

STEP 5: The settings explained

Setting What it does
PUID / PGID The user and group Jellyfin runs as inside the container. Files it creates in /config will belong to this user.
TZ Time zone, so scheduled tasks and logs use your local time. List of zones.
JELLYFIN_PublishedServerUrl Optional. The address Jellyfin advertises to apps that discover it. Set it to your public URL if you use a reverse proxy.
/config All of Jellyfin's settings, users, database and artwork. This is the folder to back up.
/transcodes Scratch space for transcoding. It can grow to several GB during playback and can be safely emptied when nothing is playing.
/media Your library. Add :ro to the end of the line if you want Jellyfin to have read-only access.
/backup A folder on a separate disk where you can keep copies of your Jellyfin backups.
devices + group_add Hands the GPU to the container and adds the container user to the groups that are allowed to use it.
healthcheck Docker checks Jellyfin's /health endpoint every 30 seconds and marks the container unhealthy if it stops answering.

STEP 6: Start Jellyfin

From the folder containing your compose file:

cd /path/to/appdata/jellyfin
docker compose up -d
docker compose logs -f

Wait until the logs settle down (press Ctrl+C to stop following them), then check the container is healthy:

docker ps --filter name=jellyfin

After a minute the status should read Up ... (healthy).

STEP 7: First-run setup

Open http://192.168.1.20:8096 in your browser, replacing the IP with your Docker host's address. The setup wizard will walk you through the basics.

  1. Pick your language and click Next.
  2. Create your admin user. Choose a username and a strong password, such as CHANGE_ME_STRONG_PASSWORD (please do change it). This is the account that can change every setting, so do not reuse it for day-to-day watching.
  3. Add your libraries. Click Add Media Library, choose a content type (Movies, Shows, Music...), then click the + next to Folders and pick the matching folder under /media, for example /media/movies. Repeat for each type.
  4. Set your metadata language and country (for example English and United Kingdom) so you get the right titles and age ratings.
  5. Remote access. Leave Allow remote connections ticked if you will use a reverse proxy or VPN. Leave automatic port mapping off; you do not want your router opening ports by itself.
  6. Finish and sign in with your new admin account. Jellyfin will start scanning your libraries in the background.

STEP 8: Turn on hardware transcoding

Transcoding happens when a device cannot play a file as-is, for example a 4K HEVC film on an older TV or a phone on mobile data. With the iGPU passed through, we can let the GPU do the heavy lifting.

First, check the container can actually see the GPU:

docker exec -it jellyfin /usr/lib/jellyfin-ffmpeg/vainfo

You are looking for a line similar to Driver version: Intel iHD driver followed by a list of profiles. If you see that, you are good to go.

  1. In Jellyfin, go to Dashboard β†’ Playback β†’ Transcoding.
  2. Set Hardware acceleration to Intel QuickSync (QSV). (VAAPI also works with Intel and is a good fallback if QSV gives you trouble.)
  3. Set the QSV device (or VA-API device) to /dev/dri/renderD128.
  4. Under Enable hardware decoding for, tick the codecs your CPU supports. For a 10th-gen (Comet Lake) iGPU such as the UHD 630, that is H264, HEVC, HEVC 10bit, VP9 and VP9 10bit. Check Intel's spec sheet for your model; AV1 decoding needs 11th gen or newer.
  5. Tick Enable hardware encoding. Leave the Low-Power encoder options off unless you have enabled the GuC/HuC firmware on the host.
  6. Set Transcode path to /transcodes so the scratch files go to the folder we created.
  7. Scroll down and click Save.

To test it, play something and use the playback settings to force a lower quality (say 720p at 4 Mbps). Then open Dashboard β†’ Activity: the session should show Transcoding with the hardware acceleration noted. On the host, intel_gpu_top (from the intel-gpu-tools package) will show the Video engine busy.

STEP 9: Put Jellyfin behind a reverse proxy

Never forward port 8096 straight from your router to Jellyfin. Put it behind a reverse proxy with HTTPS, or keep it private and use a VPN such as WireGuard or Tailscale.

Option A: Traefik (Docker labels)

If Traefik runs on the same Docker host and is attached to the media network, add these labels to the jellyfin service. They assume your HTTPS entry point is called websecure and your certificate resolver is called cloudflare; change them to match your Traefik config.

    labels:
      - traefik.enable=true
      - traefik.http.routers.jellyfin.rule=Host(`jellyfin.example.com`)
      - traefik.http.routers.jellyfin.entrypoints=websecure
      - traefik.http.routers.jellyfin.tls.certresolver=cloudflare
      - traefik.http.services.jellyfin.loadbalancer.server.port=8096
docker-compose.yml (extra lines)

Option B: Traefik (file provider)

If Traefik runs on a different machine (that is how I do it), add a router and service to your dynamic config file instead:

http:
  routers:
    jellyfin:
      rule: Host(`jellyfin.example.com`)
      entryPoints:
        - websecure
      tls:
        certResolver: cloudflare
      service: jellyfin
  services:
    jellyfin:
      loadBalancer:
        servers:
          - url: http://192.168.1.20:8096
dynamic.yml

Option C: Cloudflare Tunnel

In the Cloudflare Zero Trust dashboard, open your tunnel, add a Public hostname of jellyfin.example.com and point it at http://192.168.1.20:8096.

⚠️
Warning: Cloudflare's terms are not friendly to streaming large amounts of video through their free proxy and tunnel. It is fine for logging in and light use, but for regular remote streaming a direct reverse proxy or a VPN is the safer bet.

Once the proxy is in place, go to Dashboard β†’ Networking in Jellyfin and add your proxy's IP address under Known proxies. Without this, every visitor appears to come from the proxy, which confuses logs and bandwidth limits. Restart the container afterwards with docker compose restart.

Updating Jellyfin

The latest tag always follows the newest stable release. To update:

cd /path/to/appdata/jellyfin
docker compose pull
docker compose up -d
docker image prune -f
πŸ‘
Tip: Prefer to update on your own schedule?
linuxserver.io also publishes version tags, for example lscr.io/linuxserver/jellyfin:10.11.7ubu2404-ls26 (see the Tags list on the image page). Pin one of those in the compose file and bump it deliberately after reading the release notes. Major versions (10.10 β†’ 10.11, for example) migrate the database and cannot be rolled back, so back up /config first.

Troubleshooting

The library is empty or some folders are missing. This is almost always permissions. Run docker exec -it jellyfin ls -l /media/movies. If you get Permission denied, the PUID/PGID user cannot read the folder. Fix it on the host with chown/chmod, or add the user to the group that owns the media.

Playback fails as soon as transcoding starts. Check /dev/dri exists on the host, the group_add IDs match getent group video render, and vainfo works (STEP 8). Then look at the latest file in /path/to/appdata/jellyfin/config/log/ starting with FFmpeg.Transcode, which usually names the exact problem. Unticking a codec you are not sure about is a quick way to narrow it down.

Hardware transcoding works but the CPU is still busy. Subtitles can do this. Burning in image-based subtitles (PGS from Blu-rays) happens on the CPU. Try a text subtitle (SRT) or turn subtitles off to confirm.

The apps on my TV cannot find the server automatically. Auto-discovery and DLNA rely on broadcast and multicast traffic, which does not always make it through Docker's bridge network. Enter the server address by hand (http://192.168.1.20:8096), or switch the container to network_mode: host if you really need DLNA.

port is already allocated when starting. Something else is using 8096 or 1900 (Plex and some DLNA servers like 1900). Change the left-hand side of the port mapping, for example 8097:8096, or remove the DLNA line if you do not use it.

The container shows unhealthy. Run docker logs jellyfin --tail 50. Jellyfin can take a few minutes on first start or after a big upgrade while it migrates the database; give it time before restarting it.

That's it! You now have your own media server, with the GPU doing the hard work. Grab a cuppa and enjoy the show. 🍿