Plex raised its prices and moved remote streaming of your own server behind its paywall, which is what sent a lot of people looking for the door. Jellyfin is the usual landing spot: a free, account-free, telemetry-free media server that serves the files you host. This is the honest version of that move, built as one more service on the Proxmox backbone the master guide already set up: an unprivileged LXC, the local CA, the tailscale serve mapping, and Proxmox Backup Server, all reused. If you have not built that backbone, start there; this guide assumes it.
Two things up front, because they decide whether this guide is for you.
Jellyfin is not a streaming aggregator. It serves files on your storage. It does not and cannot pull from Netflix, Disney+, Spotify, or any other subscription service; those are DRM-locked and there is no legal integration. So Jellyfin replaces Plex’s job of serving your own library. It does not replace anyone’s subscriptions. If you are looking for self-hosted Netflix, this is not it, and that is not a Jellyfin limitation you can configure your way around.
This guide covers the server, not where the media comes from. It does not cover download automation (the *arr stack, usenet, torrents), and it is not going to tell you how to fill a library. That is a deliberate scope choice, not coyness: it keeps the guide clean and it keeps the focus on the part that is actually about Proxmox. Where a genuinely legal library comes from is covered honestly in Step 1, because that question deserves a straight answer rather than a wink.
One note, in the spirit of the other guides on this site: this was built and validated end to end, an unprivileged Jellyfin LXC reading SMB shares off a separate box, real HTTPS over the tailnet, and a PBS backup that captures the right thing. The findings below (the single mount option that makes the read work, the per-share permission gotcha that bit one library, the certificate detail that bites everyone once) are from that build, not theory. The one part deliberately not walked end to end is hardware transcoding, because it is the piece that varies most with your specific GPU, and that section flags itself. Worked-example names, IPs, and tailnet addresses are illustrative; substitute your own.
What Jellyfin Is, and What It Isn’t
Read this before you build, because it is the part that decides whether the rest is worth your time, and because the legal question has a real answer that most guides skip.
You already have the “not a streaming aggregator” point from the intro. The other half is sourcing, and here is the honest menu of where a legal library actually comes from:
- Music is the cleanest case by far. Ripping audio CDs you own is widely treated as legal in the US (audio CDs carry no encryption to circumvent, and personal space-shifting is well established in practice), and DRM-free music is everywhere now. If your collection is music, you are on the firmest footing this question offers.
- Over-the-air TV. Jellyfin has Live TV and DVR. Point an antenna and a network tuner at it and you have free, legal broadcast TV and recordings. This is the most underrated legal source and a genuinely good reason to run your own server.
- Public domain and Creative Commons. The Internet Archive, Librivox, and CC-licensed video are bottomless for some tastes and unambiguously yours to host.
- Home and personal media. Your own videos, recordings, and photos. Never a question.
- DRM-free purchases. Common for music and indie video, rare for mainstream movies and TV.
And the limitation you have to be honest with yourself about: ripping DVDs and Blu-rays you own is a gray-to-red zone in the US, because breaking the disc encryption violates the DMCA’s anti-circumvention rule even for a personal backup of a disc you paid for. “Legally ripped movies” is mostly a myth here; other countries vary, and several that allow private copying still forbid the circumvention. This guide does not teach disc ripping, and you should know that the truly-legal video library is the genuinely hard one to build from scratch. That is exactly why the cord-cutting crowd mostly went the route this guide does not cover.
So who is this actually for? The ownership and privacy crowd (people leaving Plex over its account requirement, ads, and telemetry, not over catalog size), people who already have a library, music and audiobook self-hosters, and the over-the-air TV cord-cutters. If that is you, the rest is short, because Jellyfin on the existing backbone is genuinely fast to stand up.
Tip: None of this is legal advice, and the rules vary by where you live. The safe mental model is simple: Jellyfin serves files you already have legitimately, and this guide does not help you acquire anything.
The Jellyfin LXC
This is the same container pattern from the master guide’s backbone, so it is quick. Create an unprivileged Debian 13 LXC the usual way (2 cores, 2 to 4 GB RAM, a small 8 to 12 GB root disk on your data pool, DHCP then a static reservation, the locale and timezone fix, onboot 1). The media does not live in this container, so the root disk stays small; it only holds the OS and Jellyfin’s own data.
Install Jellyfin from its official Debian repository so updates come through apt. Jellyfin also ships a one-line convenience installer, but adding the repository by hand is just as quick and keeps a remote script out of your root shell, which is what this build did:
mkdir -p /etc/apt/keyrings
curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
Then a deb822 source file at /etc/apt/sources.list.d/jellyfin.sources:
Types: deb
URIs: https://repo.jellyfin.org/debian
Suites: trixie
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
(trixie is Debian 13; the repository publishes it for amd64.) Then apt update && apt install -y jellyfin. The service comes up on port 8096, and the first browse to http://<jellyfin-lxc-ip>:8096 runs the setup wizard: create the admin user and skip the media-library step for now, the libraries come after the mount in Step 3.
Before you move on, note the jellyfin user’s UID and GID, because the media mount in the next step depends on them:
id jellyfin
On this build that was uid=102 gid=105; yours may differ, so use your own numbers below. Jellyfin’s data lives in /var/lib/jellyfin (the library database, metadata, images, watch state) with config in /etc/jellyfin. Note where that data is, because Step 7 backs it up and the media specifically does not go with it.
Connecting a Media Library on Another Box
This is the one genuinely new piece versus the master guide, and the part this build spent the most time proving. The realistic setup, and the one built here, is that the media lives on a separate machine (a NAS, or a Windows box already sharing it) and Jellyfin reaches it over the network. Keeping the media separate is fine and often better: the library outlives any one server, and the Proxmox box stays small.
Assume SMB/CIFS as the protocol. Windows boxes, every NAS appliance, and Linux-via-Samba all speak it, so it covers the most setups with one path; NFS is the Linux-to-Linux variant, noted at the end. The chain has two links, because an unprivileged LXC cannot cleanly mount a network share itself:
- Mount each share on the Proxmox host, read-only, at a stable path like
/mnt/media/movies. - Bind-mount that path into the container, read-only, so it appears inside at
/media/movies.
Here is the part that matters, and the reason this guide exists. In an unprivileged LXC the container’s users are shifted by 100000 on the host, so the in-container jellyfin user (UID 102 on this build) is host UID 100102. The files have to be readable by that mapped UID. With SMB this is a one-option fix, because SMB carries no Unix ownership of its own: the CIFS client presents whatever ownership you tell it to at mount time. Set uid= and gid= to the mapped values and the unprivileged container reads the share with nothing else to configure:
mount -t cifs //NAS-IP/Movies /mnt/media/movies -o credentials=/root/.smbcreds,uid=100102,gid=100105,file_mode=0440,dir_mode=0550,ro,vers=3.0,iocharset=utf8
Here 100102 / 100105 are 100000 + the UID/GID you noted in Step 2. credentials= points at a root-only file (chmod 600) holding the share account’s username= and password=; never inline the password. Persist each share in the host’s /etc/fstab with nofail added (a sleeping or missing NAS then never blocks the host’s boot), and note that a share name with a space, like TV Shows, is written TV\040Shows in fstab. Then bind each one into the container read-only, using a distinct mpN index per share (reusing mp0 overwrites the previous mount rather than adding another):
pct set <ctid> -mp0 /mnt/media/movies,mp=/media/movies,ro=1
pct set <ctid> -mp1 /mnt/media/music,mp=/media/music,ro=1
pct set <ctid> -mp2 /mnt/media/tv,mp=/media/tv,ro=1
Important: This is the headline finding. The mapped-UID problem that needs export squash rules or an explicit idmap on NFS is a single uid=/gid= mount option on SMB: set them to 100000 + the in-container UID/GID and the unprivileged container reads cleanly. You do not need a privileged container for this. (If you ever genuinely fight it, a privileged LXC removes the shift entirely and is a defensible shortcut for a read-only library at the cost of weaker isolation, but on SMB you should not need to.)
In Jellyfin, add one typed library per content type, Movies pointed at /media/movies, Shows at /media/tv, Music at /media/music, rather than one mixed folder. Typed libraries drive the correct metadata behavior, and it matches how the shares are already organized.
Important: A dedicated read-only share account needs read permission on each share, and that is enforced at the folder level, not just the share. On this build Movies and TV read fine but Music came up permission-denied: the Music folder’s underlying permissions had not granted the account read, even though the share itself allowed “Everyone.” The mount still succeeds (it authenticates), but file reads are denied until the folder permissions include the account. If one library scans empty while others work, check the folder-level permissions on the source, not the mount.
If your source is Linux and you prefer NFS, the host-side mount differs and the read-fix is the harder case: NFS carries real ownership, so you align it with export squash options or an idmap rather than one mount flag (and a Windows box cannot do NFS squash at all, another reason SMB is the safer default). The bind-mount and typed-library steps are identical.
A local-disk variant is simpler still: if the media sits on the Proxmox host’s own storage, bind-mount that directory the same way and skip the network entirely. But separate, networked media is the more common case, so that is the worked example.
Bandwidth and Latency When the Media Lives Elsewhere
Because the library is on another box, two numbers matter, and neither is the one people worry about. CPU is not the constraint for playback when you are not transcoding (Step 5); the network between the storage and the server, and between the server and the client, is.
For direct play, the sustained bandwidth you need tracks the file’s bitrate, with headroom for peaks and protocol overhead. When Jellyfin direct-plays (sends the file as-is, no conversion), it reads the file off the share and streams it to the client unchanged. A 1080p web release is roughly 5 to 15 Mbps; a Blu-ray remux can be 30 to 80 Mbps; 4K HDR can run higher still. Over a wired gigabit LAN, even the heavy files are comfortable and the network mount is a non-issue. The place it bites is a slow or shared link: if the path between your NAS and the Proxmox box (or between the box and a remote client) is a congested wireless bridge or a modest WAN uplink, a high-bitrate file will stutter, and no amount of server CPU fixes a bandwidth wall.
Latency shows up in the library, not in playback. Playback is a long sequential read, so round-trip latency barely matters once a stream is going. Where you feel a high-latency mount is library scans and metadata refreshes (lots of small stat and read operations across thousands of files), and in how snappy browsing feels. A library on fast local-ish storage scans in minutes; the same library across a slow or high-latency mount can crawl. This is not theoretical: on this build the initial scan over the SMB mount was clearly the slow part, fetching metadata and artwork for every item, while the media reads themselves stayed fast. It is an argument for keeping the storage on a reasonably fast LAN segment, not for co-locating it with the server.
The practical upshot, and the bridge to the next step: a fast LAN plus direct play is the whole game, and you never need to transcode. The moment you stream to a client that cannot play the codec, or across a link too thin for the file’s bitrate, direct play stops being an option and the server has to transcode instead. That is the one tradeoff that pulls everything together.
Transcoding, When You Need It and What It Costs
Direct play is the default and the goal: the server does almost no work and quality is untouched. But Jellyfin will transcode (decode the media and re-encode it on the fly) in three situations you do not fully control: the client cannot play the file’s codec or container, the client’s connection is thinner than the file’s bitrate (so the stream has to be shrunk to fit), or subtitles have to be burned in. Transcoding is what trades server CPU or GPU for lower bandwidth and broader client compatibility. It is the answer to the bandwidth wall from Step 4, at a cost.
You have two ways to pay that cost.
Software transcoding works everywhere with zero setup, and it is brutal on weak hardware. Re-encoding a single 1080p stream in software wants a genuinely capable CPU, and 4K or HEVC in software will bring a low-power chip to its knees. On the kind of old, low-TDP box this series uses as its worked example, software transcoding of anything demanding is not realistic, which is the honest reason direct play is the sane default there.
Hardware transcoding offloads the work to a GPU, and on the integrated-graphics path (Intel QuickSync via /dev/dri) it is dramatically more efficient than software. This is the real path for people who must transcode, and it is also where the build stops being hardware-agnostic, which is why it is described here rather than walked step by step:
Important: Hardware transcoding in an LXC means passing the host’s render device into the container. In practice that is binding /dev/dri/renderD128 into the LXC and making sure the mapped jellyfin user is in the right render group, then enabling the matching acceleration (VAAPI or QSV) under Jellyfin’s Dashboard, Playback. The mechanics are hardware-specific (which GPU, which codecs it can actually encode, driver state on the host) and were not validated end to end in this build, so treat this as the approach to research for your exact hardware, not a copy-paste. One concrete reality worth knowing: older integrated graphics can hardware-encode H.264 but not newer codecs like HEVC, so “I have QuickSync” does not automatically mean “I can transcode my 4K HEVC library.” Check what your specific GPU supports before counting on it.
Where this nets out for most readers: get the clients right and you sidestep the whole question. Pick playback apps and devices that direct-play your library’s codecs (the Jellyfin apps, a capable TV box, and so on), keep the storage and clients on a fast LAN, and the server never transcodes. Reserve hardware transcoding for the genuine cases: streaming to a remote device over a thin link, or a client that simply cannot decode what you have.
HTTPS and Remote Access
This reuses the master guide’s backbone exactly, and it also brings back the most useful lesson from that guide’s Home Assistant section. Jellyfin runs HTTP on 8096 by default; on a trusted LAN that is acceptable, but the moment you expose it over the tailnet on the shared host name, it inherits a problem.
Important: If you reach Jellyfin over the same tailnet host name as another HTTPS service (Nextcloud, Home Assistant), that sibling’s HSTS header has pinned the entire host name to HTTPS in your browser, including Jellyfin’s port. Your browser then forces that port to HTTPS, where plain-HTTP Jellyfin has nothing listening, so it fails to load over the tailnet. This is the exact host-scoped-HSTS finding from the master guide, and the fix is the same: give Jellyfin its own real certificate from the local CA.
Mint a cert for Jellyfin’s names with mkcert on the Proxmox host (its LAN name, LAN IP, the tailnet name, and tailnet IP, all as SANs so it validates on every path), then turn on HTTPS in Jellyfin. Jellyfin’s built-in TLS wants a single PKCS#12 file, so convert the mkcert output on the host and push it into the container:
openssl pkcs12 -export -out /root/jellyfin.pfx -inkey jellyfin-key.pem -in jellyfin.pem -passout pass:
pct push <ctid> /root/jellyfin.pfx /etc/jellyfin/jellyfin.pfx
pct exec <ctid> -- chown jellyfin:jellyfin /etc/jellyfin/jellyfin.pfx
pct exec <ctid> -- chmod 640 /etc/jellyfin/jellyfin.pfx
(pct push lands the file root-only, so the last two make it readable by the in-container jellyfin user; without them Jellyfin silently fails to load the cert.)
Important: Put your real tailnet FQDN and tailnet IP in the mkcert SAN list, and double-check them. A cert that covers the LAN name but not the tailnet name loads fine on the LAN and then fails over the tailnet with ERR_CERT_COMMON_NAME_INVALID, which looks like a serve or HSTS fault but is just a missing SAN. (mkcert files a non-numeric argument as a DNS name rather than an IP, so a mistyped or unsubstituted placeholder address silently does not become the IP SAN you intended.) Confirm with openssl x509 -in jellyfin.pem -noout -ext subjectAltName before converting to the .pfx.
Then under Dashboard, Networking, enable HTTPS and point it at /etc/jellyfin/jellyfin.pfx (leave the certificate password blank, since the export above set none). For remote access, add the host-side passthrough mapping exactly like the other services:
tailscale serve --bg --tcp=8920 tcp://192.168.1.60:8920
(8920 is Jellyfin’s HTTPS port; as the backbone guide notes, verify the tailscale serve syntax against your installed version, since it has shifted across releases.) Now LAN clients hit https://jellyfin.lan:8920/ via your resolver or a hosts entry, and tailnet clients hit the host’s tailnet name on that same port, the same cert validating on both. Tie this back to Step 4: remote playback over the tailnet is bandwidth-bound, so a high-bitrate file that direct-plays perfectly at home may be exactly the case that forces a transcode when you are away.
Back Up the Brain, Not the Library
Add the Jellyfin container to the PBS job from the backbone, and understand precisely what you are and are not backing up, because for media the instinct is backwards.
You back up the container’s root disk, which holds /var/lib/jellyfin: the library database, your watch history, users, and the metadata and artwork Jellyfin has built. That is the brain, it is small, and it is genuinely painful to rebuild, so it is exactly what you want in PBS.
You do not back up the media. The library is a read-only bind-mount of a host path (Step 3), and PBS does not capture host bind-mounts as part of a container backup anyway, so this happens correctly by default, the backup log shows each one as excluding bind mount point ... not a volume while the container’s root disk is included. That is the right outcome on purpose: nightly-dumping hundreds of gigabytes of media that already lives on another box is wasted space and time, and PBS is the wrong tool to protect it. Back up the thing that is hard to recreate (Jellyfin’s understanding of your library) and protect the media with the storage’s own backup plan, wherever it actually lives.
Tip: If your media is on a NAS, its own redundancy and backups are a separate concern from this guide, and they are the thing actually protecting your library. Jellyfin’s PBS backup only protects the server’s view of it. Restoring the container brings Jellyfin back exactly as it was; it does not bring back files that were only ever on a NAS that died.
What’s Next
- Music and audiobooks are the cleanest legal ground, and the same pattern runs them. If your collection leans that way, Navidrome (music) and Audiobookshelf are the same unprivileged-LXC recipe pointed at a different library, and ripping audio CDs you own keeps you on solid legal footing in a way a movie library rarely does. This guide builds Jellyfin because that is the general-purpose case, but the music path is arguably the more defensible one to start with.
- Building a legal library is its own project. None of this tells you how to fill the server, by design. Over-the-air recording, public-domain archives, and your own media are the honest starting points; the rest is up to you and your jurisdiction.
- Transcoding for real. If you genuinely need hardware transcoding, the GPU-passthrough path in Step 5 is its own build worth doing carefully on your specific hardware, and it is the one part of this that stops being hardware-agnostic.
- Download automation is deliberately not here. The *arr ecosystem is a large topic with real legal weight depending on how it is used, and it is not what this guide is. If you go there, that is on you, and it is a different guide than this one.
The throughline: Jellyfin on Proxmox is genuinely fast because it is one more LXC on a backbone you already built, and the only real engineering is honest about itself. Keep direct play on a fast LAN and the server barely works. Put the media where it makes sense and respect the bandwidth math. Reach for transcoding only when the codec or the link forces it, and know it is the one place the hardware starts to matter. And back up Jellyfin’s understanding of your library, not the media that lives and is protected elsewhere.
Toolkit Reference
The stack, and the couple of spots where the honest answer saves you more than a tweak.
The Stack
- Jellyfin
- The media server: free, no account, no telemetry, serves your own files. Runs HTTP on 8096, HTTPS on 8920 once you give it a cert.
- Proxmox VE
- The host. Jellyfin is one more unprivileged LXC on the backbone from the master guide; the media stays on a read-only bind-mount from a separate box, not in the container.
- Tailscale
- Remote access via the host-only
tailscale servepassthrough. Remote playback is bandwidth-bound, which is the case most likely to force a transcode. - mkcert
- The local CA again. Jellyfin needs its own cert (converted to a PKCS#12
.pfx) or a sibling service's HSTS pins the shared tailnet name to HTTPS and breaks plain-HTTP Jellyfin. - Proxmox Backup Server
- Backs up the container root disk (Jellyfin's library DB, metadata, watch state). The media bind-mount is excluded by default, which is the correct outcome.
Where the Honest Answer Saves You
- "Self-hosted Netflix" is the wrong expectation
- Jellyfin serves files you have, not streaming catalogs. Setting that expectation in the first paragraph is worth more than any configuration, because it is the single most common reason people walk away disappointed.
- Bandwidth, not CPU, is usually the wall
- For direct play the constraint is the link between storage, server, and client, not the processor. Reaching for transcoding (and a GPU) before checking whether you simply lack bandwidth solves the wrong problem.
- Back up the brain, not the bytes
- The instinct is to protect the media, but PBS does not back up the bind-mounted library and should not; that is the storage's own backup job. The hard-to-rebuild thing is Jellyfin's library database and metadata, which is small and exactly what belongs in PBS.