This is the first part of Jellyfin at home. By the end of it Jellyfin 12 is running on my Proxmox server, it answers in a browser, and the first library is in. The parts after this one are about naming the files so Jellyfin recognises them, hardware transcoding, apps and remote access, and backups and upgrades.

This is my own Jellyfin server being built, not a demo. I write down what I did, including what I would do differently.

Why Jellyfin, and why in a container

Jellyfin is a media server: it reads your folders of music, films and shows, fetches covers and descriptions, and plays them in a browser, on a phone or on a TV. It is free and open source. There is no account to create with anyone, and nothing in it is held back for paying customers, which is the usual reason people choose it over Plex.

The current version is 12.4, from 26 October 2026 [13]. It is the fourth bugfix release since 12.0 came out in September. I wrote about what changed in 12 in What’s new in Jellyfin 12.

LXC, VM or Docker?

On Proxmox there are three places Jellyfin can live:

  • A virtual machine. The strongest isolation, and the heaviest. For hardware transcoding the graphics chip has to be passed through to the VM, which takes it away from the host.
  • An LXC container. Light, and it can share the host’s graphics chip without taking it over, which matters in part 3.
  • Docker. Jellyfin publishes an official image, and the documentation has a ready Docker Compose file [1].

I use the last two together: an unprivileged LXC container with Docker inside it, and Jellyfin started from a docker-compose.yaml. It is the same layout I use for Nextcloud, one service per container and one compose file per container, and I explained it in part 3 of Proxmox from scratch.

It is one layer more than strictly needed, and Proxmox says so itself: for “maximum isolation and the ability to live-migrate, nesting containers inside a Proxmox QEMU VM remains a recommended practice” [5]. I accept that for three reasons. Every service on my server is set up the same way. The version is pinned in one file, so an upgrade is a decision and not something that happens during apt upgrade. And the things Jellyfin writes can each sit on their own volume, which is what most of this post is about.

Community scripts vs. doing it by hand

The Proxmox VE Helper-Scripts have a one-line installer that creates a Jellyfin container for you [11]. It is a fine shortcut. I build mine by hand here, because I want to know what is in the container when something stops working.

What we are building

  • One unprivileged LXC container with Debian 13, Docker and one compose file. Nothing else lives on its own disk.
  • Three extra volumes for what Jellyfin writes: its database and settings, its artwork, and its cache.
  • Three volumes for the media: music, video and books. Jellyfin only gets to read these.
  • All six are volumes that Proxmox creates and manages, so they belong to the container and move with it. From the container they are handed on to Jellyfin’s Docker container.

The container

The container is the one I created in part 3 of Proxmox from scratch, container 1002 on my server phoebe. Why each number is what it is, is explained there:

My container
Cores4
Memory4 GB
Swapnone
Disk32 GB

In short: what uses the processor is transcoding, and until the graphics chip takes over in part 3, the processor does all of it [3]. Jellyfin’s advice is 8 GB of memory for an average server, but “for a Linux server running without a GUI, 4GB may be enough” [3]. No swap, so I see it plainly if 4 GB is too little. The disk only holds Debian, Docker and the Jellyfin image, because the database, the artwork, the cache and the media all get volumes of their own.

In practice it needs far less memory than that. With the music library scanned, free -h in the container showed about 400 MiB in use and 3.6 GiB still available. Proxmox’s Summary page counts a little differently and showed 265 MiB:

The Summary page of container 1002 in Proxmox after the music scan: running, unprivileged, CPU 0.12 % of 4 CPUs, memory 264.81 MiB of 4.00 GiB, boot disk 2.30 GiB of 32 GiB, with the CPU, memory and network graphs for the last hour

This is everything I chose in the Create CT dialog, on one page:

The Create CT dialog’s Confirm tab: cores 4, features nesting=1, hostname jellyfin, memory 4096, net0 on vmbr0 with the firewall on and the address hidden, the Debian 13 template, rootfs vmdatanvme:32, swap 0, unprivileged 1, vmid 1002

The same on the command line, with your own address in place of the example. pveam available shows the current name of the template; mine was debian-13-standard_13.1-2_amd64.tar.zst:

sh
pveam update
pveam available --section system | grep debian-13
pveam download local debian-13-standard_13.1-2_amd64.tar.zst

