Building a complete home media server: ZFS storage, an unprivileged LXC container, correctly configured permissions, Jellyfin installation, transcoding, and plugins.
This page covers building a full Jellyfin media server on Proxmox from scratch: a ZFS storage pool (or a NAS/SMB share if you already have one) for your media, an unprivileged LXC container to run Jellyfin, a bind mount connecting the two, and the permissions fix that solves the single most common Jellyfin-on-Proxmox problem β an empty library after setup.
It also covers how transcoding actually works, how to offload it to an Intel iGPU, and which four plugins are worth installing.
On the Proxmox host, a ZFS pool holds your media files. Separately, a lightweight LXC container runs Jellyfin. The two are connected with a bind mount β a direct window from the container into the media on the host.
Why this matters: Keeping media on the host and Jellyfin in a container means they're completely independent. Updating Jellyfin, rebuilding the container, or starting fresh never touches your media. Never mix your OS with your data.
If you already have a NAS or a Windows machine sharing files over the network, you can mount that share on the host instead of creating a ZFS pool β see Step 2 below.
Skip this step if your media already lives on a NAS β go to Step 2 instead.
In the Proxmox web UI, click your node, then Disks, to identify the drives you want to dedicate to media.
β Common mistake: Creating a ZFS pool wipes the selected drives completely. Double-check you're not selecting the drive Proxmox itself is installed on β use the model names and sizes shown in the disk view to confirm.
Go to your node β ZFS β Create. Name the pool (e.g. mediapool) and choose a RAID level based on your drive count:
| Drives | RAID level | Notes |
|---|---|---|
| 1 | Single disk | No redundancy β make sure you have backups elsewhere |
| 2 | Mirror | Identical data on both drives β one can fail with no data loss. Sweet spot for home use |
| 3+ | RAIDZ1 | One drive's worth of space used for parity β any single drive can fail safely |
Leave ashift at 12 (correct for any modern drive) and click Create. Verify the pool:
zfs list
Why ZFS matters: ZFS isn't just RAID β it constantly checksums data and repairs silent corruption automatically, something traditional RAID can't do. For a media server that may run untouched for years, that matters.
Create a dedicated dataset for media inside the pool:
# Replace 'mediapool' with your pool name
zfs create mediapool/media
Enable compression β cheap on ZFS and effective on metadata, logs, and subtitles even though video files themselves are already compressed:
zfs set compression=lz4 mediapool/media
Your media path is now /mediapool/media. Remember it for Step 3.
Only needed if your media lives on a separate NAS/Windows machine rather than on the Proxmox host itself. If you completed Step 1, skip to Step 3.
Install the CIFS tools on the Proxmox host:
apt install cifs-utils -y
Create an empty mount point:
mkdir -p /mnt/media
Mount the share (replace the IP, share name, username, and password with your own):
mount -t cifs //192.168.1.x/MediaShare /mnt/media \
-o username=youruser,password=yourpassword,uid=0,gid=0,file_mode=0755,dir_mode=0755
Verify:
ls /mnt/media
To make the mount survive reboots, add it to /etc/fstab:
//192.168.1.x/MediaShare /mnt/media cifs username=user,password=pass,uid=0,gid=0,file_mode=0755,dir_mode=0755,_netdev 0 0
β Common mistake: The
_netdevoption is required β it tells the system to wait for the network before mounting. Without it, the mount can fail on boot because the network isn't ready yet, and Proxmox can hang for minutes at startup.
Your media path is now /mnt/media on the Proxmox host β use it wherever the guide references your media path.
An LXC container shares the host's Linux kernel directly β no emulation layer, no wasted resources, boots in seconds.
Privileged vs. unprivileged: in a privileged container, root inside maps directly to root on the host β if anything goes wrong, an attacker is root on your Proxmox node. In an unprivileged container, root inside maps to an ordinary, unprivileged user on the host. Always use unprivileged.
β Common mistake: Many guides recommend privileged containers because permissions are easier to set up. That's a shortcut, not a solution β unprivileged takes two extra minutes and is the correct approach. This guide fixes the permissions properly in Step 4.
Click Create CT in Proxmox and configure:
General
100), hostname jellyfinTemplate
Disk
CPU & Memory
Network
Uncheck Start after created β the bind mount needs to be set up before first boot.
This is the section where most tutorials fail their viewers β take it slowly.
A bind mount makes a path on the Proxmox host visible inside the container. Nothing is copied or moved β the container gets a direct window into that host folder.
On the Proxmox host shell (not inside the container):
# ZFS path example:
pct set 100 -mp0 /mediapool/media,mp=/media
# NAS/SMB path example:
pct set 100 -mp0 /mnt/media,mp=/media
Verify the config was written:
cat /etc/pve/lxc/100.conf
You should see a line starting with mp0.
Why Jellyfin shows an empty library
Because the container is unprivileged, UIDs inside it are remapped on the host: root inside the container is UID 100000 on the host, and the jellyfin service user (UID 101 inside the container) maps to UID 100101 on the host. If the media folder is owned by a different user, Jellyfin silently can't see inside it β no error, just an empty scan result.
β Common mistake: Empty library after setup, correct-looking path, restart doesn't help, scan returns nothing. This is not a Jellyfin bug β it's a silent filesystem permission denial, and it's the single most common Jellyfin-on-Proxmox support question.
Fix it on the Proxmox host. Simplest approach β make the media folder readable by everyone:
chmod -R 755 /mediapool/media
That's sufficient for a home server. For a more precise fix, grant ownership to the Jellyfin user's mapped UID specifically:
# Give ownership to the jellyfin mapped UID
chown -R 100101:100101 /mediapool/media
Why this matters:
chmod 755gives the owner full access and everyone else read/enter permissions β fine for a home server with one or two trusted users. UID100101comes from Proxmox's default unprivileged mapping: a100000base offset plus Jellyfin's UID of101inside the container.
Start the container and open its console:
pct start 100
pct enter 100
Update packages:
apt update && apt upgrade -y
Install curl and gnupg (needed to add the Jellyfin repository):
apt install curl gnupg -y
Add the official Jellyfin GPG signing key, so apt can verify packages actually came from the Jellyfin project:
curl -fsSL https://repo.jellyfin.org/ubuntu/jellyfin_team.gpg.key \
| gpg --dearmor -o /usr/share/keyrings/jellyfin.gpg
Add the repository, then update and install:
echo "deb [signed-by=/usr/share/keyrings/jellyfin.gpg arch=amd64] https://repo.jellyfin.org/ubuntu jammy main" > /etc/apt/sources.list.d/jellyfin.list
apt update && apt install jellyfin -y
Enable and start the service:
systemctl enable --now jellyfin
Verify it's running (should show active (running) in green):
systemctl status jellyfin
Jellyfin listens on port 8096. Browse to http://<container-ip>:8096 from any device on your network.
The setup wizard opens automatically on first visit. Set your language and create an admin account with a strong password.
Click Add Media Library, choose Movies or TV Shows, and point it at /media (or a subfolder if you separate movies and shows).
Naming conventions matter β Jellyfin matches filenames against TMDB (The Movie Database):
Movie Name (Year).mkvShow Name/Season 01/Show Name S01E01.mkvβ Common mistake: A file named
movie.mkvorDownload (2).mkvwill match the wrong title or nothing at all. If your library shows wrong posters, wrong titles, or missing content after scanning, check filenames first β it's the cause about 90% of the time.
Once scanned, Jellyfin automatically pulls posters, descriptions, ratings, cast, and trailers.
Networking: local-network-only use needs no changes under Dashboard β Networking. Remote access from outside the home requires a reverse proxy with HTTPS β do not just forward port 8096 to the internet.
β Common mistake: Port 8096 is plain HTTP β credentials travel unencrypted and can be intercepted by anyone on the same network path. Use an HTTPS reverse proxy for any remote access.
When you press play, one of three things happens:
| Mode | What happens | Server load |
|---|---|---|
| Direct Play | File sent to the device exactly as-is; the device decodes it | None β always the best outcome |
| Direct Stream | Video/audio untouched, only the container format changes on the fly (e.g. MKV β MP4 wrapper) | Light |
| Transcoding | Every frame is decoded and re-encoded in real time | Heavy β can max out a weak CPU |
Why this matters: MKV, MP4, and AVI are containers β wrappers around the video data. H.264 and HEVC are codecs β the actual video format. A device can support a codec perfectly but refuse to play it inside a given container. Direct stream just swaps the wrapper without touching the video itself.
Example: a 4K HEVC file played on a phone that doesn't support HEVC forces Jellyfin to decode every frame of 4K video and re-encode it as H.264 in real time β this can stutter or fail outright on a weak CPU, and maxes a strong one for the whole stream.
Hardware transcoding (Intel iGPU): most Intel CPUs from the last decade have integrated graphics capable of offloading transcoding to the GPU's dedicated video engine entirely, without touching the CPU.
Pass the GPU through to the container β on the Proxmox host shell:
echo 'lxc.cgroup2.devices.allow: c 226:* rwm' >> /etc/pve/lxc/100.conf
echo 'lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir' >> /etc/pve/lxc/100.conf
Then in Jellyfin: Dashboard β Playback β Transcoding β enable VAAPI or Intel QuickSync and point it at /dev/dri/renderD128. A stream that would otherwise max a CPU core drops to almost zero load.
If you don't have an Intel iGPU, software transcoding still works β it just uses more CPU. 1080p content is fine on most reasonable CPUs; 4K software transcoding is where you'll feel it.
Go to Dashboard β Plugins β Catalog. Four plugins worth installing:
Why this matters: Subtitle format affects transcoding. SRT subtitles are plain text and are passed to the client with no processing. PGS and ASS subtitles are image-based, so Jellyfin has to burn them into the video β forcing a full transcode even if the video itself could have been direct played. Prefer SRT where possible.
Library is empty after setup β go back to Step 4 and check the permissions on your media folder. This is the answer roughly nine times out of ten.