<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nancythedev</title>
    <description>The latest articles on DEV Community by Nancythedev (@nancy_ad2c2b600230bab88f0).</description>
    <link>https://dev.to/nancy_ad2c2b600230bab88f0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4088416%2F39af3e13-a746-4294-a786-c06cb1ea71e5.png</url>
      <title>DEV Community: Nancythedev</title>
      <link>https://dev.to/nancy_ad2c2b600230bab88f0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nancy_ad2c2b600230bab88f0"/>
    <language>en</language>
    <item>
      <title>Docker Compose merges your ports, it doesn't replace them</title>
      <dc:creator>Nancythedev</dc:creator>
      <pubDate>Fri, 21 Aug 2026 14:28:57 +0000</pubDate>
      <link>https://dev.to/nancy_ad2c2b600230bab88f0/docker-compose-merges-your-ports-it-doesnt-replace-them-3k0d</link>
      <guid>https://dev.to/nancy_ad2c2b600230bab88f0/docker-compose-merges-your-ports-it-doesnt-replace-them-3k0d</guid>
      <description>&lt;p&gt;I vendor a third-party compose file rather than hand-editing it, because upstream changes it&lt;br&gt;
between releases and edits turn every refresh into a manual merge. My own changes go in an&lt;br&gt;
override file.&lt;/p&gt;

&lt;p&gt;Upstream published a service like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That binds every interface. I wanted it on loopback only, so the override said:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8080:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reads like a fix. It is not one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose merges sequences, it doesn't replace them
&lt;/h2&gt;

&lt;p&gt;The effective configuration was both entries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
    &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&lt;/span&gt;        &lt;span class="c1"&gt;# upstream's, still every interface&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ingress&lt;/span&gt;
    &lt;span class="na"&gt;host_ip&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;127.0.0.1&lt;/span&gt;       &lt;span class="c1"&gt;# mine&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
    &lt;span class="na"&gt;published&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;My override did not override anything. It appended. The file read as a security fix and the&lt;br&gt;
service still answered every interface.&lt;/p&gt;

&lt;p&gt;I only found out because the two bindings collided on the same port and the container refused to&lt;br&gt;
start. Pick a different host port, or have upstream pick one, and it merges cleanly and ships. The&lt;br&gt;
bug would have passed review, because the override file says &lt;code&gt;127.0.0.1&lt;/code&gt; right there in it.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;!override&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1:8080:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;!override&lt;/code&gt; replaces the parent's list instead of extending it. There is also &lt;code&gt;!reset&lt;/code&gt; to drop an&lt;br&gt;
inherited value entirely.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part I changed my mind about
&lt;/h2&gt;

&lt;p&gt;I wrote a test afterwards that grepped the compose file for &lt;code&gt;127.0.0.1&lt;/code&gt;. Then I deleted it,&lt;br&gt;
because it checks a &lt;strong&gt;declaration&lt;/strong&gt; — which is the same class of mistake one level up. The&lt;br&gt;
declaration was already correct and already wrong.&lt;/p&gt;

&lt;p&gt;What replaced it opens a socket:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_it_is_not_reachable_from_the_network&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;can_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# it is actually up
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;address&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;this_machines_own_addresses&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;can_connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;PORT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# and only there
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first assertion carries as much weight as the second. Without it a stopped service makes the&lt;br&gt;
test pass for the wrong reason, which is how a security test quietly stops testing anything.&lt;/p&gt;

&lt;p&gt;One caveat worth knowing on Windows: a machine connecting to its own address does not traverse the&lt;br&gt;
host firewall. So a test like this proves the &lt;strong&gt;binding&lt;/strong&gt; and says nothing about firewall rules.&lt;br&gt;
Those have to be read separately.&lt;/p&gt;

&lt;p&gt;Which is where I found the same shape a second time. Firewall rules are a permissive union — if&lt;br&gt;
any enabled allow-rule matches, the traffic is permitted. A narrowly scoped rule sitting beside a&lt;br&gt;
broad one for the same program is not a narrow rule. It is a broad rule with a reassuring name.&lt;/p&gt;

&lt;p&gt;Two controls, same failure, found a week apart: the thing that looks like the fix is not always&lt;br&gt;
the thing doing the work. Check the behaviour, not the declaration.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>security</category>
      <category>lessons</category>
    </item>
  </channel>
</rss>