pct create 1002 local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname jellyfin \
  --unprivileged 1 \
  --features nesting=1,keyctl=1 \
  --cores 4 --memory 4096 --swap 0 \
  --rootfs vmdatanvme:32 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.1.50/24,gw=192.168.1.1 \
  --onboot 1

I gave it a fixed address. The apps remember the server’s address, so it should not change. A DHCP reservation in the router does the same job.

Two of those lines are there for Docker:

  • unprivileged 1 means root inside the container is an ordinary, powerless user on the host [5]. It is the default, and I keep it.
  • features nesting=1,keyctl=1: Docker needs both. Nesting was ticked in the dialog. keyctl is not, and goes on under the container’s Options, Features. The documentation says of it: “This is required to use docker inside a container” [6].

The last line, onboot 1, is Start at boot under Options: the container starts when the server does.

My container already existed, so I set both on it from the host, with the container stopped:

sh
pct set 1002 --features nesting=1,keyctl=1 --onboot 1
The Options page of container 1002 on phoebe: Start at boot Yes, Unprivileged container Yes, Features nesting=1,keyctl=1

Volumes: config, metadata, cache and media

This is the part to get right before Jellyfin starts for the first time, because some of the settings only apply to data written after they were set.

Jellyfin’s storage documentation has three rules that shape the layout [2]:

  • The database is SQLite, and on ZFS it wants a small record size: 4K or 8K, where the default is 128K.
  • Media files want the opposite, a large record size. 1M is “likely appropriate”.
  • The database must be on local storage, not a network share, and nothing but the media belongs on spinning disks.

In the official image, Jellyfin’s data directory is /config and its cache directory is /cache [9].

Volumes, not folders from the host

A container can get extra storage in two ways. A bind mount hands it a folder that already exists on the host. A volume mount point is a new volume that Proxmox creates on one of its storages and manages from then on, just like the container’s own disk [15]. I went through both in part 3. Here I use volumes, for one reason above the others: in part 8 of Proxmox from scratch this container moves to another server.

A container that is moved takes its volumes with it: Proxmox copies them to the storage of the same name on the other server [15]. Proxmox can also keep a copy there all the time (replication), so that a move only has to send what changed since the last copy [17]. A bind mount gets none of that. Proxmox does not manage it, and it does not copy it.

The price is that a volume belongs to one container. If I later want a second service to read the same books, the books have to be shared some other way.

The six volumes

My servers have two storages for guests, sorted by the kind of drive, as I described in Storage in Proxmox. vmdatanvme is the fast tier, on the NVMe drive. vmdatahdd is the slow tier, on a 1 TB hard disk that phoebe got last week. Jellyfin’s rule above decides which volume goes where: what Jellyfin writes goes on the NVMe, and the media goes on the hard disk.

Mount pointStorageInside the containerSeen by Jellyfin asSizeSettingsIn Proxmox backups
mp0vmdatanvme/srv/jellyfin/config/config8 GBrecordsize=8Kyes
mp1vmdatanvme/srv/jellyfin/metadata/config/metadata16 GBdefault record sizeyes
mp2vmdatanvme/srv/jellyfin/cache/cache60 GBno backup, no replicationno
mp3vmdatahdd/srv/media/music/media/music100 GBrecordsize=1M, read-onlyno
mp4vmdatahdd/srv/media/video/media/video250 GBrecordsize=1M, read-onlyno
mp5vmdatahdd/srv/media/books/media/books10 GBrecordsize=32K, read-onlyno

Some of these choices are mine and not the documentation’s:

  • Artwork apart from the database. The posters and backdrops are ordinary image files and gain nothing from an 8K record size. Giving them their own volume, mounted inside /config, leaves the small record size to the database.
  • Films and shows in one volume. A file can then be moved from one folder to the other without being copied.
  • The sizes are ceilings. On ZFS a volume’s size is a limit on that one dataset, not space set aside. Jellyfin can fill its own volumes and nothing else on the disks, which it shares with my Nextcloud. The music is about 46 GB today and the films and shows about 93 GB, so there is room to grow. For the cache, the documentation’s example is a 50 GB Blu-ray film that can take from 15 to 60 GB once transcoded [2]. A volume can be made bigger later with Resize.
  • The same storage names on both servers. priscilla has vmdatanvme and vmdatahdd too, on its own NVMe and hard disk. When the container moves there, each volume lands on the same tier it left. Nothing inside the container changes.
  • atime=off on all of them, so reading a file does not cause a write.

