Skip to content
Serverküche
Search

Loading search … (only available on the published site).

Applications Difficulty: Intermediate

Self-hosting Jellyfin: your own media server behind Traefik

Set up Jellyfin with Docker behind Traefik: stream your movies, series and music – with HTTPS and the library on netcup's Local Block Storage.

· 11 min read ·Duration: approx. 60 minutes
Table of contents

Netflix, Spotify and Google Photos in one – only on your own server and without a monthly fee: Jellyfin streams your movie, series and music collection to any device. In this recipe we hang Jellyfin behind Traefik and put the library on netcup’s Local Block Storage so you don’t run out of space.

What are we building?

By the end, Jellyfin 10.11 runs as a container on your server, reachable at https://jellyfin.YOUR_DOMAIN with valid HTTPS. Jellyfin is a completely free media server (no cloud, no telemetry, no subscription): you store your files, Jellyfin automatically pulls covers, descriptions and metadata and streams everything to the browser, phone, smart TV or the Jellyfin app.

The real challenge with a media server is storage space – a movie collection quickly bursts the small system SSD of a VPS. That’s why we mount netcup’s Local Block Storage: an additional, local disk that appears in the server like a normal drive and on which the library lives.

Honest about transcoding

Jellyfin re-encodes videos on demand live into a format your end device understands (transcoding) – that costs a lot of CPU. netcup servers are pure CPU machines without a graphics card, so real hardware transcoding (via a GPU) doesn’t exist there. On a root server with dedicated cores (RS 1000), software transcoding works well for one or two parallel streams; but the royal road remains Direct Play – more on that below.

Prerequisites

  • A running Traefik reverse proxy with the proxy network and the resolver le – see reverse proxy with Traefik.
  • A subdomain jellyfin.YOUR_DOMAIN with a DNS record to your server IP – see connecting a domain to your server.
  • Dedicated CPU cores are clearly at an advantage for smooth streaming (transcoding). On a shared VPS it works too for Direct Play, but gets tight quickly on re-encoding.
  • Optional but recommended: booked netcup Local Block Storage for the library (step 2). Without it, you simply put the media in a folder on the system disk – until the space runs out.
  • A few of your own media files (movies/series/music) that you’re allowed to host.

Whether your setup really needs dedicated cores is shown by the server calculator – with transcoding the answer is usually yes.

🍳 Recommendation Ad

Root-Server RS 1000 G12

4 dedicated cores · 8 GB RAM · 256 GB NVMe

from €12.79/month

Dedicated cores for transcoding – and Local Block Storage can be added on.

Go to netcup →

💶 5 € voucher for new netcup customers: 36nc17844976032 (new customers only, no domains)

Step by step

Step 1: Create the DNS record

Create jellyfin.YOUR_DOMAIN (A/AAAA to your server IP) and check that it resolves:

Terminal
dig +short jellyfin.YOUR_DOMAIN

Your server IP must come back – otherwise Traefik won’t fetch a certificate later.

Step 2: Mount netcup Local Block Storage

The Local Block Storage is additional storage you book to your server in the netcup customer account. Unlike the network-attached Storage Space of other providers (which is connected via NFS and is slower), it appears in the server as a completely normal local disk – ideal for a library because it’s fast and freely partitionable.

No block storage booked?

Then skip this step and simply create the library under ~/jellyfin/media – all the following commands work the same, only the path is different. You can add the block storage later at any time and move the data.

First look at which disks the server knows:

Terminal
sudo fdisk -l

The system disk is typically /dev/sda (or /dev/vda). The new block storage appears as an additional device, usually /dev/vdb – recognizable by the size you booked.

Check first, then partition

The device names can differ per server. If you accidentally partition the system disk, your server is toast. Identify the device unambiguously by its size before you continue. When in doubt, create a snapshot first.

Create a partition on the new disk (here /dev/vdb):

Terminal
sudo cfdisk /dev/vdb

Choose gpt at the prompt (the modern standard – dos/MBR can address at most 2 TiB, libraries like to grow beyond that), then New, take the full size, and write the table with Write (type yes), then Quit. The partition /dev/vdb1 is created. Format it with an ext4 filesystem:

Terminal
sudo mkfs.ext4 /dev/vdb1

Create a mount point and mount the partition:

Terminal
sudo mkdir -p /mnt/media
sudo mount /dev/vdb1 /mnt/media

So that the disk is mounted automatically even after a reboot, you enter it in /etc/fstab with its UUID (not with /dev/vdb1, which can change). Read out the UUID:

