<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Containers on Dexome</title>
        <link>https://blog.dexome.com/tags/containers/</link>
        <description>Recent content in Containers on Dexome</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Mon, 17 Aug 2026 00:00:00 +0530</lastBuildDate><atom:link href="https://blog.dexome.com/tags/containers/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Sharing Files Between Containers with Different UIDs</title>
        <link>https://blog.dexome.com/post/shared-file-permissions-across-containers/</link>
        <pubDate>Mon, 17 Aug 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/shared-file-permissions-across-containers/</guid>
        <description>&lt;p&gt;I have multiple containers which need to write into the same media folders.
Copyparty runs as UID 924, the arr applications run as UID 911 and Frigate uses
UID/GID 914 on the host. They need to create, rename and delete each other&amp;rsquo;s
files without running all services as the same user.&lt;/p&gt;
&lt;p&gt;I faced three related issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Copyparty ran as UID 924 and shared media with the arr stack at UID 911.&lt;/li&gt;
&lt;li&gt;Frigate had to initialize as root in its container while retaining host
ownership as UID/GID 914.&lt;/li&gt;
&lt;li&gt;A mergerfs view combined two branches whose copies of the same directory had
different modes.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Fixing only the owner or group was not enough. I needed idmapped mounts, correct
directory modes, application-specific chmod settings and inherited ACLs.&lt;/p&gt;
&lt;h2 id=&#34;why-a-writable-file-could-not-be-replaced&#34;&gt;Why a writable file could not be replaced
&lt;/h2&gt;&lt;p&gt;For a process to replace &lt;code&gt;movies/title/video.mkv&lt;/code&gt;, it needs write and execute on
&lt;code&gt;movies/title/&lt;/code&gt;. Write permission on &lt;code&gt;video.mkv&lt;/code&gt; controls modification of the
file&amp;rsquo;s contents; it does not grant permission to remove that directory entry.&lt;/p&gt;
&lt;p&gt;Three inputs decide the normal Unix permission check:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;UID/GID ownership chooses the owner, group, or other class.&lt;/li&gt;
&lt;li&gt;The inode mode or access ACL says what that class may do.&lt;/li&gt;
&lt;li&gt;The containing directory controls create, rename, and delete.&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
		PROC[Radarr uid 911, media group] --&gt; CLASS{Matching class?}
		CLASS --&gt;|directory gid media| GROUP[Use group mode or ACL]
		GROUP --&gt;|r-x on directory| DENY[Read and traverse, cannot replace]
		GROUP --&gt;|rwx on directory| ALLOW[Create, rename, and delete]
		FILE[Writable target file] -. does not decide unlink .-&gt; DENY
&lt;/pre&gt;
    &lt;figcaption&gt;Ownership chooses a permission class; directory permissions decide whether another service can rename or delete an entry.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;So the first check was the owner and mode of the parent directory, not only the
