🛡️

Rootless Security

User namespace (UID 1000) isolation removes host root attack surface.

⚙️

Native systemd

Quadlet generator converts declarative .container into user services.

Hardware QSV

Direct /dev/dri pass-through with 0% transcoding performance penalty.

🔄

Auto-Update & Rollback

Native AutoUpdate=registry with healthcheck automated rollback.

Architecture Comparison

Current State

Docker Compose

  • Daemon: Monolithic dockerd (Root UID 0)
  • Socket: /var/run/docker.sock (Root escalation risk)
  • Supervisor: Docker internal container manager
  • Memory: 50–120+ MB idle daemon RAM usage
  • Updates: External 3rd party tool (e.g., Watchtower)
Target State

Podman Quadlet (Rootless)

  • Daemon: Daemonless fork/exec via conmon (UID 1000)
  • Socket: User-scoped /run/user/1000/podman/podman.sock
  • Supervisor: Native OS systemd --user with cgroups v2
  • Memory: 0 MB idle memory; ~1–4 MB per container
  • Updates: Native AutoUpdate=registry + timer

Host & Storage Specifications

Component Host Parameter Configuration Mapping
Host OS Debian 13 (Trixie) x86_64 Linux Kernel with cgroups v2 & systemd user linger
GPU Hardware Intel HD Graphics 530 (Skylake) /dev/dri/renderD128 (render:992) & /dev/dri/card0 (video:44)
Reverse Proxy Nginx (SSL / HTTP/2) proxy_pass http://localhost:8096; at jellyfin.mwan.dev
Data Volumes /mnt/fanxiang-2t/jellyfin /config (rw), /cache (rw) owned by mason:mason (1000:1000)
Media Storage /mnt/fanxiang-2t/Downloads /media/movies, /media/tvshows, /media/anime (ro)

In-Depth Pros & Cons Matrix

Category Docker (Current) Rootless Podman Quadlet (Target) Impact Verdict
Security Model Daemon runs as host root (`UID 0`); access to `docker.sock` grants root escalation. Runs in unprivileged user namespace (`CLONE_NEWUSER`); container breakouts remain UID 1000. Major Win
System Supervision Docker supervisor separate from host init; requires Docker daemon active. Native systemd --user units; standard journalctl logs, automatic boot startup. Major Win
Automated Updates Requires external daemon container (Watchtower) polling root socket. Native AutoUpdate=registry with healthcheck-driven automatic rollback. Major Win
System Footprint 50–120+ MB resident RAM overhead continuously for Docker daemon. 0 MB idle overhead; lightweight `conmon` monitor (~1–4 MB) per container. Win
File Permissions Requires PUID/PGID environment variables or host chown workarounds. UserNS=keep-id aligns host UID 1000 directly with container UID 1000. Win
Hardware Transcoding Direct root pass-through of DRM devices. Requires host user in render group and GroupAdd=keep-groups. 1-Time Setup
DLNA / Discovery Standard bridge or host networking. Rootless `pasta` drops multicast unless Network=host is specified. Consideration
Web Dashboard Portainer (built for Docker). Cockpit-Podman (native rootless & systemd UI) or standard CLI. Tooling Shift

✨ Key Migration Advantages

  • Eliminates Root Escalation Risk: Since Jellyfin is exposed via reverse proxy, rootless confinement ensures zero privilege elevation on the host even in the event of an RCE exploit.
  • Unified Linux Service Management: Treat Jellyfin like any native Linux service (`systemctl --user {status|restart|stop} jellyfin`).
  • Zero Maintenance Updates: Automated daily checks via `podman-auto-update.timer` with auto-revert if startup healthchecks fail.
  • True 1000:1000 Storage Alignment: Direct ownership mapping on `/mnt/fanxiang-2t/` without messy permission resets.

⚠️ Technical Trade-offs & Nuances

  • GPU Group Mapping: Host user `mason` must be added to host `render` group (GID 992) to enable direct access to `/dev/dri/renderD128`.
  • DLNA Multicast Isolation: If UPnP/DLNA streaming to legacy Smart TVs is required, `Network=host` must be configured (Nginx reverse proxy over port 8096 works standardly).
  • SELinux / AppArmor Volume Flags: Avoid `:Z` on shared multi-terabyte media libraries to prevent slow startup relabeling; mount media as `:ro`.

