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
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)
Podman Quadlet (Rootless)
- Daemon: Daemonless fork/exec via
conmon(UID 1000) - Socket: User-scoped
/run/user/1000/podman/podman.sock - Supervisor: Native OS
systemd --userwith 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.containerThis declarative file is automatically compiled into a systemd service by podman-system-generator.
[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
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)
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
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
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