Jellyfin
Deployment rootless
1. Prepare the host
Prerequisites once as root
Install Podman (needs podman 4.3 or newer; Ubuntu 22.04 ships 3.4 and is too old, use 24.04)
apt update && apt install -y podman
Create user (if not existing)
useradd -m -s /bin/bash jellyfin passwd jellyfin
Enable linger (services keep running after logout and start after reboot)
loginctl enable-linger jellyfin
Switch to deployment user
su - jellyfin
Use su - with the dash — this creates a proper login shell as jellyfin. Without the dash, root environment variables carry over and rootless Podman will not work correctly.
Fix environment for rootless systemd (new users only — skip if already set)
Required once so systemctl --user works in non-login sessions.
cat << 'EOF' >> ~/.bashrc # Fix for systemd user services (rootless Podman/systemctl) export XDG_RUNTIME_DIR=/run/user/$(id -u) export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus EOF source ~/.bashrc
Create the host directories
The containers write to these host paths as their own user, so they need the right owner, whatever format you use. Quadlet does not even create a missing one.
mkdir -p /media # podman unshare chown UID:GID /media # owner unknown: the user the container writes as
2. Choose a format and deploy
The same pod in four forms. Pick one and follow only its steps.
Each tab says which podman it needs. Check yours with podman --version:
Debian 12 ships 4.3, Ubuntu 24.04 4.9, Debian 13 5.4, Fedora 5.8, Arch 6.x.
Kubernetes YAML for podman kube play, started by systemd through a Quadlet .kube unit (podman 4.4+; older podman like Debian 12 uses podman-kube@, see the version check below).
Kubernetes YAML
apiVersion: v1
kind: Pod
metadata:
name: jellyfin
labels:
app: jellyfin
spec:
restartPolicy: Always
containers:
- name: jellyfin
image: docker.io/jellyfin/jellyfin:latest
imagePullPolicy: Always
ports:
- containerPort: 8096
hostPort: 8096
env:
- name: JELLYFIN_PublishedServerUrl
value: http://localhost:8096
volumeMounts:
- name: vol-0
mountPath: /config
- name: vol-1
mountPath: /cache
- name: vol-2
mountPath: /media
readOnly: true
volumes:
- name: vol-0
persistentVolumeClaim:
claimName: jellyfin-config
- name: vol-1
persistentVolumeClaim:
claimName: jellyfin-cache
- name: vol-2
hostPath:
path: /media
type: DirectoryOrCreate
Save the YAML file
mkdir -p ~/.config/containers/ nano ~/.config/containers/jellyfin.yaml
Test the pod (without autostart)
podman kube play ~/.config/containers/jellyfin.yaml # Check status: podman pod ps && podman ps # Stop: podman kube down ~/.config/containers/jellyfin.yaml
Check your podman version
podman --version
4.4 or newer: continue with step 3 below.
Older, e.g. 4.3 on Debian 12? There is no Quadlet yet: use podman's own podman-kube@ service instead of steps 3–5
systemctl --user enable --now podman-kube@$(systemd-escape ~/.config/containers/jellyfin.yaml).service # Status & logs: systemctl --user status podman-kube@$(systemd-escape ~/.config/containers/jellyfin.yaml).service journalctl --user -u podman-kube@$(systemd-escape ~/.config/containers/jellyfin.yaml).service -f
The day-to-day commands below have a block for this service.
Create Quadlet .kube file
[Unit] Description=jellyfin pod After=network-online.target Wants=network-online.target [Kube] Yaml=%h/.config/containers/jellyfin.yaml [Service] Restart=always [Install] WantedBy=default.target
%h is a systemd home-directory specifier — do not replace it with ~/.
Save to ~/.config/containers/systemd/:
mkdir -p ~/.config/containers/systemd/ nano ~/.config/containers/systemd/jellyfin.kube
Start systemd service
systemctl --user daemon-reload systemctl --user start jellyfin.service
Status & Logs
systemctl --user status jellyfin.service journalctl --user -u jellyfin.service -f podman pod ps podman ps
Enable automatic image updates optional
Requires AutoUpdate=registry in the .kube file. Podman then checks for new images and restarts the pod automatically:
systemctl --user enable --now podman.socket systemctl --user daemon-reload systemctl --user enable --now podman-auto-update.timer # Check status: systemctl --user status podman-auto-update.timer # Trigger manually: podman auto-update
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f jellyfin-jellyfin podman exec -it jellyfin-jellyfin /bin/sh
Restart
systemctl --user restart jellyfin.service
Update an image by hand
Pull the new version, the restart recreates the containers from it:
podman pull <image>:<tag> systemctl --user restart jellyfin.service
Stop & remove
Stop through systemd, otherwise Restart=always brings the pod back. To remove it for good, delete the unit files and reload:
systemctl --user stop jellyfin.service rm ~/.config/containers/systemd/jellyfin.kube systemctl --user daemon-reload
podman older than 4.4 (podman-kube@ from step 3)
systemctl --user restart podman-kube@$(systemd-escape ~/.config/containers/jellyfin.yaml).service # Update an image: pull, then restart as above podman pull <image>:<tag> # Stop and remove from autostart: systemctl --user disable --now podman-kube@$(systemd-escape ~/.config/containers/jellyfin.yaml).service
Delete the data — permanent, only after stop & remove
podman volume rm jellyfin-config podman volume rm jellyfin-cache podman unshare rm -rf /media
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Native systemd units without the Kubernetes YAML: one .pod plus one .container per container.
Each container becomes its own systemd service (restart, logs and ordering per container).
Needs podman 5.0 or newer — Debian 12 ships 4.3, use the Kube YAML tab there.
Save the unit files
[Unit] Description=jellyfin pod [Pod] PodName=jellyfin PublishPort=8096:8096
[Unit] Description=jellyfin-jellyfin container [Container] Image=docker.io/jellyfin/jellyfin:latest Pod=jellyfin.pod ContainerName=jellyfin-jellyfin Pull=always Environment=JELLYFIN_PublishedServerUrl=http://localhost:8096 Volume=jellyfin-config:/config:Z Volume=jellyfin-cache:/cache:Z Volume=/media:/media:ro [Service] Restart=always [Install] WantedBy=default.target
Save all of them to ~/.config/containers/systemd/:
mkdir -p ~/.config/containers/systemd/ nano ~/.config/containers/systemd/jellyfin.pod nano ~/.config/containers/systemd/jellyfin-jellyfin.container
Start the pod
The pod service pulls in every container service. Autostart at boot comes from the [Install] section of the units, no enable needed.
systemctl --user daemon-reload systemctl --user start jellyfin-pod.service
Status & Logs
systemctl --user status jellyfin-pod.service journalctl --user -u jellyfin-jellyfin.service -f podman pod ps && podman ps
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f jellyfin-jellyfin podman exec -it jellyfin-jellyfin /bin/sh
Restart
systemctl --user restart jellyfin-pod.service
Update an image by hand
Pull the new version, the restart recreates the containers from it:
podman pull <image>:<tag> systemctl --user restart jellyfin-pod.service
Stop & remove
Stop through systemd, otherwise Restart=always brings the pod back. To remove it for good, delete the unit files and reload:
systemctl --user stop jellyfin-pod.service rm ~/.config/containers/systemd/jellyfin.pod rm ~/.config/containers/systemd/jellyfin-jellyfin.container systemctl --user daemon-reload
Delete the data — permanent, only after stop & remove
podman volume rm jellyfin-config podman volume rm jellyfin-cache podman unshare rm -rf /media
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
A docker-compose.yml for podman-compose. Handy if you already work with Compose; there are no per-container systemd services.
Install podman-compose as root
Run this as root, e.g. together with the prerequisites before su - jellyfin.
apt install -y podman-compose
Save the compose file
services:
jellyfin:
image: docker.io/jellyfin/jellyfin:latest
pull_policy: always
restart: always
ports:
- 8096:8096
environment:
- JELLYFIN_PublishedServerUrl=http://localhost:8096
volumes:
- jellyfin-config:/config:Z
- jellyfin-cache:/cache:Z
- /media:/media:ro
volumes:
jellyfin-config: null
jellyfin-cache: null
mkdir -p ~/jellyfin nano ~/jellyfin/docker-compose.yml
Start, status & logs
cd ~/jellyfin podman-compose up -d podman-compose ps podman-compose logs -f # Stop: podman-compose down
Start after reboot
podman-restart.service starts every container with restart: always at boot:
systemctl --user enable podman-restart.service
Update images
cd ~/jellyfin podman-compose pull podman-compose up -d
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f jellyfin_jellyfin_1 podman exec -it jellyfin_jellyfin_1 /bin/sh
Restart
cd ~/jellyfin podman-compose restart
Stop & remove
cd ~/jellyfin podman-compose down
Delete the data — permanent, only after stop & remove
podman volume rm jellyfin_jellyfin-config podman volume rm jellyfin_jellyfin-cache podman unshare rm -rf /media
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Plain podman pod create / podman create commands. Good for a quick test or to see what happens under the hood.
Save the script
#!/bin/bash set -e # ── Create named volumes ─────────────────────────────────────────── # Named volumes are created automatically — no UID/GID permission adjustment needed. podman volume exists jellyfin-config || podman volume create jellyfin-config podman volume exists jellyfin-cache || podman volume create jellyfin-cache # ── Create host paths ────────────────────────────────────────────── # The container process needs write access. Existing data may need chown -R. mkdir -p /media # podman unshare chown UID:GID /media # owner unknown: the user the container writes as # ── Create pod ───────────────────────────────────────────────────── podman pod create \ --name jellyfin \ -p 8096:8096 # ── Container: jellyfin-jellyfin ───────────────────────────────────────────── podman create \ --pod jellyfin \ --name jellyfin-jellyfin \ --restart always \ --pull always \ -e JELLYFIN_PublishedServerUrl='http://localhost:8096' \ -v jellyfin-config:/config:Z \ -v jellyfin-cache:/cache:Z \ -v /media:/media:ro \ docker.io/jellyfin/jellyfin:latest # ── Start pod ────────────────────────────────────────────────────── podman pod start jellyfin # ── Stop & clean up ─────────────────────────────────────────────── # podman pod stop jellyfin # podman pod rm jellyfin
nano jellyfin.sh
Run it, status & logs
bash jellyfin.sh podman pod ps && podman ps podman logs -f <container-name>
Start after reboot
podman-restart.service starts every container with --restart always at boot:
systemctl --user enable podman-restart.service
Remove or rebuild
The script creates the pod once. To apply changes or new images, remove the pod and run the script again (named volumes stay):
podman pod rm -f jellyfin bash jellyfin.sh
Day-to-day logs, restart, update, remove (click to open)
Logs & shell into a container
podman logs -f jellyfin-jellyfin podman exec -it jellyfin-jellyfin /bin/sh
Restart
podman pod stop jellyfin podman pod start jellyfin
Stop & remove
podman pod rm -f jellyfin
Delete the data — permanent, only after stop & remove
podman volume rm jellyfin-config podman volume rm jellyfin-cache podman unshare rm -rf /media
Files created by containers belong to root in the user namespace — podman unshare is required to delete them.
Ports < 1024 (e.g. 80, 443)
Rootless cannot open privileged ports. Solution:
sysctl net.ipv4.ip_unprivileged_port_start=80
Make persistent in /etc/sysctl.d/99-podman.conf.
Containers communicate via localhost
All containers in the pod share the same network namespace. Always use localhost, not container names.
# Correct (e.g. app → db): localhost:5432 # Wrong (doesn't work in a pod): db-container:5432
List open ports
Which ports are the running containers publishing? See the PORTS column:
podman ps
Custom DNS for the pod
Set a custom DNS server (e.g. local Pi-hole) in the generator under DNS server. Every format picks it up.
Set volume ownership
Fix permission errors by adjusting UID/GID in the user namespace:
podman unshare chown 1000:1000 /path/to/volume
SELinux volume labels
On SELinux systems (RHEL, Fedora) set the volume suffix:
/host/path:/container/path:Z # private /host/path:/container/path:z # shared
List all volumes
podman volume ls podman volume inspect <volume-name>
Volume backup
Back up data from a named volume:
podman run --rm \ -v <volume-name>:/data:ro \ -v $(pwd):/backup \ busybox tar czf /backup/backup.tar.gz /data
Cleanup
Remove unused images, containers and volumes:
podman system prune -f # containers + images podman image prune -f # untagged images only podman volume prune -f # unused volumes
Check for outdated images
List local images; auto-update only covers units with AutoUpdate= (Kube and Quadlet tabs):
podman images podman auto-update --dry-run
All containers, including stopped ones
podman pod ps podman ps -a
Resource usage
podman stats --no-stream