<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Traefik on Dexome</title>
        <link>https://blog.dexome.com/tags/traefik/</link>
        <description>Recent content in Traefik on Dexome</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en</language>
        <lastBuildDate>Thu, 11 Jun 2026 00:00:00 +0530</lastBuildDate><atom:link href="https://blog.dexome.com/tags/traefik/index.xml" rel="self" type="application/rss+xml" /><item>
        <title>Moving Traefik into a Podman Container</title>
        <link>https://blog.dexome.com/post/containerize-reverse-proxy-gotchas/</link>
        <pubDate>Thu, 11 Jun 2026 00:00:00 +0530</pubDate>
        
        <guid>https://blog.dexome.com/post/containerize-reverse-proxy-gotchas/</guid>
        <description>&lt;p&gt;I was running Traefik as a native NixOS service. Over time, I had added proxy
services to let it reach applications in Podman networks. Instead of keeping
those extra proxies, I decided to run Traefik itself in Podman and attach it to
the required networks.&lt;/p&gt;
&lt;p&gt;The container started and its health check passed, but some routes returned
&lt;code&gt;502 Bad Gateway&lt;/code&gt;. Requests from another VLAN timed out, and after recreating
the container the ingress IP stopped responding.&lt;/p&gt;
&lt;p&gt;There were three separate issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;loopback stopped meaning the host;&lt;/li&gt;
&lt;li&gt;a multi-homed network namespace chose the wrong return route;&lt;/li&gt;
&lt;li&gt;the macvlan MAC changed while the gateway still remembered the old one.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;the-container-network-setup&#34;&gt;The container network setup
&lt;/h2&gt;&lt;p&gt;Native Traefik had accumulated proxy shims to reach services spread across host
processes, Podman bridges, and network containers. Moving Traefik itself into
Podman let it join application networks directly and removed a layer of socket
forwarding.&lt;/p&gt;
&lt;p&gt;The resulting Traefik container joined three networks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;podman_network_vlan_100&lt;/code&gt; for the ingress VIP &lt;code&gt;10.100.100.21&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;podman_rproxy&lt;/code&gt; at &lt;code&gt;10.89.0.2&lt;/code&gt; for host and internal backends;&lt;/li&gt;
&lt;li&gt;an internet bridge at &lt;code&gt;172.28.0.12&lt;/code&gt; for outbound access.&lt;/li&gt;
&lt;/ul&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	CLIENTS[Clients and OPNsense] --&gt;|HTTPS to 10.100.100.21| VLAN[macvlan VLAN 100]
	VLAN --&gt; T[Traefik 3.7.5]
	T --&gt;|10.89.0.1 backend ports| HOST[podman_rproxy host gateway]
	HOST --&gt; SERVICES[Netdata, Glances, Cockpit, go2rtc, Home Assistant]
	T --&gt;|container DNS and updates| INET[172.28.0.0/24 Internet bridge]
&lt;/pre&gt;
    &lt;figcaption&gt;Containerized Traefik is multi-homed: ingress, private backends, and Internet access are separate network legs.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The three networks worked, but the old Traefik configuration still assumed it