Three kinds of copy, three switches

My server has three ways of copying a guest’s data, and each volume needs the right answer for each of them.

ZFS snapshots, pulled to my backup server. Everything under my guest storage is snapshotted every 15 minutes, and my backup server pulls the snapshots every hour. I described that in 3-2-1 in practice. It works on the datasets underneath Proxmox, with everything below them, so a new volume is backed up from the first hour without my doing anything. That is what I want for the database, the artwork and the media.

Proxmox’s own backups (vzdump) copy the container’s own disk, but “by default additional mount points besides the Root Disk mount point are not included in backups” [16]. A volume is included only if it says backup=1 [6]. In the web interface that is the Backup box, which is ticked for a new mount point, so a volume made there is in the backups and one made with pct set is not. I learned that from my own configuration: my first two volumes came out without it. Proxmox backups “are always full backups” [16], so including the media would copy the whole library again on every run, and the media is already backed up by the ZFS pull. Config and artwork go in with backup=1: a Proxmox backup of this container is then a working Jellyfin with its settings, its users and its database, waiting for its media.

Replication keeps a copy on another server in the cluster, which is what makes moving the container fast. My servers are becoming a cluster this month, in part 7 of Proxmox from scratch, and replication itself is part 8. Every volume takes part unless it says replicate=0 [6].

The cache is the odd one out. Transcode files are large, worthless a minute later, and would be kept in snapshots and sent across the house. So the cache stays out of all three copies. For Proxmox that is no backup=1 and a replicate=0. sanoid and syncoid don’t know about Proxmox’s flags, so the ZFS side is done by hand: a dataset with the property syncoid:sync=false “will not be synchronised to any hosts - it will be skipped” [18], so the cache never reaches my backup server. What sanoid does with it locally is further down.

If you set up your own snapshots, the rule is the same: give each volume the backup it should have, not the one its neighbours have.

A note on deduplication

Both of my pools have ZFS deduplication switched on, and every new dataset inherits it. I switch it off for all of Jellyfin’s volumes. Music and video are already compressed and every file is different, so there is nothing to find, and each block would still get an entry in the deduplication table. If your pool does not use deduplication, leave dedup=off out of the commands below.

Creating them

In the web interface: click the container, then Resources, Add, Mount Point. Choose the storage from the table, the size and the path inside the container. Under Advanced are Read-only, Backup and Skip replication. Do it with the container stopped.

The Create: Mount Point dialog for the cache: Mount Point ID 2, storage vmdatanvme, 60 GiB, path /srv/jellyfin/cache, Backup unticked, Skip replication ticked, Advanced ticked

Or on the Proxmox host, as root. vmdatanvme:8 means “a new volume of 8 GB on the storage vmdatanvme” [15]:

sh
pct set 1002 -mp0 vmdatanvme:8,mp=/srv/jellyfin/config,backup=1
pct set 1002 -mp1 vmdatanvme:16,mp=/srv/jellyfin/metadata,backup=1
pct set 1002 -mp2 vmdatanvme:60,mp=/srv/jellyfin/cache,replicate=0
pct set 1002 -mp3 vmdatahdd:100,mp=/srv/media/music,ro=1
pct set 1002 -mp4 vmdatahdd:250,mp=/srv/media/video,ro=1
pct set 1002 -mp5 vmdatahdd:10,mp=/srv/media/books,ro=1

Leaving backup out is the same as backup=0, so the cache and the media need nothing for it. In the web interface, untick Backup for those four instead.

The Resources page of container 1002: the 32G root disk and mount points mp0 to mp5, three on vmdatanvme and three on vmdatahdd

This is mine just after I made them, before I added backup=1 to the first two. The media volumes say backup=0 because I wrote it out, which does the same as leaving it out.

ro=1 makes the media read-only inside the container [6]. Jellyfin can then never change or delete a file in the library, whatever I click on. The files go in from the host instead, further down.

Each volume is a ZFS dataset of its own. Proxmox names them after the container, and the container’s own disk is number 0:

sh
pct config 1002 | grep ^mp
zfs list -o name,refquota,mountpoint -r rpool/data/vmdatanvme paula/data/vmdatahdd | grep 1002

