<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Podman on Dexome</title>
        <link>https://blog.dexome.com/tags/podman/</link>
        <description>Recent content in Podman on Dexome</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Mon, 08 Jun 2026 00:00:00 +0530</lastBuildDate><atom:link href="https://blog.dexome.com/tags/podman/index.xml" rel="self" type="application/rss+xml" /><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>
