Podman Migration Playbook dell5040 (Debian 13)
100% Migrated to Podman
Active Stacks
3
Jellyfin, Immich, TeslaMate
Total Containers
9
To be transitioned to rootless
Security Posture
Rootless
Eliminates Docker root daemon
Firewall Isolation
UFW Enforced
Drops PREROUTING bypass

Architecture & Security Rationale

Docker on Linux operates via a centralized root daemon that dynamically alters kernel iptables/nftables PREROUTING chains. This architecture inadvertently causes container ports published to 0.0.0.0 to bypass local host firewalls (such as UFW).

The Rootless Podman Advantage: In rootless mode, Podman runs entirely within the unprivileged user's namespace (mason: UID 1000). Because it possesses zero host root capabilities, it cannot inject PREROUTING NAT rules into the host kernel. All ingress connections pass through the standard host INPUT chain, ensuring complete UFW policy enforcement.
Dimension Current (Docker 27 Rootful) Target (Podman 5.4 Rootless) Operational Impact
Daemon Privilege Root Daemon Daemonless / User Eliminates root daemon privilege escalation attack surface
Host Firewall (UFW) Bypassed Strictly Respected UFW INPUT rules protect exposed ports natively
Compose Provider Docker CLI Compose plugin podman compose Reuses existing compose configs with minor namespace adjustments
Admin GUI Portainer (Root socket bind) Cockpit-Podman (Port 9090) Pre-installed; natively integrated with user systemd units

Phase 0: Host Prerequisites & Critical Permissions

Critical Finding for Jellyfin VA-API Transcoding: Inspection of /dev/dri revealed /dev/dri/renderD128 is owned by group render (GID 992). User mason is currently in group video but not in group render. Rootless Podman cannot pass hardware access without host group membership!
# 1. Grant mason user hardware render permissions
sudo usermod -aG render mason

# 2. Enable Podman API Socket for user mason
systemctl --user enable --now podman.socket

# 3. Verify user lingering is active (already confirmed Linger=yes)
loginctl show-user mason | grep Linger

Phase 1: Stack-by-Stack Migration Playbooks

🎬 Stack 1: Jellyfin Media Server (Hardware VA-API + Host Mounts)

Path: /mnt/fanxiang-2t/jellyfin/docker-compose.yml

Required Configuration Update: Replace numeric group_add: ["44", "992"] with group_add: ["keep-groups"] and specify userns_mode: "keep-id".

# Updated compose snippet for /mnt/fanxiang-2t/jellyfin/docker-compose.yml
services:
  jellyfin:
    image: jellyfin/jellyfin:latest
    container_name: jellyfin
    restart: unless-stopped
    userns_mode: "keep-id"
    ports:
      - "127.0.0.1:8096:8096"
    volumes:
      - /mnt/fanxiang-2t/jellyfin/config:/config:Z
      - /mnt/fanxiang-2t/jellyfin/cache:/cache:Z
      - /mnt/fanxiang-2t/Downloads/Movies:/media/movies:ro
      - /mnt/fanxiang-2t/Downloads/TV Shows:/media/tvshows:ro
      - /mnt/fanxiang-2t/Downloads/Anime:/media/anime:ro
    devices:
      - /dev/dri/renderD128:/dev/dri/renderD128
      - /dev/dri/card0:/dev/dri/card0
    group_add:
      - "keep-groups"
# Execution steps:
cd /mnt/fanxiang-2t/jellyfin
docker compose down
podman compose up -d
podman exec -it jellyfin /usr/lib/jellyfin-ffmpeg/vainfo
⚡ Stack 2: TeslaMate Telemetry (Named Volumes Migration)

Path: /home/mason/repos/server-setup/services/teslamate/docker-compose.yml

Migration Challenge: Data is currently held in 4 Docker root named volumes. They must be duplicated into Podman's user storage.

# 1. Stop Docker TeslaMate
cd /home/mason/repos/server-setup/services/teslamate
docker compose down

