Docker & DevOps

Self-host Jellyfin with Docker and hardware transcoding

Install Jellyfin in Docker on a Linux VPS, mount your media library, publish it over HTTPS and switch on hardware transcoding with VAAPI or Intel Quick Sync.

IntermediateLinux server6 min read

You can self-host Jellyfin with Docker in about fifteen minutes: one Compose file, one media volume and one open port. This guide walks through the install, puts Jellyfin behind HTTPS and then enables hardware transcoding so your CPU is not doing all the work when a client cannot play a file directly.

Before you start

You need:

  • A Linux VPS running Ubuntu 22.04 or 24.04, Debian 12, or AlmaLinux/Rocky 8–9, with a sudo user. The examples use deploy and the IP 203.0.113.10.
  • Docker Engine and the Compose plugin installed. If they are not, follow Deploy a Self-Hosted App with Docker Compose from Scratch first.
  • Your media already on the server, for example under /srv/media.
  • A domain such as jellyfin.example.com if you want HTTPS.

Check whether the host exposes a GPU for transcoding:

Bash
ls -l /dev/dri
getent group render

If /dev/dri/renderD128 exists, hardware transcoding is possible. If the directory is missing, most VPS plans do not pass a GPU through to the guest — Jellyfin still works, but transcoding runs on the CPU. Confirm the current situation with your provider before you plan around it.

Warning: Back up any existing media or Jellyfin configuration before you change ownership or paths. A wrong chown on a large library is slow to undo.

Self-host Jellyfin with Docker Compose

Create the folders and the Compose file

Bash
sudo mkdir -p /srv/jellyfin/config /srv/jellyfin/cache
sudo mkdir -p /srv/media/movies /srv/media/tv
sudo chown -R deploy:deploy /srv/jellyfin /srv/media
cd /srv/jellyfin

Create docker-compose.yml:

YAML
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    user: "1000:1000"
    group_add:
      - "989"
    devices:
      - /dev/dri:/dev/dri
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /srv/media:/media:ro
    ports:
      - "8096:8096"
    restart: unless-stopped

Two things to adjust:

  • user: "1000:1000" must match the owner of /srv/jellyfin and /srv/media. Check with id deploy.
  • group_add must be the numeric ID of the group that owns /dev/dri/renderD128. On Debian and Ubuntu that is usually render (989); on some images it is video (44). Use the number from getent group render or getent group video.

If you would rather not deal with group IDs, drop user and group_add and let the container run as root. It works, but the container then has more privileges than it needs.

Pin a specific image tag from the vendor's Docker Hub page if you prefer predictable upgrades instead of latest.

Start the container

Bash
docker compose up -d
docker compose logs -f jellyfin

Wait for the log to settle, then open http://203.0.113.10:8096 and complete the setup wizard. Create the admin account, add a media library pointing at /media/movies and /media/tv, and pick your metadata language.

Tip: If the container restarts in a loop with a permission error on /config, run sudo chown -R 1000:1000 /srv/jellyfin and start it again.

Put Jellyfin behind HTTPS

Jellyfin should not be exposed over plain HTTP on the public internet. The quickest route is a reverse proxy container — see Nginx Proxy Manager in Docker: HTTPS for many self-hosted apps for the full walkthrough, or Traefik in Docker: automatic SSL and routing by container labels if you prefer labels.

Point the proxy at jellyfin:8096 if both containers share a Docker network, or at 203.0.113.10:8096 if they do not. Then:

  1. Enable WebSockets support in the proxy host.
  2. Raise the proxy read timeout to at least 300 seconds — long transcoded streams can idle longer than the default.
  3. In Jellyfin, go to Dashboard → Networking and enable Allow remote connections to this server.
  4. If the proxy runs on the same machine, add its address to Known proxies so Jellyfin logs the real client IP.

Once HTTPS works, close port 8096 in your firewall and reach Jellyfin only through https://jellyfin.example.com.

Enable hardware transcoding

Hardware transcoding moves the video conversion from the CPU to the integrated GPU. On Intel chips this is Quick Sync, exposed through VAAPI.

  1. In Jellyfin, open Dashboard → Playback → Transcoding.
  2. Set Hardware acceleration to Video Acceleration API (VAAPI). On newer Intel hardware you can also try Intel Quick Sync (QSV), which uses the same device.
  3. Set VAAPI device to /dev/dri/renderD128.
  4. Tick the hardware decoding boxes for the codecs your library uses — H.264 and HEVC are the common ones.
  5. Enable hardware encoding for H.264 if your hardware supports it, and leave HEVC encoding off unless you know the chip handles it.
  6. Save, then restart the container: docker compose restart jellyfin.

Verify the GPU works inside the container

Run a one-frame encode with the bundled ffmpeg build:

Bash
docker exec jellyfin ls -l /dev/dri
docker exec jellyfin /usr/lib/jellyfin-ffmpeg/ffmpeg -v verbose \
  -init_hw_device vaapi=va:/dev/dri/renderD128 \
  -f lavfi -i nullsrc=s=1280x720 -vf format=nv12,hwupload \
  -c:v h264_vaapi -frames:v 1 -f null -

A successful run ends with frame= 1 and no Failed to initialise VAAPI connection error. If the driver is missing on the host, install the Intel media driver package for your distribution (intel-media-va-driver on Debian/Ubuntu) and try again.

Warning: Changing transcoding settings restarts playback for anyone currently watching. Do this outside peak hours.

Check it works

Play a file that forces a transcode — a 1080p H.264 file with subtitles burned in, or anything the client cannot direct-play. In the web player, open the playback settings and check the stream info: it should say Transcode (VAAPI) rather than plain Transcode.

Watch CPU usage while it plays:

Bash
docker stats jellyfin

CPU should stay low, typically well under one core, while the GPU does the encode. If CPU sits near 100% of a core, transcoding is still running in software — go back to the VAAPI device path and the group permissions.

Troubleshooting

  • "Playback failed" or the stream never starts. The container cannot see the GPU. Run docker exec jellyfin ls -l /dev/dri; if it is empty, the devices: block is missing or the host has no GPU. On a VPS without /dev/dri, disable hardware acceleration and accept software transcoding.
  • Failed to initialise VAAPI connection in the log. The render group ID in group_add does not match the host. Re-check with getent group render and update the Compose file, then docker compose up -d.
  • Library shows no files. The container user cannot read /srv/media. Compare id deploy with the user: value and fix ownership with sudo chown -R deploy:deploy /srv/media.
  • Video stalls every few seconds behind the proxy. Proxy buffering or a short read timeout. Disable response buffering for the Jellyfin host and raise the timeout to 300 seconds or more.
  • Container exits immediately after an update. The new image changed the config schema or the volume path. Check docker compose logs jellyfin, and restore /srv/jellyfin/config from your backup if needed.

Next steps

Was this guide helpful?

Still stuck?

Ask MdmHost AI for a quick answer, or open a ticket and our team will take a look. We are here 24/7.