Quadlet Container Unit Specification

~/.config/containers/systemd/jellyfin.container

This declarative file is automatically compiled into a systemd service by podman-system-generator.

jellyfin.container
[Unit]
Description=Jellyfin Media Server (Podman Quadlet)
After=network-online.target local-fs.target
Wants=network-online.target

[Container]
ContainerName=jellyfin
Image=docker.io/jellyfin/jellyfin:latest
AutoUpdate=registry

# User & Group mapping (maps host user 1000 to container user 1000)
UserNS=keep-id:uid=1000,gid=1000
GroupAdd=keep-groups

# Port Mapping (or use Network=host if DLNA multicast is required)
PublishPort=8096:8096

# Storage Mounts
Volume=/mnt/fanxiang-2t/jellyfin/config:/config:rw
Volume=/mnt/fanxiang-2t/jellyfin/cache:/cache:rw
Volume=/mnt/fanxiang-2t/Downloads/Movies:/media/movies:ro
Volume=/mnt/fanxiang-2t/Downloads/TV Shows:/media/tvshows:ro
Volume=/mnt/fanxiang-2t/Downloads/Anime:/media/anime:ro

# Hardware Acceleration (Intel VA-API / QSV)
AddDevice=/dev/dri/renderD128:/dev/dri/renderD128:rwm
AddDevice=/dev/dri/card0:/dev/dri/card0:rwm

# Environment Variables
Environment=JELLYFIN_PublishedServerUrl=https://jellyfin.mwan.dev

# Healthcheck & Native Rollback Integration
HealthCmd=curl --noproxy 'localhost' -Lk -fsS "http://localhost:8096/health" || exit 1
HealthInterval=30s
HealthTimeout=30s
HealthRetries=3
HealthStartPeriod=15s
Notify=healthy

[Service]
Type=notify
Restart=always
TimeoutStartSec=180

[Install]
WantedBy=default.target

Step-by-Step Execution Runbook

1

Phase 1: Permissions & Database Backup

Ensure host user belongs to the GPU render group and take a safe snapshot of the SQLite database.

# 1. Add mason to the render group for /dev/dri/renderD128 access
sudo usermod -aG render mason
newgrp render

# 2. Verify GPU write access
test -w /dev/dri/renderD128 && echo "GPU OK" || echo "GPU Access Failed"

# 3. Create database backup
cp /mnt/fanxiang-2t/jellyfin/config/data/jellyfin.db /mnt/fanxiang-2t/jellyfin/config/data/jellyfin.db.bak_$(date +%F)
2

Phase 2: Stop and Disable Existing Docker Container

Gracefully stop the Docker container and verify port 8096 is liberated.

cd /mnt/fanxiang-2t/jellyfin
docker compose down

# Confirm port 8096 is free
ss -tulpn | rg 8096
3

Phase 3: Deploy Quadlet Unit File

Track the Quadlet unit inside the server-setup repository and symlink to systemd directory.

# Create repo service directory
mkdir -p /home/mason/repos/server-setup/services/jellyfin
mkdir -p ~/.config/containers/systemd/

# Symlink unit file
ln -sf /home/mason/repos/server-setup/services/jellyfin/jellyfin.container ~/.config/containers/systemd/jellyfin.container

# Reload systemd generator to compile unit
systemctl --user daemon-reload
4

Phase 4: Start and Enable Services

Start the Jellyfin user service and activate automated daily registry updates.

# Enable and start Jellyfin service
systemctl --user enable --now jellyfin.service

# Enable daily auto-update timer
systemctl --user enable --now podman-auto-update.timer

Verification Checklist

Emergency Rollback Procedure

If any unresolvable issue occurs during migration, execute this quick recovery:

# 1. Stop and disable Podman service
systemctl --user stop jellyfin.service
rm -f ~/.config/containers/systemd/jellyfin.container
systemctl --user daemon-reload

# 2. Restart Docker Compose stack
cd /mnt/fanxiang-2t/jellyfin
docker compose up -d

# 3. Verify Docker container
docker ps | rg jellyfin