On mine, mp0 to mp2 became subvol-1002-disk-1 to -disk-3 on the NVMe, after the container’s own disk, number 0. The media volumes are numbered on their own storage, so they became subvol-1002-disk-0 to -disk-2 on the hard disk. Check yours, because the numbers depend on what was there before. Then set the ZFS properties, still before anything is written to them:

sh
N=rpool/data/vmdatanvme
H=paula/data/vmdatahdd

# config: the database wants a small record size
zfs set recordsize=8K atime=off dedup=off $N/subvol-1002-disk-1
# metadata: artwork
zfs set atime=off dedup=off $N/subvol-1002-disk-2
# cache: no backup of any kind
zfs set atime=off dedup=off syncoid:sync=false $N/subvol-1002-disk-3
# media: music, video, books
zfs set recordsize=1M atime=off dedup=off $H/subvol-1002-disk-0
zfs set recordsize=1M atime=off dedup=off $H/subvol-1002-disk-1
zfs set recordsize=32K atime=off dedup=off $H/subvol-1002-disk-2

zfs get -o name,property,value recordsize,atime,dedup,refquota,syncoid:sync \
  $N/subvol-1002-disk-1 $N/subvol-1002-disk-2 $N/subvol-1002-disk-3 \
  $H/subvol-1002-disk-0 $H/subvol-1002-disk-1 $H/subvol-1002-disk-2

The hard disk’s tier already has recordsize=1M for everything on it, so the two recordsize=1M settings only repeat it. I set them anyway, so the volumes keep their record size if they are ever moved. The books get 32K because that is what the books have on my backup server, where the copies were made from: with the same record size, the backup copy can deduplicate against the original there.

Settings made like this stay with the dataset. When replication copies it to another server, they go along.

sanoid still snapshots the cache on phoebe itself, and I have left that on for now: the snapshots stay on phoebe and are pruned on sanoid’s normal schedule, so the price is that deleted transcode files hold on to their space until the last snapshot with them is gone. If that starts to show, a section for the cache dataset in /etc/sanoid/sanoid.conf switches its snapshots off:

ini
[rpool/data/vmdatanvme/subvol-1002-disk-3]
        autosnap = no
        autoprune = yes

That works because the section for the whole tier above it says recursive = yes, so sanoid handles each dataset under it one at a time and lets a section for one of them override the rest. The media volumes on the hard disk need nothing: that tier has its own section in sanoid.conf, and my backup server already pulls everything under it.

Permissions in an unprivileged container

In an unprivileged container the user IDs are shifted: root inside is user 100000 on the host, and the IDs after it follow in order, so user 1000 inside is user 101000 [7]. A new volume belongs to the container’s root. Jellyfin will run as user 1000, so the three volumes it writes to must belong to 1000. The easiest place to do that is inside the container, where the numbers need no arithmetic:

sh
pct start 1002
pct enter 1002
chown 1000:1000 /srv/jellyfin/config /srv/jellyfin/metadata /srv/jellyfin/cache
ls -ln /srv/jellyfin /srv/media

Before the chown, all six belonged to 0, the container’s root. After it, the three folders under /srv/jellyfin show owner 1000.

The media only has to be readable. Files I copy in on the host belong to the host’s root, or keep the owner they had on the computer they came from, and inside the container they show up as owned by nobody. That is fine, as long as folders are readable and enterable by everyone and files are readable by everyone, which is what you normally get when you copy files in. If Jellyfin later shows an empty library, this is the first thing to check, on the host, for each media volume:

sh
chmod -R a+rX /paula/data/vmdatahdd/subvol-1002-disk-0

Proxmox VE 9.2 also has an idmap option on each mount point, which overrides the container’s ID mapping for that one mount [6]. I have not needed it for this.

Installing Docker in the container

Inside the container. First bring the template’s packages up to date. Mine had six updates waiting:

sh
pct enter 1002
apt update
apt full-upgrade -y

Then the commands from Docker’s own instructions for Debian, which list Debian 13 as supported [8]. I have left out sudo, since pct enter makes you root.

sh
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
  -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc

tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

docker run --rm hello-world

If the last command prints its greeting, Docker works inside the LXC. Mine did on the first try, with Docker 29.9.3 and Compose 5.7.0. With nesting and keyctl on, there was nothing else to do.