was running in the host network namespace.&lt;/p&gt;
&lt;h2 id=&#34;host-services-returned-502&#34;&gt;Host services returned 502
&lt;/h2&gt;&lt;p&gt;Several file-provider routes still targeted backends such as:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nt&#34;&gt;servers&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;w&#34;&gt;  &lt;/span&gt;- &lt;span class=&#34;nt&#34;&gt;url&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;l&#34;&gt;http://127.0.0.1:19999&lt;/span&gt;&lt;span class=&#34;w&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;That URL worked when Traefik was a host process. Inside the container,
&lt;code&gt;127.0.0.1&lt;/code&gt; referred to the Traefik container itself. Netdata, go2rtc, Cockpit,
and other host services had not moved with it.&lt;/p&gt;
&lt;p&gt;Services on the same Podman bridge continued to work. Only host services and
systemd-nspawn backends failed, while the Traefik health check remained green.&lt;/p&gt;
&lt;p&gt;I tested from both sides of the namespace boundary:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;3
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;4
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;5
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Works from the NixOS host&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;curl -fsS http://10.89.0.1:8123/
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Initially failed from Traefik&amp;#39;s namespace&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;podman &lt;span class=&#34;nb&#34;&gt;exec&lt;/span&gt; traefik wget -qO- -T2 http://10.89.0.1:8123/
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The second command showed the problem without involving DNS, TLS or OPNsense.&lt;/p&gt;
&lt;p&gt;The fix had three parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Bind host services to the &lt;code&gt;podman_rproxy&lt;/code&gt; gateway, &lt;code&gt;10.89.0.1&lt;/code&gt;, rather than
relying on loopback.&lt;/li&gt;
&lt;li&gt;Allow only the required backend ports on the &lt;code&gt;podman_rproxy&lt;/code&gt; interface in the
NixOS firewall.&lt;/li&gt;
&lt;li&gt;Change Traefik&amp;rsquo;s backend URLs from &lt;code&gt;127.0.0.1&lt;/code&gt; to &lt;code&gt;10.89.0.1&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For Netdata, I also widened its application-level allow list from localhost to
the &lt;code&gt;10.89.0.0/24&lt;/code&gt; proxy subnet. Opening the host firewall alone would not have
overridden the service&amp;rsquo;s own filter.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	subgraph NATIVE[Native Traefik]
		NT[Traefik process] --&gt;|127.0.0.1| NS[Host service]
	end
	subgraph CONTAINER[Containerized Traefik]
		CT[Traefik container] --&gt;|127.0.0.1| SELF[Traefik container loopback]
		CT --&gt;|10.89.0.1| HS[Host service on podman_rproxy]
	end
	NT -. moved into container .-&gt; CT
&lt;/pre&gt;
    &lt;figcaption&gt;The same URL points at a different machine after Traefik crosses into a container namespace.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;The 502s disappeared, but off-subnet clients still timed out.&lt;/p&gt;
&lt;h2 id=&#34;requests-from-other-vlans-timed-out&#34;&gt;Requests from other VLANs timed out
&lt;/h2&gt;&lt;p&gt;Traefik had more than one default route because netavark attached multiple
networks. The route selected during one container creation was not guaranteed to
be the same route selected after another.&lt;/p&gt;
&lt;p&gt;Clients on VLAN 100 were on-link with the macvlan VIP, so replies used the
connected route and worked. A client from another VLAN reached &lt;code&gt;10.100.100.21&lt;/code&gt;
through OPNsense, but Traefik&amp;rsquo;s reply followed the internet bridge&amp;rsquo;s default
route. The request entered through the macvlan and the response tried to leave
through a different gateway.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;span class=&#34;lnt&#34;&gt;2
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;off-subnet request: client -&amp;gt; OPNsense -&amp;gt; Traefik macvlan
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;wrong reply:        Traefik -&amp;gt; Podman Internet bridge -&amp;gt; nowhere useful
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;Testing only from VLAN 100 made the setup look correct. I had to test from
another VLAN and inspect the route inside Traefik&amp;rsquo;s network namespace.&lt;/p&gt;
&lt;p&gt;Inside Traefik&amp;rsquo;s network namespace, &lt;code&gt;ip route get &amp;lt;client-address&amp;gt;&lt;/code&gt; exposed the
selected gateway. I installed an explicit lower-metric default route through the
VLAN 100 gateway:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;ip route replace default via 10.100.100.1 metric &lt;span class=&#34;m&#34;&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The internet-bridge route remained at metric 100. This was applied by the NixOS
unit after Podman created the namespace, so every recreation restored the same
decision.&lt;/p&gt;
&lt;figure class=&#34;article-diagram&#34;&gt;
    &lt;pre class=&#34;mermaid&#34;&gt;