movie file.&lt;/p&gt;
&lt;h2 id=&#34;copyparty-rename-blocked-a-radarr-import&#34;&gt;Copyparty rename blocked a Radarr import
&lt;/h2&gt;&lt;p&gt;Radarr failed while upgrading a movie:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Access to the path &amp;#39;.../Run (2002)/Run (2002) SDTV.mp4&amp;#39; is denied
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The destination directory told the story:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;drwxr-sr-x  924 media  Run (2002)/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;UID 924 was Copyparty. GID &lt;code&gt;media&lt;/code&gt; was correct, and the setgid bit ensured new
children inherited that group. But the group had &lt;code&gt;r-x&lt;/code&gt;, not &lt;code&gt;rwx&lt;/code&gt;. Radarr&amp;rsquo;s UID
911 could enter the directory and read the old file but could not unlink it.&lt;/p&gt;
&lt;p&gt;I had set &lt;code&gt;UMASK=002&lt;/code&gt; on the container. Copyparty&amp;rsquo;s image did not honor that for
this operation and produced a &lt;code&gt;0755&lt;/code&gt; directory. The permanent fix was
application-level creation policy:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;flags&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;	&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;chmod_f&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;664&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;	&lt;/span&gt;&lt;span class=&#34;nt&#34;&gt;chmod_d&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;m&#34;&gt;775&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;I applied that policy to the Copyparty volumes shared with the arr stack,
qBittorrent, and Picard. Existing entries needed a one-time repair because
creation policy is not retroactive.&lt;/p&gt;
&lt;h2 id=&#34;fixing-both-mergerfs-branches&#34;&gt;Fixing both mergerfs branches
&lt;/h2&gt;&lt;p&gt;My first repair changed the affected directory under the &lt;code&gt;btank&lt;/code&gt; backing tree.
The error persisted. The visible media path was a mergerfs union of &lt;code&gt;btank&lt;/code&gt; and
&lt;code&gt;btwo&lt;/code&gt;, and the directory existed on both branches:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;btank branch:  drwxrwsr-x 924 media
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;btwo branch:   drwxr-sr-x 924 media
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;merged view:   drwxr-sr-x
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;mergerfs surfaced metadata from the stale &lt;code&gt;btwo&lt;/code&gt; copy. With
&lt;code&gt;default_permissions&lt;/code&gt;, the kernel enforced the mode visible through the union.
The branch containing the media file was not necessarily the branch supplying
the directory metadata.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
		VIEW[&#34;/mnt/merged-media/title&#34;] --&gt; PICK{Metadata selected by mergerfs}
		B1[&#34;/mnt/btank/@media/title: 2775&#34;] --&gt; PICK
		B2[&#34;/mnt/btwo/@media/title: 2755&#34;] --&gt; PICK
		PICK --&gt;|stale branch wins| MODE[Visible mode 2755]
		MODE --&gt; DENIED[Radarr group write denied]
&lt;/pre&gt;
    &lt;figcaption&gt;Repairing only the branch with the file left a stale directory mode on the other mergerfs branch.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The correct repair swept every raw backing branch, never only the merged view:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;find /mnt/btank/@media/movies /mnt/btwo/@media/movies &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;	-type d ! -perm -2775 -exec chmod &lt;span class=&#34;m&#34;&gt;2775&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;{}&lt;/span&gt; +
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;After both branch copies were corrected, Radarr deleted the old file and
completed the import.&lt;/p&gt;
&lt;h2 id=&#34;frigate-runs-as-root-inside-the-container&#34;&gt;Frigate runs as root inside the container
&lt;/h2&gt;&lt;p&gt;Frigate&amp;rsquo;s official image starts through s6-overlay and needs container root for
initialization. Forcing &lt;code&gt;--user 914:914&lt;/code&gt; broke startup. Running it rootful with a
plain bind mount would create host &lt;code&gt;root:root&lt;/code&gt; state, which would break the
native rollback path and Copyparty access.&lt;/p&gt;
&lt;p&gt;A per-bind idmapped mount solved ownership without changing the process:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;idmap=uids=914-0-1;gids=914-0-1
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Podman&amp;rsquo;s triplet is backing ID, mapped ID, length. Through that mount, host
&lt;code&gt;914:914&lt;/code&gt; appears as &lt;code&gt;0:0&lt;/code&gt; to the container. Frigate can initialize as root and
its files still land on btrfs and ZFS as host &lt;code&gt;914:914&lt;/code&gt;.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart LR
		F[Frigate container root 0:0] --&gt; M[Idmapped bind mount]
		M --&gt;|uids 914-0-1, gids 914-0-1| DISK[Backing files 914:914]
		N[Native rollback process 914:914] --&gt; DISK
		C[Copyparty uid 924 plus group 914] --&gt; DISK
&lt;/pre&gt;
    &lt;figcaption&gt;A per-bind idmap changes the ownership view for one mount; it does not recursively mutate the backing files.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;This is different from the alternatives:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Mechanism&lt;/th&gt;
          &lt;th&gt;What it changes&lt;/th&gt;
          &lt;th&gt;Why I did or did not use it&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;--user 914:914&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Process identity&lt;/td&gt;
          &lt;td&gt;Broke Frigate&amp;rsquo;s root-requiring init&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;:U&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Recursively changes backing ownership&lt;/td&gt;
          &lt;td&gt;Mutates data and is expensive&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Whole-container user namespace&lt;/td&gt;
          &lt;td&gt;Ownership view for the container&lt;/td&gt;
          &lt;td&gt;Complicated GPU and group mappings&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Per-bind idmap&lt;/td&gt;
          &lt;td&gt;Ownership view for one mount&lt;/td&gt;
          &lt;td&gt;Preserved both image and host contracts&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;s6-reset-the-process-umask&#34;&gt;s6 reset the process umask
&lt;/h2&gt;&lt;p&gt;In a disposable directory, &lt;code&gt;--umask=0002&lt;/code&gt; produced directories at &lt;code&gt;0775&lt;/code&gt; and
files at &lt;code&gt;0664&lt;/code&gt;. Copyparty, running as UID 924 with supplementary GID 914, could
create, rename, and delete them.&lt;/p&gt;
&lt;p&gt;The real Frigate processes produced &lt;code&gt;2755&lt;/code&gt; directories and &lt;code&gt;0644&lt;/code&gt; files.
s6-overlay had reset the live process umask to &lt;code&gt;0022&lt;/code&gt; after Podman launched the
container. A shell test that bypassed the supervisor had proved the kernel and
idmap mechanics, not the application&amp;rsquo;s final runtime behavior.&lt;/p&gt;
&lt;p&gt;The reliable check was the live process state:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;grep &lt;span class=&#34;s1&#34;&gt;&amp;#39;^Umask:&amp;#39;&lt;/span&gt; /proc/PID/status
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Because the image did not preserve the desired umask, I used default ACLs on
the recordings tree. Newly created directories inherit group write even when
the process requests a stricter base mode. Copyparty also joins supplementary
GID 914, and its own creation policy emits group-writable entries when it writes
into the same tree.&lt;/p&gt;
&lt;h2 id=&#34;final-permission-setup&#34;&gt;Final permission setup
&lt;/h2&gt;&lt;p&gt;The working setup uses:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;stable host ownership through Frigate&amp;rsquo;s per-bind idmap;&lt;/li&gt;
&lt;li&gt;shared or supplementary groups selecting the intended permission class;&lt;/li&gt;
&lt;li&gt;setgid directories preserving group ownership;&lt;/li&gt;
&lt;li&gt;application &lt;code&gt;chmod_f&lt;/code&gt; and &lt;code&gt;chmod_d&lt;/code&gt; policy where the image ignored &lt;code&gt;UMASK&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;inherited default ACLs where s6 reset the process umask;&lt;/li&gt;
&lt;li&gt;repairs performed on every mergerfs backing branch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For each shared mount, I test read, create, modify, rename, delete, &lt;code&gt;mkdir&lt;/code&gt; and
&lt;code&gt;rmdir&lt;/code&gt; using the real service UIDs and groups. I also test files created by the
actual application, not only a shell started in the same image. This was important
for Frigate because s6 changed the umask after Podman started the container.&lt;/p&gt;
&lt;p&gt;If mergerfs is involved, check and repair every backing branch. The permissions
shown through the merged path may come from a different branch than the file
being modified.&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Moving Traefik into a Podman Container</title>
        <link>https://blog.dexome.com/post/containerize-reverse-proxy-gotchas/</link>
        <pubDate>Thu, 11 Jun 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/containerize-reverse-proxy-gotchas/</guid>
        <description>&lt;p&gt;I was running Traefik as a native NixOS service. Over time, I had added proxy
services to let it reach applications in Podman networks. Instead of keeping
those extra proxies, I decided to run Traefik itself in Podman and attach it to
the required networks.&lt;/p&gt;
&lt;p&gt;The container started and its health check passed, but some routes returned
&lt;code&gt;502 Bad Gateway&lt;/code&gt;. Requests from another VLAN timed out, and after recreating
the container the ingress IP stopped responding.&lt;/p&gt;
&lt;p&gt;There were three separate issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;loopback stopped meaning the host;&lt;/li&gt;
&lt;li&gt;a multi-homed network namespace chose the wrong return route;&lt;/li&gt;
&lt;li&gt;the macvlan MAC changed while the gateway still remembered the old one.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;the-container-network-setup&#34;&gt;The container network setup
&lt;/h2&gt;&lt;p&gt;Native Traefik had accumulated proxy shims to reach services spread across host
processes, Podman bridges, and network containers. Moving Traefik itself into
Podman let it join application networks directly and removed a layer of socket
forwarding.&lt;/p&gt;
&lt;p&gt;The resulting Traefik container joined three networks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;podman_network_vlan_100&lt;/code&gt; for the ingress VIP &lt;code&gt;10.100.100.21&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;podman_rproxy&lt;/code&gt; at &lt;code&gt;10.89.0.2&lt;/code&gt; for host and internal backends;&lt;/li&gt;
&lt;li&gt;an internet bridge at &lt;code&gt;172.28.0.12&lt;/code&gt; for outbound access.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	CLIENTS[Clients and OPNsense] --&gt;|HTTPS to 10.100.100.21| VLAN[macvlan VLAN 100]
	VLAN --&gt; T[Traefik 3.7.5]
	T --&gt;|10.89.0.1 backend ports| HOST[podman_rproxy host gateway]
	HOST --&gt; SERVICES[Netdata, Glances, Cockpit, go2rtc, Home Assistant]
	T --&gt;|container DNS and updates| INET[172.28.0.0/24 Internet bridge]
&lt;/pre&gt;
    &lt;figcaption&gt;Containerized Traefik is multi-homed: ingress, private backends, and Internet access are separate network legs.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The three networks worked, but the old Traefik configuration still assumed it
was running in the host network namespace.&lt;/p&gt;
&lt;h2 id=&#34;host-services-returned-502&#34;&gt;Host services returned 502
&lt;/h2&gt;&lt;p&gt;Several file-provider routes still targeted backends such as:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;servers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;http://127.0.0.1:19999&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That URL worked when Traefik was a host process. Inside the container,
&lt;code&gt;127.0.0.1&lt;/code&gt; referred to the Traefik container itself. Netdata, go2rtc, Cockpit,
and other host services had not moved with it.&lt;/p&gt;
&lt;p&gt;Services on the same Podman bridge continued to work. Only host services and
systemd-nspawn backends failed, while the Traefik health check remained green.&lt;/p&gt;
&lt;p&gt;I tested from both sides of the namespace boundary:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Works from the NixOS host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsS http://10.89.0.1:8123/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Initially failed from Traefik&amp;#39;s namespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;podman &lt;span class=&#34;nb&#34;&gt;exec&lt;/span&gt; traefik wget -qO- -T2 http://10.89.0.1:8123/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The second command showed the problem without involving DNS, TLS or OPNsense.&lt;/p&gt;
&lt;p&gt;The fix had three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bind host services to the &lt;code&gt;podman_rproxy&lt;/code&gt; gateway, &lt;code&gt;10.89.0.1&lt;/code&gt;, rather than
relying on loopback.&lt;/li&gt;
&lt;li&gt;Allow only the required backend ports on the &lt;code&gt;podman_rproxy&lt;/code&gt; interface in the
NixOS firewall.&lt;/li&gt;
&lt;li&gt;Change Traefik&amp;rsquo;s backend URLs from &lt;code&gt;127.0.0.1&lt;/code&gt; to &lt;code&gt;10.89.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For Netdata, I also widened its application-level allow list from localhost to
the &lt;code&gt;10.89.0.0/24&lt;/code&gt; proxy subnet. Opening the host firewall alone would not have
overridden the service&amp;rsquo;s own filter.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	subgraph NATIVE[Native Traefik]
		NT[Traefik process] --&gt;|127.0.0.1| NS[Host service]
	end
	subgraph CONTAINER[Containerized Traefik]
		CT[Traefik container] --&gt;|127.0.0.1| SELF[Traefik container loopback]
		CT --&gt;|10.89.0.1| HS[Host service on podman_rproxy]
	end
	NT -. moved into container .-&gt; CT
&lt;/pre&gt;
    &lt;figcaption&gt;The same URL points at a different machine after Traefik crosses into a container namespace.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The 502s disappeared, but off-subnet clients still timed out.&lt;/p&gt;
&lt;h2 id=&#34;requests-from-other-vlans-timed-out&#34;&gt;Requests from other VLANs timed out
&lt;/h2&gt;&lt;p&gt;Traefik had more than one default route because netavark attached multiple
networks. The route selected during one container creation was not guaranteed to
be the same route selected after another.&lt;/p&gt;
&lt;p&gt;Clients on VLAN 100 were on-link with the macvlan VIP, so replies used the
connected route and worked. A client from another VLAN reached &lt;code&gt;10.100.100.21&lt;/code&gt;
through OPNsense, but Traefik&amp;rsquo;s reply followed the internet bridge&amp;rsquo;s default
route. The request entered through the macvlan and the response tried to leave
through a different gateway.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;off-subnet request: client -&amp;gt; OPNsense -&amp;gt; Traefik macvlan
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;wrong reply:        Traefik -&amp;gt; Podman Internet bridge -&amp;gt; nowhere useful
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Testing only from VLAN 100 made the setup look correct. I had to test from
another VLAN and inspect the route inside Traefik&amp;rsquo;s network namespace.&lt;/p&gt;
&lt;p&gt;Inside Traefik&amp;rsquo;s network namespace, &lt;code&gt;ip route get &amp;lt;client-address&amp;gt;&lt;/code&gt; exposed the
selected gateway. I installed an explicit lower-metric default route through the
VLAN 100 gateway:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip route replace default via 10.100.100.1 metric &lt;span class=&#34;m&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The internet-bridge route remained at metric 100. This was applied by the NixOS
unit after Podman created the namespace, so every recreation restored the same
decision.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	C[Client on another VLAN] --&gt;|request| FW[OPNsense]
	FW --&gt;|10.100.100.21| T[Traefik]
	T -. wrong default before fix .-&gt; I[Internet bridge gateway]
	I -. dropped reply .-&gt; C
	T --&gt;|metric 50 after fix| FW
	FW --&gt;|symmetric reply| C
&lt;/pre&gt;
    &lt;figcaption&gt;Equal default routes made off-subnet replies nondeterministic; the lower-metric VLAN route restored symmetry.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;After the route fix, off-subnet curls returned HTTP 200 and &lt;code&gt;ip route get&lt;/code&gt;
showed the macvlan gateway. Then a later container recreation broke ingress once
more.&lt;/p&gt;
&lt;h2 id=&#34;the-macvlan-mac-changed-after-recreation&#34;&gt;The macvlan MAC changed after recreation
&lt;/h2&gt;&lt;p&gt;The macvlan attachment had a static IP but no static MAC. Podman generated a new
MAC when Traefik was recreated. OPNsense and the upstream switching path still
associated &lt;code&gt;10.100.100.21&lt;/code&gt; with the previous MAC.&lt;/p&gt;
&lt;p&gt;The container health and route table still looked correct because the stale
entry was on OPNsense and the switching path, not inside the container.&lt;/p&gt;
&lt;p&gt;I pinned the attachment identity in the Podman network specification:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;podman_network_vlan_100:ip=10.100.100.21,mac=02:42:0a:64:64:15&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The locally administered &lt;code&gt;02:42&lt;/code&gt; prefix avoids claiming a vendor identity. The
remaining octets encode the private address, which makes the value easy to audit.
More importantly, recreation no longer changes the layer-2 identity.&lt;/p&gt;
&lt;h2 id=&#34;checks-after-the-migration&#34;&gt;Checks after the migration
&lt;/h2&gt;&lt;p&gt;A reverse proxy health check proves only that Traefik can answer inside its own
namespace. My final test matrix covered each boundary explicitly:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Test&lt;/th&gt;
          &lt;th&gt;What it proves&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Curl backend from NixOS host&lt;/td&gt;
          &lt;td&gt;Backend process is listening&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Curl backend from Traefik container&lt;/td&gt;
          &lt;td&gt;Host firewall, bind address, and app allow list work&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Request ingress from VLAN 100&lt;/td&gt;
          &lt;td&gt;Macvlan listener and on-link return path work&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Request ingress from a different VLAN&lt;/td&gt;
          &lt;td&gt;Routed return path uses the correct gateway&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Inspect &lt;code&gt;ip route get&lt;/code&gt; in the namespace&lt;/td&gt;
          &lt;td&gt;Route choice is deterministic&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Recreate Traefik and repeat all tests&lt;/td&gt;
          &lt;td&gt;MAC and post-start route configuration persist&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I also checked the gateway neighbor entry after recreation rather than waiting
for a browser symptom.&lt;/p&gt;
&lt;h2 id=&#34;key-points&#34;&gt;Key points
&lt;/h2&gt;&lt;p&gt;Moving Traefik into a container changed more than the process manager. The native
service could reach host loopback and used the host routing table. The container
could use only its attached interfaces and had multiple default routes.&lt;/p&gt;
&lt;p&gt;For a similar migration, check all backend bind addresses, test from inside the
Traefik container, test ingress from another subnet and recreate the container
once before considering it complete. A static macvlan IP should also have a
stable MAC when upstream devices keep neighbor entries for it.&lt;/p&gt;
</description>
        </item>
        <item>
        <title>Moving My NixOS Containers from Docker to Podman</title>
        <link>https://blog.dexome.com/post/docker-to-podman-migration-gotchas/</link>
        <pubDate>Mon, 08 Jun 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/docker-to-podman-migration-gotchas/</guid>
        <description>&lt;p&gt;NixOS makes it simple to change the backend used by
&lt;code&gt;virtualisation.oci-containers&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;virtualisation&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;oci-containers&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;backend&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;podman&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;In my setup, this changed 36 containers with custom networks, static IP
addresses, shared network namespaces, health checks, GPU devices and bind
mounts. Traefik, Netdata, Glances and Dozzle were also using the Docker API.&lt;/p&gt;
&lt;p&gt;Most Docker options worked with Podman, but a few differences stopped containers
from starting. Network recreation and systemd health checks caused another set
of problems after the initial migration.&lt;/p&gt;
&lt;h2 id=&#34;why-i-moved-to-podman&#34;&gt;Why I moved to Podman
&lt;/h2&gt;&lt;p&gt;I wanted a runtime that fit the host&amp;rsquo;s existing systemd ownership model. Podman
gave me generated &lt;code&gt;podman-&amp;lt;name&amp;gt;.service&lt;/code&gt; units, native health actions, a rootful
socket at &lt;code&gt;/run/podman/podman.sock&lt;/code&gt;, and no long-running Docker daemon.&lt;/p&gt;
&lt;p&gt;I intentionally did not enable &lt;code&gt;dockerCompat&lt;/code&gt;. Traefik, Netdata, Glances, and
Dozzle were pointed at Podman&amp;rsquo;s Docker-compatible API explicitly. Portainer and
the autoheal sidecar were removed; Cockpit Podman and
&lt;code&gt;--health-on-failure=kill&lt;/code&gt; took their places.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart LR
	NIX[NixOS OCI declarations] --&gt; UNITS[Generated systemd units]
	UNITS --&gt;|before| D[Docker containers]
	UNITS --&gt;|after| P[Podman containers]
	API[Traefik, Netdata, Glances, Dozzle] --&gt;|before| DS[&#34;/var/run/docker.sock&#34;]
	API --&gt;|after| PS[&#34;/run/podman/podman.sock&#34;]
	NET[Declared networks] --&gt; NAV[netavark and aardvark-dns]
	NAV --&gt; P
&lt;/pre&gt;
    &lt;figcaption&gt;The runtime changed under a declarative NixOS service graph; every explicit Docker unit and socket edge had to move with it.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The migration began with mechanical changes: rename unit references from
&lt;code&gt;docker-*&lt;/code&gt; to &lt;code&gt;podman-*&lt;/code&gt;, change supplementary group membership from &lt;code&gt;docker&lt;/code&gt; to
&lt;code&gt;podman&lt;/code&gt;, and replace socket paths. Then the behavioral differences appeared.&lt;/p&gt;
&lt;h2 id=&#34;--health-on-failure-needs-a-health-check&#34;&gt;&lt;code&gt;--health-on-failure&lt;/code&gt; needs a health check
&lt;/h2&gt;&lt;p&gt;I replaced autoheal with Podman&amp;rsquo;s native unhealthy action:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;--health-on-failure=kill
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That option is valid only when the container has an explicit health command or
the image contains a baked &lt;code&gt;HEALTHCHECK&lt;/code&gt;. Docker had tolerated the old setup.
Podman refused to create affected containers with exit 125:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Error: cannot set on-failure action to kill without a health check
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Documentation and upstream Dockerfiles were not reliable enough because the
pinned image was the thing actually being executed. This became the
authoritative test:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;podman image inspect IMAGE &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;se&#34;&gt;&lt;/span&gt;  --format &lt;span class=&#34;s1&#34;&gt;&amp;#39;{{if .Config.Healthcheck}}HAS{{else}}NONE{{end}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Only Plex had the baked health check I expected. Images for Dozzle, Gluetun,
qBittorrent, Stirling PDF, Gogs, MeTube, and several Immich components did not.
For those I either added a real &lt;code&gt;--health-cmd&lt;/code&gt; or removed the unhealthy action.&lt;/p&gt;
&lt;p&gt;Two more parser differences stopped containers before their applications even
started:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Docker-era option&lt;/th&gt;
          &lt;th&gt;Podman result&lt;/th&gt;
          &lt;th&gt;Replacement&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;--tmpfs=/transcode:uid=912,gid=912&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Unknown mount option&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;mode=1777&lt;/code&gt; where appropriate&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;--network=name=NET,ip=IP&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Comma split into two network names&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;--network=NET:ip=IP&lt;/code&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;These errors were visible in the generated systemd unit logs and were simple to
reproduce before starting the application.&lt;/p&gt;
&lt;h2 id=&#34;transient-health-checks-caused-exit-code-4&#34;&gt;Transient health checks caused exit code 4
&lt;/h2&gt;&lt;p&gt;Podman implements periodic health checks using transient systemd services. On
the first activation, containers were pulling images and warming up while these
checks began to run. Some transient &lt;code&gt;podman healthcheck run &amp;lt;id&amp;gt;&lt;/code&gt; services
returned non-zero during &lt;code&gt;health_status=starting&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;switch-to-configuration&lt;/code&gt; saw failed units and returned exit code 4. A minute
later, the declared network unit had succeeded, every container was healthy,
and &lt;code&gt;systemctl --failed&lt;/code&gt; was empty.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
sequenceDiagram
	participant A as NixOS activation
	participant S as systemd
	participant C as Podman container
	A-&gt;&gt;S: restart generated units
	S-&gt;&gt;C: start container
	C--&gt;&gt;S: health_status=starting
	S-&gt;&gt;C: transient healthcheck
	C--&gt;&gt;S: non-zero while warming
	S--&gt;&gt;A: failed transient unit, exit 4
	C--&gt;&gt;S: health_status=healthy
	Note over S,C: Final state is healthy despite activation result
&lt;/pre&gt;
    &lt;figcaption&gt;An activation-time health probe can fail while the final steady state becomes healthy.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;Exit code 4 should not be ignored, but it can be caused by a health check which
has already recovered. I check the failed units, network service and current
container state separately:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl --failed --no-pager
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;systemctl status podman-networks.service --no-pager --full
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;podman ps --format &lt;span class=&#34;s1&#34;&gt;&amp;#39;table {{.Names}}\t{{.Status}}&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;A transient probe that has already cleared is different from a network
reconciler that is still failed.&lt;/p&gt;
&lt;h2 id=&#34;stopped-containers-are-still-attached-to-networks&#34;&gt;Stopped containers are still attached to networks
&lt;/h2&gt;&lt;p&gt;I wrote a declarative network reconciler so changes to subnet, gateway, or
interface name would recreate a drifted Podman network. Container units were
&lt;code&gt;partOf&lt;/code&gt; the network service, so restarting the network stopped its users first.&lt;/p&gt;
&lt;p&gt;The assumption was plausible and wrong. A stopped container remains associated
with its network. The reconciler ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;podman network rm podman_network_rproxy
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Podman correctly refused:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;network is being used
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The drift path needed &lt;code&gt;podman network rm -f&lt;/code&gt;. Force removal disconnected and
removed the stopped container records; systemd recreated them after the network
returned.&lt;/p&gt;
&lt;h2 id=&#34;network-recreation-cleared-all-ipam-leases&#34;&gt;Network recreation cleared all IPAM leases
&lt;/h2&gt;&lt;p&gt;The force-removal fix exposed the next bug. Recreating the proxy bridge erased
netavark&amp;rsquo;s IPAM leases. A dynamic container started first and received
&lt;code&gt;10.89.0.2&lt;/code&gt;, the static address reserved in configuration for Traefik.&lt;/p&gt;
&lt;p&gt;Traefik then looped with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;IPAM error: requested ip address 10.89.0.2 is already allocated
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The old network had worked only because its historical lease happened to keep
&lt;code&gt;.2&lt;/code&gt; occupied by the intended container. Start order became visible after a
cold reconstruction.&lt;/p&gt;
&lt;p&gt;I separated the static and dynamic address ranges:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;subnet&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.89.0.0/24&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gateway&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.89.0.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;ipRange&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.89.0.128/25&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;leaseRange&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.89.0.129-10.89.0.255&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Addresses &lt;code&gt;.2&lt;/code&gt; through &lt;code&gt;.127&lt;/code&gt; are available for deliberate static assignments;
dynamic leases begin at &lt;code&gt;.129&lt;/code&gt;. I verified the expanded lease range with a
disposable network before trusting it in the drift comparison.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	R[Recreate 10.89.0.0/24 network] --&gt; EMPTY[IPAM lease table is empty]
	EMPTY --&gt;|without range separation| DYN[Dynamic backend starts first and takes .2]
	DYN --&gt; FAIL[Traefik static .2 fails]
	EMPTY --&gt;|dynamic pool .129-.255| HIGH[Backend receives a high address]
	HIGH --&gt; OK[Traefik always claims static .2]
&lt;/pre&gt;
    &lt;figcaption&gt;A network rebuild forgets historical leases. Reserving separate static and dynamic ranges removes start-order dependence.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;nixos-firewall-reload-broke-aardvark-dns&#34;&gt;NixOS firewall reload broke aardvark-dns
&lt;/h2&gt;&lt;p&gt;aardvark-dns listens on each netavark bridge gateway. Netavark inserted rules
allowing DNS, but the NixOS firewall reload flushed those runtime-generated
rules. Existing internal bridges were not recreated afterward, so their rules
did not return.&lt;/p&gt;
&lt;p&gt;Containers began timing out on names served by the bridge gateway. OAuth2 Proxy
and Immich crash-looped; containers with explicit DNS servers were unaffected.&lt;/p&gt;
&lt;p&gt;The fix was to make bridge DNS part of the host firewall declaration. My bridge
interfaces deliberately retain a &lt;code&gt;podman&lt;/code&gt; prefix, so the NixOS &lt;code&gt;podman+&lt;/code&gt;
interface wildcard can allow TCP and UDP port 53 after every reload.&lt;/p&gt;
&lt;p&gt;This incident also taught me that &lt;code&gt;--internal&lt;/code&gt; does not mean &amp;ldquo;DNS-free.&amp;rdquo; An
internal bridge may need its embedded resolver for other names on that same
private network even when it has no Internet route.&lt;/p&gt;
&lt;h2 id=&#34;checks-i-would-use-for-another-migration&#34;&gt;Checks I would use for another migration
&lt;/h2&gt;&lt;p&gt;I would split validation into four layers:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Inspect every pinned image for a baked health check instead of relying on its
documentation.&lt;/li&gt;
&lt;li&gt;Create every container once to find unsupported option formats.&lt;/li&gt;
&lt;li&gt;Delete and recreate the declared networks so the test starts with empty IPAM
state.&lt;/li&gt;
&lt;li&gt;Reload the NixOS firewall and check DNS from an internal bridge.&lt;/li&gt;
&lt;li&gt;Reboot and make sure the same containers and static IPs return without relying
on the old start order.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Podman has been working well after these fixes. The container applications did
not need changes; most of the migration work was around image metadata, command
line parsing, systemd health checks and netavark state.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
