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).
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
/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