There is one harmless thing you may see on the way: a screenful of “perl: warning: Setting locale failed” from apt. The container inherits the language setting en_US.UTF-8 from the shell you came from, but the Debian template does not have that locale installed. It does not stop anything. The locales package is already in the template, so getting rid of it takes two lines in the container:

sh
sed -i 's/^# *en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen
locale-gen

Installing Jellyfin 12

The compose file

Create a folder for it and write the file:

sh
mkdir -p /opt/jellyfin
nano /opt/jellyfin/docker-compose.yaml
yaml
services:
  jellyfin:
    image: jellyfin/jellyfin:12.4
    container_name: jellyfin
    user: 1000:1000
    ports:
      - 8096:8096/tcp
      - 7359:7359/udp
    environment:
      - TZ=Europe/Copenhagen
    volumes:
      - /srv/jellyfin/config:/config
      - /srv/jellyfin/metadata:/config/metadata
      - /srv/jellyfin/cache:/cache
      - /srv/media/music:/media/music:ro
      - /srv/media/video:/media/video:ro
      - /srv/media/books:/media/books:ro
    restart: unless-stopped

It is the example from Jellyfin’s documentation [1] with my folders in it. Line by line, where there was something to choose:

  • image: jellyfin/jellyfin:12.4 is the official image. The documentation also mentions images from LinuxServer.io and hotio [1]; I stay with the project’s own. The tag is the version [14]. I do not use latest, and I do not let anything update it for me: upgrading Jellyfin changes the database, so I do it on a day I choose, with a backup first. That is part 5.
  • user: 1000:1000 runs Jellyfin as an ordinary user and not as root. This is the user 1000 that owns the three folders.
  • ports: 8096 is the web interface, and 7359 lets the apps find the server on the network by themselves [1]. The alternative is host networking, which you need for DLNA [1]. I do not use DLNA.
  • volumes: the first three are the volumes Jellyfin writes to. The artwork is mounted inside /config, at /config/metadata. The media folders end in :ro, read-only, a second time.
  • restart: unless-stopped starts Jellyfin again after a reboot.

The lines for the graphics chip come in part 3, and a proper address with HTTPS in part 4.

Starting it

sh
cd /opt/jellyfin
docker compose up -d
docker compose logs -f

The first start downloads the image. Leave the log with Ctrl+C when it goes quiet, and check:

sh
docker compose ps
ls /srv/jellyfin/config /srv/jellyfin/cache

docker compose ps should say healthy after half a minute; the image checks itself on port 8096 [9]. Mine pulled the image in 17 seconds, Jellyfin reported “Startup complete” after nine, and it was healthy when I looked 45 seconds later. The log’s times were in Danish time, so TZ works.

The volumes filled the way they should:

text
/srv/jellyfin/config:
config  data  log  metadata  plugins  root

/srv/jellyfin/cache:
CACHEDIR.TAG  fontconfig  transcodes

data is where the database is. The metadata in that list is the artwork volume mounted there, and it stays empty until the first scan.

If the log is full of “permission denied”, the chown in the container is the place to look.

Alternative: the Debian repository

Without Docker, Jellyfin installs from its own package repository on Debian and Ubuntu [10]. It then runs as a service user called jellyfin, and its files are in /var/lib/jellyfin and /etc/jellyfin. It is one layer less. I prefer the compose file for the reasons at the top, but nothing later in the series depends on Docker except where I say so.

The setup wizard

Open http://<the container's address>:8096 in a browser. The wizard has five steps and a closing screen [4]:

  1. Language of the interface.
  2. The administrator account. Pick a name that is not admin, and a real password. This account can delete libraries and users.
  3. Media libraries. Skip this step. I add the libraries afterwards, one at a time, so I can watch each scan.
  4. Metadata language and country. This decides which language the descriptions and titles are fetched in.
  5. Remote access. Leave Allow remote connections to this server ticked, as the documentation recommends [4]. It does not open anything to the internet: nothing is reachable from outside until you open a port in your router, and I will not. I reach my home network through Tailscale when I’m out, and part 4 sets Jellyfin up for that. Unticking it blocks every connection that doesn’t come from the local network.
The wizard’s library step, Set up your media libraries, with only the Add Media Library tile
The wizard’s Configure Remote Access step, with Allow remote connections to this server ticked