flowchart TB
	C[Client on another VLAN] --&gt;|request| FW[OPNsense]
	FW --&gt;|10.100.100.21| T[Traefik]
	T -. wrong default before fix .-&gt; I[Internet bridge gateway]
	I -. dropped reply .-&gt; C
	T --&gt;|metric 50 after fix| FW
	FW --&gt;|symmetric reply| C
&lt;/pre&gt;
    &lt;figcaption&gt;Equal default routes made off-subnet replies nondeterministic; the lower-metric VLAN route restored symmetry.&lt;/figcaption&gt;
&lt;/figure&gt;
&lt;p&gt;After the route fix, off-subnet curls returned HTTP 200 and &lt;code&gt;ip route get&lt;/code&gt;
showed the macvlan gateway. Then a later container recreation broke ingress once
more.&lt;/p&gt;
&lt;h2 id=&#34;the-macvlan-mac-changed-after-recreation&#34;&gt;The macvlan MAC changed after recreation
&lt;/h2&gt;&lt;p&gt;The macvlan attachment had a static IP but no static MAC. Podman generated a new
MAC when Traefik was recreated. OPNsense and the upstream switching path still
associated &lt;code&gt;10.100.100.21&lt;/code&gt; with the previous MAC.&lt;/p&gt;
&lt;p&gt;The container health and route table still looked correct because the stale
entry was on OPNsense and the switching path, not inside the container.&lt;/p&gt;
&lt;p&gt;I pinned the attachment identity in the Podman network specification:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;div class=&#34;chroma&#34;&gt;
&lt;table class=&#34;lntable&#34;&gt;&lt;tr&gt;&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code&gt;&lt;span class=&#34;lnt&#34;&gt;1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;
&lt;td class=&#34;lntd&#34;&gt;
&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nix&#34; data-lang=&#34;nix&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;podman_network_vlan_100:ip=10.100.100.21,mac=02:42:0a:64:64:15&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;
&lt;/div&gt;
&lt;/div&gt;&lt;p&gt;The locally administered &lt;code&gt;02:42&lt;/code&gt; prefix avoids claiming a vendor identity. The
remaining octets encode the private address, which makes the value easy to audit.
More importantly, recreation no longer changes the layer-2 identity.&lt;/p&gt;
&lt;h2 id=&#34;checks-after-the-migration&#34;&gt;Checks after the migration
&lt;/h2&gt;&lt;p&gt;A reverse proxy health check proves only that Traefik can answer inside its own
namespace. My final test matrix covered each boundary explicitly:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Test&lt;/th&gt;
          &lt;th&gt;What it proves&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Curl backend from NixOS host&lt;/td&gt;
          &lt;td&gt;Backend process is listening&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Curl backend from Traefik container&lt;/td&gt;
          &lt;td&gt;Host firewall, bind address, and app allow list work&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Request ingress from VLAN 100&lt;/td&gt;
          &lt;td&gt;Macvlan listener and on-link return path work&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Request ingress from a different VLAN&lt;/td&gt;
          &lt;td&gt;Routed return path uses the correct gateway&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Inspect &lt;code&gt;ip route get&lt;/code&gt; in the namespace&lt;/td&gt;
          &lt;td&gt;Route choice is deterministic&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Recreate Traefik and repeat all tests&lt;/td&gt;
          &lt;td&gt;MAC and post-start route configuration persist&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;I also checked the gateway neighbor entry after recreation rather than waiting
for a browser symptom.&lt;/p&gt;
&lt;h2 id=&#34;key-points&#34;&gt;Key points
&lt;/h2&gt;&lt;p&gt;Moving Traefik into a container changed more than the process manager. The native
service could reach host loopback and used the host routing table. The container
could use only its attached interfaces and had multiple default routes.&lt;/p&gt;
&lt;p&gt;For a similar migration, check all backend bind addresses, test from inside the
Traefik container, test ingress from another subnet and recreate the container
once before considering it complete. A static macvlan IP should also have a
stable MAC when upstream devices keep neighbor entries for it.&lt;/p&gt;
</description>
        </item>
        
    </channel>
</rss>
