Skip to main content

podman

Overview: StandaloneAll container30 workloadscontainerized services (40 containers total) on ul run exclusively under rootless Podman (podman 6.x1.4) managed as native systemd --user user services via Podman Quadlets under the dedicated unprivileged account docker-data (UID 1100, GID 1100). ThisRootful eliminatesDocker (docker.service, docker.socket, and containerd.service) is completely disabled and decommissioned, eliminating the privileged root container daemon attack surface while preserving the per-service ULA IPv6 reverse-proxy architectureisolation on eth77.


1. Architecture & Security Comparison: Rootless Podman vs. Rootful Docker

Under traditional Docker, dockerd runsand containerd execute permanently as root (UID 0) inside the host init user namespace (user:[4026531837]). Every container spawned by dockerd without user-namespace remapping shares thekernel host kernel's root UID 0 whenever the container process runs as root, mutates host iptables/nftables chains directly as root, and anyrequires vulnerabilityroot insocket theaccess daemon(/var/run/docker.sock) for orchestration or runtimemaintenance starts with full host privileges.timers.

Under rootless Podman,Podman on ul, there is no central root container daemon anywhere on the host.:

Instead,
  • systemd managesstarts aan per-unprivileged user service manager (user@1100.service) via lingering) for docker-data,.
whichEach Quadlet .container unit directly spawns lightweight conmon and crun processes entirely inside an unprivileged Linux user namespace (user:[4026534031]). Shared stateful databases (PostgreSQL on 127.0.0.1:5432 and Valkey on 127.0.0.1:6379) run natively on the host locked strictly to loopback, with zero LAN or Docker bridge exposure.

1.1 User Namespace UID/GID Translation Model

Rootless Podman mapstranslates container UIDs and GIDs into unprivileged host IDs using /etc/subuid and /etc/subgid (docker-data:624288:65536). Under the default rootless mapping (UserNS=host within the rootless user namespace),mapping, any container UID $u \in [0, 65536]$ translatesmaps deterministically to a host kernel UID according to:UID:

$$ \text{UID}_{\text{host}}(u) = \begin{cases} 1100 & \text{if } u = 0 \quad (\text{container } \texttt{root} \longrightarrow \text{host } \texttt{docker-data}) \cr 624287 + u & \text{if } 1 \le u \le 65536 \quad (\text{subordinate range } 624288\text{--}689823) \end{cases} $$