I chose English and Denmark. We speak English at home, and a guide in English is easier to follow for most readers too. The country is where we live.

After the wizard

The wizard ends on an empty home screen, which is right: there are no libraries yet.

Jellyfin’s home screen after the wizard: Nothing here. Seems like you haven’t created any libraries yet.

A few things before the first library:

  • Dashboard → Playback → Transcoding: leave hardware acceleration at None until part 3. Further down the same page, check that the transcode path points into /cache. With this compose file it is /cache/transcodes without any change.
  • A second user for daily use, without administrator rights. Log in as that one on the TV and the phones.
  • Trickplay is the small preview pictures you see when you drag the progress bar. Making them means decoding every film once, and without the graphics chip that is hours of processor time. It is switched on per library, in the dialog for a film or video library, and in 12.4 it is off by default, as are the chapter images. I switched both on anyway, but only while the library is a small test set of a few films, to see what the processor alone makes of it. Before the rest of the films go in, I switch them off again, and they stay off until the graphics chip takes over in part 3. Part 3 has the numbers.
Dashboard, Playback, Transcoding in Jellyfin 12.4: hardware acceleration None, HEVC and AV1 encoding unticked, tone mapping BT.2390

The settings for how the pictures are made are under Dashboard → Playback → Trickplay. Out of the box they don’t use the graphics chip, they run at below-normal priority, and they take one picture every ten seconds, 320 pixels wide. They are made by a scheduled task at night, and a scan doesn’t wait for them.

Dashboard, Playback, Trickplay: hardware decoding off, scan behaviour non-blocking, process priority Below Normal, image interval 10000 ms, width 320

The first library: music

I load my library in batches: the music first, then the films, then the shows in two or three rounds. Each batch gets scanned and backed up before the next one goes in. If something is wrong with the setup, I find out with 45 GB and not with all of it, and my backup server gets one batch at a time over a slow link.

The media volumes are read-only inside the container, so the files go in from the host, straight into the music volume’s dataset. From the computer that has them:

sh
rsync -ah --info=progress2 ~/jellyfin_tmp_media/music/ \
  root@phoebe:/paula/data/vmdatahdd/subvol-1002-disk-0/

Then in Jellyfin: Dashboard → Libraries → Add Media Library, content type Music, folder /media/music. The scan starts by itself.

Jellyfin 12.4’s Add Media Library dialog: content type Music, display name Music, the folder /media/music, Enable the library ticked, preferred download language English

The scan hardly touches the processor. On mine it took 54 minutes. For the first 25 minutes the container used under 10 % of its four cores, most of the time around 4 %. The rest was waiting for MusicBrainz, the online music database, which answered “busy” to many of the artist lookups:

Container 1002’s CPU usage in Proxmox over an hour on 6 November: near zero, then 4 to 10 % from about 10:17 to 10:41 while the music library was scanned, then under 1 %

The library can be used while the scan is still running. Albums turn up first, and the covers and artist pictures come in as Jellyfin finds them:

Jellyfin’s home screen during the first music scan: the Music library and a Playlists library under My Media, and Recently Added in Music with some album covers in and some still showing the grey placeholder

When the scan is done, the dashboard’s Paths panel is the quickest way to see that the volumes work as planned. Each folder Jellyfin uses is listed with the filesystem it is on. The artwork is on its own 16 GiB volume, the database and the logs on the 8 GiB config volume, and the transcodes go to the cache:

The Paths panel on Jellyfin’s dashboard after the music scan: cache, image cache and transcodes on /cache (4.8 MiB of 60 GiB), programme data and logs on /config (39.1 MiB of 8 GiB), metadata on /config/metadata (160.6 MiB of 16 GiB), and the web files on the container’s root disk

The films go in /media/video/movies and the shows in /media/video/shows, each as its own library. The two folders are made on the host, in the video volume, before the first copy:

sh
mkdir /paula/data/vmdatahdd/subvol-1002-disk-1/movies \
      /paula/data/vmdatahdd/subvol-1002-disk-1/shows

How to name the folders so Jellyfin recognises them is part 2, and it is worth reading before you copy them in.

Home videos

One part of the library is different from the rest: our own home videos, about 25 GB of them. A film can be bought or ripped again. These can’t be made again, and they are private.

