<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Linux on Dexome</title>
        <link>https://blog.dexome.com/tags/linux/</link>
        <description>Recent content in Linux 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/linux/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 My NixOS Network to systemd-networkd</title>
        <link>https://blog.dexome.com/post/scripted-to-systemd-networkd/</link>
        <pubDate>Thu, 18 Jun 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/scripted-to-systemd-networkd/</guid>
        <description>&lt;p&gt;My NixOS server &lt;code&gt;heavymetal&lt;/code&gt; already had multiple VLANs, static IP addresses and
macvlans used by containers. They were created using startup scripts and worked,
but restarts and NixOS activations did not always recreate them in the correct
order.&lt;/p&gt;
&lt;p&gt;I first moved the same setup to systemd-networkd. Later, when I needed to attach
VM tap interfaces to the VLANs, I added per-VLAN bridges and finally replaced
them with one VLAN-aware bridge.&lt;/p&gt;
&lt;p&gt;The migration happened in three steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;replace imperative networking with networkd units;&lt;/li&gt;
&lt;li&gt;move host addresses from VLAN devices onto stable per-VLAN bridges;&lt;/li&gt;
&lt;li&gt;consolidate those bridges into a single VLAN-aware trunk.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I did not apply the bridge migrations live because they moved the same management
IP used by SSH. Those changes were staged for boot with console access available.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	S[&#34;Scripted VLAN and&lt;br/&gt;macvlan devices&#34;] --&gt; N[&#34;systemd-networkd owns&lt;br/&gt;the same topology&#34;]
	N --&gt;|Parent restart exposes child lifecycle| P[&#34;Per-VLAN bridges&lt;br/&gt;brvlan20 / brvlan30 / brvlan100&#34;]
	P --&gt;|Stable parents enable VM taps| T[&#34;VLAN-aware bridge&lt;br/&gt;brtrunk&#34;]
&lt;/pre&gt;
    &lt;figcaption&gt;The network evolved in three controlled steps rather than jumping directly from scripts to the final VLAN-aware bridge.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;h2 id=&#34;the-old-startup-scripts&#34;&gt;The old startup scripts
&lt;/h2&gt;&lt;p&gt;The old system created VLAN and macvlan devices through a mixture of higher-level
configuration and startup services. Conceptually it did this:&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;span class=&#34;lnt&#34;&gt;6
&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 link add link enp5s0 name enp5s0.100 &lt;span class=&#34;nb&#34;&gt;type&lt;/span&gt; vlan id &lt;span class=&#34;m&#34;&gt;100&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip address add 10.100.100.20/24 dev enp5s0.100
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip link &lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt; enp5s0.100 up
&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;ip link add link enp5s0.100 name tfkshim &lt;span class=&#34;nb&#34;&gt;type&lt;/span&gt; macvlan mode bridge
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip link &lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt; tfkshim up
&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;Imperative commands are easy to prototype, but they hide ownership questions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which service owns the VLAN device?&lt;/li&gt;
&lt;li&gt;What should restart when the parent disappears?&lt;/li&gt;
&lt;li&gt;Is the address ready before a dependent service binds it?&lt;/li&gt;
&lt;li&gt;What removes stale devices after a failed partial run?&lt;/li&gt;
&lt;li&gt;Does a config reload destroy children created by another service?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The scripts encoded answers in execution order rather than in the topology.&lt;/p&gt;
&lt;h2 id=&#34;recreate-the-same-network-with-networkd&#34;&gt;Recreate the same network with networkd
&lt;/h2&gt;&lt;p&gt;I moved each concern into an explicit networkd object: VLAN &lt;code&gt;.netdev&lt;/code&gt; units,
matching &lt;code&gt;.network&lt;/code&gt; units for addresses and routes, and dependencies for services
that needed those links.&lt;/p&gt;
&lt;p&gt;On NixOS, a simplified VLAN looks like this:&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;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;netdevs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10-vlan20&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;netdevConfig&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vlan20&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;Kind&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vlan&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;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;vlanConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Id&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;20&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;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&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;40-vlan20&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;matchConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vlan20&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;address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.20.0.10/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;routes&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&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.20.0.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&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;networkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RequiredForOnline&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;routable&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;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;The exact syntax is distribution-specific; the systemd concepts are not. A
&lt;code&gt;.netdev&lt;/code&gt; creates a virtual device. A &lt;code&gt;.network&lt;/code&gt; matches a device and assigns
addresses, routes, VLAN membership, bridge membership, and online-state
requirements.&lt;/p&gt;
&lt;p&gt;The important part was preserving topology exactly. This was not the moment to
rename every interface or collapse five networks into one bridge. First I needed
networkd to reproduce the working system.&lt;/p&gt;
&lt;h2 id=&#34;systemd-resolved-broke-container-dns&#34;&gt;systemd-resolved broke container DNS
&lt;/h2&gt;&lt;p&gt;Enabling networkd on NixOS also enabled systemd-resolved through a distribution
default. The host continued to resolve names, so the change initially looked
healthy. But &lt;code&gt;/etc/resolv.conf&lt;/code&gt; now pointed at the loopback stub:&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;nameserver 127.0.0.53
&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;Containers copied that file into their own network namespaces. From inside a
container, &lt;code&gt;127.0.0.53&lt;/code&gt; meant the container itself, not the host&amp;rsquo;s resolved
service. External name resolution failed even though host DNS worked.&lt;/p&gt;
&lt;p&gt;I explicitly disabled resolved and retained a static resolver address reachable
from both the host and containers. Running resolved would also have been valid if
the container DNS path had been designed for it. The failure came from changing
resolver architecture as an accidental side effect of changing interface
management.&lt;/p&gt;
&lt;p&gt;The check I added was simple:&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;cat /etc/resolv.conf
&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; &amp;lt;container&amp;gt; cat /etc/resolv.conf
&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; &amp;lt;container&amp;gt; getent hosts example.com
&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 host-only lookup is not enough after a network-manager migration.&lt;/p&gt;
&lt;h2 id=&#34;restarting-a-vlan-removed-its-macvlan-children&#34;&gt;Restarting a VLAN removed its macvlan children
&lt;/h2&gt;&lt;p&gt;An older deployment had already revealed a more disruptive lifecycle problem.
When the service owning a VLAN netdev restarted, it deleted and recreated the
parent device. Linux also deleted every macvlan child attached to that parent.&lt;/p&gt;
&lt;p&gt;The container runtime&amp;rsquo;s database still believed the container was attached. The
actual network namespace contained only loopback and an unrelated bridge. The
reverse proxy logged that its interface had been removed and lost its virtual
address.&lt;/p&gt;
&lt;p&gt;The journal made the sequence visible:&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-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;VLAN netdev service stopped
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;parent link deleted
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;macvlan child removed by kernel
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;container remains running without expected interface
&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;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
sequenceDiagram
	participant Apply as NixOS activation
	participant Parent as enp5s0.100 netdev
	participant Kernel as Linux kernel
	participant Child as tfkshim macvlan
	participant Podman as Traefik container

	Apply-&gt;&gt;Parent: Stop and delete VLAN netdev
	Parent-&gt;&gt;Kernel: ip link del enp5s0.100
	Kernel--xChild: Delete every macvlan child
	Child--xPodman: eth0 removed from namespace
	Note over Podman: Container keeps running and DB still says attached
	Apply-&gt;&gt;Parent: Recreate VLAN netdev
	Note over Parent,Podman: Parent returns but child does not
&lt;/pre&gt;
    &lt;figcaption&gt;Deleting and recreating the VLAN parent also deleted the live macvlan child, while Podman&amp;#39;s stored attachment remained stale.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;At first I coupled the container-network service lifecycle to the parent netdev
service: if the parent restarted, the container networks and their consumers
restarted and reattached. That repaired the immediate inconsistency.&lt;/p&gt;
&lt;p&gt;It also made the weakness of the topology clear. Long-lived workloads were
attached directly to a device that configuration reconciliation was allowed to
destroy.&lt;/p&gt;
&lt;h2 id=&#34;adding-one-bridge-per-vlan&#34;&gt;Adding one bridge per VLAN
&lt;/h2&gt;&lt;p&gt;I needed the same VLANs to be shared with ThingsHQ microVM tap devices. A macvlan
parent cannot serve that role cleanly, so I created always-on host bridges such
as &lt;code&gt;brvlan20&lt;/code&gt; and &lt;code&gt;brvlan100&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The VLAN uplink became an addressless bridge port. The host address moved to the
bridge. Containers and VMs attached to the bridge rather than directly to the
VLAN netdev.&lt;/p&gt;
&lt;p&gt;A simplified networkd definition looks like:&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;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;17
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;18
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;19
&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;netdevs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10-br-vlan20&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;netdevConfig&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;br-vlan20&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;Kind&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;bridge&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;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;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&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;40-vlan20&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;matchConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;vlan20&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;networkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Bridge&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;br-vlan20&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;linkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RequiredForOnline&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;enslaved&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;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&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;40-br-vlan20&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;matchConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;br-vlan20&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;address&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;10.20.0.10/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;routes&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&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.20.0.1&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;}&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;networkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RequiredForOnline&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;routable&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;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;This transition could not be safely applied live. A macvlan cannot be re-parented
in place. Networkd could not enslave the VLAN uplink to the new bridge while the
live macvlan child still depended on it, and deleting the child would remove the
remote management path carried by the reverse proxy.&lt;/p&gt;
&lt;p&gt;Instead of a live configuration switch, I staged the new boot generation and
rebooted through a console-backed maintenance path. At boot, every device was
created in the new topology from an empty state. The previous generation remained
selectable in the boot menu if the new management address did not come up.&lt;/p&gt;
&lt;p&gt;That was not excessive caution. &amp;ldquo;Same IP, different owning device&amp;rdquo; is still a
remote access migration.&lt;/p&gt;
&lt;h2 id=&#34;moving-to-one-vlan-aware-bridge&#34;&gt;Moving to one VLAN-aware bridge
&lt;/h2&gt;&lt;p&gt;Per-VLAN bridges solved the VM-sharing problem, but the host eventually needed a
larger trunk topology. Five bridges and five VLAN uplinks repeated the same
structure and made VM trunk attachment awkward.&lt;/p&gt;
&lt;p&gt;The next design used a single bridge named &lt;code&gt;brtrunk&lt;/code&gt; with VLAN filtering.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart LR
	SWITCH[Omada trunk] ==&gt;|tagged VLANs| NIC[enp5s0]
	NIC --&gt; BR[brtrunk VLAN-aware bridge]
	BR --&gt; ADMIN[brtrunk.100: host admin 10.100.100.20]
	BR --&gt; THINGS[brtrunk.20: Things services]
	BR --&gt; QUANTUM[brtrunk.30: VPN services]
	BR --&gt; VMS[OPNsense and ThingsHQ VM tap ports]
	BR --&gt; ISOLATED[VLAN 67 sync and VLAN 999 WAN transport: no host L3]
&lt;/pre&gt;
    &lt;figcaption&gt;In the final design, enp5s0 is a bridge port; brtrunk owns VLAN filtering, while host L3 exists only on selected VLAN interfaces.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;In networkd terms, the bridge enables VLAN filtering and the physical interface
is enslaved as a trunk port:&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;span class=&#34;lnt&#34;&gt; 6
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 7
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 8
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt; 9
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;10
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;11
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;12
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;13
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;14
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;15
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;16
&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;netdevs&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;10-br-trunk&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;netdevConfig&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;brtrunk&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;Kind&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;bridge&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;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;bridgeConfig&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;VLANFiltering&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;no&#34;&gt;true&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;DefaultPVID&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&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;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;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&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;systemd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;network&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;networks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;40-physical-trunk&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;matchConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Name&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;enp5s0&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;networkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Bridge&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;brtrunk&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;linkConfig&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;RequiredForOnline&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;enslaved&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;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;The host&amp;rsquo;s management address moved from a per-VLAN bridge onto a VLAN interface
of the trunk bridge. Some transport VLANs existed only inside the bridge and had
no host-layer address at all.&lt;/p&gt;
&lt;p&gt;Again, I staged the configuration for the next boot rather than attempting to
replace the bridge beneath an active SSH session. The migration changed the
master of the physical NIC, removed several bridges, recreated VLAN interfaces,
and moved the default route. A reboot was the deterministic path.&lt;/p&gt;
&lt;h2 id=&#34;conflicting-forwarding-sysctls&#34;&gt;Conflicting forwarding sysctls
&lt;/h2&gt;&lt;p&gt;After one deployment, host-to-container routing failed even though
&lt;code&gt;net.ipv4.ip_forward&lt;/code&gt; appeared in the configuration.&lt;/p&gt;
&lt;p&gt;Linux exposes closely related forwarding sysctls:&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;net.ipv4.ip_forward
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;net.ipv4.conf.all.forwarding
&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;Modules had written both aliases with conflicting values. The canonical
per-family setting remained zero at runtime, leaving forwarding disabled. I made
both values explicit:&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-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;boot&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;kernel&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sysctl&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&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;s2&#34;&gt;&amp;#34;net.ipv4.ip_forward&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&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;s2&#34;&gt;&amp;#34;net.ipv4.conf.all.forwarding&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&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;s2&#34;&gt;&amp;#34;net.ipv6.conf.all.forwarding&amp;#34;&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&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;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;Both names refer to closely related kernel settings and another module can write
one after the other. So I check the runtime values after activation:&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;sysctl net.ipv4.ip_forward
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sysctl net.ipv4.conf.all.forwarding
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sysctl net.ipv6.conf.all.forwarding
&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 kernel is the final source of truth.&lt;/p&gt;
&lt;h2 id=&#34;source-only-addresses-and-automatic-routes&#34;&gt;Source-only addresses and automatic routes
&lt;/h2&gt;&lt;p&gt;The host also had secondary interfaces used only to originate scans or reach
macvlan workloads. Those addresses must not automatically install connected
prefix routes if another interface owns the real return path.&lt;/p&gt;
&lt;p&gt;I set &lt;code&gt;AddPrefixRoute=false&lt;/code&gt; on those source-only legs and kept reverse-path
filtering loose where asymmetric routes were intentional. Otherwise Linux could
prefer the newly connected &lt;code&gt;/24&lt;/code&gt;, send replies out the wrong interface, and turn
a harmless scan address into an SSH lockout.&lt;/p&gt;
&lt;p&gt;This is a niche detail with a broad principle: every new address can also create
a route. During migrations, compare the routing table, not just the address list.&lt;/p&gt;
&lt;h2 id=&#34;checks-after-each-migration&#34;&gt;Checks after each migration
&lt;/h2&gt;&lt;p&gt;After each stage I checked the system from the bottom up.&lt;/p&gt;
&lt;h3 id=&#34;link-ownership&#34;&gt;Link ownership
&lt;/h3&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;networkctl status
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip -br link
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip -br address
&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;ul&gt;
&lt;li&gt;Is the physical NIC enslaved to the intended bridge?&lt;/li&gt;
&lt;li&gt;Are VLAN devices attached to the intended parent?&lt;/li&gt;
&lt;li&gt;Is each host address on the bridge/VLAN device, not the addressless port?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;routes-and-forwarding&#34;&gt;Routes and forwarding
&lt;/h3&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;ip route
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip -6 route
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;sysctl net.ipv4.conf.all.forwarding
&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;ul&gt;
&lt;li&gt;Is there exactly one intended default route?&lt;/li&gt;
&lt;li&gt;Did source-only addresses add unwanted connected routes?&lt;/li&gt;
&lt;li&gt;Is forwarding enabled in runtime state?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;namespaces&#34;&gt;Namespaces
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Can a container resolve DNS?&lt;/li&gt;
&lt;li&gt;Does its namespace contain every expected interface?&lt;/li&gt;
&lt;li&gt;Can it reach both an internal backend and an external endpoint?&lt;/li&gt;
&lt;li&gt;Does restarting the parent/network service reattach the child cleanly?&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;reboot-behavior&#34;&gt;Reboot behavior
&lt;/h3&gt;&lt;ul&gt;
&lt;li&gt;Does the host return on the management address without manual intervention?&lt;/li&gt;
&lt;li&gt;Do services wait for the interface state they actually require?&lt;/li&gt;
&lt;li&gt;Does a second ordinary configuration apply leave the topology intact?&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;final-setup&#34;&gt;Final setup
&lt;/h2&gt;&lt;p&gt;systemd-networkd did not make the network itself simple, but it made device
ownership and startup dependencies visible. The order I would use again is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;reproduce the working topology declaratively;&lt;/li&gt;
&lt;li&gt;observe the lifecycle failures that the old scripts concealed;&lt;/li&gt;
&lt;li&gt;introduce stable bridge ownership;&lt;/li&gt;
&lt;li&gt;consolidate only after the dependencies are understood;&lt;/li&gt;
&lt;li&gt;stage non-live-reparentable changes for reboot with console rollback.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Do not assume a declarative network change is safe to apply over SSH. Moving a
management IP from a VLAN to a bridge still removes and recreates interfaces. For
those changes, I used a boot-time migration and kept the previous NixOS generation
available from the console.&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>