# 2. Create Podman user volumes
podman volume create teslamate_teslamate-db
podman volume create teslamate_teslamate-grafana-data
podman volume create teslamate_mosquitto-conf
podman volume create teslamate_mosquitto-data

# 3. Synchronize data from Docker to Podman volume store
sudo rsync -aAX /var/lib/docker/volumes/teslamate_teslamate-db/_data/ ~/.local/share/containers/storage/volumes/teslamate_teslamate-db/_data/
sudo rsync -aAX /var/lib/docker/volumes/teslamate_teslamate-grafana-data/_data/ ~/.local/share/containers/storage/volumes/teslamate_teslamate-grafana-data/_data/
sudo rsync -aAX /var/lib/docker/volumes/teslamate_mosquitto-conf/_data/ ~/.local/share/containers/storage/volumes/teslamate_mosquitto-conf/_data/
sudo rsync -aAX /var/lib/docker/volumes/teslamate_mosquitto-data/_data/ ~/.local/share/containers/storage/volumes/teslamate_mosquitto-data/_data/

# 4. Reassign ownership to mason user
sudo chown -R mason:mason ~/.local/share/containers/storage/volumes/teslamate_*

# 5. Launch under Podman
podman compose up -d
podman logs -f teslamate-database-1
📸 Stack 3: Immich Photos (Postgres VectorChord + SubUID Permissions)

Path: /home/mason/repos/server-setup/services/immich/docker-compose.yml

Migration Challenge: Immich Postgres stores data at /mnt/immich/postgres with container UID 999. In rootless mode, ownership must be mapped using podman unshare.

# 1. Stop Docker Immich stack
cd /home/mason/repos/server-setup/services/immich
docker compose down

# 2. Replicate model-cache volume
podman volume create immich_model-cache
sudo rsync -aAX /var/lib/docker/volumes/immich_model-cache/_data/ ~/.local/share/containers/storage/volumes/immich_model-cache/_data/
sudo chown -R mason:mason ~/.local/share/containers/storage/volumes/immich_model-cache

# 3. Set namespace ownership for Postgres DB directory
podman unshare chown -R 999:999 /mnt/immich/postgres

# 4. Ensure mason owns media directory
sudo chown -R mason:mason /mnt/immich

# 5. Launch under Podman
podman compose up -d
podman logs -f immich_server

Phase 2: Autostart via Systemd User Units

Because lingering is enabled for user mason, systemd user units run at system boot even when no interactive session is active.

# Create template unit: ~/.config/systemd/user/podman-compose@.service
[Unit]
Description=Podman Compose Stack %i
After=network-online.target
Wants=network-online.target

[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=%h/repos/server-setup/services/%i
ExecStart=/usr/bin/podman compose up -d
ExecStop=/usr/bin/podman compose down
TimeoutStartSec=15m

[Install]
WantedBy=default.target
# Enable automated boot for compose stacks:
systemctl --user daemon-reload
systemctl --user enable --now podman-compose@teslamate
systemctl --user enable --now podman-compose@immich

Phase 3: Docker Decommissioning & Verification

# 1. Stop and disable Docker daemon
sudo systemctl stop docker.service docker.socket
sudo systemctl disable docker.service docker.socket

# 2. Mask Docker service to prevent accidental restarts
sudo systemctl mask docker.service docker.socket

# 3. Verify Cockpit-Podman GUI
# Navigate to https://dell5040:9090 (Portainer can be safely deleted)

# 4. Verify host firewall (UFW)
sudo ufw status verbose
sudo nft list ruleset | grep -i "docker"

Rollback Strategy

Because the migration does not delete or overwrite the original Docker volumes under /var/lib/docker/volumes/, rolling back to Docker is instantaneous at any point:

# Emergency Rollback Sequence
podman compose down  # in each stack directory
sudo systemctl unmask docker.service docker.socket
sudo systemctl start docker.service
docker compose up -d # in each stack directory