Today they exist in one place only, on the big pool in my backup server. The copy that goes into Jellyfin is therefore more than a convenience: it is the second copy, on another machine and another disk. Two copies is the minimum for anything that can’t be replaced, as I wrote in 3-2-1 in practice.

They go in a folder of their own in the video volume, and get a library of their own:

sh
mkdir /paula/data/vmdatahdd/subvol-1002-disk-1/home-videos
rsync -ah --chmod=a+rX --info=progress2 /path/to/home-movies/ \
  root@phoebe:/paula/data/vmdatahdd/subvol-1002-disk-1/home-videos/

Mine came straight from the backup server, which is on a PowerLine link: 24.58 GB in a little over two hours.

In Jellyfin the content type is Home Videos and Photos, folder /media/video/home-videos. Then it is a matter of who may see it: under Dashboard → Users, each user has an Access tab, and there I untick Enable access to all libraries for anyone who shouldn’t see them, and leave the home videos unticked. To show how it looks, I did it for the user on our TV:

The Access tab of a Jellyfin user: Enable access to all libraries unticked, Home Videos and Photos unticked, Movies and Music ticked

The --chmod=a+rX in the copy command above is there because of what happened to mine. On the backup server the home videos could only be read by their owner, and rsync -a keeps permissions as they are. Inside the container the files belong to nobody, so Jellyfin couldn’t read them, and the new library stayed empty. The chmod from the permissions section fixed it, followed by a new scan of the library.

The scan then filled the log with errors of another kind: Read-only file system. Jellyfin tried to save a poster and the trickplay pictures next to each video, in the media folder. It can’t, because the media volume is read-only, and it falls back to the metadata volume by itself, which is where I want them anyway. To keep the log quiet, untick the options to save artwork and trickplay pictures in the media folders, in each library’s settings.

Dos and don’ts

  • Do create the volumes and set their record sizes before the first start. A record size set afterwards only applies to new data [2].
  • Do keep the database on a local SSD. Not on a network share [2].
  • Do pin the version in the compose file.
  • Do load a big library in batches.
  • Don’t let the cache be backed up, replicated or sent to the backup server. That is three switches, not one.
  • Don’t give Jellyfin write access to the media unless you have a reason.
  • Don’t expect the Proxmox backup of the container to contain the media: I left it out on purpose, because the ZFS pull already has it.

What’s next

Jellyfin is running and the music is in. Not all of it looks right yet: after the first scan most of my albums had no cover, and one band turned up as three different artists. Part 2 is about folders, file names and metadata, so that the music gets tidied up and the films and shows are recognised the first time.

Sources

  1. Jellyfin: container installation, pulled 2026-10-29
  2. Jellyfin: storage, pulled 2026-10-29
  3. Jellyfin: hardware selection, pulled 2026-10-25
  4. Jellyfin: setup wizard, pulled 2026-10-29
  5. Linux Container (Proxmox VE wiki), pulled 2026-10-29
  6. pct.conf (Proxmox VE 9.2 documentation), pulled 2026-10-29
  7. Unprivileged LXC containers (Proxmox VE wiki), pulled 2026-10-29
  8. Install Docker Engine on Debian (Docker documentation), pulled 2026-10-29
  9. Jellyfin’s Dockerfile (jellyfin-packaging on GitHub), pulled 2026-10-29
  10. Jellyfin: Linux installation, pulled 2026-09-25
  11. Jellyfin (Proxmox VE Helper-Scripts), pulled 2026-09-25
  12. [Guide] Jellyfin + remote network shares + HW transcoding with Intel’s QSV + unprivileged LXC (Proxmox forum), further reading, pulled 2026-09-25
  13. Jellyfin Server 12.4 release notes (GitHub), with the dates of 12.0 to 12.4 from the GitHub releases list, pulled 2026-10-29
  14. jellyfin/jellyfin tags (Docker Hub), pulled 2026-10-29
  15. Proxmox Container Toolkit: mount points, migration and moving volumes (Proxmox VE 9.2 documentation), pulled 2026-10-29
  16. Backup and Restore (Proxmox VE 9.2 documentation), pulled 2026-10-09
  17. Storage Replication (Proxmox VE 9.2 documentation), pulled 2026-10-29
  18. sanoid and syncoid (GitHub, jimsalterjrs), pulled 2026-10-29