Common live worker translations visible in ps / podman-map-tree on ul:

    Container root (u = 0) $\longrightarrow$ Host UID 1100 (docker-data) with zero host capabilities. Container www-data (u = 33, e.g., nextcloud, yourls, webrcade) $\longrightarrow$ Host UID 624320. Container nginx (u = 101, e.g., audiomass, bentopdf, excalidraw, penpot-frontend, tldraw, wick) $\longrightarrow$ Host UID 624388. Container postgres (u = 999, e.g., penpot-postgres) $\longrightarrow$ Host UID 625286. Container authentik / degoog (u = 1000) $\longrightarrow$ Host UID 625287. Container penpot (u = 1001) $\longrightarrow$ Host UID 625288.

    When a servicebinary explicitly sets UserNS=keep-id with User=1100:1100 (such as drawpile, whose binary refuses to start if internal getuid() == 0 (specifically drawpile-srv), thesetting identityUserNS=keep-id mappingwith User=1100:1100 preserves UID 1100 across the namespace boundary:

    $$ \text{UID}_{\text{keep-id}}(1100) \stackrel{\texttt{userns}}{\longleftrightarrow} 1100 \quad (\texttt{docker-data}) $$

    Why Default Rootless UserNS Beats UserNS=keep-id for Large ROCm Images: Avoid setting UserNS=keep-id unless an application explicitly aborts when getuid() == 0. On multi-gigabyte images such as immich-machine-learning:v3.3.1-rocm (23.3 GB) or subocr, UserNS=keep-id triggers a full recursive ownership copy-up across thousands of Python/ROCm files that stalls startup for over 5 minutes. Under the default rootless user namespace, container UID 0 already maps directly to host docker-data (1100:1100) with zero layer copy-up and sub-2-second startup.

    1.2 Side-by-Side Comparison

    Dimension Legacy Rootful Docker (dockerd) Current Rootless Podman (docker-data Quadlets)
    Daemon Model Persistent root daemondaemons (dockerd + containerd) running as UID 0 Daemonless fork/exec (systemduser@1100 $\longrightarrow$ conmon $\longrightarrow$ crun) running as UID 1100
    Host Kernel UID of Container root Host UID 0 (root in init user:[4026531837]) Host UID 1100 (docker-data in isolated user:[4026534031])
    Host Kernel UID of Container Worker (u > 0) Same numeric host UID on(33 collides with host (e.g.http, 101999 orcollides 33)with system accounts) SubordinateIsolated subordinate UID $624287 + u$ (e.g.624288--689823, nginxzero $101host \longrightarrowUID 624388$, www-data $33 \longrightarrow 624320$)collisions)
    Service Definition & Lifecycle /data/docker/<svc>/docker-compose.yml managed by dockerd restart policies Declarative <svc>.container + <svc>.network Quadlets in /data/docker/<svc>/<svc>.container Quadlet compiled into native systemd --user unit <svc>.service
    ULAOffline IPv6Boot Port Forwarding (eth77)Resilience docker-proxyCached runninglocally asunless UIDpull 0policy + root iptables/nftables NAToverridden rootlessport +Explicit netavarkPull=never (Network=bridge) binding unprivileged sockets on all 40 [fd77:...]:PORTcontainer asunits; UID100% 1100WAN-outage-safe boot
    Host Firewall Mutation Mutates host iptables/nftables chains directly as root Zero host firewall mutation; binds unprivileged user sockets bind directly to pre-assigned eth77 ULA addressesIPs
    Privileged Ports (389 / 636 LDAP/LDAPS) Direct root port bind in container + mounted /etc/nginx/certs Rootless authentik-ldap on :3389/:6636 fronted by host HAProxy TCP passthrough (/etc/haproxy/conf.d/ldap.cfg); zero TLS key mounts in containers Shared Host PostgreSQL & Valkey Exposed on Docker bridges (172.16.0.0/12) + LAN (10.0.0.0/24) Locked strictly to 127.0.0.1 / ::1; reached from containers via pasta --map-host-loopback 169.254.1.2 ROCm GPU Access (/dev/kfd, /dev/dri) devices: + group_add: ["91", "991"]group_add as root AddDevice= + GroupAdd=keep-groups inheriting docker-data's video (91) and render (991) groups Image & Overlay Storage /var/lib/docker/overlay2 (root:root) /home/docker-data/.local/share/containers/storage (overlay driver owned by docker-data)

    2. Host Configuration & Intra-Service Networking on ul

    2.1 Unprivileged Account, Subordinate IDs & Systemd Lingering

    1. Service Account (docker-data):
      • Primary UID/GID: 1100:1100 (docker-data)
      ,
    2. Home Directory:home /home/docker-data.
    3. Supplementary Groups: video (91) and render (991) for AMD Radeon AI PRO R9700 (gfx1201) ROCm GPU devicecompute access (/dev/dri and /dev/kfd).
    4. Subordinate UID/GID Ranges (/etc/subuid & /etc/subgid):
      docker-data:624288:65536
      
    5. Systemd UserLingering Lingering& Automatic Maintenance: Enabled
      via
    6. Lingering is enabled at /var/lib/systemd/linger/docker-data (sudo loginctl enable-linger docker-data) (creating /var/lib/systemd/linger/docker-data). This ensuresso user@1100.service starts automatically at system boot and keeps all rootless Podman Quadlet services running without requiring an activeinteractive loginlogin.
    session.Weekly image pruning runs rootlessly via /etc/systemd/system/podman-prune.{service,timer} (User=docker-data, Sundays at 04:00 EDT/EST), replacing the retired docker-prune.timer.

    2.2 Global Configuration Files & Storage Layout

    • /etc/containers/registries.containers.conf: Configures unqualifiedpasta image(passt) namesas the default rootless network backend and maps the synthetic guest address 169.254.1.2 (host.containers.internal) cleanly to resolvehost against127.0.0.1 Dockerloopback: Hub
      [network]
      bydefault_rootless_network_cmd default:= "pasta"
      pasta_options = ["--map-host-loopback", "169.254.1.2"]
      
    /etc/containers/registries.conf:
    unqualified-search-registries = ["docker.io"]
    
    /etc/containers/containers.conf:
    [network]
    default_network_backend = "pasta"
    pasta_options = ["-a", "10.0.2.100", "-a", "fd00::100"]
    
    Storage Paths:
    • Persistent image/layer store (GraphRoot): /home/docker-data/.local/share/containers/storage (native kernel overlay filesystemdriver onowned NVMeby /docker-data).
    • Ephemeral runtime state & DNS tables (RunRoot): /run/user/1100/containers (including /run/user/1100/containers/networks/aardvark-dns/).
    • Quadlet unit directory: /home/docker-data/.config/containers/systemd/ (contains symlinks pointing to /data/docker/<service>/*.{container,network}).
    Per-service persistent data & definitions: /data/docker/<service>.container/). on the ZFS storage pool.

    2.3 The Four Intra-Service Networking ArchitecturePatterns on ul: Why Network=bridge Is Used

    EveryAll container servicetraffic on ul bindsfollows itsfour publishedclean, portdeterministic topatterns awithout dedicatedshared mega-bridges like the retired infra_net:

      Ingress (Nginx $\longrightarrow$ Container ULA IPv6 addressSocket): on eth77 (fd77:7777:5010:0:1337:f1ac:d00d:<hex>/64), and hostHost Nginx (UID 33 http) reverse-terminates public HTTPS (*.drheck.dev) and proxies requestsupstream HTTP/WebSocket traffic to http://[fd77:...:<hex>]:<port>.

      each

      All rootless Quadlet definitions on ul specify Network=bridge rather than default pasta networking for two architectural reasons:

        Preventing Local ULA IPv6 Hairpin Collisions: By default, pasta clones the host'service's eth77 interface and copies all 36dedicated /64 ULA IPv6 addressesaddress fromon eth77:

        $$ \text{Client HTTPS} \longrightarrow \text{Host Nginx } (\texttt{UID 33}) \longrightarrow [\texttt{fd77:7777:5010:0:1337:f1ac:d00d:hex}]:P_{\text{host}} \longrightarrow \text{Rootless Container } (\texttt{UID 1100}) $$

        Container $\longrightarrow$ Native Host Daemons (PostgreSQL, Valkey, Ollama) via 169.254.1.2: Native postgresql.service (127.0.0.1:5432), valkey.service (127.0.0.1:6379, per-service ACL users), and ollama.service (127.0.0.1:11434) listen exclusively on host loopback. Inside any rootless container, pasta translates 169.254.1.2 directly to host 127.0.0.1:

        $$ \text{Container App} \longrightarrow \texttt{169.254.1.2}:(5432, 6379, 11434) \stackrel{\texttt{pasta}}{\longrightarrow} \text{Host } \texttt{127.0.0.1}:(5432, 6379, 11434) $$

        Container $\longrightarrow$ Host OIDC / WOPI / LDAPS Hairpin (AddHost=auth.drheck.dev:169.254.1.2): Inside a rootless pasta network namespace, connecting to an IP address owned by the host intoitself (10.0.0.100, 10.0.0.3, or a host fd77:... ULA address) loops back inside the container namespace.namespace When(Connection refused) rather than hairpinning out to host Nginx connectsor fromHAProxy. theEvery sameOIDC, machineWOPI, or LDAPS client container therefore declares:

        AddHost=auth.drheck.dev:169.254.1.2
        

        (plus AddHost=cloud.drheck.dev:169.254.1.2 and AddHost=office.drheck.dev:169.254.1.2 on Nextcloud + Collabora). This preserves full Let's Encrypt TLS SNI and hostname verification while routing cleanly over loopback to host Nginx (src127.0.0.1:443) =and fd77:HAProxy (127.0.0.1:636).

        Multi-Container Private Stack Bridges (<service>..network + aardvark-dns) & Privileged Port Proxy (HAProxy):<hex>

          Multi-container stacks (authentik, dst = fd77:...:<hex>), a pasta-networked container sees the source IP as a local address on its own interface unless isolated. Seamless IPv6-to-IPv4 Protocol Translation (rootlessport): Many container images (tldrawimmich, sopropenpot, demucsnextcloud+collabora, drawpilebookstack, navidromeromm, vaultwardenyourls, wipstudio, webshell+step-ca) bindeach theirhave internala serverdedicated toisolated IPv4 0.0.0.0:<port> onlybridge (not IPv6 [::]:<port>10.89.X.0/24). Withwith Network=bridge,container-name PodmanDNS spawnsserved by /usr/lib/podman/rootlessportaardvark-dns. onDatabase thesidecars (bookstack_db, romm-db, yourls-db, penpot-postgres, penpot-redis) publish zero host sideports. toBecause acceptunprivileged IPv6UID connections1100 does not bind privileged ports < 1024, authentik-ldap listens on [fd77:...:<hex>]aaaa]:<host_port>3389 (LDAP) and proxy them cleanly over IPv4 into the container's netavark:6636 bridge(LDAPS), interfaceand host HAProxy (10.88.0.x:<container_port>/etc/haproxy/conf.d/ldap.cfg): performs

          $$zero-copy \text{NginxLayer-4 }TCP (\texttt{http})passthrough \stackrel{\text{IPv6from TCP}}{\longrightarrow}127.0.0.1, 10.0.0.3, 10.0.0.100, and [\texttt{fd77:...:hex}]aaaa] ports 389/636 to [fd77:...:P_{\text{host}}aaaa]:3389/6636.

          \ (\texttt{rootlessport}) \stackrel{\text{IPv4 TCP}}{\longrightarrow} \texttt{10.88.0.x}:P(\text{container}) $$

          3. Quadlet Service Architecture & Migrated Inventory

          3.1 How Quadlets Work

          Instead of docker-compose.yml, each service directory /data/docker/<service>/ contains a declarative <service>.container file symlinked into /home/docker-data/.config/containers/systemd/<service>.container.

          Whenever sudo systemctl --user -M docker-data@ daemon-reload runs (or on boot), /usr/lib/systemd/user-generators/podman-system-generator compiles each .container file into a transient systemd unit at /run/user/1100/systemd/generator/<service>.service.

          3.2 MigratedComplete Rootless Podman Service Inventory

           (30 Services / 40 Containers)
          Service Systemd UnitUnit(s) (docker-data@) ImageNetwork ULAPublished IPv6Host Binding (eth77)Binding(s) HostBacking $\leftrightarrow$ Container Port
          Host Process UID UserNS Mode VolumesStore / DevicesNotes anchor anchor.service anchor_net [fd77:...:c4a1]:3000 -> 3000 Host PG (169.254.1.2:5432), OIDC audiomass audiomass.service docker.io/azrikahar/audiomass:latestpodman [fd77:7777:5010:0:1337:f1ac:d00d:a0d1 ...:a0d1]:9004 -> 80 Stateless WebAudio editor 1100authentik authentik-server.service
          authentik-worker.service
          authentik-ldap.service authentik_net [fd77:...:aaaa]:9000,9443
          (internal worker)
          [fd77:...:aaaa]:3389,6636 (via HAProxy 624388:389,:636 workers)) DefaultHost rootlessPG Stateless+ Host Valkey (db 0), zero host TLS key mounts bentopdf bentopdf.service ghcr.io/alam00000/bentopdf:latestpodman [fd77:7777:5010:0:1337:f1ac:d00d:bdf ...:bdf]:8080 -> 8080 Stateless PDF toolkit 624388bookstack bookstack.service
          bookstack_db.service bookstack_net [fd77:...:b00c]:6875 -> 80
          (internal MariaDB 3306) MariaDB sidecar + OIDC cam cam.service cam_net [fd77:...:ca30]:8000 -> 8000, 127.0.0.1:8089 -> 8000 /proc/net/arp:ro, YOLOv8n + Reolink Baichuan/RTSP collabora collabora.service nextcloud_net [fd77:...:7a31]:9980 -> 9980 Strict cloud\.drheck\.dev WOPI allowlist degoog degoog.service degoog_net [fd77:...:de90]:4444 -> 4444 Host Valkey (containerdb 4), updated via 101/usr/local/bin/degoog-update demucs demucs.service podman [fd77:...:de3]:8000 -> 8000 ROCm GPU (/dev/dri, /dev/kfd, GroupAdd=keep-groups) Default rootlessdrawpile Statelessdrawpile.service (podman [fd77:.env..:d7a0]:27750-27752) UserNS=keep-id, User=1100:1100, SQLite sessions excalidraw excalidraw.service docker.io/excalidraw/excalidraw:latestpodman [fd77:7777:5010:0:1337:f1ac:d00d:e8ca ...:e8ca]:9003 -> 80 Stateless whiteboard 1100funkwhale funkwhale.service funkwhale_net [fd77:...:f7a0]:8085 -> 80 Embedded PG/Redis + LDAPS (624388auth.drheck.dev:636 workers)via 169.254.1.2) grill Default rootlessgrill.service Statelessgrill_net [fd77:...:fb02]:8000, 10.0.0.3:8000,8088 -> 8000 SQLite + ESP32-C3 HTTP ingest immich immich_server.service
          immich_machine_learning.service immich_net [fd77:...:2283]:2283 -> 2283
          (internal ML worker) Host PG (pgvecto.rs) + Host Valkey (db 1) + ROCm GPU navidrome navidrome.service podman [fd77:...:5a70]:4533 -> 4533 /data/music/flac:ro, SQLite nextcloud nextcloud.service nextcloud_net [fd77:...:7a31]:8081 -> 80 Host PG (nextcloud_db) + Host Valkey (db 2) + OIDC + Collabora penpot penpot-frontend.service
          penpot-backend.service
          penpot-exporter.service
          penpot-postgres.service
          penpot-redis.service penpot_net [fd77:...:be49]:9001 -> 8080
          (4 internal services) ZFS /data/docker/penpot/{postgres_data,assets} + OIDC romm romm.service
          romm-db.service romm_net [fd77:...:7033]:8080 -> 8080
          (internal MariaDB 3306) MariaDB 11 sidecar + /data/games/roms + OIDC scontrol scontrol.service scontrol_net [fd77:...:5c01]:8000 -> 8000 id_scontrol SSH orchestration to studio-kvm-ssh sopro sopro.service podman [fd77:...:509]:7860 -> 7860 Local TTS image (localhost/sopro:latest) step-ca step-ca.service webshell_net [fd77:...:ca]:9000 -> 9000 Smallstep SSH & X.509 CA (used by webshell & ipctl) subocr subocr.service subocr_net [fd77:...:c70]:8000 -> 8000, 127.0.0.1:8077 -> 8000 ROCm MIGraphX GPU + Host Ollama (169.254.1.2:11434) tldraw tldraw.service docker.io/ratneo/tldraw:latestpodman [fd77:7777:5010:0:1337:f1ac:d00d:71d7 ...:71d7]:9002 -> 80 1100Stateless (624388 workers) Default rootless Statelesscanvas wickvaultwarden wick.vaultwarden.service docker.io/library/nginx:alpinepodman [fd77:7777:5010:0:1337:f1ac:d00d:41c8 9005...:a117]:8222 -> 80 1100SQLite + OIDC (624388AddHost=auth.drheck.dev:169.254.1.2 workers)) Default rootlessvikunja /data/docker/wick/html:/usr/share/nginx/html:rovikunja.service vikunja_net [fd77:...:71c0]:3456 -> 3456 Host PG + Host Valkey (db 3) + OIDC; JSON HealthCmd webrcade webrcade.service docker.io/webrcade/webrcade:latestpodman [fd77:7777:5010:0:1337:f1ac:d00d:a1ca ...:a1ca]:8080 -> 80 1100 (624320 workers) Default rootless /data/docker/webrcade/content, /data/games:ro drawpilewebshell drawpile.webshell.service docker.io/drawpile/drawpile-srv:2webshell_net [fd77:7777:5010:0:1337:f1ac:d00d:d7a0...:771d]:7681 -> 7681 Dual-stack ttyd + ephemeral 5m step-ca SSH certs wick wick.service 27750-27752 -> 27750-27752podman 1100[fd77:...:41c8]:9005 (container-> 1100) UserNS=keep-id80 Static HTML5 studio on /data/docker/drawpile/sessions:/data/sessionsnginx:alpine navidromewipstudio navidrome.wipstudio.service
          wipstudio-minio.service docker.io/deluan/navidrome:latestwipstudio_net [fd77:7777:5010:0:1337:f1ac:d00d:5a70...:7190]:3000 -> 3000
          [fd77:...:7190]:9000 -> 9000 4533Host ->PG 4533+ 1100MinIO (containerS3 0)sidecar Default+ rootless /data/docker/navidrome/data:/data, /data/music/flac:/music:ro vaultwarden vaultwarden.service docker.io/vaultwarden/server:latest fd77:7777:5010:0:1337:f1ac:d00d:a117 8222 -> 80 1100 (container 0) Default rootless /data/docker/vaultwarden/vw-data:/dataOIDC (.envDISABLE_SIGN_UP=true) soproyourls sopro.yourls.service
          yourls-db.service localhost/sopro:latestyourls_net [fd77:7777:5010:0:1337:f1ac:d00d:509 7860...:77]:8080 -> 78608080
          (internal MariaDB 3306) 1100MariaDB 10.11 sidecar (container 0YOURLS_PRIVATE=true) Default rootless /data/docker/sopro/cache:/root/.cache/huggingface demucs demucs.service localhost/demucs:latest fd77:7777:5010:0:1337:f1ac:d00d:de3 8000 -> 8000 1100 (container 0) Default + keep-groupsForwardAuth /data/docker/demucs/{app.py,static,cache,media},on /dev/dri, /dev/kfdAPI)

          3.3 Reference Quadlet PatternsExamples

          Pattern A: StandardOIDC Stateful+ Host PostgreSQL/Valkey Service (e.g., /data/docker/vaultwarden/vaultwarden.vikunja/vikunja.container)
          [Unit]
          Description=VaultwardenVikunja Task Manager (Rootless Podman ContainerPodman)
          After=network-online.target
          Wants=network-online.target
          
          [Container]
          Pull=never
          ContainerName=vaultwardenvikunja
          Image=docker.io/vaultwarden/server:vikunja/vikunja:latest
          Network=bridgevikunja.network
          User=0:0AddHost=auth.drheck.dev:169.254.1.2
          PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:a117]71c0]:8222:803456:3456
          EnvironmentFile=/data/docker/vaultwarden/vikunja/.env
          Environment=TZ=America/New_York
          Volume=/data/docker/vaultwarden/vw-data:vikunja/config.yml:/dataetc/vikunja/config.yml:ro
          Volume=/data/docker/vikunja/files:/app/vikunja/files:rw
          HealthCmd=["/app/vikunja/vikunja", "healthcheck"]
          HealthInterval=30s
          HealthRetries=3
          
          [Service]
          Restart=always
          RestartSec=5
          TimeoutStartSec=120
          
          [Install]
          WantedBy=default.target
          

          Because container 0:0 maps directly to host docker-data:docker-data (1100:1100), files written inside /data are owned on the host ZFS dataset by docker-data:docker-data (1100:1100).

          Pattern B: Non-RootMulti-Container EnforcedROCm BinaryGPU with UserNS=keep-idStack (e.g., /data/docker/drawpile/drawpile.immich/immich_machine_learning.container)
          [Unit]
          Description=DrawpileImmich ROCm Machine Learning Worker (Rootless Podman ContainerPodman)
          After=network-online.target
          Wants=network-online.target
          
          [Container]
          Pull=never
          ContainerName=drawpileimmich_machine_learning
          Image=docker.ghcr.io/drawpile/drawpile-srv:2immich-app/immich-machine-learning:v3.3.1-rocm
          Network=bridge
          UserNS=keep-id
          User=1100:1100
          Exec=--port 27750 --websocket-port 27751 --web-admin-port 27752 --web-admin-access all --web-admin-auth admin:drawpile123 --sessions /data/sessions --database /data/sessions/drawpile.db
          Volume=/data/docker/drawpile/sessions:/data/sessions
          PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:d7a0]:27750:27750
          PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:d7a0]:27751:27751
          PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:d7a0]:27752:27752
          
          [Service]
          Restart=always
          
          [Install]
          WantedBy=default.target
          

          drawpile-srv aborts on startup if internal getuid() == 0. Setting UserNS=keep-id and User=1100:1100 makes the process see UID 1100 inside the container while still mapping to UID 1100 (docker-data) on the host filesystem.

          Pattern C: AMD ROCm GPU Passthrough with GroupAdd=keep-groups (e.g., /data/docker/demucs/demucs.container)
          [Unit]
          Description=Demucs ROCm Rootless Podman Container
          After=network-online.target
          Wants=network-online.target
          
          [Container]
          ContainerName=demucs
          Image=localhost/demucs:latest
          Network=bridgeimmich.network
          AddDevice=/dev/dri:/dev/dri
          AddDevice=/dev/kfd:/dev/kfd
          GroupAdd=keep-groups
          PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:de3]:8000:8000
          EnvironmentFile=/data/docker/demucs/immich/.env
          Environment=TZ=America/New_York
          Environment=TORCH_HOME=/root/.cache/torch
          Environment=HF_HOME=/root/.cache/huggingface
          Environment=MIOPEN_USER_DB_PATH=/root/.cache/miopen
          Environment=MIOPEN_CUSTOM_CACHE_DIR=/root/.cache/miopen
          Volume=/data/docker/demucs/app.py:immich/model-cache:/app/app.py:ro
          Volume=/data/docker/demucs/static:/app/static:ro
          Volume=/data/docker/demucs/cache/torch:/root/.cache/torch
          Volume=/data/docker/demucs/cache/huggingface:/root/.cache/huggingface
          Volume=/data/docker/demucs/cache/miopen:/root/.cache/miopen
          Volume=/data/docker/demucs/media:/app/mediacache:rw
          
          [Service]
          Restart=always
          RestartSec=5
          TimeoutStartSec=180
          
          [Install]
          WantedBy=default.target
          

          GroupAdd=keep-groups instructs crun not to call setgroups(0, NULL) when entering the user namespace, preserving docker-data's host membership in video (91) and render (991) so /dev/kfd and /dev/dri/renderD128 are accessible to PyTorch ROCm.


          4. Day-to-Day Operations: Status, Logs & Lifecycle

          Important Execution Context:Context on Arch Linux: Rootless Podman containers belongrun tounder docker-data (UID 1100), not root or luke. RunningTwo bareArch Linux systemd/podman details matter in CLI commands:
          1. Always run sudo podman-u ps inspects root's empty store, not docker-data's store.from Alwaysa useworld-traversable directory such as sudocd /tmp (otherwise podman tries to stat("/home/luke") on mode 0700 and warns about Permission denied).
          2. On systemd 258+, systemctl --user -M docker-data@ ... forworks servicedirectly, management, or setwhereas HOME=/home/journalctl --user -M docker-data@ returns Connecting to a machine as non-root is not supported. Always read user unit journals via sudo -u docker-data XDG_RUNTIME_DIR=/run/user/1100 journalctl --user -u <service>.service from a directory readable by docker-data (such as cd /home/docker-data) when invoking podman directly..

          4.1 Checking ServiceService, Container & ContainerNamespace Status

          # 1. Host-wide health summary across all nginx, native systemd, and rootless Podman services
          $ status_all
          $ services
          
          # 2. Live container-to-host UID, PID, and UserNS translation audit tree
          $ sudo /usr/local/sbin/podman-map-tree
          
          # 3. Check systemd unit status for a specificany Quadlet servicecontainer
          $ sudo systemctl --user -M docker-data@ status vaultwarden.authentik-server.service
          
          # 2. List all active Quadlet container units under docker-data
          $ sudo systemctl --user -M docker-data@ list-units --type=service --state=running
          
          # 3.4. List all running rootless Podman containers, networks, ports, and uptimehealth states
          $ (cd /home/docker-datatmp && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman ps -a)
          
          # 4.5. View liveLive CPU, memory, and I/O resource usagetelemetry across all rootless40 containers
          $ (cd /home/docker-datatmp && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman stats --no-stream)
          # 5. Inspect processes running inside a container (showing container UID vs host UID)
          $ (cd /home/docker-data && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman top vaultwarden user uid huser huid pid comm)
          

          4.2 Viewing Service & Container Logs

          Because Quadlet services integrate directly with systemd (--sdnotify=conmon), container output is available through both journalctl and podman logs:

          # Follow live systemd journaluser unit logs for a service (includes systemd start/stop eventslifecycle + container stdout/stderr)
          $ sudo -u docker-data XDG_RUNTIME_DIR=/run/user/1100 journalctl --user -M docker-data@ -u navidrome.authentik-server.service -f
          
          # View the last 100 journal lines ofwithout systemd journal logspaging
          $ sudo -u docker-data XDG_RUNTIME_DIR=/run/user/1100 journalctl --user -M docker-data@ -u demucs.immich_server.service -n 100 --no-pager
          
          # ViewFollow container stdout/stderr directly via podman logs
          $ (cd /home/docker-datatmp && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman logs --tail 100 -f sopro)bookstack)
          

          4.3 Starting, Stopping, Restarting & EditingApplying QuadletsQuadlet Edits

          # Restart a servicerunning Quadlet container
          $ sudo systemctl --user -M docker-data@ restart vaultwarden.service
          
          # StopReload orsystemd startgenerator aafter service
          $ sudo systemctl --user -M docker-data@ stop drawpile.service
          $ sudo systemctl --user -M docker-data@ start drawpile.service
          
          # Apply edits made toediting /data/docker/<service>/<service>.container
          $ sudo systemctl --user -M docker-data@ daemon-reload
          $ sudo systemctl --user -M docker-data@ restart <service>.service
          

          4.4 Updating Images & Rebuilding Local Dockerfiles

            Because

            Updating a Registry-Backed Image (vaultwarden,every navidrome.container, unit sets bentopdfPull=never, etc.):for offline boot safety, image updates are always explicit:
            # Pull updated registry image into docker-data's store and restart
            $ (cd /home/docker-datatmp && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman pull docker.io/vaultwarden/server:latest)
            $ sudo systemctl --user -M docker-data@ restart vaultwarden.service
            
            
            # RebuildingRebuild a Locallocally built Dockerfile Imageimage (sopro,use demucs): Use TMPDIR=/dev/shm sofor large multi-gigabyte layer builds (demucs ROCm isimages) ~16.4 GB) stage in RAM rather than disk:
            $ (cd /data/docker/demucssubocr && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 TMPDIR=/dev/shm podman build -t localhost/demucs:subocr:latest /data/docker/demucs)subocr)
            $ sudo systemctl --user -M docker-data@ restart demucs.subocr.service
            
            Pruning Unused/Dangling Images:
            $ (cd /home/docker-data && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman image prune -f)
            

            4.5 Onboarding a New Standalone Service

              Assign a dedicated ULA IPv6 address on eth77 (fd77:7777:5010:0:1337:f1ac:d00d:<hex>/64) in /etc/systemd/network/ and bind it live with sudo ip -6 addr add ... dev eth77. Create /data/docker/<service>/<service>.container with Network=bridge and PublishPort=[fd77:7777:5010:0:1337:f1ac:d00d:<hex>]:<host_port>:<container_port>. Set ownership and permissions:
              $ sudo chown -R docker-data:docker-data /data/docker/<service>
              $ sudo chmod 755 /data/docker/<service>
              $ sudo -u docker-data ln -sf /data/docker/<service>/<service>.container /home/docker-data/.config/containers/systemd/<service>.container
              
              Reload and start:
              $ sudo systemctl --user -M docker-data@ daemon-reload
              $ sudo systemctl --user -M docker-data@ start <service>.service
              
              Create /home/luke/git/backups/ul/scripts/backup/<service> and /home/luke/git/backups/ul/scripts/restore/<service> archiving <service>.container and any data volumes, and verify with shellcheck.

              5. Troubleshooting & Diagnostics Runbook

              5.1 Symptom: LocalOIDC curlLogin or NginxContainer TimesLDAPS OutFails / Resets onwith [fd77:...]:PORTConnection refused to auth.drheck.dev

              • Root Cause:
                  Inside
                1. Ifa Network=pasta (the default when Network= is omitted) is used without isolated -a addresses,rootless pasta copiesnetwork allnamespace, 36auth.drheck.dev resolves via DNS to host ULA10.0.0.100 IPv6 addresses fromor eth77fd77:...:aaaa, intowhich the container namespace, causing replies to local connections from the host's own ULA IPv6 address to looploops back inside the container.
                container's Evenown withnetwork Network=pasta:-a,10.0.2.100,-a,fd00::100,namespace pastawhere forwards connections arriving on an IPv6 host socket as IPv6 packets inside the container. If the container application onlynothing listens on IPv4 0.0.0.0:<port>:443 (such as tldraw, sopro, demucs, or drawpile:636), the container kernel sends TCP RST (curl: (56) Recv failure: Connection reset by peer). Fix: Ensure theAdd .containerAddHost=auth.drheck.dev:169.254.1.2 file specifiesunder Network=bridge[Container], then runin sudo systemctl --user -M docker-data@ daemon-reload && sudo systemctl --user -M docker-data@ restart <service>.service. With Network=bridgecontainer, reload rootlessportdaemon-reload, terminatesand restart the IPv6service connectionso on the hostOIDC and connectsLDAPS overconnections IPv4route (cleanly through 10.88.--map-host-loopback to 127.0.x0.1:443) inside/ the container.127.0.0.1:636.

                5.2 Symptom: Multi-Container Service Fails to StartConnect withto Sibling Hostname (penpot-redis / bookstack_db) After Abrupt Kill

                  Root Cause: If a container on a custom <service>.network bridge is killed abruptly without running netavark teardown, a stale container ID + dead IP line can remain ahead of the new container's IP in /run/user/1100/containers/networks/aardvark-dns/<net_name>. Clients that connect only to the first A record returned by DNS (such as Java's Lettuce client in penpot-backend) time out against the stale IP. Diagnosis & Fix:
                    Inspect /run/user/1100/containers/networks/aardvark-dns/<net_name> against live container IDs from podman ps --no-trunc. Remove any line whose 64-character container ID is no longer running, then reload aardvark-dns without restarting healthy containers:
                    $ sudo kill -HUP "$(cat /run/user/1100/containers/networks/aardvark-dns/aardvark.pid)"
                    

                    5.3 Symptom: Permission denied onInside VolumesNon-Root orWorker RefusesContainer (UID 0624320 / 625288) on /data/docker/<svc>/...

                    • CaseRoot 1Cause: —Worker Applicationprocesses refuses to runrunning as root (getuid() == 0) inside the container (e.g., drawpile-srv): Under default rootless Podman, User=0:0 maps to UID 1100$u > 0$ execute on the host, so the process is unprivileged on the host—but inside the container getuid() returns 0. If a binary explicitly checks getuid() == 0 and exits, add both UserNS=keep-id and User=1100:1100 to [Container].
                    Case 2 — Multi-process container (nginx, apache2) startshost as root (0 -> 1100) and drops worker privileges tosubordinate UID 101$624287 + u$ (624388)outside or UID 33 (624320): Because worker processes run as host UID 624388 or 624320 (notgroup 1100),. they access bind-mounted directories via the other permission bits. EnsureIf the parent directory /data/docker/<servicesvc> has mode chmod 7552770 (drwxr-xr-drwxrws---, missing other execute +x) and read-only content directories (html, content) have world-readable traversal (chmod -R a+rX). Case 3 — Inspecting host-mappedsubordinate UIDs insidecannot a container: Runtraverse podman unshare as docker-data/data/docker/<svc> to viewreach filesystemchild ownershipbind frommounts, insideand tools run on the container's user namespace:
                    $host (cd /home/docker-data && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman unshare ls -lan /data/docker/vaultwarden/vw-data)
                    

                    5.3 Symptom:like Error:git chdir("/home/luke"pull): Permissioncan deniedaccidentally When Runningwrite sudoroot-owned -ufiles docker-data podman

                      Cause: When you runover sudo625287-owned -u docker-data ... while your current working directory is /home/luke (0700 luke:luke), podman or its OCI runtime attempts to stat()/chdir() the inherited working directory and fails with Permission denied.files. Fix: Always
                      changeKeep parent directories traversable without allowing directory tolisting (sudo chmod 2771 /data/docker/<svc>, i.e., /home/docker-datadrwxrws--x). Execute inside-container file updates inside the container namespace (or via podman unshare), just as /tmpusr/local/bin/degoog-update) in a subshell and passruns HOMEgit fetch and XDG_RUNTIME_DIRbun install:
                      inside $degoog.
                      (cd /home/docker-data && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman ps)

                      5.4 Symptom: QuadletDistroless Container Marks .containerunhealthy FileDespite EditsListening NotNormally Applied(e.g., or Service Not Foundvikunja)

                      • DiagnosticRoot CommandCause: RunDistroless theimages Quadletcontain systemdno generator/bin/sh, so string-form HealthCmd=/app/vikunja/vikunja healthcheck fails because Podman invokes /bin/sh -c unless HealthCmd is formatted as a JSON exec array.
                      Fix: Write HealthCmd=["/app/vikunja/vikunja", "healthcheck"] as a JSON array in --dry-run mode as docker-data to inspect any syntax or parsing errors in /home/docker-data/.config/containers/systemd/*<service>.container.

                      5.5 Symptom: 502 Bad Gateway on Dual-Stack Service When Passing IPv6-Only Flag (e.g., ttyd -6 -i ::)

                        Root Cause: Rootless port forwarding may connect into the container over IPv4 even when accepting connections on an eth77 ULA IPv6 socket. Passing IPv6-only bind flags inside the container rejects IPv4 loopback/bridge forwards. Fix: Let internal container daemons listen on default dual-stack (0.0.0.0 / ::) rather than forcing IPv6-only flags.

                        5.6 Validating Quadlet Syntax Before Restarting

                        $ (cd /home/docker-datatmp && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 /usr/lib/systemd/user-generators/podman-system-generator --user --dry-run)
                        
                        Checklist:
                          Confirm /home/docker-data/.config/containers/systemd/<service>.container is a valid symlink to /data/docker/<service>/<service>.container and that /data/docker/<service> is readable by docker-data. Run sudo systemctl --user -M docker-data@ daemon-reload. If the service previously hit start-limit-hit, clear the failed state before starting:
                          $ sudo systemctl --user -M docker-data@ reset-failed <service>.service
                          $ sudo systemctl --user -M docker-data@ restart <service>.service
                          

                          5.5 Symptom: ROCm GPU (torch.cuda.is_available() == False) Inside Rootless Container

                            Checklist:
                              Verify docker-data is a member of video (91) and render (991) on the host (id docker-data). Verify the .container file includes all three directives under [Container]:
                              AddDevice=/dev/dri:/dev/dri
                              AddDevice=/dev/kfd:/dev/kfd
                              GroupAdd=keep-groups
                              
                              Test GPU detection live inside demucs:
                              $ (cd /home/docker-data && sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman exec demucs python3 -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))")
                              True AMD Radeon AI PRO R9700
                              

                              5.6 Full Audit Script: Verify Host UIDs, User Namespaces & HTTP Health

                              Run the following one-liner on ul to audit every rootless Podman service's host kernel UID, user namespace isolation, and ULA IPv6 HTTP response:

                              $ (cd /home/docker-data && init_ns=$(sudo readlink /proc/1/ns/user) && echo "Init UserNS: $init_ns" && \
                                for c in audiomass bentopdf excalidraw tldraw wick webrcade drawpile navidrome vaultwarden sopro demucs; do \
                                  pid=$(sudo -u docker-data HOME=/home/docker-data XDG_RUNTIME_DIR=/run/user/1100 podman inspect "$c" --format "{{.State.Pid}}"); \
                                  uid=$(awk "/^Uid:/ {print \$2}" "/proc/$pid/status"); \
                                  ns=$(sudo readlink "/proc/$pid/ns/user"); \
                                  printf "%-12s PID=%-8s HostUID=%-8s UserNS=%s\n" "$c" "$pid" "$uid" "$ns"; \
                                done)