Terminal
sudo blkid /dev/vdb1
Ausgabe
/dev/vdb1: UUID="a1b2c3d4-...." TYPE="ext4" ...

Open the fstab and append the line with your UUID:

Terminal
sudo nano /etc/fstab
Ausgabe
UUID=a1b2c3d4-....   /mnt/media   ext4   defaults   0   2

Check the entries before you rely on the reboot – a typo in the fstab can block the boot process:

Terminal
sudo umount /mnt/media && sudo mount -a && df -h /mnt/media

If no error appears here and df -h shows your new disk under /mnt/media, everything is entered correctly.

Step 3: Create the project and media folders

Create the Compose project – the configuration is small and may go on the system disk. You create the subfolders config and cache right along, before the container starts for the first time: if the Docker daemon created them first, they would belong to root – but Jellyfin runs as UID 1000 (see step 4) and couldn’t write into its own /config, the container would crash on the first start.

Terminal
mkdir -p ~/jellyfin/{config,cache} && cd ~/jellyfin

Audiobooks belong elsewhere

Jellyfin can do music – but it is awkward for audiobooks and podcasts: it does not sync your listening position across devices, and it knows nothing about chapter marks or a sleep timer. If you want both, use Audiobookshelf for those; the two services run side by side on the same server without trouble.

And the folder structure for the library on the block storage. Jellyfin sorts best when movies, series and music are kept separate:

Terminal
sudo mkdir -p /mnt/media/{filme,serien,musik}

So that the Jellyfin container may read the files, they have to belong to your user. Determine your user and group ID (usually 1000) and hand over the folder:

Terminal
id -u && id -g
sudo chown -R 1000:1000 /mnt/media

Now copy your media into the appropriate subfolders (via scp, rsync or an SFTP client). For clean recognition, a clear naming helps, e.g. filme/The Godfather (1972)/The Godfather (1972).mkv.

Step 4: The compose.yaml

Now the central file. Replace jellyfin.YOUR_DOMAIN with your subdomain:

YAML
services:
  jellyfin:
    image: jellyfin/jellyfin:10.11.11
    user: "1000:1000"
    environment:
      - JELLYFIN_PublishedServerUrl=https://jellyfin.YOUR_DOMAIN
    volumes:
      - ./config:/config
      - ./cache:/cache
      - /mnt/media:/media:ro
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jellyfin.rule=Host(`jellyfin.YOUR_DOMAIN`)"
      - "traefik.http.routers.jellyfin.entrypoints=websecure"
      - "traefik.http.routers.jellyfin.tls.certresolver=le"
      - "traefik.http.services.jellyfin.loadbalancer.server.port=8096"
    networks:
      - proxy
    restart: unless-stopped

networks:
  proxy:
    external: true

What the lines mean:

  • image: …:10.11.11 – the fixed, current stable version. No latest (see “Maintenance & backups”).
  • user: "1000:1000" – Jellyfin runs with your user/group ID so it can read the media. If you adjust other IDs in step 3, change them here too.
  • JELLYFIN_PublishedServerUrl – tells Jellyfin under which public address it’s reachable. This prevents app links from suddenly pointing to an internal IP.
  • volumes: config (settings, users, metadata) and cache live small in the project folder; the library is mounted from the block storage under /mnt/media into the container as /media:ro (read-only), so Jellyfin never changes your originals.
  • The four Traefik labels are the familiar pattern. New is only the port: Jellyfin listens on 8096, hence the loadbalancer.server.port label. Without it, there’d be a 502 Bad Gateway.
  • No ports: – Jellyfin is reachable exclusively via Traefik (HTTPS), not directly from the internet.

Start the stack and watch it boot:

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

On the first start, certificate issuance takes a few seconds. Once Startup complete appears in the log, Jellyfin is ready.

Step 5: The initial setup wizard

Open https://jellyfin.YOUR_DOMAIN in the browser. On the very first call, a wizard guides you through the basic setup:

  1. Choose the language.
  2. Create an administrator account – set a long, unique password, because the login page is publicly on the net.
  3. Add media libraries: click “Add media library”, choose the type (e.g. Movies), and enter the container path as the folder – i.e. /media/filme (not /mnt/media/filme, that’s the path on the host!). Repeat this for serien and musik.
  4. Set the metadata language and you can accept the rest.

The Jellyfin setup wizard on first launch with fields for server name and preferred display language
The setup wizard on first launch

After the wizard, Jellyfin scans the folders and loads covers and descriptions. For large collections the first scan takes a while – you see the progress under Dashboard → Scheduled Tasks. After that you land on the home page and see your library with the first titles:

The Jellyfin home page after setup with the “Movies” library and a recently added movie
The home page with the first library

