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
deployand the IP203.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.comif you want HTTPS.
Check whether the host exposes a GPU for transcoding:
ls -l /dev/dri
getent group renderIf /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
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/jellyfinCreate docker-compose.yml:
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-stoppedTwo things to adjust:
user: "1000:1000"must match the owner of/srv/jellyfinand/srv/media. Check withid deploy.group_addmust be the numeric ID of the group that owns/dev/dri/renderD128. On Debian and Ubuntu that is usuallyrender(989); on some images it isvideo(44). Use the number fromgetent group renderorgetent 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
docker compose up -d
docker compose logs -f jellyfinWait 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:
- Enable WebSockets support in the proxy host.
- Raise the proxy read timeout to at least 300 seconds — long transcoded streams can idle longer than the default.
- In Jellyfin, go to Dashboard → Networking and enable Allow remote connections to this server.
- 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.
- In Jellyfin, open Dashboard → Playback → Transcoding.
- 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.
- Set VAAPI device to
/dev/dri/renderD128. - Tick the hardware decoding boxes for the codecs your library uses — H.264 and HEVC are the common ones.
- Enable hardware encoding for H.264 if your hardware supports it, and leave HEVC encoding off unless you know the chip handles it.
- 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:
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:
docker stats jellyfinCPU 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, thedevices: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 connectionin the log. The render group ID ingroup_adddoes not match the host. Re-check withgetent group renderand update the Compose file, thendocker compose up -d.- Library shows no files. The container user cannot read
/srv/media. Compareid deploywith theuser:value and fix ownership withsudo 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/configfrom your backup if needed.
Next steps
- Back up the Jellyfin config volume so a bad upgrade is recoverable: Back up Docker volumes with restic and restore them.
- Keep the image current without surprises: Update self-hosted containers safely with Watchtower.
- Understand the file you just wrote: Docker Compose file explained: services, volumes, networks and ports.