Step 6: Configure Jellyfin cleanly behind the proxy

Two settings make Jellyfin work correctly behind Traefik. Open Dashboard → Networking:

The Jellyfin administration dashboard with server version 10.11.11, active devices and the storage paths, on the left the menu with network and playback settings
The admin dashboard – networking and playback sit here too

  • Under Known proxies you enter the Traefik network so Jellyfin sees the real client IP (instead of Traefik’s container IP). You determine the subnet with:

    Terminal
    docker network inspect proxy --format '{{ (index .IPAM.Config 0).Subnet }}'

    Enter the returned range (e.g. 172.19.0.0/16) there.

  • HTTPS: since Traefik handles the encryption, Jellyfin itself needs no TLS. Leave the HTTPS options in Jellyfin empty – everything runs via the proxy.

WebSockets (for live updates of the interface) Traefik forwards automatically, nothing to do there.

Step 7: Transcoding vs. Direct Play

Whether your server breaks a sweat is decided here. Two cases:

  • Direct Play – the end device can play the file as-is. The server just pushes bytes through, almost no CPU load. That’s the ideal case.
  • Transcoding – format, codec or bitrate don’t fit the device (or the line is too slow), so Jellyfin re-encodes live. On a netcup server this happens via the CPU – with a 4K stream that can max out a whole server.

This is how you keep the load low:

  • Provide suitable formats: H.264/AAC in an .mp4/.mkv plays directly on practically every device. Exotic codecs (e.g. certain 4K HEVC audio tracks) force transcoding.
  • Set the client quality in the app to “Original/Direct Play” when the line is enough.
  • Under Dashboard → Playback you can set the transcoding settings and a limit for simultaneous streams.

Hardware transcoding needs a GPU

The hardware acceleration selectable in Jellyfin (VAAPI, QSV, NVENC) requires a graphics unit under /dev/dri. Standard VPS and root servers at netcup are KVM machines without a passed-through GPU – leave hardware acceleration off there, it would only produce errors. Instead plan with dedicated CPU cores and Direct Play.

When things go wrong

502 Bad Gateway on access, even though the container is running. Traefik reaches the container but hits the wrong port. Jellyfin listens on 8096 – the label traefik.http.services.jellyfin.loadbalancer.server.port=8096 must be set. Also check that Jellyfin is on the proxy network.

The libraries stay empty, even though files are there. Almost always a permission problem. The container runs as 1000:1000 (step 4), so the media must belong to that user: sudo chown -R 1000:1000 /mnt/media. And: in Jellyfin the container path must be entered (/media/filme), not the host path. Afterwards start “Scan all libraries” under Dashboard → Scheduled Tasks.

Video stutters/buffers, docker stats shows Jellyfin at ~100% CPU. It’s transcoding. Under Dashboard → Playback the activity monitor shows whether it says “Transcode” instead of “Direct Play”. Set the client quality to original, change exotic codecs to a widely supported format, or give the server more (dedicated) cores.

After a reboot the library is gone and Jellyfin shows empty folders. The block storage wasn’t mounted – usually a missing or wrong fstab entry (step 2). Check with df -h /mnt/media and sudo mount -a. The line must use the UUID, not /dev/vdb1.

The login page loads, but the app can’t find the server / links point to an internal address. JELLYFIN_PublishedServerUrl is missing or wrong. Set it in the compose.yaml to https://jellyfin.YOUR_DOMAIN and restart with docker compose up -d (a restart isn’t enough for the environment change to take effect).

Maintenance & backups

  • Apply updates deliberately. The fixed tag (jellyfin:10.11.11) means: you decide when to update. Bump the tag → docker compose pulldocker compose up -d. Before jumping to a new major version, read the release notes – database migrations aren’t always reversible.
  • Back up the config, treat the media separately. The config volume (users, settings, playback state, metadata) is small and the actually valuable part – back it up encrypted off-site with Restic. The library itself is usually too large for a classic backup and reproducible from the original media; decide deliberately whether you back it up or classify it as “replaceable”. Honestly: a second backup of your movie collection costs real storage.
  • Monitor disk space. A library grows steadily. Keep an eye on usage (df -h /mnt/media) and best set up a monitor in Uptime Kuma that raises the alarm before the block storage is full.
  • Publicly reachable = secure it. The login page is on the net, and Jellyfin brings no two-factor authentication (not even via an official plugin). So consistently assign strong passwords, deactivate unused accounts – and for purely private use, consider making Jellyfin reachable not publicly but only via a VPN.

Last updated: Jul 25, 2026

You might also like