<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Jungletronics - Medium]]></title>
        <description><![CDATA[Education &amp; STEAM Learning by building. Practical explorations in AI, programming, electronics, robotics, IoT, and other STEAM topics — from ideas and experiments to working products. Follow and build with us. - Medium]]></description>
        <link>https://medium.com/jungletronics?source=rss----1cb6b57a12c8---4</link>
        <image>
            <url>https://cdn-images-1.medium.com/proxy/1*TGH72Nnw24QL3iV9IOm4VA.png</url>
            <title>Jungletronics - Medium</title>
            <link>https://medium.com/jungletronics?source=rss----1cb6b57a12c8---4</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Thu, 12 Nov 2026 11:04:30 GMT</lastBuildDate>
        <atom:link href="https://medium.com/feed/jungletronics" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[XML Canonicalization: The Normalization Step Your Ingestion Pipeline May Need]]></title>
            <link>https://medium.com/jungletronics/xml-canonicalization-the-normalization-step-your-ingestion-pipeline-may-need-2db1daffee96?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/2db1daffee96</guid>
            <category><![CDATA[c14n]]></category>
            <category><![CDATA[xmlsecurity]]></category>
            <category><![CDATA[xml]]></category>
            <category><![CDATA[canonicalization]]></category>
            <category><![CDATA[data-engineering]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Sat, 03 Oct 2026 21:52:07 GMT</pubDate>
            <atom:updated>2026-10-03T22:37:55.042Z</atom:updated>
            <content:encoded><![CDATA[<h4>XML looks simple, but applications can take quite a few liberties with how the same information is represented</h4><p>Whitespace can change. Attributes can appear in a different order. Empty elements can be written differently. Encoding and namespace details can also affect the raw representation.</p><p>For humans, these differences may be irrelevant.</p><p>For a computer calculating a hash, comparing documents, or verifying a digital signature, they can be significant.</p><p>That’s where <strong>XML canonicalization</strong>, commonly called <strong>C14N</strong>, comes in.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*j01OYdMszaswMHvtY2viDA.png" /><figcaption><a href="https://colab.research.google.com/drive/1Y0bGDV65gv7Bi0c6bsppnpKVsnupY7hx?usp=sharing">https://colab.research.google.com/drive/1Y0bGDV65gv7Bi0c6bsppnpKVsnupY7hx?usp=sharing</a></figcaption></figure><h3>Canonicalization is not just formatting</h3><p>A useful way to think about it is:</p><blockquote><strong><em>A formatter makes XML easier for humans to read. A canonicalizer makes XML deterministic for machines.</em></strong></blockquote><p>The <a href="https://www.w3.org/TR/xml-c14n11/?utm_source=chatgpt.com">W3C</a> standardized XML canonicalization specifically to provide a defined way of producing a canonical representation of XML.</p><p>This becomes particularly interesting in an <strong>ingestion pipeline</strong>.</p><h3>Canonicalization as a pipeline component</h3><p>Imagine an ingestion system receiving XML from different sources:</p><pre>             XML input<br>                 │<br>                 ▼<br>        ┌─────────────────┐<br>        │ Parse / Validate│<br>        └────────┬────────┘<br>                 │<br>                 ▼<br>        ┌─────────────────┐<br>        │ Canonicalization│<br>        │      (C14N)     │<br>        └────────┬────────┘<br>                 │<br>                 ▼<br>        ┌─────────────────┐<br>        │ Hash / Compare / │<br>        │ Sign / Identify  │<br>        └────────┬────────┘<br>                 │<br>                 ▼<br>          Store / Process</pre><p>The important idea is that canonicalization creates a <strong>deterministic representation</strong> before operations that depend on the representation itself.</p><p>For example, an ingestion system might use the canonical XML to calculate a fingerprint:</p><pre>XML<br> ↓<br>C14N<br> ↓<br>SHA-256<br> ↓<br>content fingerprint</pre><p>This is particularly useful in ingestion systems for:</p><ul><li><strong>Deduplication</strong> — recognize equivalent XML representations.</li><li><strong>Integrity verification</strong> — detect meaningful changes.</li><li><strong>Digital signatures</strong> — ensure formatting changes don’t invalidate the signed XML.</li><li><strong>Change detection</strong> — distinguish serialization differences from actual data changes.</li><li><strong>Reproducible processing</strong> — normalize input before downstream operations.</li></ul><h3>Let’s see it in Python</h3><p>You can run the following example directly in <a href="https://colab.research.google.com/"><strong>Google Colab</strong></a>.</p><p>Python’s xml.etree.ElementTree provides XML canonicalization through canonicalize() in modern Python versions.</p><h3>1. Create two XML documents</h3><p>Let’s start with two XML documents that contain the same attributes, but serialize those attributes differently:</p><pre>import xml.etree.ElementTree as ET<br><br>xml_a = &#39;&lt;product name=&quot;Widget&quot; id=&quot;123&quot;&gt;&lt;/product&gt;&#39;<br>xml_b = &#39;&lt;product id=&quot;123&quot; name=&quot;Widget&quot;&gt;&lt;/product&gt;&#39;<br></pre><p>The XML represents the same element and attributes, but the <strong>attribute order is different</strong>.</p><p>At the byte level, they are clearly different:</p><pre>print(xml_a.encode() == xml_b.encode())</pre><p>Output:</p><pre>False</pre><p>Now let’s calculate a hash directly from the original XML strings:</p><pre>import hashlib<br><br>hash_a = hashlib.sha256(xml_a.encode()).hexdigest()<br>hash_b = hashlib.sha256(xml_b.encode()).hexdigest()<br><br>print(hash_a)<br>print(hash_b)</pre><p>The hashes are different:</p><pre>f312802c117a6b84f8e6b65c9b8a5adf8148b00ab5985b5ca5938e1a985a8f99<br>d01eb593bfc5662ab9ab89744e4a6fcafe08f7cc31f40d41eff6a3b3a7965e30</pre><p>Look closely ☝️ — these are <strong>two different fingerprints</strong>.</p><p>That’s expected.</p><p>The computer is hashing the <strong>serialized bytes</strong>, not trying to understand that both XML documents represent the same structure.</p><p>This is where canonicalization can help.</p><p>Instead of hashing the original XML directly, we first canonicalize both documents:</p><pre>canonical_a = ET.canonicalize(xml_data=xml_a)<br>canonical_b = ET.canonicalize(xml_data=xml_b)<br><br>print(canonical_a)<br>print(canonical_b)<br><br>print(canonical_a == canonical_b)</pre><p>Output:</p><pre>True</pre><p>Now both XML documents have the same canonical representation.</p><p>We can then hash <strong>the canonical representation</strong>, rather than the original serialization:</p><pre>canonical_hash_a = hashlib.sha256(<br>    canonical_a.encode(&quot;utf-8&quot;)<br>).hexdigest()<br><br>canonical_hash_b = hashlib.sha256(<br>    canonical_b.encode(&quot;utf-8&quot;)<br>).hexdigest()<br><br>print(canonical_hash_a)<br>print(canonical_hash_b)<br><br>print(canonical_hash_a == canonical_hash_b)</pre><p>Output:</p><pre>True</pre><p>The important distinction is:</p><pre>Original XML A → → SHA-256 → → Fingerprint A<br>Original XML B → → SHA-256 → → Fingerprint B<br><br>                        ↓<br><br>                 Different hashes</pre><pre>But when canonicalization is appropriate:</pre><pre>XML A → → C14N → → Canonical A → → SHA-256 → → Fingerprint<br>XML B → → C14N → →C anonical B → → SHA-256 → → Same fingerprint</pre><p>So the core lesson is:</p><blockquote><strong><em>Hashing the original XML can produce different fingerprints for different serializations. Canonicalizing first can produce the same fingerprint when the differences are ones that C14N is designed to normalize.</em></strong></blockquote><p>And one important caveat:</p><blockquote><strong><em>C14N is not a general-purpose “make equivalent XML identical” operation.</em></strong><em> It follows precisely defined normalization rules and preserves information that may be meaningful, including text-node content. For example, whitespace inside an XML element can affect the canonical result.</em></blockquote><h3>2. Canonicalize the XML</h3><p>Now let’s introduce the pipeline component:</p><pre>canonical_a = ET.canonicalize(xml_data=xml_a)<br>canonical_b = ET.canonicalize(xml_data=xml_b)<br><br>print(canonical_a)<br>print(canonical_b)</pre><p>The canonicalizer produces a deterministic representation.</p><p>Now compare them:</p><pre>print(canonical_a == canonical_b)</pre><p>The result is:</p><pre>True</pre><p>That’s the important part.</p><p>Although the original XML strings were serialized differently, canonicalization gives us an equivalent canonical representation.</p><h3>3. Hash the canonical representation</h3><p>Now we can use that canonical representation as the input to our fingerprinting step:</p><pre>canonical_hash_a = hashlib.sha256(<br>    canonical_a.encode(&quot;utf-8&quot;)<br>).hexdigest()<br><br>canonical_hash_b = hashlib.sha256(<br>    canonical_b.encode(&quot;utf-8&quot;)<br>).hexdigest()<br><br>print(canonical_hash_a)<br>print(canonical_hash_b)<br><br>print(canonical_hash_a == canonical_hash_b)</pre><p>The final result is:</p><pre>d01eb593bfc5662ab9ab89744e4a6fcafe08f7cc31f40d41eff6a3b3a7965e30<br>d01eb593bfc5662ab9ab89744e4a6fcafe08f7cc31f40d41eff6a3b3a7965e30<br>True</pre><p>Our pipeline is now:</p><pre>Different XML serializations<br>          │<br>          ▼<br>      Canonicalize<br>          │<br>          ▼<br> Same canonical representation<br>          │<br>          ▼<br>       SHA-256<br>          │<br>          ▼<br>    Same fingerprint</pre><p>This is a much better foundation for applications that need to reason about <strong>XML equivalence rather than XML formatting</strong>.</p><h3>Why would an ingestion system care?</h3><p>Imagine an ingestion service receiving thousands of XML documents.</p><p>Without normalization, the system might encounter:</p><pre>Document A → hash X<br>Document B → hash Y</pre><p>even though both documents represent the same XML structure.</p><p>Depending on the application’s requirements, that can complicate:</p><ul><li>deduplication;</li><li>integrity checks;</li><li>change detection;</li><li>digital signatures;</li><li>reproducible processing.</li></ul><p>Canonicalization gives the pipeline a standardized point at which the representation becomes deterministic.</p><p>However, it is important to understand what C14N <strong>doesn’t</strong> do.</p><p>It does not magically determine that two XML documents are semantically equivalent in every possible sense.</p><p>It applies a defined set of canonicalization rules.</p><p>For example, two documents could contain different values:</p><pre>&lt;price&gt;100&lt;/price&gt;</pre><p>versus:</p><pre>&lt;price&gt;101&lt;/price&gt;</pre><p>Canonicalization will not make those equal — and it shouldn’t.</p><p>Canonicalization is about <strong>normalizing representation</strong>, not changing meaning.</p><h3>The mental model</h3><p>A simple way to remember it:</p><pre>Pretty printing<br>     ↓<br>&quot;Make it nice for humans&quot;<br><br><br><br>Canonicalization<br>     ↓<br>&quot;Make its representation deterministic for machines&quot;</pre><p>And in an ingestion architecture:</p><pre>             INPUT<br>               │<br>               ▼<br>        Parse / Validate<br>               │<br>               ▼<br>        ┌──────────────┐<br>        │     C14N     │  ← deterministic representation<br>        └──────┬───────┘<br>               │<br>          ┌────┴─────┐<br>          ▼          ▼<br>        Hash       Sign<br>          │          │<br>          └────┬─────┘<br>               ▼<br>            Storage</pre><p>That makes canonicalization less mysterious. ❤️</p><p>It isn’t simply an XML formatter.</p><p><strong>It is a normalization component that can sit between XML ingestion and operations that depend on a stable representation.</strong></p><h4>Note ¹</h4><h3><strong><em>Why C14N?</em></strong></h3><p>Because <strong>C14N is a numeronym</strong>.</p><p>It comes from the word:</p><p><strong>Canonicalization</strong></p><p>Take the first and last letters:</p><ul><li><strong>C</strong> — first letter</li><li><strong>14</strong> — number of letters between the first and last letters</li><li><strong>N</strong> — last letter</li></ul><p>So:</p><blockquote><strong><em>Canonicalization → C + 14 + N → C14N</em></strong></blockquote><p>It’s the same naming pattern as:</p><ul><li><strong>i18n</strong> → internationalization</li><li><strong>l10n</strong> → localization</li><li><strong>a11y</strong> → accessibility</li></ul><p>In XML security, you’ll often see <strong>C14N</strong> used as shorthand for <strong>XML Canonicalization</strong>, especially when discussing the W3C canonicalization algorithms.</p><p>A nice little detail for your tutorial would be:</p><blockquote><strong><em>Why C14N?</em></strong><em> It’s a numeronym: </em>Canonicalizatio<strong>n</strong><em> has 14 letters between its first and last letters, giving us </em><strong><em>C14N</em></strong><em>.</em></blockquote><p>See also: <a href="https://medium.com/jungletronics/using-gemini-chat-on-collab-2626fb035176">https://medium.com/jungletronics/using-gemini-chat-on-collab-2626fb035176</a></p><blockquote><strong>Disclosure:</strong> This tutorial is an educational exploration of XML canonicalization and ingestion pipelines. The examples are intentionally simplified and designed for experimentation in Google Colab; they should not be treated as production-ready security or data-processing guidance without appropriate validation for the target environment.</blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2db1daffee96" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/xml-canonicalization-the-normalization-step-your-ingestion-pipeline-may-need-2db1daffee96">XML Canonicalization: The Normalization Step Your Ingestion Pipeline May Need</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[What Does 172.22.0.0/16 Actually Mean? IP Masks Explained with a Real Docker Bug]]></title>
            <link>https://medium.com/jungletronics/what-does-172-22-0-0-16-actually-mean-ip-masks-explained-with-a-real-docker-bug-ea72a82b1eed?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/ea72a82b1eed</guid>
            <category><![CDATA[beginners-guide]]></category>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[docker]]></category>
            <category><![CDATA[networking]]></category>
            <category><![CDATA[troubleshooting]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Mon, 28 Sep 2026 22:10:43 GMT</pubDate>
            <atom:updated>2026-10-03T12:39:05.947Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>A beginner’s guide to IP addresses, subnet masks and CIDR, built around a bug that took hours to find: a container that could reach the internet, but not the database sitting right next door.</em></h4><h3>The bug that motivates this tutorial</h3><p>A Python pipeline ran fine on a laptop. Inside Docker, the same code failed to reach a database:</p><pre>No route to host</pre><p>The credentials were right, the VPN was up, the database was online. The cause turned out to be two numbers:</p><ul><li>the database lived at <strong>172.22.1.10</strong> on the company network;</li><li>Docker had created the project network as <strong>172.22.0.0/16</strong>.</li></ul><p>If you can explain why those two lines clash, you understand IP masks. By the end of this tutorial, you will.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6WVqNBuxAwWtv_cv6Pv5zA.png" /></figure><h3>1. An IP address is just 32 bits</h3><p>An IPv4 address such as 172.22.1.10 is a 32-bit number, written as four <strong>octets</strong> (8 bits each) in decimal:</p><pre>172      .22       .1        .10<br>10101100  00010110  00000001  00001010</pre><p>Every octet goes from 0 to 255, because that’s what 8 bits can hold.</p><h3>2. Network part and host part</h3><p>An address has two parts, like a street address:</p><ul><li>the <strong>network</strong> part: which street;</li><li>the <strong>host</strong> part: which house on that street.</li></ul><p>The <strong>mask</strong> says where the street ends and the house number begins.</p><h3>3. The mask, two ways of writing it</h3><p>The mask is also 32 bits: a run of <strong>1s</strong> (network part) followed by <strong>0s</strong> (host part).</p><pre>/16  →  11111111.11111111.00000000.00000000  →  255.255.0.0<br>/24  →  11111111.11111111.11111111.00000000  →  255.255.255.0</pre><ul><li><strong>Dotted notation:</strong> 255.255.0.0</li><li><strong>CIDR notation:</strong> /16, meaning &quot;the first 16 bits are the network&quot;</li></ul><p>They mean the same thing. CIDR (<em>Classless Inter-Domain Routing</em>) is shorter and is what Docker, cloud providers and most tools use.</p><h3>4. Reading 172.22.0.0/16</h3><p>/16 fixes the first 16 bits, which is exactly the first two octets:</p><pre>172.22 . x.x<br>└─────┘  └──┘<br>network  host (free)</pre><p>So the network <strong>172.22.0.0/16</strong> covers every address from 172.22.0.0 to 172.22.255.255.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/321/1*eKg4Rc1_YycoRwMFYfrHlQ.png" /></figure><p>The first address names the network itself and the last one is for broadcast, so neither goes to a machine.</p><p><strong>Quick rule:</strong> a network with prefix /n has <strong>2^(32 − n)</strong> addresses.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/405/1*0iiYGnQIPgOL_CLpLGBduA.png" /></figure><p>A smaller prefix means a bigger network. Every step down doubles the size: /23 is twice a /24.</p><h3>5. Private address ranges</h3><p>Three ranges are reserved for private networks (RFC 1918). They never appear on the public internet, so any company, home or Docker install can reuse them:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/510/1*drkyocErGD4S8VYH3DIlww.png" /></figure><p>Why does 172.16.0.0/12 stop at 172.31? With /12, the network part is the first octet plus the <strong>first 4 bits</strong> of the second octet:</p><pre>16 = 0001 0000<br>31 = 0001 1111<br>     └──┘ fixed    └──┘ free → 16 values: 16, 17, ..., 31</pre><p><strong>This is the root of the bug.</strong> Everyone reuses the same private ranges. Your company may use 172.22.x.x for servers, and Docker, on the same machine, may pick 172.22.x.x for its own network. Nothing stops them from colliding.</p><h3>6. How a machine decides where to send a packet</h3><p>Every machine, and every container, has a <strong>routing table</strong>. Here is roughly what a container on the network 172.22.0.0/16 has:</p><pre>$ ip route<br>default via 172.22.0.1 dev eth0          ← anything else: send to the gateway<br>172.22.0.0/16 dev eth0 scope link        ← this network: deliver directly</pre><p>To send a packet, the machine picks the <strong>most specific</strong> entry that contains the destination. This is the <strong>longest prefix match</strong>: /16 beats default, which is /0.</p><p>Now send a packet to the database at 172.22.1.10:</p><ol><li>Does 172.22.1.10 fall inside 172.22.0.0/16? <strong>Yes.</strong></li><li>So the container treats it as a <strong>neighbor</strong> and tries to deliver it directly on its local network, without the gateway.</li><li>It asks “who has 172.22.1.10?” on the Docker network (an ARP request). The real database is on the company network, outside Docker, so <strong>nobody answers</strong>.</li><li>After a few tries, the kernel gives up with EHOSTUNREACH: <strong>No route to host</strong>.</li></ol><pre>Overlapping (broken):<br>container 172.22.0.2 ── &quot;172.22.1.10 is local&quot; ──► asks the Docker network ──► silence ✘</pre><pre>Not overlapping (works):<br>container 192.168.250.2 ── &quot;not local&quot; ──► gateway ──► host ──► VPN ──► database ✔</pre><p>This also explains why the code worked <strong>outside</strong> Docker: the laptop’s own routing table had no 172.22.0.0/16 entry, so the packet went to the VPN as expected.</p><h3>7. The fix</h3><p>Pin the Docker network to a range that can’t overlap your company’s:</p><pre># docker-compose.yml<br>networks:<br>  default:<br>    ipam:<br>      config:<br>        - subnet: 192.168.250.0/24</pre><p>192.168.250.0/24 gives 254 usable addresses, far more than a few containers need, and sits well away from 172.16.0.0/12.</p><p>To stop Docker from picking risky ranges for <strong>all</strong> new networks, set its default pools in /etc/docker/daemon.json (or Docker Desktop → Settings → Docker Engine), then restart Docker:</p><pre>{<br>  &quot;default-address-pools&quot;: [<br>    { &quot;base&quot;: &quot;10.200.0.0/16&quot;, &quot;size&quot;: 24 }<br>  ]<br>}</pre><p>Check with your network team first that the range you choose isn’t used anywhere in your company or VPN.</p><h3>8. Check for overlaps yourself</h3><p><strong>What subnets is Docker using right now?</strong></p><pre>docker network ls -q | xargs docker network inspect \<br>  --format &#39;{{.Name}}: {{range .IPAM.Config}}{{.Subnet}}{{end}}&#39;</pre><p><strong>Does an address fall inside a network?</strong> Python answers without any binary math:</p><pre>import ipaddress<br><br>docker_net = ipaddress.ip_network(&quot;172.22.0.0/16&quot;)<br>database   = ipaddress.ip_address(&quot;172.22.1.10&quot;)<br>print(database in docker_net)                                            # True  → trouble<br>print(docker_net.num_addresses)                                          # 65536<br>print(docker_net.overlaps(ipaddress.ip_network(&quot;172.22.1.0/24&quot;)))        # True<br>print(ipaddress.ip_network(&quot;192.168.250.0/24&quot;).overlaps(docker_net))     # False → safe</pre><p>A three-line habit before choosing a subnet: list the ranges your company and VPN use, and test every one with .overlaps().</p><h3>Practice</h3><ol><li>How many addresses are in 10.0.0.0/8?</li><li>What are the first and last addresses of 192.168.250.0/24?</li><li>Is 172.30.5.9 inside 172.16.0.0/12?</li><li>Docker created a network 172.20.0.0/16. Can a container reach a server at 172.22.1.10?</li><li>What is the mask of /20 in dotted notation?</li></ol><p>Answers</p><ol><li>2^(32−8) = 2²⁴ = <strong>16,777,216</strong>.</li><li>First (network) 192.168.250.0, last (broadcast) 192.168.250.255. Hosts use .1 to .254.</li><li><strong>Yes.</strong> /12 covers 172.16 to 172.31, and 30 is in that range.</li><li><strong>Yes.</strong> 172.20.0.0/16 only covers 172.20.x.x, so 172.22.1.10 is not local and goes to the gateway.</li><li>20 ones: 11111111.11111111.11110000.00000000 = <strong>255.255.240.0</strong>.</li></ol><h3>Key takeaways</h3><ul><li><strong>An IP address is 32 bits.</strong> The mask splits it into a network part and a host part.</li><li><strong>/n means the first n bits are the network.</strong> The network holds 2^(32 − n) addresses.</li><li><strong>172.22.0.0/16</strong> means everything from 172.22.0.0 to 172.22.255.255.</li><li><strong>Private ranges are shared by everyone</strong>, so overlaps between Docker, VPNs and corporate networks are common.</li><li><strong>Routing picks the most specific match.</strong> If a destination falls inside a local network, the packet never reaches the gateway.</li><li><strong>“Works outside Docker, fails inside” is a networking clue.</strong> Check for subnet overlap first.</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*U8gytfE623V7ZxWKg5xJmQ.png" /></figure><p><strong>Disclosure:</strong> This article is an educational and/or technical exploration based on concepts, experiments, and practical experience. Code samples, configurations, architectures, and examples may be simplified, adapted, or illustrative. No institutional source code, real production data, credentials, confidential information, or sensitive internal infrastructure details are reproduced.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=ea72a82b1eed" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/what-does-172-22-0-0-16-actually-mean-ip-masks-explained-with-a-real-docker-bug-ea72a82b1eed">What Does 172.22.0.0/16 Actually Mean? IP Masks Explained with a Real Docker Bug</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[From Invoice to Small Language Model: A Pipeline That Never Rounds]]></title>
            <link>https://medium.com/jungletronics/from-invoice-to-small-language-model-a-pipeline-that-never-rounds-21e45d3ad324?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/21e45d3ad324</guid>
            <category><![CDATA[data-engineering]]></category>
            <category><![CDATA[llm]]></category>
            <category><![CDATA[python]]></category>
            <category><![CDATA[machine-learning]]></category>
            <category><![CDATA[fine-tuning]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Mon, 28 Sep 2026 21:52:22 GMT</pubDate>
            <atom:updated>2026-10-03T12:24:16.076Z</atom:updated>
            <content:encoded><![CDATA[<h4><em>How to prepare millions of invoice line items to train an SLM that reads messy product descriptions, suggests the correct tax classification and flags inconsistencies. Including the traps almost nobody talks about.</em></h4><h3>The challenge</h3><p>In Brazil, every sale between businesses and consumers is backed by an <strong>electronic invoice</strong> (NF-e / NFC-e): a signed XML document with a fixed, public schema. For each line item, the invoice carries two fields that should say the same thing:</p><ul><li><strong>the commercial description</strong>, typed freely by the seller: CERV PILS LT 350ML (pilsner beer, 350 ml can), REFRI COLA PET 2L (2 L cola soda), PARAF SEXT ZB 1/4X2 (zinc-plated hex bolt), KIT PRESENTE C/3 (gift kit, 3 items);</li><li><strong>the tax classification code</strong> (NCM, the Mercosur Common Nomenclature, built on top of the international Harmonized System): 8 hierarchical digits that determine how the product is taxed.</li></ul><p>They don’t always agree. Descriptions are abbreviated, ambiguous and vary from seller to seller. The code is sometimes just wrong, by accident or not. Checking this by hand across millions of items a month is impossible.</p><p>The project question was:</p><blockquote><strong><em>Can a small language model (SLM), </em>running in a controlled environment<em>, read the description, suggest the correct code with a confidence level and flag when the declared code looks inconsistent?</em></strong></blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OOXJd4RhaHYeox3cS_UQ7Q.png" /></figure><p>“Small” is a requirement, not a detail. For sensitive tax data, the model should run inside the controlled environment where the data resides, the cost per inference must be low, and answers must come in batch, over millions of rows.</p><p>This tutorial covers the whole project, in two halves:</p><ol><li><strong>Data engineering:</strong> turning a raw extraction into a standardized, pseudonymized, validated and verifiable dataset (Parquet + ZSTD).</li><li><strong>Modeling:</strong> from that dataset to an SLM that classifies, gives a confidence and flags inconsistencies.</li></ol><p>The first half has been implemented and validated. The second half describes the design of a possible next phase.</p><h3>Architecture overview</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*RSAViLG2puDRLTLsHY8h_A.png" /><figcaption><strong>Figure 1.</strong> Data flow from HMAC-pseudonymized extraction through validation and model inference, ending in suggestions reviewed by human auditors.</figcaption></figure><h3>Part 1: Data engineering</h3><h3>1. Contract first, code second</h3><p>Before writing a single line of transformation, the output file was described as a <strong>contract</strong>: the name, type and requirement level of every field. Each field also records <strong>where it came from</strong> in the source, which gives end-to-end lineage.</p><pre>from dataclasses import dataclass<br>import polars as pl<br><br>ERROR, WARNING, FREE = &quot;ERROR&quot;, &quot;WARNING&quot;, &quot;FREE&quot;<br>@dataclass(frozen=True)<br>class Field:<br>    number: int<br>    name: str<br>    dtype: pl.DataType<br>    required: bool<br>    null_severity: str   # what happens if the value is null<br>    source: str          # source expression (lineage)<br>FIELDS = (<br>    Field(1,  &quot;invoice_hash&quot;,  pl.String,         True, ERROR,   &quot;HMAC(invoice access key)&quot;),<br>    Field(7,  &quot;xProd&quot;,         pl.String,         True, ERROR,   &quot;item description&quot;),<br>    Field(11, &quot;qCom&quot;,          pl.Decimal(15, 4), True, ERROR,   &quot;commercial quantity&quot;),<br>    Field(15, &quot;declared_ncm&quot;,  pl.String,         True, WARNING, &quot;NCM stated on the invoice&quot;),<br>    # ...<br>)</pre><p><em>(</em><em>xProd and </em><em>qCom are the official field names in the invoice XML schema.)</em></p><p><strong>The severity matrix</strong> separates three situations:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/567/1*4iCB0yW6F5ogavlsQ49eBQ.png" /></figure><p>It looks like bureaucracy. In practice, this contract ended endless discussions: any “can this field be empty?” question has a written, versioned, tested answer.</p><p>Three sources, three roles:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/706/1*jN2MiX13Zrp3CXuJKlAjxw.png" /></figure><p>One lesson: the extraction SQL is <strong>not</strong> the truth about types. When checking the dictionary, a year-month field that the script produced as &#39;YYYY/MM&#39; was stored as &#39;YYYYMM&#39;, and a numeric field arrived as text. Trust the database dictionary; use the SQL to understand lineage.</p><h3>2. Pseudonymization: the HMAC that protected nothing</h3><p>Identifiers (invoice key, issuer tax ID) cannot go into the dataset. The standard solution is an <strong>HMAC with a secret key</strong>: the same identifier always produces the same hash, so you can still group by it, but without the key nobody can get back to the original.</p><pre>-- Oracle: STANDARD_HASH takes no key; HMAC comes from DBMS_CRYPTO<br>RAWTOHEX(DBMS_CRYPTO.MAC(UTL_RAW.CAST_TO_RAW(access_key),<br>                         DBMS_CRYPTO.HMAC_SH256, :secret_key))</pre><p>During a review of the extraction script, this showed up:</p><pre>DBMS_CRYPTO.MAC(..., &lt;weak_placeholder_key&gt;)</pre><p><strong>A deliberately weak key would have only 256 possible values.</strong> Anyone who knows a single real identifier (a publicly printed invoice key, for instance) tries all 256 in milliseconds, finds the key and re-identifies the entire dataset:</p><pre>import hmac, hashlib<br><br>def recover_key(known_identifier: str, published_hash: str) -&gt; bytes | None:<br>    for k in range(256):<br>        key = bytes([k])<br>        h = hmac.new(key, known_identifier.encode(), hashlib.sha256).hexdigest().upper()<br>        if h == published_hash:<br>            return key<br>    return None</pre><p>In this case, the real key had been stripped from the shared script, and the value was just a placeholder. The rule still stands: <strong>an HMAC key must be at least 32 random bytes, live in a vault, and never appear in code, scripts or logs.</strong> A hash with a weak key is pseudonymization in name only.</p><h3>3. Exact conversion: silent rounding</h3><p>This was the most subtle trap in the project. Monetary values arrive from Oracle as NUMBER. The obvious approach is to read them with an explicit schema:</p><pre># DON&#39;T do this with money<br>df = pl.DataFrame(rows, schema={&quot;Prod_value&quot;: pl.Decimal(15, 2), ...}, orient=&quot;row&quot;)</pre><p>With an explicit schema, Polars <strong>rounds decimals and nulls out incompatible values without warning</strong>. 12.345 becomes 12.35, and no error shows up. In a tax dataset, that is unacceptable.</p><p>The solution has three parts.</p><p><strong>a) Read without imposing types.</strong> With fetch_decimals=True, the driver returns NUMBER as Python Decimal, and Polars infers the type from the values themselves:</p><pre>import oracledb<br><br>oracledb.defaults.fetch_decimals = True<br><br>with connection.cursor() as cur:<br>    cur.arraysize = 10_000<br>    cur.execute(sql)<br>    names = [d.name for d in cur.description]<br>    batches = []<br>    while rows := cur.fetchmany():<br>        batches.append(pl.DataFrame(rows, schema=names, orient=&quot;row&quot;, infer_schema_length=None))<br># batches may infer different scales: supertyping only widens, never rounds<br>df = pl.concat(batches, how=&quot;vertical_relaxed&quot;)</pre><p><strong>b) Convert, then prove the conversion was exact.</strong> Cast to the contract type, cast back to the source type and compare. If anything changed, the value does <strong>not</strong> go into the file:</p><pre>def exact_cast(name, source, target):<br>    original = pl.col(name)<br>    converted = original.cast(target, strict=False)<br>    failed = original.is_not_null() &amp; (<br>        converted.is_null() | (converted.cast(source) != original)<br>    )<br>    exact = pl.when(failed).then(None).otherwise(converted).cast(target)<br>    return exact.alias(name), failed.alias(f&quot;conversion:{name}&quot;)</pre><p>The final file holds <strong>either the exact source value or null, never an approximation</strong>. Every null produced by a conversion is flagged and becomes an ERROR in the report.</p><p><strong>c) Distrust regex.</strong> To validate numeric text, \d looks like the natural choice. But in Polars&#39; regex engine (Rust), \d matches Unicode digits from any script, including Arabic-Indic ٣٤٥. Use [0-9]:</p><pre>YEAR_MONTH = r&quot;^[0-9]{4}(0[1-9]|1[0-2])$&quot;<br>INTEGER    = r&quot;^(0|[1-9][0-9]*)$&quot;   # no sign, spaces or leading zeros</pre><p>For the same reason, <strong>code fields</strong> (NCM, tax operation codes, barcodes) are always text. If one arrives as a number, the run stops: 01012100 cast to an integer loses its leading zero and becomes a different code.</p><h3>4. Validate, never fix</h3><p>The validator applies about 36 rules: nulls by severity, domains ({0, 1}), formats (8-digit NCM, 64-character uppercase hex hashes), consistency (the document type matches the file) and duplicates.</p><p>The golden rule: <strong>identify and count, never fix or drop.</strong> A dataset “cleaned” by hidden heuristics is worse than a dirty one that is documented. The results go into a report:</p><pre>{<br>  &quot;status&quot;: &quot;OK&quot;,<br>  &quot;records&quot;: 1000000,<br>  &quot;records_with_error&quot;: 0,<br>  &quot;records_with_warning&quot;: 24107,<br>  &quot;findings&quot;: [<br>    {&quot;field&quot;: &quot;tax_status_code&quot;, &quot;rule&quot;: &quot;null&quot;, &quot;severity&quot;: &quot;WARNING&quot;, &quot;count&quot;: 4938}<br>  ],<br>  &quot;duplicate_item_hash&quot;: 0<br>}</pre><p><em>(Illustrative numbers.)</em></p><p>Duplicates deserve attention. A repeated invoice_hash is legitimate: one invoice has many items. A repeated item_hash (invoice + item number), however, means a JOIN in the extraction multiplied rows. That is a classic, silent bug.</p><p>The process exit code also carries meaning: 0 is OK, 1 means some records have ERRORs (the file was written), and 2 means a structural failure (no file was written). An orchestrator knows what to do without parsing logs.</p><h3>5. Parquet with ZSTD 18, atomic writes and proof</h3><pre>def write(df: pl.DataFrame, path: Path) -&gt; None:<br>    tmp = path.with_suffix(path.suffix + &quot;.tmp&quot;)<br>    df.write_parquet(tmp, compression=&quot;zstd&quot;, compression_level=18, statistics=True)<br>    os.replace(tmp, path)   # atomic: either the old file or the complete new one</pre><p>After writing, the pipeline <strong>reopens the file</strong> and proves four things:</p><ol><li>the schema matches the contract;</li><li>the content read back is identical to what was written (read_back.equals(expected));</li><li>every column in every row group uses ZSTD;</li><li>validating the file read back gives exactly the same result as validating before the write.</li></ol><p>A little-known detail: <strong>the compression level is not stored in Parquet metadata</strong>, only the codec. There is no way to prove, by reading the file, that it was compressed at level 18. The report says so honestly: “codec verified: ZSTD; level configured: 18”.</p><p><strong>Why level 18?</strong> ZSTD 18 compresses much more slowly than the default level (3), but decompresses at the same speed. For a file written once and read many times, that is a good trade.</p><h3>6. Estimating the size before having the data</h3><p>Before touching real data, we had to say how much space the files would take. The estimate started from synthetic data, column by column:</p><pre>import pyarrow.parquet as pq<br><br>meta = pq.ParquetFile(&quot;sample.parquet&quot;).metadata<br>rg = meta.row_group(0)<br>for i in range(rg.num_columns):<br>    c = rg.column(i)<br>    print(f&quot;{c.path_in_schema:28s} {c.total_compressed_size / meta.num_rows:6.1f} B/row&quot;)</pre><pre>item_hash                      33.0 B/row   ← HMAC: random, does not compress<br>invoice_hash                    8.2 B/row   ← repeats within an invoice<br>xProd                           2.0 B/row   ← synthetic: far too repetitive<br>...<br>total                          78   B/row</pre><p>Three takeaways:</p><ul><li><strong>Hashes set the floor.</strong> A hex-encoded HMAC-SHA256 is pure entropy: 64 characters compress to about 32 bytes. The hash columns alone accounted for more than half of the file.</li><li><strong>Synthetic data</strong><a href="#3318">¹</a><strong> lies about entropy.</strong> I revised the estimate upward (95 to 120 B/row), assuming real descriptions would be more varied than synthetic ones.</li><li><strong>Real data surprised in the other direction.</strong> The real file came in at about <strong>68 B/row</strong>, below even the synthetic one. Real commercial descriptions are far more repetitive than they look (the same product sold thousands of times with the same text), and ZSTD 18 takes full advantage of that.</li></ul><p>Moral: estimate from the structure (the hash floor was right), and <strong>measure on a real sample as soon as possible</strong>. With 5% of the data, the extrapolation error is small.</p><h3>7. The bug that wasn’t in the code: “No route to host”</h3><p>The pipeline runs in Docker. On the first run against real data, the database connection failed. Credentials, VPN and database were all fine, and <strong>outside the container the connection worked</strong>.</p><p>The cause: the database was reachable through a private network’s 172.x.x.x range. Docker automatically picks subnets for its internal networks between 172.17 and 172.31. When the project network landed on a subnet that <strong>covered the database address</strong>, the container started treating the database as a &quot;neighbor&quot; on the internal network. Packets never left for the VPN.</p><pre>container 172.x.0.2 ──► &quot;the database is a neighbor, deliver locally&quot; ──► nobody answers ✘</pre><p>What made it hard to diagnose:</p><ul><li>the failure was <strong>intermittent</strong>, because the subnet depends on how many networks Docker has already created, including for other projects;</li><li>the message (“No route to host”) points at the VPN or a firewall.</li></ul><p>The fix is a fixed subnet, far from the corporate network:</p><pre>networks:<br>  default:<br>    ipam:<br>      config:<br>        - subnet: 192.x.x.x/xx</pre><p><em>192.168.x.x is </em><strong><em>not public</em></strong><em> because the entire </em><em>192.168.0.0/16 range is reserved for </em><strong><em>private networks (RFC 1918)</em></strong><em>. These addresses are not directly routable on the public Internet.</em></p><p><em>So a subnet such as:</em></p><pre>192.168.x.x/xx</pre><p><em>represents a </em><strong><em>private/internal address range</em></strong><em>, not a public IP network.</em></p><p>For example, a Docker network may be configured with a private IPv4 subnet from the RFC 1918 address space. The exact subnet is intentionally omitted.</p><p>A better diagnosis came with it. For security, the log does not print the driver message (which contains host and service), but it now walks the exception chain down to the OSError and shows its type and errno, with a hint:</p><pre>def network_cause(error: BaseException) -&gt; str:<br>    cause = error.__cause__ or error.__context__<br>    while cause is not None and not isinstance(cause, OSError):<br>        cause = cause.__cause__ or cause.__context__<br>    if cause is None:<br>        return &quot;&quot;<br>    hint = {errno.EHOSTUNREACH: &quot;no route: VPN or Docker subnet overlap&quot;,<br>            errno.ENETUNREACH: &quot;network unreachable&quot;}.get(cause.errno, &quot;&quot;)<br>    return f&quot; | cause: {type(cause).__name__} errno={cause.errno} {hint}&quot;</pre><p><strong>A heuristic worth keeping:</strong> <em>works outside Docker, fails inside</em> points to Docker networking, not to your code.</p><h3>Part 2: From Parquet to SLM</h3><p>With a trustworthy dataset, the part that gets the most attention begins, and this is where the engineering in Part 1 pays off.</p><h3>8. The label is noisy, and that is the core problem</h3><p>The temptation is to train on xProd → declared_ncm. But the declared NCM is <strong>exactly what we want to audit</strong>: some of it is wrong. Training a model to imitate the declared label teaches it to repeat the mistakes.</p><p>The strategy:</p><ol><li><strong>Gold set:</strong> items whose code was <strong>reviewed by experts</strong>. The dataset has reviewed_ncm and reviewed_ncm_confidence fields for exactly this. It is small but reliable, and is used for evaluation and final fine-tuning.</li><li><strong>Silver set:</strong> declared labels filtered by <strong>consensus</strong>. If the same normalized description appears with the same code across many different issuers, the label is probably right.</li><li><strong>Drop from training</strong> descriptions with conflicting labels. They go to analysis, not to learning.</li></ol><pre>norm = (pl.col(&quot;xProd&quot;).str.to_uppercase()<br>        .str.replace_all(r&quot;[^A-Z0-9 ]&quot;, &quot; &quot;)<br>        .str.replace_all(r&quot;\s+&quot;, &quot; &quot;).str.strip_chars())<br><br>silver = (<br>    df.with_columns(desc=norm)<br>      .group_by(&quot;desc&quot;, &quot;declared_ncm&quot;)<br>      .agg(issuers=pl.col(&quot;issuer_hash&quot;).n_unique(), n=pl.len())<br>      .with_columns(share=pl.col(&quot;n&quot;) / pl.col(&quot;n&quot;).sum().over(&quot;desc&quot;))<br>      .filter((pl.col(&quot;issuers&quot;) &gt;= 5) &amp; (pl.col(&quot;share&quot;) &gt;= 0.9))<br>)</pre><h3>9. Split by issuer, or the model memorizes</h3><p>Every issuer has its own “dialect” of descriptions. If the same issuer appears in both training and test, the model memorizes its catalog, and the metric looks great and lies. The split is done <strong>by group</strong>, using the issuer hash. This is where pseudonymization shows its value: you can group without knowing who the issuer is.</p><pre>from sklearn.model_selection import GroupShuffleSplit<br><br>gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42)<br>train_idx, test_idx = next(gss.split(X, y, groups=issuers))</pre><p>Deduplicate before splitting as well: identical descriptions must not land on both sides.</p><h3>10. Start with an embarrassingly strong baseline</h3><p>Before any LLM, a classic baseline. With character n-grams, it handles abbreviations (CERV, REFRI, LT) well and is often surprisingly strong:</p><pre>from sklearn.feature_extraction.text import TfidfVectorizer<br>from sklearn.linear_model import SGDClassifier<br>from sklearn.pipeline import make_pipeline<br><br>baseline = make_pipeline(<br>    TfidfVectorizer(analyzer=&quot;char_wb&quot;, ngram_range=(2, 5), min_df=3, sublinear_tf=True),<br>    SGDClassifier(loss=&quot;log_loss&quot;, alpha=1e-6, max_iter=20),<br>)<br>baseline.fit(train_desc, train_ncm)</pre><p>The SLM has to <strong>beat this baseline</strong> to justify its cost. If it doesn’t, the honest answer is to ship the baseline.</p><h3>11. Evaluate along the hierarchy</h3><p>NCM is hierarchical: chapter (2 digits) → heading (4) → subheading (6) → item (8). Mistaking 22030000 (beer) for 22029900 (another beverage) is far less serious than mistaking it for a code in the machinery chapter. Evaluation measures accuracy at every level:</p><pre>def hierarchical_accuracy(y_true, y_pred):<br>    return {level: sum(t[:n] == p[:n] for t, p in zip(y_true, y_pred)) / len(y_true)<br>            for level, n in [(&quot;chapter&quot;, 2), (&quot;heading&quot;, 4), (&quot;subheading&quot;, 6), (&quot;item&quot;, 8)]}</pre><p>Also report top-3 accuracy at item level and macro-F1 per chapter. Micro-accuracy hides the rare chapters, which tend to be exactly the ones with the highest tax risk.</p><h3>12. SLM architecture: retrieve, then decide</h3><p>There are roughly 10,000 NCM codes. Asking a small model to “generate” a code freely leads to codes that don’t exist. The architecture splits the work in two steps:</p><pre>description ──► retriever (kNN on n-grams / embeddings) ──► top-10 candidates<br>                                                               │<br>                                        LoRA-tuned SLM ◄───────┘<br>                                               │<br>                              scores each candidate (log-prob)<br>                                               │<br>                           probability distribution over the 10</pre><p><strong>a) Candidate retrieval.</strong> Nearest neighbors among labeled descriptions, plus the official nomenclature descriptions:</p><pre>from sklearn.neighbors import NearestNeighbors<br><br>vectorizer = TfidfVectorizer(analyzer=&quot;char_wb&quot;, ngram_range=(2, 5), min_df=2)<br>V = vectorizer.fit_transform(reference_desc)<br>knn = NearestNeighbors(n_neighbors=50, metric=&quot;cosine&quot;).fit(V)<br>def candidates(desc: str, k: int = 10) -&gt; list[str]:<br>    _, idx = knn.kneighbors(vectorizer.transform([desc]))<br>    seen = []<br>    for i in idx[0]:<br>        if reference_ncm[i] not in seen:<br>            seen.append(reference_ncm[i])<br>        if len(seen) == k:<br>            break<br>    return seen</pre><p>The metric that matters here is <strong>recall@10</strong>: if the right code is not among the candidates, the SLM has no way to get it right.</p><p><strong>b) LoRA fine-tuning.</strong> An instruction model with 0.5 to 3 billion parameters, trained to choose among the candidates:</p><pre>from peft import LoraConfig<br>from trl import SFTConfig, SFTTrainer<br><br>def to_chat(ex):<br>    return {&quot;messages&quot;: [<br>        {&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: &quot;Classify the product into its NCM code. Answer with the 8 digits only.&quot;},<br>        {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: f&quot;Description: {ex[&#39;xProd&#39;]}\nUnit: {ex[&#39;uCom&#39;]}\n&quot;<br>                                    f&quot;Candidates: {&#39;, &#39;.join(ex[&#39;candidates&#39;])}&quot;},<br>        {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: ex[&quot;target_ncm&quot;]},<br>    ]}<br>trainer = SFTTrainer(<br>    model=&quot;&lt;1-to-3B-instruct-model&gt;&quot;,<br>    train_dataset=train_dataset.map(to_chat),<br>    args=SFTConfig(output_dir=&quot;slm-ncm&quot;, num_train_epochs=2,<br>                   per_device_train_batch_size=16, learning_rate=2e-4),<br>    peft_config=LoraConfig(r=16, lora_alpha=32, target_modules=&quot;all-linear&quot;,<br>                           task_type=&quot;CAUSAL_LM&quot;),<br>)<br>trainer.train()</pre><p>One training detail that matters: <strong>shuffle the candidate order</strong> and make sure the right code is not always first in the list. Otherwise the model learns to copy a position instead of understanding the description.</p><p><strong>c) Score instead of generate.</strong> Instead of letting the model generate free text, compute the log-probability of <strong>each candidate</strong> and apply a softmax across them. The answer is always a valid code, and it comes with a probability distribution:</p><pre>import torch<br><br>@torch.no_grad()<br>def score(model, tok, messages, candidates):<br>    prompt = tok.apply_chat_template(messages, add_generation_prompt=True, return_tensors=&quot;pt&quot;)<br>    logps = []<br>    for c in candidates:<br>        target = tok(c, add_special_tokens=False, return_tensors=&quot;pt&quot;).input_ids<br>        ids = torch.cat([prompt, target], dim=1)<br>        logits = model(ids).logits[0, prompt.shape[1] - 1 : -1]<br>        logps.append(torch.log_softmax(logits, -1).gather(1, target[0].unsqueeze(1)).sum())<br>    return dict(zip(candidates, torch.softmax(torch.stack(logps), 0).tolist()))</pre><h3>13. Confidence that means something</h3><p>A model that says “95% confident” should be right about 95% of the times it says so. Neural networks tend to be <strong>overconfident</strong>. The fix is calibration with <em>temperature scaling</em>, fitted on a held-out set, and the check is <strong>ECE</strong> (<em>expected calibration error</em>):</p><pre>import numpy as np<br><br>def ece(confidences, correct, n_bins=10):<br>    confidences, correct = np.asarray(confidences), np.asarray(correct)<br>    bins = np.linspace(0, 1, n_bins + 1)<br>    total = 0.0<br>    for lo, hi in zip(bins[:-1], bins[1:]):<br>        m = (confidences &gt; lo) &amp; (confidences &lt;= hi)<br>        if m.any():<br>            total += m.mean() * abs(correct[m].mean() - confidences[m].mean())<br>    return total</pre><p>The model output uses the same scale as the dataset: confidence <strong>0, 1 or 2</strong> (low, medium, high), with bands set from the calibration set rather than picked by eye.</p><h3>14. Flag inconsistencies without crying wolf</h3><p>The end product is not “the model thinks the code is X”. It is a <strong>triage queue</strong> for human auditors, ranked by evidence:</p><pre>def inconsistency(probs: dict[str, float], declared: str, threshold: float = 0.85) -&gt; dict | None:<br>    suggested, p_sug = max(probs.items(), key=lambda kv: kv[1])<br>    p_decl = probs.get(declared, 0.0)   # declared code not among candidates: strong evidence against it<br>    if suggested == declared or p_sug &lt; threshold:<br>        return None<br>    return {<br>        &quot;suggested&quot;: suggested,<br>        &quot;confidence&quot;: p_sug,<br>        &quot;p_declared&quot;: p_decl,<br>        &quot;level&quot;: next(lvl for lvl, k in [(&quot;chapter&quot;, 2), (&quot;heading&quot;, 4), (&quot;subheading&quot;, 6), (&quot;item&quot;, 8)]<br>                      if suggested[:k] != declared[:k]),<br>    }</pre><p>High-confidence <strong>chapter-level</strong> inconsistencies go to the top of the queue. Item-level ones are often legitimate ambiguity in the nomenclature.</p><p><strong>The metric that decides whether the system is useful</strong> is not accuracy. It is <strong>queue precision</strong>: out of every 100 flagged items, how many the auditor confirms as real inconsistencies. Measuring it takes a pilot with human review, and the threshold is tuned from it.</p><h3>15. Going to production without the data leaving home</h3><ul><li><strong>Quantization</strong> (4 or 8 bits) to run on a modest GPU or even on CPU.</li><li><strong>Batch inference</strong>: Parquet in, Parquet out, with suggested_ncm, confidence and inconsistency columns. Same format, same contract, same validation as Part 1.</li><li><strong>Fully on-premise</strong>: the model goes to the data, not the other way around.</li><li><strong>Versioning</strong>: every run records the model, retriever and threshold versions, along with the SHA-256 of the input file.</li></ul><h3>Lessons from the whole project</h3><ol><li><strong>Contract before code.</strong> Half the discussions disappear once the format is written, versioned and tested.</li><li><strong>Never round silently.</strong> Convert, cast back and compare. If it changed, it’s an error, not an “adjustment”.</li><li><strong>Validate, don’t fix.</strong> Count, classify and document. Hidden cleaning destroys trust in a dataset.</li><li><strong>Prove what you wrote.</strong> Read the file back, compare and revalidate.</li><li><strong>Pseudonymization is only as strong as its key.</strong> An HMAC is only as strong as its key.</li><li><strong>Synthetic data tests the code, not reality.</strong> Measure on a real sample as early as possible.</li><li><strong>“Works outside Docker” is a networking clue.</strong></li><li><strong>The declared label is the suspect, not the answer key.</strong> Train on reviewed or consensus labels.</li><li><strong>Split by group, or the metric lies.</strong></li><li><strong>Baseline first; SLM only if it wins.</strong></li><li><strong>Confidence without calibration is just a number.</strong></li><li><strong>The product is the triage queue, not the model.</strong> Measure precision with humans in the loop.</li></ol><figure><img alt="" src="https://cdn-images-1.medium.com/max/751/1*Ov2QCXgNKQAMmT1sA_q8Uw.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*htCjUrpdRjErfhw-1HHYyA.png" /></figure><p>Note ¹</p><blockquote>The idea is that <strong>synthetic data often looks more repetitive than real-world data</strong>, so it can make compression and storage estimates appear artificially optimistic.</blockquote><h4>“Synthetic data lies about entropy”</h4><p><strong>Entropy</strong> here means, roughly, <strong>how varied and unpredictable </strong>the data is.</p><p>For example, synthetic descriptions might look like:</p><pre>&quot;PRODUCT 000001&quot;<br>&quot;PRODUCT 000002&quot;<br>&quot;PRODUCT 000003&quot;</pre><p>They have a lot of repetition and predictable structure, so compression algorithms can compress them very efficiently.</p><p>Real descriptions might look more like:</p><pre>&quot;Type 1 White Rice 5kg&quot;<br>&quot;Premium Parboiled Rice 5kg&quot;<br>&quot;T1 Long-Grain Rice 5kg&quot;</pre><p>There is considerably more variation. That generally means <strong>higher entropy and potentially worse compression</strong>.</p><p>So:</p><blockquote><strong><em>Synthetic data can lie about entropy because it may underestimate the variability of the real dataset.</em></strong></blockquote><p>That’s why revising the estimate from <strong>95 B/row to 120 B/row</strong> is a conservative planning assumption: you’re allowing approximately <strong>26% more storage per row</strong> rather than assuming that the compression observed on synthetic data will represent production data.</p><p>The important nuance is that <strong>95 → 120 B/row is an estimate, not a measured compression ratio</strong>. Once real data is available, the actual Parquet/ZSTD output should replace the estimate.</p><p>G<strong>enerally speaking</strong>:</p><blockquote><strong><em>Higher entropy → more variability/unpredictability → less efficient compression.</em></strong></blockquote><p>And conversely:</p><blockquote><strong><em>Lower entropy → more repetition/predictability → more efficient compression.</em></strong></blockquote><p><strong><em>Disclosure:</em></strong><em> This article is an educational reconstruction of a data-engineering problem. Code samples are simplified and adapted, examples are illustrative, and no institutional source code, real fiscal data, credentials, internal infrastructure identifiers or confidential documentation are reproduced.</em></p><p><em>Code samples are simplified and names have been adapted. No real data is shown. Volume and compression figures are given in bytes per row so as not to reveal the actual size of the database.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=21e45d3ad324" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/from-invoice-to-small-language-model-a-pipeline-that-never-rounds-21e45d3ad324">From Invoice to Small Language Model: A Pipeline That Never Rounds</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Demystifying Callback Hell]]></title>
            <link>https://medium.com/jungletronics/demystifying-callback-hell-0d2ec578ef94?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/0d2ec578ef94</guid>
            <category><![CDATA[front-end-development]]></category>
            <category><![CDATA[asynchronous]]></category>
            <category><![CDATA[programming]]></category>
            <category><![CDATA[webdev]]></category>
            <category><![CDATA[javascript]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Sun, 27 Sep 2026 16:51:33 GMT</pubDate>
            <atom:updated>2026-10-03T12:39:53.238Z</atom:updated>
            <content:encoded><![CDATA[<h4>Mastering the transition from messy synchronous dependencies to clean, non-blocking asynchronous execution</h4><h3>Introduction</h3><p>Have you ever opened a JavaScript file only to find your code tunneling sideways off the right side of the screen?</p><p>You aren’t alone.</p><p>For years, managing complex sequences of asynchronous operations often meant nesting one callback inside another, creating a structure that developers commonly refer to as <strong>Callback Hell</strong> or the <strong>Pyramid of Doom</strong>.</p><p>The problem isn’t that callbacks are inherently bad. Callbacks are a valid mechanism for responding to asynchronous events. The difficulty appears when multiple operations depend on the results of previous operations and each new step introduces another level of nesting.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*tseN7mpWFgh5w5To4Ob6XA.jpeg" /></figure><p>JavaScript executes application code on a single main thread in the browser, but the browser itself can handle asynchronous operations such as network requests and timers outside the JavaScript execution stack.</p><p>If a long-running JavaScript computation runs synchronously on the main thread, it can block the user interface. Network requests, however, do not normally block the JavaScript thread while the browser waits for the response.</p><p>To build responsive web applications, we therefore need to understand how synchronous JavaScript execution interacts with asynchronous APIs, Promises, and the event loop.</p><p>In this tutorial, we’ll follow the evolution from nested callbacks to Promise chains and finally to async/await, using a simple CodePen experiment to observe the different programming styles.</p><h4>Here are the clean, simplified steps to get your specific HTML layout and JavaScript code running perfectly inside <strong>CodePen</strong> for your tutorial’s practice section.</h4><h4>Step 1: Open CodePen</h4><p>Go to <a href="https://codepen.io/">CodePen.io</a> and click <strong>“Start Coding”</strong> (or <strong>“Create &gt; New Pen”</strong>).</p><h4>Step 2: Set Up the HTML</h4><p>Copy and paste this code block directly into the <strong>HTML panel</strong> (left or top box depending on your layout):</p><p><em>html</em></p><pre>&lt;div class=&quot;container&quot;&gt;<br>  &lt;h1&gt;Developer Fetch Comparison&lt;/h1&gt;<br>  &lt;p&gt;Open your browser console (F12) to see the logs!&lt;/p&gt;<br>  <br>  &lt;!-- Button 1: Modern Approach --&gt;<br>  &lt;button id=&quot;btn-async&quot; class=&quot;btn btn-modern&quot;&gt;<br>    Method A: Async / Await<br>  &lt;/button&gt;<br>  <br>  &lt;!-- Button 2: Traditional Approach --&gt;<br>  &lt;button id=&quot;btn-then&quot; class=&quot;btn btn-traditional&quot;&gt;<br>    Method B: Promises (.then)<br>  &lt;/button&gt;<br>&lt;/div&gt;</pre><p>Use code with caution.</p><h4>Step 3: Set Up the JavaScript</h4><p>Copy and paste this exact logic into the <strong>JS panel</strong> to hook up the API triggers [finance]:</p><p>javascript</p><pre>// ========================================================<br>// METHOD A: THE MODERN APPROACH (ASYNC / AWAIT)<br>// ========================================================<br>document.getElementById(&#39;btn-async&#39;).addEventListener(&#39;click&#39;, async () =&gt; {<br>  console.clear(); // Clears console so you only see this run<br>  console.log(&quot;🛰️ [Async/Await] Initiating background network lane...&quot;);<br>  <br>  try {<br>    // Execution pauses here until the server responds<br>    const response = await fetch(&#39;https://jsonplaceholder.typicode.com/users/1&#39;);<br>    <br>    // Execution pauses here until bytes turn into an interactive object<br>    const dataPackage = await response.json();<br>    <br>    console.log(&quot;📦 [Async/Await] Success! Payload received:&quot;, dataPackage.name);<br>  } catch (error) {<br>    console.error(&quot;❌ [Async/Await] Safe catch triggered:&quot;, error.message);<br>  }<br>});<br><br>// ========================================================<br>// METHOD B: THE TRADITIONAL APPROACH (.THEN / .CATCH)<br>// ========================================================<br>document.getElementById(&#39;btn-then&#39;).addEventListener(&#39;click&#39;, () =&gt; {<br>  console.clear(); // Clears console so you only see this run<br>  console.log(&quot;🛰️ [.then()] Registering network event hooks...&quot;);<br>  <br>  // fetch() runs instantly and returns a placeholder Promise object<br>  fetch(&#39;https://jsonplaceholder.typicode.com/users/1&#39;)<br>    .then((response) =&gt; {<br>      // Passes the next promise down the chain<br>      return response.json(); <br>    })<br>    .then((dataPackage) =&gt; {<br>      // Executed much later when data is fully processed<br>      console.log(&quot;📦 [.then()] Success! Payload received:&quot;, dataPackage.name);<br>    })<br>    .catch((error) =&gt; {<br>      // Catches errors originating from any point in the chain above<br>      console.error(&quot;❌ [.then()] Catch block triggered:&quot;, error.message);<br>    });<br>});</pre><p>Use code with caution.</p><h4>Step 4: Add Minimal Styling (Optional CSS Panel)</h4><p>Drop this into your <strong>CSS panel</strong> to center the buttons and align them beautifully on a modern dark theme:</p><p><em>css</em></p><pre>body { font-family: sans-serif; background: #1e1e1e; color: #ffffff; padding: 40px; text-align: center; }<br>.container { max-width: 600px; margin: 0 auto; }<br>button { padding: 14px 24px; margin: 12px; font-size: 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: bold; transition: background 0.2s; }<br>.btn-modern { background-color: #2ecc71; color: #fff; }<br>.btn-traditional { background-color: #3498db; color: #fff; }<br>button:hover { filter: brightness(0.9); }<br></pre><p>Use code with caution.</p><h4>Step 5: Execute the Code</h4><ol><li>Press <strong>F12</strong> (or right-click -&gt; <strong>Inspect -&gt; Console</strong>) to open your native web browser console.</li><li>Click <strong>Method A: Async / Await</strong> to see the clean, modern response lifecycle fire.</li><li>Click <strong>Method B: Promises (.then)</strong> to see the exact same result using traditional downward event chain hooks.</li></ol><p>Here is a brief, developer-level explanation of what is happening under the hood for each output:</p><h4>Output 1: Async / Await</h4><pre>&quot;🛰️ [Async/Await] Initiating background network lane...&quot;<br>&quot;📦 [Async/Await] Success! Payload received:&quot; &quot;Leanne Graham&quot;</pre><p>Use code with caution.</p><ul><li><strong>The Mechanics:</strong> The code executes sequentially down the line like a book. The await keyword tells JavaScript to pause execution right there inside that event block, freeze the line, and wait for the remote data package to return.</li><li><strong>The Result:</strong> The moment the network finishes streaming, the code wakes back up instantly, moves to the next line, reads the user’s name parameter, and prints it out.</li></ul><h4>Output 2: Promises (.then())</h4><pre>&quot;🛰️ [.then()] Registering network event hooks...&quot;<br>&quot;📦 [.then()] Success! Payload received:&quot; &quot;Leanne Graham&quot;</pre><p>Use code with caution.</p><ul><li><strong>The Mechanics:</strong> The code executes all at once without stopping. When fetch() runs, it immediately sets up a background listener hook (.then()) and forgets about it, allowing JavaScript to instantly move on.</li><li><strong>The Result:</strong> The second log doesn’t print immediately. It sits in a separate network callback queue until the server responds. Once the data arrives minutes or milliseconds later, the browser fires the registered .then() hook and pushes the payload out to your console logs.</li></ul><h4>The Key Takeaway</h4><p>Both methods accomplish the exact same thing: they keep the browser window interactive and non-blocking while waiting for an external server. The only difference is <strong>syntax structure</strong> — async/await lets you read the process linearly from top-to-bottom, while .then() uses callback event handlers chained together.</p><p>Now that you have verified both outputs work perfectly in your CodePen console, would you like to <strong>read my latest article, <em>Demystifying CORS: Decoding Security Headers on Massive Infrastructure</em></strong>, and learn how to diagnose cross-origin issues, configure server-side headers, and manage trusted domains at scale?</p><p><a href="http://medium.com/jungletronics/demystifying-cors-decoding-security-headers-on-massive-infrastructure-bd7ba7ff3bfc?utm_source=chatgpt.com">Read the article on Medium →</a></p><p>Thank You for Reading!</p><blockquote>Thank you for diving into the evolution of asynchronous JavaScript with me! Moving from the messy nesting of Callback Hell to the clean predictability of async/await is a major milestone for any developer. Keep practicing, keep your code clean, and happy hacking!</blockquote><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*OXldjkv2DFDiV8m-oEnWZw.jpeg" /></figure><p>Both of your tutorials — <a href="https://medium.com/jungletronics/demystifying-cors-decoding-security-headers-on-massive-infrastructure-bd7ba7ff3bfc"><strong>CORS Policies</strong></a> and <strong>Demystifying Callback Hell</strong> — are now complete with code, explanations, and custom thumbnails. Would you like to compile them into a <strong>single reference document</strong> or start brainstorming a <strong>new development topic</strong>?</p><p><strong>Disclosure:</strong> This article is an educational and/or technical exploration based on concepts, experiments, and practical experience. Code samples, configurations, architectures, and examples may be simplified, adapted, or illustrative. No institutional source code, real production data, credentials, confidential information, or sensitive internal infrastructure details are reproduced.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0d2ec578ef94" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/demystifying-callback-hell-0d2ec578ef94">Demystifying Callback Hell</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Demystifying CORS: Decoding Security Headers on Massive Infrastructure]]></title>
            <link>https://medium.com/jungletronics/demystifying-cors-decoding-security-headers-on-massive-infrastructure-bd7ba7ff3bfc?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/bd7ba7ff3bfc</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[api]]></category>
            <category><![CDATA[cors]]></category>
            <category><![CDATA[webdev]]></category>
            <category><![CDATA[security]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Sun, 27 Sep 2026 16:18:31 GMT</pubDate>
            <atom:updated>2026-10-02T15:22:28.482Z</atom:updated>
            <content:encoded><![CDATA[<h4>Stop guessing why your API calls are blocked. Learn to diagnose cross-origin issues, configure server-side headers, and manage trusted domains at scale</h4><p>Ever spent hours staring at a cryptic Blocked by CORS policy error in your browser console? You aren&#39;t alone. <strong>Cross-Origin Resource Sharing (</strong><a href="https://web.dev/articles/cross-origin-resource-sharing?utm_source=devtools&amp;utm_campaign=stable"><strong>CORS</strong></a><strong>)</strong> is the browser&#39;s digital security guard, designed to keep sensitive web data safe from unauthorized domains.</p><p>While a simple wildcard (*) works for a basic public API, massive enterprise infrastructure demands a more sophisticated approach. In this tutorial, we will strip away the confusion behind cross-origin headers, see how huge systems manage trusted sites at scale without hardcoding thousands of domains, and give you the tools to uncover exactly how your company&#39;s network gateway validates incoming origins.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*6-1Ovo-GeqmsoIn7FCCSpw.jpeg" /></figure><h3>1. The <strong>Wildcard </strong>(*)</h3><p>This is the open-door policy. The server does not check the identity or origin of the incoming request. It simply attaches a universal stamp of approval to every single response payload.</p><ul><li><strong>The Header:</strong> Access-Control-Allow-Origin: *</li><li><strong>Best For:</strong> <strong>Public APIs</strong>. Ideal for government financial transparency portals, open-source mock servers, or public weather data feeds.</li><li><strong>The Verdict:</strong> <strong>Simple to operate and generally lightweight</strong>, but inappropriate when browser access must be restricted to specific origins, particularly for applications involving private user data or credentialed requests.</li></ul><h3>2. Dynamic Origin Validation</h3><p>This is a customized approach used by many modern web platforms. When a request arrives, the server evaluates the incoming origin against an approved policy and determines whether that origin should receive CORS access.</p><ul><li><strong>The Header:</strong></li></ul><pre>Access-Control-Allow-Origin: https://trusted-client.example<br>Vary: Origin</pre><ul><li>Origin (The server dynamically mirrors the requesting domain only if it passes the security check).</li><li><strong>Best For:</strong> <strong>Private SaaS Applications</strong>. Ideal for paid software platforms where clients manage and whitelist their own target domains via an admin dashboard.</li><li><strong>The Verdict:</strong> <strong>More restrictive and precise</strong>, but it introduces policy-management complexity. The approved origins may come from static configuration, application logic, a cache, a database, or another policy mechanism, depending on the architecture.</li></ul><h3>3. The Network Gateway</h3><p>This is the institutional approach used at a massive architectural scale. The responsibility for evaluating CORS rules is completely removed from the developer’s application code and moved to the edge of the network infrastructure.</p><ul><li><strong>The Header:</strong> Injected dynamically by routing layers like Kong, <a href="https://www.moesif.com/blog/api-product-management/api-strategy/What-Is-Apigee/">Apigee</a>, <a href="https://apicenter.io/the-ultimate-guide-to-apis-everything-you-need-to-know/">AWS API Gateway</a>, or <a href="https://www.youtube.com/watch?v=VL9DNCDzJBY">NGINX</a>.</li><li><strong>Best For:</strong> <strong>Enterprise Infrastructure</strong>. Essential for large financial institutions, public utility systems, or expansive microservice environments.</li><li><strong>The Advantage:</strong> <strong>Centralized, consistent, and scalable.</strong> It can standardize CORS policies across hundreds of independent services while reducing duplicated security logic in application code.</li></ul><h3>Part 4: Playing Detective — Uncovering Your Infrastructure Strategy</h3><p>Working with <strong>public finance data on a massive infrastructure scale</strong> means security and auditability are critical. As enterprise environments grow, maintaining origin policies across many applications and services can become increasingly complex.</p><p>When you get back to your internal machine, you can play detective right from your browser or terminal to investigate how your infrastructure handles CORS. Here are three technical paths to uncover where the decision is being made:</p><p><strong>Hint 1 → observe the HTTP behavior</strong><br><strong>Hint 2 → investigate the infrastructure</strong><br><strong>Hint 3 → confirm in the code where the decision is implemented</strong></p><h4>1˚ Hint</h4><p><strong>Inspect the “Origin Reflection” (The Terminal Test)</strong></p><p>One of the simplest ways to investigate a server’s CORS behavior is to send a request with a specific Origin header and inspect how the server responds.</p><p>This does <strong>not</strong> reveal whether the server uses a database, cache, configuration file, or gateway policy internally. It only shows the CORS behavior that is visible at the HTTP response level.</p><p>Open your terminal or command prompt and run this curl command against an API endpoint you are authorized to test:</p><pre>curl -i \<br>  -H &quot;Origin: https://fake-evil-domain.com&quot; \<br>  https://your-company-api.com</pre><p><strong><em>Important:</em></strong><em> </em><em>curl does not enforce CORS. It simply sends the </em><em>Origin header and lets you inspect the server&#39;s response. CORS enforcement happens in the browser, which determines whether JavaScript is allowed to access the cross-origin response.</em></p><p>Now inspect the response headers.</p><p><strong>Scenario A:</strong> The response contains:</p><pre>Access-Control-Allow-Origin: https://fake-evil-domain.com</pre><ul><li><strong>Diagnosis:</strong> The server reflected the supplied origin. This is consistent with <strong>dynamic CORS handling</strong>, but the response alone does not prove that the origin was validated against an allowlist — or even that a database was involved.</li></ul><p><strong>Scenario B:</strong> The response contains:</p><pre>Access-Control-Allow-Origin: *</pre><ul><li><strong>Diagnosis:</strong> The endpoint is allowing cross-origin access for any origin under the CORS policy represented by *. This is commonly appropriate for genuinely public, non-credentialed APIs, but it is not appropriate when the API needs to restrict browser access to specific origins.</li></ul><p><strong>Scenario C:</strong> The response does not contain an Access-Control-Allow-Origin header for the supplied origin.</p><ul><li><strong>Diagnosis:</strong> The response does not grant CORS access to that origin. The HTTP request may still have reached and been processed by the server; the absence of the CORS header means that a browser will not make the response available to calling JavaScript.</li></ul><p><strong>Scenario D:</strong> The server returns an HTTP error or the connection fails.</p><ul><li><strong>Diagnosis:</strong> This indicates a server, network, routing, authentication, or other HTTP-level problem. It should not automatically be classified as a CORS rejection. Check the HTTP status and response before drawing conclusions about the CORS policy.</li></ul><h3>A useful follow-up test</h3><p>To investigate whether the behavior changes for an approved origin, repeat the request with a domain that you are authorized to test:</p><pre>curl -i \<br>  -H &quot;Origin: https://trusted-client.example&quot; \<br>  https://your-company-api.com</pre><p>Compare the two responses.</p><p>If the server returns a specific origin only for the trusted domain and omits Access-Control-Allow-Origin for the untrusted one, that is evidence of <strong>origin-dependent CORS behavior</strong>. However, the test still cannot tell you whether the decision is implemented in application code, a database lookup, a cache, an API gateway, a reverse proxy, or another policy mechanism.</p><h3>2˚ Hint</h3><p><strong>Look for Gateway Infrastructure (API Management)</strong></p><p>In large enterprise environments, CORS policies may be managed centrally at the API gateway or edge infrastructure layer rather than implemented independently in every backend application.</p><p>When reviewing architecture documentation, infrastructure-as-code repositories, or DevOps configuration, look for:</p><ul><li><strong>API Gateways / API Management:</strong> Kong, Apigee, AWS API Gateway, or similar platforms. Look for CORS policies, allowed origins, methods, and headers.</li><li><strong>Reverse Proxies / Load Balancers:</strong> NGINX, HAProxy, F5 BIG-IP, or similar infrastructure. Depending on the configuration, these layers may add, modify, or conditionally return CORS headers.</li><li><strong>Infrastructure as Code:</strong> Terraform, Kubernetes manifests, Helm charts, or gateway policies. Search for terms such as:</li></ul><pre>cors<br>allowedOrigins<br>allow-origin<br>Access-Control-Allow-Origin<br>Origin</pre><p>The objective is to establish <strong>where the CORS decision is being made</strong>.</p><p>For example, the architecture might look like:</p><pre>Browser → API Gateway / Reverse Proxy → Backend</pre><p>Or:</p><pre>Browser → API Gateway → Backend → CORS logic</pre><p>The presence of a gateway does <strong>not</strong> prove that the gateway owns the CORS policy. Inspect the configuration or trace the request flow to determine which layer is actually responsible.</p><p>If the application configuration looks correct but the response differs from what the application appears to generate, an upstream gateway, reverse proxy, or load balancer may be adding or modifying the response headers.</p><h4>3˚ Hint</h4><p><strong>Search the Codebase for Architectural Footprints</strong></p><p>If you have authorized access to your team’s Git repositories, search the codebase for configuration, middleware, or policy definitions related to CORS.</p><p>You can use <strong>Ctrl + Shift + F</strong> in VS Code or tools such as grep from the terminal to search across the project.</p><p>Look for indicators such as:</p><ul><li><strong>Configuration:</strong> Search for keys and environment variables such as allowedOrigins, corsOptions, CORS_WHITELIST, ALLOWED_ORIGINS, or similar names.</li><li><strong>CORS Middleware:</strong> Look for framework-specific CORS configuration or middleware. Depending on the technology stack, this might appear in application startup code, middleware registration, route configuration, or security modules.</li><li><strong>Origin Validation:</strong> Search for code that reads the incoming Origin header and compares it against an approved list or policy:</li></ul><pre>const origin = request.headers.origin;</pre><pre>if (allowedOrigins.includes(origin)) {<br>  // Apply the appropriate CORS response headers<br>}</pre><ul><li><strong>Dynamic Policy Lookup:</strong> In some architectures, approved origins may be retrieved from a database, cache, configuration service, or another policy store:</li></ul><pre>const client = await policyStore.findByOrigin(origin);</pre><h3>What you are trying to establish</h3><p>The objective is to identify <strong>where the CORS decision is implemented</strong> and what data it depends on.</p><p>For example, you might discover a simple static configuration:</p><pre>ALLOWED_ORIGINS=https://app.example.com,https://portal.example.com</pre><p>Or you may find application logic that evaluates the incoming origin dynamically.</p><p>You may also find <strong>no CORS logic in the application at all</strong>. That is useful evidence too: it suggests that the policy may be implemented upstream at an API gateway, reverse proxy, load balancer, or another infrastructure layer.</p><p>The key is to distinguish between what the HTTP response <strong>reveals</strong> and what the source code or infrastructure configuration <strong>actually confirms</strong>.</p><h3>Part 5: Practice — Test CORS Behavior</h3><p>Here are the quick, simplified steps to set up this test directly inside your current <a href="https://codepen.io/"><strong>CodePen</strong></a> setup. This allows you to simulate how your browser reacts to the three different server responses in real time.</p><p><strong>Step 1</strong>: Update Your HTML</p><p>Add three new test buttons inside your existing HTML container in CodePen:</p><p><em>html</em></p><pre><br>&lt;h3&gt;Part 5: Test CORS Behavior&lt;/h3&gt;<br><br>&lt;button id=&quot;btn-wildcard&quot;&gt;Test CORS Access&lt;/button&gt;<br>&lt;button id=&quot;btn-reflection&quot;&gt;Test CORS-Enabled Endpoint&lt;/button&gt;<br>&lt;button id=&quot;btn-blocked&quot;&gt;Test CORS Access Not Granted&lt;/button&gt;<br></pre><p>Use code with caution.</p><p><strong>Step 2</strong>: Update Your JavaScript</p><p>Paste this code at the bottom of your <strong>JS panel</strong> in <a href="https://codepen.io/">CodePen</a>.</p><p>Instead of using a real corporate server, this example uses public endpoints to demonstrate different <strong>observable browser outcomes</strong>.</p><p>These endpoints are useful for experimentation, but they do not reproduce the internal architecture of an enterprise gateway. The browser only sees the HTTP behavior exposed by the remote server; it cannot reveal whether a database, cache, configuration store, gateway, or application code produced that behavior.</p><p><em>javascript</em></p><pre>// 1. Simulating Strategy 1: The Global Wildcard<br>document.getElementById(&#39;btn-wildcard&#39;).addEventListener(&#39;click&#39;, async () =&gt; {<br>  console.clear();<br><br>  try {<br>    const response = await fetch(&#39;https://jsonplaceholder.typicode.com/users/1&#39;);<br>    const data = await response.json();<br><br>    console.log(<br>      &quot;✅ WILDCARD SUCCESS: The browser was allowed to read the cross-origin response.&quot;,<br>      data.name<br>    );<br>  } catch (err) {<br>    console.error(&quot;❌ Failed:&quot;, err.message);<br>  }<br>});<br><br><br>// 2. Simulating Strategy 2: Dynamic CORS Handling<br>document.getElementById(&#39;btn-reflection&#39;).addEventListener(&#39;click&#39;, async () =&gt; {<br>  console.clear();<br><br>  try {<br>    const response = await fetch(&#39;https://httpbin.org/get&#39;);<br><br>    const data = await response.json();<br><br>    console.log(<br>      &quot;🔄 DYNAMIC CORS: The request succeeded and the browser exposed the response.&quot;<br>    );<br><br>    console.log(&quot;Origin observed by the endpoint:&quot;, data.headers?.Origin || &quot;Not exposed&quot;);<br>  } catch (err) {<br>    console.error(&quot;❌ Failed:&quot;, err.message);<br>  }<br>});<br><br><br>// 3. Simulating Strategy 3: CORS Access Not Granted<br>document.getElementById(&#39;btn-blocked&#39;).addEventListener(&#39;click&#39;, async () =&gt; {<br>  console.clear();<br><br>  console.log(<br>    &quot;🛰️ Testing a cross-origin resource that does not grant browser access...&quot;<br>  );<br><br>  try {<br>    await fetch(&#39;https://google.com&#39;);<br><br>    console.log(<br>      &quot;⚠️ The request completed, but this does not necessarily mean CORS access was granted.&quot;<br>    );<br>  } catch (err) {<br>    console.log(<br>      &quot;❌ DIAGNOSIS: The browser blocked JavaScript from reading the cross-origin response.&quot;<br>    );<br><br>    console.error(<br>      &quot;Browser error message:&quot;,<br>      err.message<br>    );<br>  }<br>});</pre><p>Use code with caution.</p><p>This test demonstrates that the browser can access a cross-origin response when the remote server provides the appropriate CORS headers. The response alone does not reveal whether those headers were generated from a database, configuration file, cache, or another policy mechanism.</p><p><strong>Step 3</strong>: Run the Test</p><ol><li>Open your browser console (F12).</li><li>Click <strong>Test CORS Access</strong> → The console should show whether the browser was able to read the cross-origin response.</li><li>Click <strong>Test CORS Access Not Granted</strong> → Depending on the endpoint and browser, the console may show a CORS-related error.</li></ol><p>This demonstrates the <strong>browser-side symptom</strong> that occurs when a cross-origin response does not grant JavaScript access. It does not prove that a firewall, whitelist, or gateway rejected the underlying HTTP request.</p><h3>Part 6:</h3><h4>The Ultimate CORS Troubleshooting Checklist</h4><p>If you, reader, hit a wall with a CORS error, use this sequential step-by-step checklist to isolate and resolve the issue quickly:</p><h4><strong>1. Identify the Core Issue</strong></h4><ul><li><strong>Check the Console:</strong> Does the log explicitly mention a CORS policy violation, or is it a generic network error?</li><li><em>Developer Tip:</em> If the backend server drops entirely or has a broken DNS record, the browser will output Failed to fetch. This can sometimes look like a CORS error, but it is actually a standard infrastructure outage.</li></ul><h4>2. Inspect the Request Headers</h4><ul><li>Open your browser’s Developer Tools (F12) and navigate to the <strong>Network</strong> tab.</li><li>Click on the failing request and locate the automatic <strong>Origin</strong> request header.</li><li>Verify your environment details: Did the protocol change (e.g., http to https) or the port shift (e.g., localhost:3000 to localhost:8080)? The browser treats any structural difference as a completely distinct origin.</li></ul><h4>3. Verify the Server Response</h4><ul><li>Look closely at the <strong>Response Headers</strong> sent back by your API.</li><li>Is the <strong>Access-Control-Allow-Origin</strong> header present?</li><li>If <strong>yes</strong>, does its value match the exact string sent in your request’s Origin header?</li><li>If no, the response does not grant CORS access to the requesting origin. The request may still have reached and been processed by the server.</li></ul><h4>4. Check for Preflight Handshakes</h4><ul><li>Did your browser fire an extra <strong>OPTIONS</strong> network request right before your actual GET or POST request?</li><li>Requests that are not considered CORS-safelisted — for example, many requests using custom headers or certain content types — may trigger a preflight OPTIONS request.</li></ul><h4>5. Audit the Infrastructure Layer</h4><ul><li>If your application’s source code looks flawless but requests are still being blocked, move up the network pipeline:</li><li>Does the <strong>API Gateway</strong> (e.g., Kong, Apigee) have a global CORS plugin running that is overriding your local configuration?</li><li>Is an edge proxy server or load balancer (e.g., <strong>NGINX</strong>, HAProxy) configured with strict routing maps that filter out unfamiliar external traffic?</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*am3XpJ_ulz_W0gViXss1iQ.jpeg" /></figure><blockquote>Thank You for Reading!</blockquote><blockquote>Thank you so much for taking the time to read this guide. I hope it helps demystify the inner workings of async JavaScript and complex CORS policies for you. Learning by doing is the best way to grow as a developer, so keep testing, keep querying your APIs, and happy coding!</blockquote><blockquote>If you found this helpful or discovered your company’s network strategy using these steps, let me know in the comments below!</blockquote><h4>Note ¹ Enterprise-Focused</h4><ul><li><strong>Title:</strong> 🛠️ Advanced Insight: Mitigating Information Disclosure</li><li>Why it works: It sounds like an official security compliance document, which fits perfectly with massive infrastructure contexts.</li></ul><p>🔒 Pro-Tip: Infrastructure Hardening &amp; Information Disclosure</p><p>When auditing your network infrastructure via the terminal, you might notice headers like Server: example-gateway or X-Powered-By: Express in the server&#39;s response. While this looks like harmless metadata, in large enterprise or public finance environments, exposing your exact backend technology is considered a security risk known as <strong>Information Disclosure</strong>.</p><p>Attackers can use techniques such as <strong>banner grabbing</strong> and service fingerprinting to collect implementation details from exposed systems. A disclosed product and version can then be correlated with public vulnerability information, helping attackers prioritize further investigation.</p><p>How the Infrastructure Team Protects the Headers</p><p>To mitigate this without breaking any client or partner integrations, DevOps and Security teams apply server hardening techniques at the gateway layer:</p><p><strong>1. Disabling Server Tokens (Basic)</strong></p><p>In the core configuration file (example-gateway.conf), teams add a directive to hide the specific version numbers from headers and default error pages:</p><p>example-gateway</p><pre># Inside the http, server, or location block<br>server_tokens off;</pre><p>Use code with caution.</p><ul><li><strong>Result:</strong> The response no longer exposes the specific server version through the standard NGINX Server header. The exact header value depends on the server and proxy configuration.</li></ul><p><strong>2. Header Removal (Advanced)</strong></p><p>If the architecture requires the `Server` header to be removed entirely, that is a separate edge-layer configuration task and depends on the proxy or gateway in use.</p><p>The exact mechanism varies by platform. For example, `server_tokens off;` in NGINX hides version information, but it does not remove the `Server` header itself.</p><p>The important distinction is that reducing version disclosure and completely removing a server header are two different configuration goals.</p><p>In most standards-compliant HTTP clients, changing or removing the Server metadata does not affect how the application processes the response. However, compatibility should always be verified when changing infrastructure behavior, especially in legacy or tightly integrated environments.</p><p>Here is an illustrative terminal command and a simplified response for the <strong>Technical Bonus</strong> section:</p><p><em>bash</em></p><pre>curl -I \<br>  -H &quot;Origin: https://example-attacker.test&quot; \<br>  https://example-api.test</pre><pre>Example response:<br><br>HTTP/2 200<br>server: example-gateway<br>content-type: application/json</pre><p><em>illustrative terminal command</em></p><p>Use code with caution.</p><p>🔍 What this output proves:</p><ol><li><strong>The server returned an HTTP response</strong>: The endpoint is reachable and returned `HTTP/2 200`. This alone does not establish how the application processed the request.</li><li><strong>CORS access was not granted in this response</strong>: No `Access-Control-Allow-Origin` header is present. This observation alone does not reveal whether the decision came from a whitelist, application logic, gateway policy, or another mechanism.</li><li><strong>Information Disclosure:</strong> The response exposes a server or gateway identifier. Such metadata can contribute to fingerprinting and may be correlated with publicly known technologies or vulnerabilities.</li></ol><h4>SubNote ².⚠️:</h4><h4><strong>Server Fingerprinting</strong></h4><p>Exposing the server technology through response headers can provide additional information for server fingerprinting. Hiding unnecessary implementation details is therefore a reasonable hardening measure, although it should not be treated as a substitute for patching, secure configuration, authentication, authorization, or other primary security controls.</p><h3><em>Security Disclosure</em></h3><p><em>The examples in this article are illustrative and intended for educational purposes. They do not reproduce real institutional infrastructure, internal endpoints, credentials, production configurations, or confidential security findings.</em></p><p><em>Any domains, headers, responses, and command examples shown are synthetic or adapted for demonstration. Security testing should only be performed against systems for which you have explicit authorization.</em></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bd7ba7ff3bfc" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/demystifying-cors-decoding-security-headers-on-massive-infrastructure-bd7ba7ff3bfc">Demystifying CORS: Decoding Security Headers on Massive Infrastructure</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Why django does not serve static files?]]></title>
            <link>https://medium.com/jungletronics/why-django-does-not-serve-static-files-65b33751e5e8?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/65b33751e5e8</guid>
            <category><![CDATA[asgi-vs-wsgi]]></category>
            <category><![CDATA[python-static-files]]></category>
            <category><![CDATA[django]]></category>
            <category><![CDATA[nginx-reverse-proxy]]></category>
            <category><![CDATA[gunicorn]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 20:13:38 GMT</pubDate>
            <atom:updated>2026-09-27T01:57:04.307Z</atom:updated>
            <content:encoded><![CDATA[<h4>Why Django’s WSGI and ASGI Don’t Serve Static Files — and Why Production Apps Rely on Nginx for Images, CSS, JavaScript, and Other Assets</h4><p>This is an excellent question, and it gets to one of Django’s core design philosophies.</p><p>The short answer is:</p><blockquote><strong><em>Django is an application framework, not a web server.</em></strong></blockquote><p>The longer explanation is much more interesting.</p><h3>The job of Django</h3><p>Django’s responsibility is to generate dynamic responses.</p><p>For example:</p><pre>Browser<br>   │<br>   ▼<br>GET /products/42<br>   │<br>   ▼<br>Django<br>   │<br>Queries database<br>Processes business logic<br>Renders template<br>   │<br>   ▼<br>HTML</pre><p>That’s where Django shines.</p><h3>The job of static files</h3><p>Static files are different.</p><pre>logo.png<br>style.css<br>app.js<br>video.mp4<br>font.ttf</pre><p>These files never change while being served.</p><p>Serving them is simply:</p><pre>Client<br>   │<br>GET /static/logo.png<br>   │<br>Read file from disk<br>   │<br>Send bytes</pre><p>No Python code is needed.</p><h3>Why not let Django do it?</h3><p>It <em>can</em>.</p><p>In development, when DEBUG=True, Django already serves static files.</p><p>That’s why:</p><pre>python manage.py runserver</pre><p>works perfectly.</p><p>But this implementation is intentionally simple and not optimized for production.</p><p>Suppose your homepage contains:</p><ul><li>HTML</li><li>CSS</li><li>8 JavaScript files</li><li>25 in</li><li>6 fonts</li></ul><p>One page load becomes roughly:</p><pre>1 HTML request<br>40 static requests<br>--------------<br>41 requests</pre><p>Now imagine:</p><pre>10,000 visitors</pre><p>That’s approximately:</p><pre>410,000 requests</pre><p>If Django handled every one of those, every request would:</p><pre>Browser<br> ↓<br>Gunicorn<br> ↓<br>Python interpreter<br> ↓<br>Django<br> ↓<br>StaticFileHandler<br> ↓<br>Read disk<br> ↓<br>Return file</pre><p>Every request wakes up Python, allocates memory, and occupies a worker process.</p><h3>Nginx is built for exactly this</h3><p>For the same request:</p><pre>Browser<br> ↓<br>Nginx<br> ↓<br>Read file<br> ↓<br>Send file</pre><p>Django is never involved.</p><p>No Python.</p><p>No ORM.</p><p>No middleware.</p><p>No templates.</p><p>Just raw file transfer.</p><h3>A nice analogy</h3><p>Imagine a restaurant.</p><p>You</p><pre>Steak</pre><p>The chef cooks it.</p><p>But imagine asking the chef:</p><blockquote><em>“Can I have another napkin?”</em></blockquote><p>Should the chef stop cooking to hand you a napkin?</p><p>Of course not.</p><p>A waiter handles that.</p><p>Static files are the “napkins” of a web application.</p><p>Nginx is the waiter.</p><h3>But modern apps have lots of images…</h3><p>Exactly.</p><p>Against of:</p><ul><li>logos</li><li>product images</li><li>icons</li><li>PDFs</li><li>we</li><li>CSS bundles</li><li>JavaScript</li><li>web fonts</li></ul><p>Django typically doesn’t serve these directly in production.</p><p>Instead, it tells the web server where they live:</p><pre>/static/</pre><p>For user-uploaded files:</p><pre>/media/</pre><p>Nginx usually serves those too.</p><h3>Then what is WSGI?</h3><p>Think of <a href="https://wsgi.readthedocs.io/en/latest/what.html">WSGI</a> as an agreement between Python applications and web servers.</p><pre>Browser<br>    │<br>    ▼<br>Nginx<br>    │<br>    ▼<br>Gunicorn<br>    │<br>    ▼<br>WSGI<br>    │<br>    ▼<br>Django</pre><p>WSGI defines how the web server passes an HTTP request to your Python application and how the application returns a response.</p><h3>And ASGI?</h3><p><a href="https://asgi.readthedocs.io/en/latest/">ASGI</a> is the modern successor to WSGI.</p><p>It supports:</p><ul><li>asynchronous code</li><li>WebSockets</li><li>Server-Sent Events</li><li>long-lived connections</li><li>HTTP/2-friendly patterns</li></ul><p>The architecture is similar:</p><pre>Browser<br>     │<br>     ▼<br>Nginx<br>     │<br>     ▼<br>Uvicorn<br>     │<br>     ▼<br>ASGI<br>     │<br>     ▼<br>Django</pre><p>ASGI expands what kinds of connections your application can handle, but it still doesn’t aim to be a high-performance static file server.</p><h3>Then why does WhiteNoise exist?</h3><p>Many deployments don’t use Nginx. For example, on platforms like <a href="https://render.com/">Render</a>, <a href="https://railway.com/">Railway</a>, or <a href="https://fly.io/">Fly.io</a>, you may only control your Python process.</p><p><a href="https://whitenoise.readthedocs.io/en/latest/"><strong>WhiteNoise</strong></a> lets your Django application serve static files efficiently enough for many production use cases by adding optimized static-file handling inside the Python stack.</p><pre>Browser<br>     │<br>     ▼<br>Gunicorn<br>     │<br>     ▼<br>WhiteNoise<br>     │<br>     ▼<br>Static file</pre><p>If the request is for /static/..., WhiteNoise serves it directly without passing it through Django&#39;s URL routing or view logic. It&#39;s not as specialized as <a href="https://nginx.org/">Nginx</a>, but it&#39;s simple, reliable, and widely used for small to medium-sized applications.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*S3K82jMmuW0qEQ0bLIUpBg.png" /></figure><h3>The common product</h3><p>Large applications often use this separation of responsibilities:</p><pre>              Browser<br>                 │<br>                 ▼<br>              Nginx<br>          ┌──────┴──────┐<br>          │             │<br>          ▼             ▼<br>      /static/*     Gunicorn/Uvicorn<br>          │             │<br>          ▼             ▼<br>    CSS, JS, Images    Django<br>                           │<br>                           ▼<br>                      PostgreSQL</pre><p>This design lets each component do what it’s best at:</p><ul><li><strong>Nginx</strong>: fast delivery of static files, TLS termination, caching, compression, and reverse proxying.</li><li><strong>Gunicorn/Uvicorn</strong>: manage Python worker processes and communicate via WSGI or ASGI.</li><li><strong>DJ</strong>: business logic, authentication, templates, APIs, and database interaction.</li></ul><p>Keeping these concerns separate is one of the reasons Django deployments scale well from small personal projects to large production systems.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*d_USxW-Xu-r6W0UyA965FA.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=65b33751e5e8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/why-django-does-not-serve-static-files-65b33751e5e8">Why django does not serve static files?</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[ Ubuntu Running Slower? This 5-Minute Cleanup Might Surprise You]]></title>
            <link>https://medium.com/jungletronics/ubuntu-running-slower-this-5-minute-cleanup-might-surprise-you-dd30b9e5a8ee?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/dd30b9e5a8ee</guid>
            <category><![CDATA[linux-tutorial]]></category>
            <category><![CDATA[linux-performance]]></category>
            <category><![CDATA[linux-ubuntu]]></category>
            <category><![CDATA[open-source]]></category>
            <category><![CDATA[system-administration]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Wed, 23 Sep 2026 20:01:23 GMT</pubDate>
            <atom:updated>2026-09-23T20:01:23.726Z</atom:updated>
            <content:encoded><![CDATA[<h4>Quick maintenance steps that can help keep your system lean and responsive</h4><p>Ubuntu’s Ext4 filesystem rarely needs defragmentation because it is designed to minimize fragmentation automatically. This procedure is mainly useful for traditional HDDs. Avoid defragmenting SSDs, as it causes unnecessary wear.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*AMcDAuhvMXCnaKvWMuw6Kw.png" /></figure><h3>1. Install the Defragmentation Tool</h3><pre>sudo apt update &amp;&amp; sudo apt install e2fsprogs</pre><h3>2. Check Fragmentation Level</h3><p>Analyze the root filesystem:</p><pre>sudo e4defrag -c /</pre><p>Interpretation:</p><ul><li>Score &lt; 30: Good condition</li><li>Score &gt; 30: Defragmentation may help</li></ul><h3>3. Defragment the Filesystem</h3><pre>sudo e4defrag /</pre><p>This reorganizes fragmented files and may improve HDD performance.</p><h3>4. Clean Package Cache</h3><p>Remove downloaded package files that are no longer needed:</p><pre>sudo apt clean</pre><h3>5. Remove Unused Dependencies</h3><p>Delete orphaned packages left behind by software removals:</p><pre>sudo apt autoremove</pre><h3>Summary</h3><p>Commands used:</p><pre>sudo apt update &amp;&amp; sudo apt install e2fsprogs<br>sudo e4defrag -c /<br>sudo e4defrag /<br>sudo apt clean<br>sudo apt autoremove</pre><p>For most Ubuntu systems, especially those running on SSDs, regular package cleanup provides more benefit than filesystem defragmentation.</p><p>Thank you!</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dd30b9e5a8ee" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/ubuntu-running-slower-this-5-minute-cleanup-might-surprise-you-dd30b9e5a8ee">🚀 Ubuntu Running Slower? This 5-Minute Cleanup Might Surprise You</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cucumber + RSpec: Testes que Falam e Testes que Garantem]]></title>
            <link>https://medium.com/jungletronics/cucumber-rspec-testes-que-falam-e-testes-que-garantem-61fe31cc894e?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/61fe31cc894e</guid>
            <category><![CDATA[rspec]]></category>
            <category><![CDATA[docker-deployment]]></category>
            <category><![CDATA[bdd-cucumber-framework]]></category>
            <category><![CDATA[ruby-on-rails]]></category>
            <category><![CDATA[docker-compose]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Sun, 13 Sep 2026 17:47:45 GMT</pubDate>
            <atom:updated>2026-09-17T20:09:13.855Z</atom:updated>
            <content:encoded><![CDATA[<h4><strong>Da especificação ao teste automatizado — agora sobre uma infraestrutura Rails + PostgreSQL + Docker</strong></h4><p>No episódio <a href="https://medium.com/jungletronics/rails-postgresql-docker-5f7a00afc18c?postPublishedType=repub">anterior</a>, construímos uma base Docker reutilizável para projetos Rails. O objetivo foi separar a aplicação da infraestrutura: Ruby, Rails e PostgreSQL ficam encapsulados nos containers, enquanto o código permanece no projeto local.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/700/0*VyLEdj1lBWyJAjPb.png" /><figcaption><a href="https://github.com/giljr/rails-docker-cucumber-rspec">Github Repository</a></figcaption></figure><p>Agora vamos aproveitar essa mesma base para outra preocupação fundamental do desenvolvimento: <strong>testes automatizados</strong>.</p><p>Cucumber e RSpec cumprem papéis diferentes, mas complementares:</p><ul><li><strong>Cucumber</strong> descreve o comportamento esperado da aplicação em linguagem natural, usando Gherkin;</li><li><strong>RSpec</strong> verifica o comportamento interno do código Ruby/Rails.</li></ul><p>A ideia é trabalhar de fora para dentro e de dentro para fora:</p><pre>                COMPORTAMENTO<br>                      │<br>                      ▼<br>                  CUCUMBER<br>             &quot;O que o usuário espera?&quot;<br>                      │<br>                      ▼<br>                   RSpec<br>             &quot;O código está correto?&quot;<br>                      │<br>                      ▼<br>                   RAILS<br>                      │<br>                      ▼<br>                  POSTGRESQL</pre><p>A vantagem é que <strong>não precisamos mais preparar toda a infraestrutura antes de começar</strong>.</p><p>Nosso projeto já possui Ruby, Rails, PostgreSQL e Docker configurados. Vamos apenas adicionar a camada de testes.</p><h3>0. Partindo da base existente</h3><p>Estamos assumindo que você terminou o tutorial <a href="https://medium.com/jungletronics/rails-postgresql-docker-5f7a00afc18c?postPublishedType=repub">anterior</a> e está na raiz de um projeto Rails:</p><pre>cd ~/Documents/projects/rails/meu-projeto</pre><p>Confirme que os containers estão funcionando:</p><pre>docker compose ps</pre><p>E confirme o Rails:</p><pre>docker compose run --rm app \<br>  bundle exec rails -v</pre><p>Se o PostgreSQL ainda não estiver iniciado:</p><pre>docker compose up -d</pre><p>A partir daqui, <strong>todos os comandos que anteriormente eram executados diretamente com </strong><strong>rails, </strong><strong>bundle, </strong><strong>rspec ou </strong><strong>cucumber serão executados dentro do container </strong><strong>app</strong>.</p><p>Por exemplo:</p><pre>docker compose run --rm app \<br>  bundle install</pre><p>em vez de:</p><pre>bundle install</pre><p>Essa é a principal adaptação do <a href="https://medium.com/jungletronics/cucumber-rspec-testes-que-falam-e-testes-que-garantem-30170c7e7a99">tutorial antigo</a>.</p><h3>2. Adicionando Cucumber</h3><p>Agora acrescentamos o Cucumber ao mesmo projeto.</p><p>No Gemfile:</p><pre>group :development, :test do<br>  gem &quot;cucumber-rails&quot;, require: false<br>  gem &quot;database_cleaner-active_record&quot;<br>  gem &quot;capybara&quot;<br>  gem &quot;selenium-webdriver&quot;<br>end</pre><p>Instale novamente:</p><pre>docker compose run --rm app \<br>  bundle install</pre><p>Inicialize o Cucumber:</p><pre>docker compose run --rm app \<br>  bundle exec rails generate cucumber:install</pre><p>O Rails/Cucumber criará a estrutura:</p><pre>features/<br>├── step_definitions/<br>└── support/</pre><p>Teste:</p><pre>docker compose run --rm app \<br>  bundle exec cucumber</pre><p>Neste momento ainda não temos cenários:</p><pre>0 scenarios<br>0 steps</pre><p>Mas a infraestrutura de testes está funcionando.</p><h3>A principal mudança em relação ao artigo original</h3><p>Antes tínhamos:</p><pre>rails generate scaffold ...<br>rails db:migrate<br>rspec<br>cucumber</pre><p>Agora temos:</p><pre>docker compose run --rm app \<br>  bundle exec rails generate scaffold ...</pre><p>e:</p><pre>docker compose run --rm app \<br>  bundle exec rails db:migrate</pre><p>e:</p><pre>docker compose run --rm app \<br>  bundle exec rspec</pre><p>e:</p><pre>docker compose run --rm app \<br>  bundle exec cucumber</pre><p><strong>O Rails continua sendo Rails.</strong></p><p>A única diferença é <strong>onde ele está sendo executado</strong>.</p><pre>ANTES<br><br>Ubuntu<br> ├── Ruby<br> ├── Rails<br> ├── Bundler<br> ├── PostgreSQL<br> └── Cucumber/RSpec</pre><pre>AGORA<br>Ubuntu<br> └── Docker<br>      │<br>      ├── app<br>      │    ├── Ruby<br>      │    ├── Rails<br>      │    ├── Bundler<br>      │    ├── RSpec<br>      │    └── Cucumber<br>      │<br>      └── db<br>           └── PostgreSQL</pre><p>Isso é justamente a abstração que conseguimos no tutorial anterior.</p><h3>Vamos fazer uma pequena melhoria adicional</h3><p>Em vez de repetir:</p><pre>docker compose run --rm app bundle exec ...</pre><p>em <strong>cada comando do artigo</strong>, podemos criar uma seção de <strong>atalhos para testes</strong> depois que a infraestrutura estiver funcionando.</p><p>Por exemplo:</p><pre>docker compose run --rm app bundle exec rspec</pre><pre>docker compose run --rm app bundle exec cucumber</pre><p>ou, se você preferir entrar no container:</p><pre>docker compose exec app bash</pre><p>e então trabalhar normalmente:</p><pre>bundle exec rspec<br>bundle exec cucumber</pre><p>Isso deixa o tutorial muito mais parecido com o artigo original, <strong>sem abandonar a infraestrutura Docker</strong>.</p><p>A documentação atual do artigo original pode ser preservada na parte conceitual — especialmente a ideia de Cucumber como especificação de comportamento e RSpec como validação interna — mas a preparação do ambiente deve ser completamente substituída pela base Docker que acabamos de construir.</p><p>Perfeito. Vamos <strong>recriar o tutorial novo a partir daqui</strong>, mas já corrigindo algumas coisas do artigo antigo para o ambiente atual <strong>Rails + PostgreSQL + Docker + Cucumber 11 + RSpec</strong>.</p><p>A principal mudança será esta:</p><blockquote><strong><em>Tudo que antes era executado diretamente no computador agora será executado dentro do container </em></strong><strong><em>app.</em></strong></blockquote><p>Começamos assumindo que você está na raiz do projeto.</p><h3>1. Criando a aplicação inicial</h3><p>Entre no container:</p><pre>docker compose exec app bash</pre><p>A partir daqui, todos os comandos abaixo são executados <strong>dentro do container</strong>.</p><h3>Gerador + banco</h3><p>Vamos criar o CRUD inicial de tarefas:</p><pre>rails generate scaffold tarefa nota:text feito:boolean --skip-test<br>rails db:migrate</pre><p>O scaffold cria automaticamente:</p><ul><li>model Tarefa;</li><li>controller;</li><li>views;</li><li>migration;</li><li>nada de testes básicos (criaremos manualmente);</li><li>rotas REST.</li></ul><p>A estrutura principal será:</p><pre>app/<br>├── controllers/<br>│   └── tarefas_controller.rb<br>├── models/<br>│   └── tarefa.rb<br>└── views/<br>    └── tarefas/</pre><p>E a migration criará a tabela tarefas no PostgreSQL.</p><h3>2. Configurando as rotas</h3><p>Abra:</p><pre>config/routes.rb</pre><p>Deixe assim:</p><pre>Rails.application.routes.draw do<br>  resources :tarefas<br>  root &quot;tarefas#index&quot;<br>end</pre><p>Temos agora duas coisas importantes:</p><pre>resources :tarefas</pre><p>cria as rotas CRUD da aplicação.</p><p>E:</p><pre>root &quot;tarefas#index&quot;</pre><p>define a página inicial.</p><p>Portanto:</p><pre>/</pre><p>aponta para:</p><pre>TarefasController#index</pre><h3>3. Preparando a página inicial</h3><p>Vamos substituir a view gerada automaticamente pelo scaffold.</p><p>Abra:</p><pre>app/views/tarefas/index.html.erb</pre><p>e substitua seu conteúdo por:</p><pre>&lt;h1&gt;Listando Tarefas&lt;/h1&gt;<br><br>&lt;table class=&quot;table&quot;&gt;<br>  &lt;thead&gt;<br>    &lt;tr&gt;<br>      &lt;th&gt;Feito&lt;/th&gt;<br>      &lt;th&gt;Nota&lt;/th&gt;<br>      &lt;th colspan=&quot;3&quot;&gt;&lt;/th&gt;<br>    &lt;/tr&gt;<br>  &lt;/thead&gt;<br>  &lt;tbody&gt;<br>    &lt;% @tarefas.each do |tarefa| %&gt;<br>      &lt;tr&gt;<br>        &lt;td&gt;&lt;%= tarefa.feito? ? &quot;true&quot; : &quot;false&quot; %&gt;&lt;/td&gt;<br>        &lt;td&gt;&lt;%= tarefa.nota %&gt;&lt;/td&gt;<br>        &lt;td&gt;&lt;%= link_to &quot;Mostrar&quot;, tarefa %&gt;&lt;/td&gt;<br>        &lt;td&gt;&lt;%= link_to &quot;Editar&quot;, edit_tarefa_path(tarefa) %&gt;&lt;/td&gt;<br>        &lt;td&gt;<br>          &lt;%= link_to &quot;Apagar&quot;, tarefa,<br>              method: :delete,<br>              data: { confirm: &quot;Está certo?&quot; } %&gt;<br>        &lt;/td&gt;<br>      &lt;/tr&gt;<br>    &lt;% end %&gt;<br>  &lt;/tbody&gt;<br>&lt;/table&gt;<br>&lt;br&gt;<br>&lt;%= link_to &quot;Nova Tarefa&quot;, new_tarefa_path, class: &quot;btn btn-primary&quot; %&gt;</pre><p>Por enquanto temos <strong>uma única lista</strong>.</p><p>É exatamente isso que queremos.</p><p>O objetivo deste episódio será modificar esse comportamento utilizando TDD.</p><h3>4. Criando alguns dados</h3><p>Opcionalmente, podemos criar algumas tarefas para visualizar a aplicação.</p><p>Abra:</p><pre>db/seeds.rb</pre><p>e acrescente:</p><pre>Tarefa.create!(nota: &quot;Estudar Ruby&quot;, feito: false)<br>Tarefa.create!(nota: &quot;Lavar o carro&quot;, feito: true)</pre><p>Execute:</p><pre>rails db:seed</pre><p>Podemos verificar:</p><pre>rails db:migrate:status</pre><h3>5. Iniciando o servidor</h3><p>Como estamos dentro do container, nao precisamos executar:</p><pre>rails server</pre><p>Abra no navegador:</p><pre>http://127.0.0.1:3000</pre><p>Você deverá encontrar algo semelhante a:</p><pre>Listando Tarefas<br><br>Feito    Nota<br>false    Estudar Ruby<br>true     Lavar o carro</pre><p>Temos agora nosso ponto de partida.</p><h3>6. O novo requisito</h3><p>Imagine que o cliente agora faça um novo pedido:</p><blockquote><strong><em>A lista de tarefas deve ser dividida em duas seções: tarefas concluídas e tarefas não concluídas.</em></strong></blockquote><p>Em vez de simplesmente alterar a view, vamos implementar essa mudança utilizando <strong>TDD</strong>.</p><p>A ideia será:</p><pre>REQUISITO<br>    ↓<br>CUCUMBER<br>    ↓<br>TESTE FALHA<br>    ↓<br>IMPLEMENTAÇÃO<br>    ↓<br>TESTE PASSA<br>    ↓<br>RSPEC<br>    ↓<br>VALIDAÇÃO INTERNA</pre><p>É aqui que começam a trabalhar juntos os dois protagonistas deste episódio:</p><p><strong>Cucumber</strong> — descreve o comportamento.</p><p><strong>RSpec</strong> — verifica a implementação.</p><h3>7. Especificando o requisito</h3><p>Antes de escrever código, vamos transformar o pedido do cliente em comportamento observável.</p><h4>Exibição de tarefas concluídas</h4><p>Ao visitar a página principal:</p><ul><li>devo ver a seção <strong>Tarefas Concluídas</strong>;</li><li>se existir uma tarefa concluída chamada &quot;Lavar o carro&quot;, ela deve aparecer nessa seção;</li><li>se existir uma tarefa não concluída chamada &quot;Estudar Ruby&quot;, ela não deve aparecer na seção de concluídas.</li></ul><p>Podemos resumir:</p><pre>Exibição de tarefas concluídas<br><br>Ao visitar a página principal<br>    eu devo ver a seção tarefas concluídas<br>    Teste 1:<br>      criar uma tarefa concluída chamada &quot;Lavar o carro&quot;<br>      eu devo ver &quot;Lavar o carro&quot; na seção concluídas<br>    Teste 2:<br>      criar uma tarefa não concluída chamada &quot;Estudar Ruby&quot;<br>      eu não devo ver &quot;Estudar Ruby&quot; na seção concluídas</pre><p>Observe que ainda não estamos falando de HTML, CSS, controller ou ActiveRecord.</p><p>Estamos falando apenas de <strong>comportamento</strong>.</p><p>Essa é uma das ideias mais interessantes do Cucumber.</p><h3>8. Transformando o requisito em passos</h3><p>Agora podemos transformar nossa linguagem de negócio em ações que futuramente serão implementadas.</p><pre>visitar_a_pagina_principal<br>criar_uma_tarefa_concluida_anotada(nota)<br>criar_uma_tarefa_nao_concluida_anotada(nota)<br>eu_devo_ver_a_secao_tarefas_concluidas<br>eu_devo_ver_na_secao_concluidas_a_tarefa(nota)<br>eu_nao_devo_ver_na_secao_concluidas_a_tarefa(nota)</pre><p>Ainda não precisamos saber <strong>como</strong> cada função será implementada.</p><p>Primeiro definimos <strong>o que queremos que aconteça</strong>.</p><h3>9. Criando nossa primeira Feature</h3><p>Dentro do container:</p><pre>mkdir -p features<br>touch features/tarefas.feature</pre><p>Abra:</p><pre>features/tarefas.feature</pre><p>e escreva:</p><pre>Feature: Exibicao de Tarefas Concluidas<br>  A pagina principal do app deve exibir corretamente as tarefas concluidas<br>  e garantir que tarefas nao concluidas nao aparecam na secao especificada.<br><br>Background:<br>    Given eu estou na pagina principal do app<br>  Scenario: Exibir uma tarefa concluida na secao &quot;tarefas-concluidas&quot;<br>    Given existe uma tarefa concluida chamada &quot;Lavar o carro&quot;<br>    When eu envio essa tarefa concluida<br>    Then eu devo ver &quot;Lavar o carro&quot; na secao &quot;tarefas-concluidas&quot;<br>  Scenario: Garantir que uma tarefa nao concluida nao apareca na secao &quot;tarefas-concluidas&quot;<br>    Given existe uma tarefa nao concluida chamada &quot;Estudar Ruby&quot;<br>    When eu envio essa tarefa nao concluida<br>    Then eu devo ver &quot;Estudar Ruby&quot; na secao &quot;tarefas-nao-concluidas&quot;<br>  Scenario: Exibir somente tarefas concluidas na secao &quot;tarefas-concluidas&quot;<br>    Given existe uma tarefa concluida chamada &quot;Lavar o carro&quot;<br>    And existe uma tarefa nao concluida chamada &quot;Estudar Ruby&quot;<br>    When eu envio ambas as tarefas<br>    Then eu devo ver &quot;Lavar o carro&quot; na secao &quot;tarefas-concluidas&quot;<br>    And eu nao devo ver &quot;Estudar Ruby&quot; na secao &quot;tarefas-concluidas&quot;</pre><h3>Por que não usamos acentos?</h3><p>Aqui estamos deliberadamente usando:</p><pre>concluida<br>nao<br>pagina<br>secao</pre><p>em vez de:</p><pre>concluída<br>não<br>página<br>seção</pre><p>Não é uma exigência do Cucumber, mas simplifica a correspondência exata entre os textos da feature e as definições de passos.</p><p>Na interface do usuário, naturalmente, podemos usar a acentuação correta.</p><p>Acrescente no:</p><p>features/support/<strong>env.rb</strong></p><pre># features/support/env.rb<br>ENV[&#39;RAILS_ENV&#39;] ||= &#39;test&#39;<br><br>require File.expand_path(&#39;../../../config/environment&#39;, __FILE__)<br><br>require &#39;cucumber/rails&#39;<br><br>Capybara.default_driver = :rack_test<br>Capybara.javascript_driver = :selenium_chrome_headless<br><br>ActionController::Base.allow_rescue = false<br><br># Limpa o banco entre cenários<br>begin<br>  DatabaseCleaner.strategy = :transaction<br>rescue NameError<br>  raise &quot;Add database_cleaner-active_record to your Gemfile&quot;<br>end<br><br>Cucumber::Rails::Database.autorun_database_cleaner = true</pre><h3>10. Instalando o RSpec</h3><p>No Gemfile, acrescente:</p><pre>group :development, :test do<br>  gem &quot;rspec-rails&quot;<br>end</pre><p>Depois instale a dependência:</p><pre>bundle install</pre><p>Agora inicialize o RSpec:</p><pre>bundle exec rails generate rspec:install</pre><p>Isso cria a estrutura básica:</p><pre>spec/<br>├── spec_helper.rb<br>└── rails_helper.rb</pre><p>Podemos verificar:</p><pre>bundle exec rspec</pre><p>Inicialmente, é normal não haver exemplos:</p><pre>No examples found.<br><br>Finished in ...<br>0 examples, 0 failures</pre><p>O RSpec está instalado e funcionando.</p><p>Como o RSpec já foi instalado e configurado agora, precisamos apenas gerar o <strong>binstub</strong> para executá-lo de forma consistente dentro do projeto.</p><p>Ainda dentro do container:</p><pre>bundle binstub rspec-core</pre><p>Isso cria o executável:</p><pre>bin/rspec</pre><p>Verifique:</p><pre>ls -l bin/rspec</pre><p>A partir daqui, podemos executar os testes usando:</p><pre>bin/rspec</pre><p>Verifique:</p><pre>chmod +x bin/rspec</pre><p>E execute:</p><pre>bin/rspec</pre><p>O resultado esperado é semelhante a:</p><pre>No examples found.<br><br>Finished in 0.00... seconds<br>0 examples, 0 failures</pre><p>Está tudo certo.</p><p>Ainda não temos testes RSpec.</p><h3>11. Um pequeno atalho</h3><p>Como vamos executar RSpec muitas vezes, podemos criar um alias.</p><p>Dentro do container:</p><pre>echo &#39;alias br=&quot;bin/rspec&quot;&#39; &gt;&gt; ~/.bashrc<br>source ~/.bashrc</pre><p>Agora:</p><pre>br</pre><p>é equivalente a:</p><pre>bin/rspec</pre><p>E podemos executar:</p><pre>br spec/system/tarefas_spec.rb</pre><p>ou:</p><pre>br spec/system/tarefas_spec.rb -fd</pre><p>Isso deixa o fluxo bastante confortável.</p><h3>12. Criando o suporte para nossa DSL</h3><p>Crie:</p><pre>mkdir -p spec/support<br>touch spec/support/tarefas_steps.rb</pre><p>O arquivo:</p><pre>spec/support/tarefas_steps.rb</pre><p>terá inicialmente:</p><pre>module TarefasSteps<br><br>  # Arrange<br><br>  def visitar_a_pagina_principal<br>    visit root_path<br>  end<br>  def criar_uma_tarefa_concluida_anotada(nota)<br>    Tarefa.create!(nota: nota, feito: true)<br>  end<br>  def criar_uma_tarefa_nao_concluida_anotada(nota)<br>    Tarefa.create!(nota: nota, feito: false)<br>  end<br><br>  # Act &amp; Assert<br><br>  def eu_devo_ver_a_secao_tarefas_concluidas<br>    expect(page).to have_css(&quot;.tarefas-concluidas&quot;)<br>  end<br>  def eu_devo_ver_na_secao_concluidas_a_tarefa(nota)<br>    within(&quot;.tarefas-concluidas&quot;) do<br>      expect(page).to have_content(nota)<br>    end<br>  end<br>  def eu_nao_devo_ver_na_secao_concluidas_a_tarefa(nota)<br>    within(&quot;.tarefas-concluidas&quot;) do<br>      expect(page).not_to have_content(nota)<br>    end<br>  end<br>end</pre><p>A ideia é criar uma pequena <strong>DSL (Domain Specific Language)</strong> para nossos testes.</p><p>Em vez de escrever:</p><pre>visit root_path</pre><p>podemos escrever:</p><pre>visitar_a_pagina_principal</pre><p>Em vez de:</p><pre>Tarefa.create!(nota: &quot;Lavar o carro&quot;, feito: true)</pre><p>podemos escrever:</p><pre>criar_uma_tarefa_concluida_anotada(&quot;Lavar o carro&quot;)</pre><p>A DSL funciona como uma ponte entre a linguagem técnica e a linguagem utilizada na especificação.</p><h3>13. Configurando o RSpec para carregar a DSL</h3><p>Abra:</p><pre>spec/rails_helper.rb</pre><p>Adicione (ou comente):</p><pre>Rails.root.glob(&quot;spec/support/**/*.rb&quot;).sort_by(&amp;:to_s).each do |file|<br>  require file<br>end</pre><p>E configure o módulo:</p><pre>RSpec.configure do |config|<br>  config.include TarefasSteps, type: :system<br>  config.include TarefasSteps, type: :request<br>end</pre><p>Assim os métodos definidos em TarefasSteps estarão disponíveis nos testes RSpec.</p><h3>14. Configurando o Cucumber</h3><p>Como já estamos usando Cucumber, precisamos das dependências de teste.</p><p>No Gemfile:</p><pre>group :test do<br>  gem &quot;cucumber-rails&quot;, require: false<br>  gem &quot;database_cleaner-active_record&quot;<br>  gem &quot;capybara&quot;<br>  gem &quot;selenium-webdriver&quot;<br>end</pre><p>Depois:</p><pre>bundle install</pre><p>Agora gere a estrutura do Cucumber:</p><pre>rails generate cucumber:install</pre><p>Se o gerador perguntar se deve sobrescrever um arquivo que já criamos, <strong>preserve nossa configuração respondendo </strong><strong>n</strong>.</p><h3>15. Primeiro teste: vamos fazer quebrar</h3><p>Agora vem uma parte importante do TDD.</p><p>Execute:</p><pre>bin/cucumber</pre><p>Neste momento, <strong>esperamos que os testes falhem</strong>.</p><pre>3 scenarios (3 undefined)<br>14 steps (14 undefined)<br>0m0.118s</pre><p>Por quê?</p><p>Nossa view ainda possui apenas:</p><pre>&lt;tbody&gt;<br>  &lt;% @tarefas.each do |tarefa| %&gt;</pre><p>Não existem ainda:</p><pre>tarefas-concluidas<br>tarefas-nao-concluidas</pre><p>Portanto o teste deverá procurar uma seção que ainda não existe.</p><p>Esse é exatamente o comportamento que queremos no ciclo TDD:</p><pre>🔴 RED<br>Teste escrito<br>     ↓<br>Teste falha<br><br>🟢 GREEN<br>Implementação mínima<br>     ↓<br>Teste passa<br><br><br>🔵 REFACTOR<br>Melhorar o código<br>     ↓<br>Testes continuam passando<br><br></pre><h3>16. Implementando a funcionalidade</h3><p>Agora sim vamos alterar a view.</p><p>Substitua o conteúdo de:</p><pre>app/views/tarefas/index.html.erb</pre><p>por:</p><pre>&lt;h1&gt;Listando Tarefas&lt;/h1&gt;<br><br>&lt;h2&gt;Tarefas Concluídas&lt;/h2&gt;<br>&lt;section id=&quot;tarefas-concluidas&quot; class=&quot;tarefas-concluidas&quot;&gt;<br>  &lt;table class=&quot;table&quot;&gt;<br>    &lt;thead&gt;<br>      &lt;tr&gt;<br>        &lt;th&gt;Feito&lt;/th&gt;<br>        &lt;th&gt;Nota&lt;/th&gt;<br>        &lt;th colspan=&quot;3&quot;&gt;&lt;/th&gt;<br>      &lt;/tr&gt;<br>    &lt;/thead&gt;<br>    &lt;tbody&gt;<br>      &lt;% @tarefas.where(feito: true).each do |tarefa| %&gt;<br>        &lt;tr&gt;<br>          &lt;td&gt;true&lt;/td&gt;<br>          &lt;td&gt;&lt;%= tarefa.nota %&gt;&lt;/td&gt;<br>          &lt;td&gt;&lt;%= link_to &quot;Mostrar&quot;, tarefa %&gt;&lt;/td&gt;<br>          &lt;td&gt;&lt;%= link_to &quot;Editar&quot;, edit_tarefa_path(tarefa) %&gt;&lt;/td&gt;<br>          &lt;td&gt;<br>            &lt;%= link_to &quot;Apagar&quot;, tarefa,<br>                method: :delete,<br>                data: { confirm: &quot;Está certo?&quot; } %&gt;<br>          &lt;/td&gt;<br>        &lt;/tr&gt;<br>      &lt;% end %&gt;<br>    &lt;/tbody&gt;<br>  &lt;/table&gt;<br>&lt;/section&gt;<br>&lt;h2&gt;Tarefas Não Concluídas&lt;/h2&gt;<br>&lt;section id=&quot;tarefas-nao-concluidas&quot; class=&quot;tarefas-nao-concluidas&quot;&gt;<br>  &lt;table class=&quot;table&quot;&gt;<br>    &lt;thead&gt;<br>      &lt;tr&gt;<br>        &lt;th&gt;Feito&lt;/th&gt;<br>        &lt;th&gt;Nota&lt;/th&gt;<br>        &lt;th colspan=&quot;3&quot;&gt;&lt;/th&gt;<br>      &lt;/tr&gt;<br>    &lt;/thead&gt;<br>    &lt;tbody&gt;<br>      &lt;% @tarefas.where(feito: false).each do |tarefa| %&gt;<br>        &lt;tr&gt;<br>          &lt;td&gt;false&lt;/td&gt;<br>          &lt;td&gt;&lt;%= tarefa.nota %&gt;&lt;/td&gt;<br>          &lt;td&gt;&lt;%= link_to &quot;Mostrar&quot;, tarefa %&gt;&lt;/td&gt;<br>          &lt;td&gt;&lt;%= link_to &quot;Editar&quot;, edit_tarefa_path(tarefa) %&gt;&lt;/td&gt;<br>          &lt;td&gt;<br>            &lt;%= link_to &quot;Apagar&quot;, tarefa,<br>                method: :delete,<br>                data: { confirm: &quot;Está certo?&quot; } %&gt;<br>          &lt;/td&gt;<br>        &lt;/tr&gt;<br>      &lt;% end %&gt;<br>    &lt;/tbody&gt;<br>  &lt;/table&gt;<br>&lt;/section&gt;<br>&lt;br&gt;<br>&lt;%= link_to &quot;Nova Tarefa&quot;, new_tarefa_path, class: &quot;btn btn-primary&quot; %&gt;</pre><p>A implementação é deliberadamente simples.</p><p>A regra é:</p><pre>@tarefas.where(feito: true)</pre><p>para concluídas.</p><p>E:</p><pre>@tarefas.where(feito: false)</pre><p>para não concluídas.</p><h3>17. Rodando novamente o Cucumber</h3><p>Agora:</p><pre>bin/cucumber</pre><p>Erro de novo.</p><p>o Cucumber encontrou o .feature, mas <strong>não encontrou os step definitions</strong>.</p><p>A mensagem 3 scenarios (3 undefined) confirma que o problema ainda não é a view nem o Rails.</p><h3>1. Confira se o arquivo existe</h3><p>Dentro do container:</p><pre>ls -la features/step_definitions</pre><p>Você deve ter:</p><pre>tarefas_steps.rb</pre><p>Se não existir:</p><pre>mkdir -p features/step_definitions<br>touch features/step_definitions/tarefas_steps.rb</pre><h3>2. Coloque este conteúdo no arquivo</h3><p>features/step_definitions/tarefas_steps.rb:</p><pre># features/step_definitions/tarefas_steps.rb<br><br>Given(&quot;eu estou na pagina principal do app&quot;) do<br>  visit &quot;/&quot;<br>end<br><br>Given(&quot;existe uma tarefa concluida chamada {string}&quot;) do |nome|<br>  @tarefa_concluida = Tarefa.create!(nota: nome, feito: true)<br>end<br><br>Given(&quot;existe uma tarefa nao concluida chamada {string}&quot;) do |nome|<br>  @tarefa_nao_concluida = Tarefa.create!(nota: nome, feito: false)<br>end<br><br>When(&quot;eu envio essa tarefa concluida&quot;) do<br>  # Se já existe no banco, apenas recarrega a página<br>  visit &quot;/&quot;<br>end<br><br>When(&quot;eu envio essa tarefa nao concluida&quot;) do<br>  # Apenas recarrega para refletir a lista<br>  visit &quot;/&quot;<br>end<br><br>When(&quot;eu envio ambas as tarefas&quot;) do<br>  visit &quot;/&quot;<br>end<br><br>Then(&quot;eu devo ver {string} na secao {string}&quot;) do |texto, secao|<br>  within(:xpath, &quot;//*[contains(@class,&#39;#{secao.parameterize}&#39;) or contains(@id,&#39;#{secao.parameterize}&#39;)]&quot;) do<br>    expect(page).to have_content(texto)<br>  end<br>end<br><br>Then(&quot;eu nao devo ver {string} na secao {string}&quot;) do |texto, secao|<br>  within(:xpath, &quot;//*[contains(@class,&#39;#{secao.parameterize}&#39;) or contains(@id,&#39;#{secao.parameterize}&#39;)]&quot;) do<br>    expect(page).not_to have_content(texto)<br>  end<br>end</pre><h3>3. Rode novamente</h3><pre>bin/cucumber</pre><p>Agora o resultado <strong>não deve mais ser </strong><strong>undefined</strong>.</p><p>O resultado esperado:</p><pre>3 scenarios (3 passed)<br>14 steps (14 passed)</pre><p>🎉</p><p>Temos nosso primeiro ciclo TDD completo:</p><pre>REQUISITO<br>   ↓<br>FEATURE<br>   ↓<br>TESTE FALHANDO<br>   ↓<br>IMPLEMENTAÇÃO<br>   ↓<br>TESTE PASSANDO</pre><p>E o mais interessante:</p><p><strong>não escrevemos o teste para validar a implementação.</strong></p><p>Escrevemos o teste para validar o comportamento que o usuário pediu.</p><h3>18. Agora entra o RSpec</h3><p>O Cucumber respondeu:</p><blockquote><strong><em>O comportamento esperado está funcionando?</em></strong></blockquote><p>Agora queremos outra pergunta:</p><blockquote><strong><em>A implementação interna também está correta?</em></strong></blockquote><p>Vamos criar um system spec:</p><pre>rails generate rspec:system tarefas</pre><p>Isso criará:</p><pre>spec/system/tarefas_spec.rb</pre><p>Substitua o conteúdo por:</p><pre>require &quot;rails_helper&quot;<br><br>RSpec.describe &quot;Exibição de tarefas concluídas&quot;, type: :system do<br>  before do<br>    driven_by(:rack_test)<br>  end<br>  context &quot;Ao visitar a página principal&quot; do<br>    before do<br>      visitar_a_pagina_principal<br>    end<br>    it &quot;eu devo ver a seção tarefas concluídas&quot; do<br>      eu_devo_ver_a_secao_tarefas_concluidas<br>    end<br>    context &quot;Teste 1: criar uma tarefa concluída anotada&quot; do<br>      let!(:tarefa_concluida) do<br>        criar_uma_tarefa_concluida_anotada(&quot;Lavar o carro&quot;)<br>      end<br>      before do<br>        visitar_a_pagina_principal<br>      end<br>      it &quot;eu devo ver na seção concluídas a tarefa&quot; do<br>        eu_devo_ver_na_secao_concluidas_a_tarefa(&quot;Lavar o carro&quot;)<br>      end<br>    end<br>    context &quot;Teste 2: criar uma tarefa não concluída anotada&quot; do<br>      let!(:tarefa_nao_concluida) do<br>        criar_uma_tarefa_nao_concluida_anotada(&quot;Estudar Ruby&quot;)<br>      end<br>      before do<br>        visitar_a_pagina_principal<br>      end<br>      it &quot;eu não devo ver na seção concluídas a tarefa&quot; do<br>        eu_nao_devo_ver_na_secao_concluidas_a_tarefa(&quot;Estudar Ruby&quot;)<br>      end<br>    end<br>  end<br>end</pre><p>Execute:</p><pre>br spec/system/tarefas_spec.rb -fd</pre><p>Esperamos:</p><pre>Exibição de tarefas concluídas<br>  Ao visitar a página principal<br>    eu devo ver a seção tarefas concluídas<br>    Teste 1: criar uma tarefa concluída anotada<br>      eu devo ver na seção concluídas a tarefa<br>    Teste 2: criar uma tarefa não concluída anotada<br>      eu não devo ver na seção concluídas a tarefa<br><br>3 examples, 0 failures</pre><h3>19. O que acabamos de construir?</h3><p>Temos agora <strong>duas perspectivas para o mesmo requisito</strong>.</p><h3>Cucumber</h3><p>Expressa o comportamento:</p><pre>Then eu devo ver &quot;Lavar o carro&quot; na secao &quot;tarefas-concluidas&quot;</pre><h3>RSpec</h3><p>Verifica o comportamento através da aplicação:</p><pre>expect(page).to have_content(&quot;Lavar o carro&quot;)</pre><p>Podemos enxergar assim:</p><pre>REQUISITO<br>                 │<br>                 ▼<br>        ┌─────────────────┐<br>        │    CUCUMBER     │<br>        │                 │<br>        │ O que o usuário │<br>        │ espera?         │<br>        └────────┬────────┘<br>                 │<br>                 ▼<br>             APLICAÇÃO<br>                 │<br>                 ▼<br>        ┌─────────────────┐<br>        │     RSpec       │<br>        │                 │<br>        │ A implementação │<br>        │ funciona?       │<br>        └─────────────────┘</pre><p><strong>Cucumber fala. RSpec garante.</strong></p><p>E agora tudo isso está rodando sobre a mesma infraestrutura que construímos no episódio anterior:</p><pre>Docker<br>  │<br>  ├── app<br>  │    ├── Ruby<br>  │    ├── Rails<br>  │    ├── Cucumber<br>  │    └── RSpec<br>  │<br>  └── db<br>       └── PostgreSQL</pre><p>Isso deixa a série muito mais coerente: <strong>o primeiro episódio constrói a infraestrutura reproduzível; este episódio usa essa infraestrutura para desenvolver com testes.</strong></p><blockquote><em>Obrigado por ler!<br>Agradeço por você dedicar um tempo para explorar este guia — seu apoio significa muito. Desejo muito sucesso no seu projeto!</em></blockquote><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=61fe31cc894e" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/cucumber-rspec-testes-que-falam-e-testes-que-garantem-61fe31cc894e">Cucumber + RSpec: Testes que Falam e Testes que Garantem</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rails + PostgreSQL + Docker]]></title>
            <link>https://medium.com/jungletronics/rails-postgresql-docker-5f7a00afc18c?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/5f7a00afc18c</guid>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[docker-compose]]></category>
            <category><![CDATA[how-to-docker]]></category>
            <category><![CDATA[ruby-on-rails-development]]></category>
            <category><![CDATA[dockerfiles]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Sat, 12 Sep 2026 20:18:21 GMT</pubDate>
            <atom:updated>2026-09-17T19:45:46.080Z</atom:updated>
            <content:encoded><![CDATA[<h4>Rails + PostgreSQL sem complicação: um ambiente reproduzível, isolado e pronto para desenvolver</h4><p>Criar uma aplicação Rails pode parecer simples até chegarmos à infraestrutura: versão do Ruby, Bundler, PostgreSQL, dependências nativas, permissões, portas, volumes e configuração do banco.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*M4z3SeFuf6K6nL5fWvxrmA.png" /><figcaption><a href="https://github.com/giljr/rails-postgres-docker">GitHub Repo</a></figcaption></figure><p>A proposta deste tutorial é justamente <strong>tirar essa complexidade do caminho</strong>.</p><p>Até aqui, construímos uma infraestrutura na qual o sistema operacional precisa apenas do Docker. Ruby, Rails, PostgreSQL, gems e suas dependências ficam isolados nos containers.</p><p>A abstração conquistada é simples:</p><pre>SEU COMPUTADOR<br>      │<br>      │ Docker<br>      ▼<br>┌──────────────────────────────┐<br>│          Aplicação           │<br>│     Ruby + Rails + Gems      │<br>└──────────────┬───────────────┘<br>               │<br>               │ rede Docker<br>               ▼<br>┌──────────────────────────────┐<br>│         PostgreSQL           │<br>│          Banco de dados      │<br>└──────────────────────────────┘</pre><p>Você desenvolve no seu projeto normalmente. O Docker cuida da infraestrutura.</p><p>Isso significa que, para começar outro projeto Rails, não precisamos repetir toda a preparação do ambiente. A mesma estrutura pode ser reutilizada, alterando apenas as configurações específicas da aplicação.</p><p>E há outro detalhe importante: <strong>o código continua no seu computador</strong>. Os containers fornecem o ambiente de execução, mas o projeto permanece acessível pelo editor, pelo Git e pelas ferramentas que você já utiliza.</p><p>O resultado é uma separação clara:</p><blockquote><strong><em>Seu computador fornece o Docker.<br>O projeto fornece a aplicação.<br>Os containers fornecem o ambiente.</em></strong></blockquote><p>O objetivo aqui não é construir uma infraestrutura sofisticada. É chegar ao ponto em que <strong>Rails + PostgreSQL simplesmente funcionem</strong>, de forma reproduzível, isolada e fácil de entender.</p><p>No final, incluímos uma <a href="#ceeb"><strong>nota técnica</strong></a> avaliando as decisões adotadas e os pontos que poderiam ser aprimorados em um ambiente mais robusto ou de produção.</p><h3>Resultado final</h3><pre>Ubuntu<br> └── Docker Engine<br>      ├── Rails 8.1.x + Ruby 3.4.x<br>      └── image: postgres:17</pre><p>Neste cenário, Ruby, Rails e PostgreSQL são executados dentro dos containers, sem necessidade de instalação dessas ferramentas diretamente no Ubuntu.</p><p>E, na sua configuração final, teremos três boas medidas de segurança: <strong>Rails sem root</strong>, <strong>remoção de capabilities + </strong><strong>no-new-privileges no app</strong>, e <strong>PostgreSQL sem porta publicada no host</strong>.</p><h3>1. Criar o projeto</h3><p>Escolha um diretório para seus projetos Rails:</p><pre>mkdir -p ~/Documents/projects/rails<br>cd ~/Documents/projects/rails</pre><p>Crie um diretório para a aplicação:</p><pre>mkdir meu-projeto<br>cd meu-projeto</pre><p>Crie os arquivos necessários:</p><pre>touch compose.yml .env Dockerfile<br>code .</pre><blockquote><em>Neste tutorial, </em><em>meu-projeto é apenas um nome de exemplo. Substitua pelo nome da sua aplicação.</em></blockquote><p><strong>Como os três se relacionam:</strong></p><p>Pode pensar assim:</p><pre>.env<br>  │<br>  │ fornece valores<br>  ▼<br>compose.yml<br>  │<br>  │ define os containers<br>  ▼<br>Dockerfile<br>  │<br>  │ constrói a imagem do app<br>  ▼<br>┌─────────────────────────┐<br>│ Docker                  │<br>│                         │<br>│  ┌───────────────────┐  │<br>│  │ app               │  │<br>│  │ Rails 8.1.3.1     │  │<br>│  │ Ruby 3.4.10       │  │<br>│  │ usuário: app      │  │<br>│  └─────────┬─────────┘  │<br>│            │            │<br>│          db:5432        │<br>│            │            │<br>│  ┌─────────▼─────────┐  │<br>│  │ PostgreSQL 17     │  │<br>│  │ volume persistente│  │<br>│  └───────────────────┘  │<br>└─────────────────────────┘</pre><h3>2. Credenciais no .env</h3><p>Como o rails new já cria o .gitignore da aplicação, não precisamos criar esse arquivo manualmente. Basta garantir que o .env esteja listado nele.</p><p>Crie ou edite o .env:</p><pre>UID=1000<br>GID=1000<br><br>APP_PORT=3000<br>DB_PORT=5432<br><br>POSTGRES_DB=app_development<br>POSTGRES_USER=app<br>POSTGRES_PASSWORD=app_dev<br><br>DATABASE_TEST_NAME=app_test</pre><p><strong>1. </strong><strong>env — configurações e variáveis</strong></p><p>É onde ficam os valores que podem variar conforme o ambiente, sem precisar alterar o compose.yml.</p><p>No seu caso:</p><ul><li>UID / GID → definem o usuário/grupo Linux usado pelo container da aplicação.</li><li>APP_PORT=3000 → porta da aplicação Rails.</li><li>DB_PORT=5432 → porta padrão do PostgreSQL; <strong>atualmente não está sendo publicada no host</strong>, apesar de estar definida aqui.</li><li>POSTGRES_DB → nome do banco.</li><li>POSTGRES_USER → usuário do PostgreSQL.</li><li>POSTGRES_PASSWORD → senha do PostgreSQL.</li><li>DATABASE_TEST_NAME → nome previsto para o banco de testes.</li></ul><p><strong>Importante:</strong> o .env não deveria ser versionado no Git quando contém senhas reais. Normalmente fica no .gitignore.</p><h3>3. compose.yml</h3><pre>services:<br><br>  app:<br>    build:<br>      context: .<br>      dockerfile: Dockerfile<br>      args:<br>        UID: ${UID:-1000}<br>        GID: ${GID:-1000}<br><br>    working_dir: /app<br><br>    volumes:<br>      - .:/app<br>      - bundle:/usr/local/bundle<br><br>    ports:<br>      - &quot;127.0.0.1:${APP_PORT:-3000}:3000&quot;<br><br>    environment:<br>      DATABASE_HOST: db<br>      DATABASE_NAME: ${POSTGRES_DB}<br>      DATABASE_USER: ${POSTGRES_USER}<br>      DATABASE_PASSWORD: ${POSTGRES_PASSWORD}<br><br>    depends_on:<br>      - db<br><br>    # Hardening<br>    security_opt:<br>      - no-new-privileges:true<br><br>    cap_drop:<br>      - ALL<br><br>    stdin_open: true<br>    tty: true<br><br>  db:<br>    image: postgres:17<br><br>    environment:<br>      POSTGRES_DB: ${POSTGRES_DB}<br>      POSTGRES_USER: ${POSTGRES_USER}<br>      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}<br><br>    volumes:<br>      - postgres_data:/var/lib/postgresql/data<br><br>volumes:<br>  bundle:<br>  postgres_data:</pre><p><strong>2. </strong><strong>compose.yml — orquestra os containers</strong></p><p>É o arquivo que diz ao Docker <strong>como montar e executar o ambiente</strong>.</p><p>Você tem dois serviços:</p><h4>app</h4><p>É o container da aplicação Rails.</p><p>Ele:</p><ul><li>constrói a imagem usando o Dockerfile;</li><li>monta seu projeto em /app;</li><li>mantém as gems no volume bundle;</li><li>publica Rails somente em:</li></ul><pre>127.0.0.1:3000</pre><ul><li>encontra o PostgreSQL pelo nome:</li></ul><pre>db</pre><ul><li>recebe as credenciais do .env;</li><li>inicia depois do serviço db;</li><li>possui alguns controles de segurança:</li></ul><pre>security_opt:<br>  - no-new-privileges:true<br><br>cap_drop:<br>  - ALL</pre><p>Ou seja, você já colocou uma camada de <strong>hardening do container da aplicação</strong>.</p><h4>db</h4><p>É o container do PostgreSQL 17.</p><p>Ele:</p><ul><li>usa a imagem oficial postgres:17;</li><li>recebe banco, usuário e senha do .env;</li><li>grava os dados permanentemente no volume:</li></ul><pre>postgres_data:/var/lib/postgresql/data</pre><p>E um ponto importante da configuração atual:</p><pre>db:<br>  ...<br>  volumes:<br>    - postgres_data:/var/lib/postgresql/data</pre><p>Não existe ports: no db.</p><p>Portanto, o PostgreSQL <strong>não está exposto diretamente no host</strong>. Ele fica acessível para o app pela rede interna do Docker através de:</p><pre>db:5432</pre><p>Por essas configuracoes:</p><pre># Hardening<br>    security_opt:<br>      - no-new-privileges:true<br><br>    cap_drop:<br>      - ALL<br><br>    stdin_open: true<br>    tty: true</pre><p><strong>O processo Rails roda como </strong><strong>app, sem capabilities e sem possibilidade de ganhar novos privilégios:</strong></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/574/1*5ZPa9V4P7tBryrAonb8aWQ.png" /></figure><p><strong>Para entender o usuário do container e o alinhamento de UID/GID com o host, consulte a </strong><a href="#ebde"><strong>Nota ¹</strong></a><strong>.</strong></p><p>Como seu compose.yml já está pronto, precisamos apenas colocar de volta o <strong>Dockerfile bootstrap</strong>, aquele que existe antes do rails new:</p><p>Dockerfile</p><pre>FROM ruby:3.4.10-bookworm<br><br>ARG UID=1000<br>ARG GID=1000<br><br>RUN apt-get update -qq &amp;&amp; \<br>    apt-get install -y --no-install-recommends \<br>    build-essential \<br>    libpq-dev \<br>    libyaml-dev \<br>    git \<br>    curl &amp;&amp; \<br>    rm -rf /var/lib/apt/lists/*<br><br># Cria o grupo e usuário da aplicação<br>RUN groupadd --gid ${GID} app &amp;&amp; \<br>    useradd --uid ${UID} \<br>             --gid ${GID} \<br>             --create-home \<br>             --shell /bin/bash \<br>             app<br><br>WORKDIR /app<br><br>RUN gem install bundler -v 2.6.9<br>RUN gem install rails -v 8.1.3.1<br><br># Permissões<br>RUN chown -R app:app /app /usr/local/bundle<br><br>USER app<br><br># A aplicação será montada pelo docker compose em /app.<br># As gems serão instaladas no volume bundle.<br>CMD [&quot;bin/rails&quot;, &quot;server&quot;, &quot;-b&quot;, &quot;0.0.0.0&quot;]</pre><p>Depois:</p><pre>docker compose build app</pre><p>O Dockerfile define <strong>como criar o ambiente onde o Rails vai executar</strong>.</p><p>A sequência é basicamente:</p><pre>Ruby 3.4.10<br>      ↓<br>Debian Bookworm<br>      ↓<br>instala ferramentas/dependências<br>      ↓<br>cria usuário &quot;app&quot;<br>      ↓<br>instala Bundler<br>      ↓<br>instala Rails 8.1.3.1<br>      ↓<br>ajusta permissões<br>      ↓<br>USER app<br>      ↓<br>inicia Rails</pre><p>Um ponto importante de segurança está aqui:</p><pre>USER app</pre><p>Isso faz com que o processo principal do Rails <strong>não execute como root</strong>.</p><p>E aqui:</p><pre>RUN groupadd --gid ${GID} app<br>useradd --uid ${UID} ...</pre><p>você ainda alinha o UID/GID do usuário dentro do container com o usuário do ambiente Linux.</p><h3>4. Criar o Rails</h3><p>O UID e o GID já estão definidos no .env:</p><pre>UID=1000<br>GID=1000</pre><p>Crie a aplicação Rails dentro do container:</p><pre>docker compose run --rm app \<br>  rails new . \<br>  --database=postgresql \<br>  --skip-test</pre><p>Esse comando cria a aplicação Rails já configurada para usar <strong>PostgreSQL</strong>.</p><blockquote><strong><em>Por que o </em></strong><strong><em>.?</em></strong><em><br>O ponto indica o </em><strong><em>diretório atual</em></strong><em>. Como o projeto está montado no container em </em><strong><em>/app</em></strong><em>, o Rails cria os arquivos diretamente nesse diretório — que, por sua vez, corresponde à pasta do projeto no computador.</em></blockquote><p>Durante a execução, o Rails poderá perguntar se deve sobrescrever arquivos que já existem.</p><p><strong>Quando perguntar sobre o </strong><strong>Dockerfile, responda </strong><strong>n (não).</strong></p><p>Por enquanto, vamos <strong>manter o nosso Dockerfile como está</strong>, pois ele foi preparado especificamente para este ambiente Docker. Não precisamos substituí-lo pelo Dockerfile gerado automaticamente pelo Rails.</p><h3>5. Configurar PostgreSQL</h3><p>O Rails deve utilizar PostgreSQL, portanto o Gemfile deve conter:</p><pre>gem &quot;pg&quot;, &quot;~&gt; 1.5&quot;</pre><p>e não deve depender de:</p><pre>gem &quot;sqlite3&quot;</pre><h4>database.yml</h4><p>O Rails gera um database.yml bastante completo, incluindo configurações voltadas a diferentes cenários de produção.</p><p>Como nosso objetivo neste tutorial é construir um ambiente de desenvolvimento simples com Docker Compose, vamos utilizar uma configuração menor e baseada em variáveis de ambiente.</p><p>A principal diferença é que o Rails precisa saber que o PostgreSQL está em <strong>outro container</strong>. No Docker Compose, o serviço é chamado db, portanto esse será o nosso host:</p><pre>Rails → db:5432 → PostgreSQL</pre><p>Substitua o conteúdo de config/database.yml por:</p><pre>default: &amp;default<br>  adapter: postgresql<br>  encoding: unicode<br>  host: &lt;%= ENV.fetch(&quot;DATABASE_HOST&quot;, &quot;db&quot;) %&gt;<br>  username: &lt;%= ENV.fetch(&quot;DATABASE_USER&quot;, &quot;app&quot;) %&gt;<br>  password: &lt;%= ENV.fetch(&quot;DATABASE_PASSWORD&quot;, &quot;app_dev&quot;) %&gt;<br>  pool: &lt;%= ENV.fetch(&quot;RAILS_MAX_THREADS&quot;, 5) %&gt;<br><br>development:<br>  &lt;&lt;: *default<br>  database: &lt;%= ENV.fetch(&quot;DATABASE_NAME&quot;, &quot;app_development&quot;) %&gt;<br>test:<br>  &lt;&lt;: *default<br>  database: &lt;%= ENV.fetch(&quot;DATABASE_TEST_NAME&quot;, &quot;app_test&quot;) %&gt;<br>production:<br>  &lt;&lt;: *default<br>  database: &lt;%= ENV.fetch(&quot;DATABASE_NAME&quot;, &quot;app_production&quot;) %&gt;</pre><p>Dessa forma, o Rails não depende de valores específicos gravados no código. As configurações são fornecidas pelo ambiente e podem ser alteradas sem modificar a aplicação.</p><p>Os valores vêm do ambiente definido no .env.</p><h3>6. Instalar as gems</h3><p>Depois que o Rails foi criado e o Gemfile está configurado:</p><pre>docker compose run --rm app bundle install</pre><p>As gems serão instaladas no volume Docker:</p><pre>bundle:/usr/local/bundle</pre><p>Assim, elas não precisam ser reinstaladas a cada recriação do container.</p><h3>7. Criar o banco</h3><p>Com as gems instaladas e o PostgreSQL em execução, podemos criar os bancos utilizados pelo Rails:</p><pre>docker compose run --rm app \<br>  bundle exec rails db:create</pre><p>O Rails utiliza as configurações do database.yml para se conectar ao serviço db e criar os bancos de desenvolvimento e testes.</p><p>Se os bancos já existirem, o Rails apenas informará:</p><pre>Database &#39;app_development&#39; already exists<br>Database &#39;app_test&#39; already exists</pre><p>Isso significa que a conexão está funcionando e que os bancos já estão disponíveis.</p><h3>8. Subir a aplicação</h3><p>Agora:</p><pre>docker compose up --build</pre><p>A aplicação estará disponível em:</p><pre>http://localhost:3000</pre><p>Se quiser executar em segundo plano:</p><pre>docker compose up -d --build</pre><p>E conferir:</p><pre>docker compose ps</pre><p>Pronto. 🎯</p><blockquote><strong><em>Observe que não definimos nomes de containers, nomes de projeto ou credenciais diretamente no </em></strong><strong><em>Dockerfile. O ambiente é configurado pelo </em></strong><strong><em>.env, enquanto o nome da instância do Compose pode ser escolhido com </em></strong><strong><em>-p.</em></strong></blockquote><p>Por exemplo:</p><pre>docker compose -p meu-projeto up -d</pre><p>ou simplesmente:</p><pre>docker compose up -d</pre><p>Isso reforça justamente a ideia que queremos transmitir: <strong>Rails + PostgreSQL + Docker, mas sem amarrar a infraestrutura ao nome de uma aplicação específica.</strong></p><h3>O que você ganha com essa arquitetura</h3><p>O projeto fica independente da instalação Ruby da máquina:</p><pre>┌─────────────────────────────────────┐<br>│              Ubuntu                 │<br>│                                     │<br>│  Docker Engine                      │<br>│       │                             │<br>│       ├── Rails / Ruby              │<br>│       │      │                      │<br>│       │      └── /app ← projeto     │<br>│       │                             │<br>│       └── PostgreSQL                │<br>│              │                      │<br>│              └── postgres_data      │<br>└─────────────────────────────────────┘</pre><p>Na máquina host você <strong>não precisa de</strong>:</p><pre>Ruby<br>Rails<br>Bundler<br>PostgreSQL</pre><p>Você precisa basicamente de:</p><pre>Docker<br>Git<br>VS Code</pre><h3>Comandos que você realmente precisa lembrar</h3><p>No dia a dia:</p><pre>export UID=$(id -u)<br>export GID=$(id -g)<br><br>docker compose up</pre><p>Para entrar no container:</p><pre>docker compose exec app bash</pre><p>Para Rails:</p><pre>docker compose run --rm app bundle exec rails ...</pre><p>significa:</p><blockquote><em>“Crie temporariamente um container baseado no serviço </em><em>app, entre nele, execute </em><em>bundle exec rails db:migrate e depois remova o container.&quot;</em></blockquote><h3>Mas você não precisa decorar isso</h3><p>Se o seu objetivo é <strong>trabalhar normalmente dentro do container</strong>, existe uma forma mais confortável:</p><pre>docker compose exec app bash</pre><p>Aí você entra no container:</p><pre>root@container:/app#</pre><p>E pode executar normalmente:</p><pre>bundle exec rails db:migrate<br>bundle exec rails console<br>bundle exec rails routes</pre><p>Ou simplesmente:</p><pre>rails db:migrate</pre><p>Para parar:</p><pre>docker compose down</pre><p>Para apagar também os dados do PostgreSQL:</p><pre>docker compose down -v</pre><p>⚠️ <strong>Esse último apaga o volume do banco.</strong></p><h3>A ideia central</h3><p>O tutorial inteiro pode ser resumido em quatro decisões:</p><p><strong>1. Ruby/Rails dentro do Docker.</strong></p><p><strong>2. PostgreSQL em outro container.</strong></p><p><strong>3. Credenciais no </strong><strong>.env, não no código.</strong></p><p><strong>4. Projeto montado como volume, para editar normalmente pelo VS Code.</strong></p><p>Foi isso que construímos — só que, durante a montagem, fomos resolvendo cada detalhe individualmente.</p><h3>Note ¹</h3><h4>Quando um diretório do host é montado no container, alinhar UID/GID entre host e container evita a maior parte dos problemas de permissões</h4><p>Entendendo esses conceitos :</p><pre>UID <br>GID <br>/etc/passwd <br>usuário do host <br>usuário do container <br>permissões de volume</pre><p>Esses conceitos parecem complicados porque <strong>Docker mistura dois mundos Linux</strong>: o seu computador e o container. A chave para entender tudo é esta:</p><blockquote><strong><em>Linux não identifica usuários pelo nome. Identifica principalmente pelo número UID.</em></strong></blockquote><h3>1. UID — User ID</h3><p>É o <strong>número que identifica um usuário no Linux</strong>.</p><p>Por exemplo, no seu Ubuntu:</p><pre>id</pre><p>pode retornar:</p><pre>uid=1000(j3) gid=1000(j3) groups=1000(j3),...</pre><p>Aqui:</p><pre>j3  → nome<br>1000 → UID</pre><p>Para o Linux, o que realmente importa é:</p><pre>UID 1000</pre><p>O nome j3 é uma associação feita pelo sistema.</p><h3>2. GID — Group ID</h3><p>É a mesma ideia, mas para <strong>grupos</strong>.</p><p>Por exemplo:</p><pre>gid=1000(j3)</pre><p>Significa:</p><pre>grupo: j3<br>GID:   1000</pre><p>Um arquivo Linux pode ter:</p><pre>UID → dono<br>GID → grupo</pre><p>Por exemplo:</p><pre>-rw-r--r-- 1000 1000 Gemfile</pre><p>Significa:</p><pre>dono  = UID 1000<br>grupo = GID 1000</pre><h3>3. /etc/passwd</h3><p>Agora vem a parte que explica:</p><pre>I have no name!</pre><p>O arquivo:</p><pre>cat /etc/passwd</pre><p>contém o cadastro dos usuários conhecidos <strong>por aquele sistema Linux</strong>.</p><p>Por exemplo:</p><pre>rails:x:1000:1000::/home/rails:/bin/bash</pre><p>Isso diz:</p><pre>nome       = rails<br>UID        = 1000<br>GID        = 1000<br>home       = /home/rails<br>shell      = /bin/bash</pre><p>Imagine que o processo dentro do container esteja rodando como:</p><pre>UID 1000</pre><p>mas /etc/passwd não tenha ninguém com UID 1000.</p><p>O Linux sabe:</p><blockquote><em>“Existe alguém com UID 1000.”</em></blockquote><p>Mas não sabe:</p><blockquote><em>“Qual é o nome dessa pessoa?”</em></blockquote><p>Por isso:</p><pre>I have no name!</pre><h3>4. Usuário do host</h3><p>Seu <strong>host</strong> é o Ubuntu onde você está trabalhando.</p><p>Por exemplo:</p><pre>Ubuntu<br>└── usuário: j3<br>    └── UID: 1000</pre><p>Quando você cria:</p><pre>touch teste.txt</pre><p>o arquivo pertence ao seu UID:</p><pre>UID 1000</pre><h3>5. Usuário do container</h3><p>O container possui seu <strong>próprio ambiente Linux</strong>.</p><p>Ele também pode ter:</p><pre>rails<br>UID 1000</pre><p>Perceba algo importante:</p><blockquote><em>O </em><em>rails do container não precisa ser a mesma pessoa que </em><em>j3 do Ubuntu.</em></blockquote><p>São ambientes diferentes.</p><p>Mas ambos podem ter:</p><pre>UID 1000</pre><p>E isso é justamente o que queremos aproveitar.</p><pre>HOST                         CONTAINER<br><br> j3                           rails<br> UID 1000                     UID 1000<br>    │                             │<br>    └──────── mesmo número ───────┘</pre><h3>6. Permissões de volume</h3><p>Aqui está a razão de toda essa preocupação.</p><p>Imagine que seu projeto esteja no Ubuntu:</p><pre>/home/j3/projeto</pre><p>e o Docker monte essa pasta dentro do container:</p><pre>volumes:<br>  - .:/app</pre><p>Agora temos:</p><pre>Ubuntu                         Container<br><br>/home/j3/projeto  ──────────→  /app<br>       │                          │<br>       │                          │<br>       └── arquivos               └── mesmos arquivos</pre><p>O container <strong>não recebe uma cópia independente</strong>.</p><p>Ele está vendo os mesmos arquivos do host.</p><p>Imagine que:</p><pre>Gemfile<br>dono = UID 1000</pre><p>Se dentro do container você estiver como:</p><pre>root<br>UID 0</pre><p>o root normalmente consegue alterar o arquivo.</p><p>Mas se criar arquivos como outro UID, você pode acabar com uma bagunça de permissões.</p><h3>E é aí que entra o nosso rails</h3><p>Queremos:</p><pre>HOST<br><br>j3<br>UID 1000<br>   │<br>   │<br>   ▼<br>Docker<br>   │<br>   ▼<br>CONTAINER<br>rails<br>UID 1000</pre><p>Assim, quando o Rails dentro do container criar:</p><pre>tmp/<br>log/<br>db/<br>algum_arquivo.rb</pre><p>esses arquivos terão:</p><pre>UID 1000</pre><p>No host, o Ubuntu reconhece esse UID como seu usuário j3.</p><p>Resultado:</p><p><strong>menos problemas de permissão.</strong></p><h3>E agora fica fácil entender o seu erro</h3><p>Você estava com:</p><pre>root@d390de9a0bad:/app#</pre><p>Isso significa:</p><pre>root<br>UID 0</pre><p>E o Git encontrou arquivos em /app pertencentes a outro UID.</p><p>Então ele disse:</p><pre>detected dubious ownership</pre><p>Quando apareceu:</p><pre>I have no name!@d390de9a0bad:/app$</pre><p>era outra situação:</p><pre>UID 1000<br>       ↓<br>não existe esse UID no /etc/passwd do container<br>       ↓<br>I have no name!</pre><p>Portanto, nossa solução de criar:</p><pre>rails<br>UID 1000<br>GID 1000</pre><p>resolve <strong>duas coisas ao mesmo tempo</strong>:</p><pre>HOST                  CONTAINER<br><br>j3 / UID 1000        rails / UID 1000<br>               │                     │<br>               └─────────┬───────────┘<br>                         │<br>                    /app (volume)<br>                         │<br>                         ▼<br>                  mesmas permissões</pre><h3>A frase para guardar</h3><blockquote><strong><em>Nome é para humanos. UID/GID são para o Linux.</em></strong></blockquote><p>E no Docker:</p><blockquote><strong><em>Quando um diretório do host é montado no container, alinhar UID/GID entre host e container evita a maior parte dos problemas de permissões.</em></strong></blockquote><h3>Note ²</h3><h3>Um <strong>princípio importante do projeto</strong>:</h3><blockquote>O nome da aplicação, o nome do banco e o nome do projeto Docker Compose são coisas diferentes.</blockquote><h3>Configuração genérica do ambiente</h3><p>Crie um arquivo .env na raiz do projeto:</p><pre>UID=1000<br>GID=1000<br><br>APP_PORT=3000<br>DB_PORT=5432<br>POSTGRES_DB=app_development<br>POSTGRES_USER=app<br>POSTGRES_PASSWORD=app_dev</pre><p>O .env <strong>não deve conter </strong><strong>tarefas nem </strong><strong>meu-projeto</strong>. Assim, o mesmo Dockerfile e compose.yml podem ser reutilizados em qualquer aplicação Rails.</p><h3>Nome do projeto Docker Compose</h3><p>O nome do projeto é definido no momento em que você sobe o ambiente:</p><pre>docker compose -p meu-projeto up -d</pre><p>Por exemplo, outro projeto poderia usar:</p><pre>docker compose -p minha-api up -d</pre><p>Isso apenas muda os nomes dos recursos criados pelo Docker:</p><pre>meu-projeto-app-1<br>meu-projeto-db-1</pre><p>ou:</p><pre>minha-api-app-1<br>minha-api-db-1</pre><p><strong>O </strong><strong>.env continua igual.</strong></p><h3>Por que separar as coisas?</h3><pre>                 PROJETO DOCKER<br>                       │<br>             -p meu-projeto<br>                       │<br>          ┌────────────┴────────────┐<br>          │                         │<br>        app                        db<br>          │                         │<br>     Rails :3000              PostgreSQL :5432<br>          │                         │<br>          └──────────┬──────────────┘<br>                     │<br>                   .env<br>                     │<br>          configuração do ambiente</pre><p>Essa separação torna o ambiente <strong>reproduzível e reutilizável</strong>.</p><blockquote><strong><em>O </em></strong><strong><em>Dockerfile define como construir.<br> O </em></strong><strong><em>compose.yml define como executar.<br> O </em></strong><strong><em>.env define as configurações.<br> O </em></strong><strong><em>-p define a identidade daquela instância do Compose.</em></strong></blockquote><h3>Nota técnica</h3><p><strong>Nota: 9,0/10</strong></p><p>A solução está tecnicamente <strong>bem estruturada e adequada para um tutorial introdutório de Rails + PostgreSQL + Docker</strong>.</p><p><strong>Pontos fortes:</strong></p><ul><li>Ruby/Rails executados dentro do container, sem dependências instaladas diretamente no Ubuntu.</li><li>PostgreSQL isolado em container próprio.</li><li>Comunicação entre Rails e PostgreSQL pelo nome do serviço db.</li><li>Persistência do PostgreSQL por volume nomeado.</li><li>Persistência do código por bind mount.</li><li>Usuário não-root dentro do container.</li><li>UID/GID configuráveis para evitar problemas de permissões no Linux.</li><li>Bundler com volume próprio e permissões corrigidas.</li><li>Portas configuráveis pelo .env.</li><li>Configuração do banco desacoplada do nome da aplicação.</li><li>Estrutura reproduzível com Dockerfile + compose.yml + .env.</li></ul><p><strong>Pontos que merecem atenção:</strong></p><ul><li>postgres:17 é uma dependência externa que pode mudar de comportamento entre versões menores; para um ambiente de produção, seria recomendável fixar uma versão mais específica.</li><li>O .env deve obrigatoriamente ficar fora do Git.</li><li>depends_on garante ordem de inicialização, mas <strong>não garante que o PostgreSQL esteja pronto para aceitar conexões</strong>. Para um tutorial simples isso é aceitável; em um ambiente mais robusto, entraria um healthcheck.</li><li>gem install bundler durante o build torna a versão do Bundler dependente do momento em que a imagem é construída. Para máxima reprodutibilidade, pode-se fixar a versão.</li><li>A imagem poderia ser otimizada posteriormente, mas isso não é necessário para o objetivo didático.</li></ul><h3>Veredito</h3><p><strong>Para desenvolvimento local e finalidade didática: 9/10.</strong></p><p>O mais importante é que o tutorial não está apenas “funcionando”: ele está ensinando alguns problemas reais de Docker em Linux — <strong>permissões, UID/GID, volumes, usuário não-root e comunicação entre containers</strong>.</p><p>🚀 <strong>NOVO EPISÓDIO</strong></p><p><a href="https://medium.com/jungletronics/cucumber-rspec-testes-que-falam-e-testes-que-garantem-61fe31cc894e"><strong>Cucumber + RSpec: testes que falam e testes que garantem</strong></a><strong>.</strong></p><p>Do comportamento descrito em linguagem simples ao teste automatizado de verdade.</p><p>Neste episódio, avançamos na nossa infraestrutura <strong>Rails + PostgreSQL + Docker</strong> e começamos a transformar requisitos em testes.</p><p>🧪 <strong>Cucumber</strong> → especificação que fala.<br>✅ <strong>RSpec</strong> → teste que garante.</p><p><strong>Do “o sistema deve fazer” ao “o sistema realmente faz”.</strong></p><h3>Se a intenção é <strong>zerar o Docker localmente</strong>, removendo containers, imagens, volumes, redes não utilizadas e cache, use esta sequência.</h3><blockquote><strong><em>Atenção:</em></strong><em> isso remove também volumes, portanto pode apagar bancos PostgreSQL persistidos localmente.</em></blockquote><h3>1. Parar e remover todos os containers</h3><pre>docker rm -f $(docker ps -aq) 2&gt;/dev/null</pre><h3>2. Remover todas as imagens</h3><pre>docker rmi -f $(docker images -aq) 2&gt;/dev/null</pre><h3>3. Remover todos os volumes</h3><pre>docker volume rm $(docker volume ls -q) 2&gt;/dev/null</pre><h3>4. Remover redes criadas pelo usuário</h3><pre>docker network prune -f</pre><h3>5. Limpar absolutamente tudo que restou</h3><pre>docker system prune -a --volumes -f</pre><h3>6. Conferir</h3><pre>docker ps -a<br>docker images<br>docker volume ls<br>docker network ls</pre><p>Você deve ficar basicamente com as redes padrão:</p><pre>bridge<br>host<br>none</pre><p>e sem containers, imagens ou volumes.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5f7a00afc18c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/rails-postgresql-docker-5f7a00afc18c">Rails + PostgreSQL + Docker</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Por Trás do Categoric: O Caminho Secreto de Cada Requisição]]></title>
            <link>https://medium.com/jungletronics/por-tr%C3%A1s-do-categoric-o-caminho-secreto-de-cada-requisi%C3%A7%C3%A3o-c374d1df2919?source=rss----1cb6b57a12c8---4</link>
            <guid isPermaLink="false">https://medium.com/p/c374d1df2919</guid>
            <category><![CDATA[flask-restful]]></category>
            <category><![CDATA[postgresql]]></category>
            <category><![CDATA[nginx]]></category>
            <category><![CDATA[docker-compose]]></category>
            <dc:creator><![CDATA[J3]]></dc:creator>
            <pubDate>Tue, 08 Sep 2026 19:51:24 GMT</pubDate>
            <atom:updated>2026-09-08T19:51:23.641Z</atom:updated>
            <content:encoded><![CDATA[<h4>Descubra como Docker, Nginx, Gunicorn, Flask e PostgreSQL trabalham juntos para transformar uma simples ação no navegador em uma aplicação completa, rápida e organizada</h4><p>O Categoric¹ é uma aplicação moderna construída para transformar uma ideia em um sistema real, seguro e organizado. Por trás de cada tela existe um fluxo de comunicação que conecta o navegador, o Nginx, o Docker, o Gunicorn, o Flask e o PostgreSQL.</p><p>Neste tutorial, você vai acompanhar esse caminho como se estivesse explorando os bastidores de uma aplicação: entender como uma requisição entra, como os serviços conversam entre si e como os dados são processados até a resposta voltar para o usuário. Mais do que conhecer o Categoric, você verá como uma arquitetura profissional é construída, protegida e documentada para continuar compreensível no futuro.</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*PQUm6Z74nabf5UMWRqUa-A.png" /></figure><p>O fluxo completo funciona assim:</p><p>Copiar</p><pre>Cliente/Navegador<br>        │<br>        │  Requisição HTTP pela porta 80<br>        ▼<br>Docker<br>        │<br>        │  Rede interna dos containers<br>        ▼<br>Nginx, Proxy reverso<br>        │<br>        │  Encaminha para o serviço web na porta 8000<br>        ▼<br>Gunicorn<br>        │<br>        │  Executa a aplicação Python através do protocolo WSGI<br>        ▼<br>Flask<br>        │<br>        │  Processa rotas, regras de negócio e autenticação<br>        ▼<br>PostgreSQL<br>        │<br>        │  Consulta ou grava dados na porta interna 5432<br>        ▼<br>Flask → Gunicorn → Nginx → Cliente</pre><h3>1. Cliente envia a requisição</h3><p>O usuário acessa uma página ou executa uma ação no navegador. O navegador envia uma requisição HTTP, por exemplo:</p><pre>GET /dashboard HTTP/1.1<br>Host: exemplo.com</pre><p>A requisição chega ao servidor pela porta pública <strong>80</strong>. O cliente não acessa diretamente o Flask, o Gunicorn ou o PostgreSQL.</p><h3>2. Docker fornece o ambiente</h3><p>O Docker mantém os componentes isolados em containers e conecta os serviços por uma rede interna. Neste projeto, o Docker não é exatamente uma etapa de processamento da requisição; ele fornece o ambiente e a comunicação entre:</p><pre>categoric-nginx-1<br>categoric-web-1<br>categoric-db-1</pre><p>Somente o container do Nginx possui a porta 80 publicada no host. As portas 8000 e 5432 permanecem internas à rede Docker.</p><h3>3. Nginx recebe a requisição</h3><p>O Nginx atua como <strong>proxy reverso</strong>. Ele recebe a requisição externa, pode aplicar regras de rota, servir arquivos estáticos e encaminhar requisições dinâmicas para o container web.</p><p>O encaminhamento ocorre internamente, de forma semelhante a:</p><pre>Nginx:80 → categoric-web-1:8000</pre><p>O Nginx também pode adicionar informações como:</p><pre>X-Real-IP<br>X-Forwarded-For<br>X-Forwarded-Proto<br>Host</pre><p>Esses cabeçalhos permitem que a aplicação saiba o host acessado, o protocolo original e o endereço do cliente.</p><h3>4. Gunicorn recebe a chamada WSGI</h3><p>O Gunicorn é o servidor de aplicação. Ele recebe a solicitação encaminhada pelo Nginx na porta interna <strong>8000</strong> e distribui o trabalho entre seus workers.</p><p>A estrutura atual é:</p><pre>Gunicorn master<br>├── worker 1<br>└── worker 2</pre><p>O processo principal supervisiona os workers. Um worker recebe a requisição e chama a aplicação Flask por meio do protocolo <strong>WSGI</strong>.</p><h3>5. Flask processa a lógica</h3><p>O Flask interpreta:</p><ul><li>método HTTP, como GET ou POST;</li><li>caminho, como /login ou /dashboard;</li><li>parâmetros;</li><li>cookies e sessão;</li><li>cabeçalhos;</li><li>regras de autenticação;</li><li>validações;</li><li>regras de negócio.</li></ul><p>Por exemplo:</p><pre>GET /dashboard<br>        │<br>        ▼<br>Rota Flask /dashboard<br>        │<br>        ▼<br>Consulta dados no PostgreSQL<br>        │<br>        ▼<br>Renderiza página ou retorna JSON</pre><p>Se a requisição exigir dados, o Flask abre ou reutiliza uma conexão com o PostgreSQL usando a rede interna do Docker.</p><h3>6. PostgreSQL responde à aplicação</h3><p>O PostgreSQL escuta na porta interna <strong>5432</strong>. Ele não deve ficar publicado diretamente para a Internet.</p><p>A comunicação ocorre assim:</p><pre>categoric-web-1 → categoric-db-1:5432</pre><p>O banco processa consultas, insere dados ou atualiza registros e devolve o resultado ao Flask. Depois que a aplicação termina a operação, a conexão pode ser devolvida ao pool ou encerrada conforme a configuração utilizada.</p><h3>7. A resposta retorna pelo caminho inverso</h3><p>Depois que o Flask produz a resposta, o fluxo volta:</p><pre>Flask<br>  ↓<br>Gunicorn<br>  ↓<br>Nginx<br>  ↓<br>Cliente</pre><p>Exemplo de resposta:</p><pre>HTTP/1.1 200 OK<br>Content-Type: text/html</pre><p>O Nginx então entrega a resposta ao navegador. Em caso de erro, o status pode ser, por exemplo:</p><pre>404, rota não encontrada<br>500, erro interno da aplicação<br>502, Nginx não conseguiu falar com o Gunicorn<br>503, serviço indisponível</pre><h3>Resumo das responsabilidades</h3><figure><img alt="" src="https://cdn-images-1.medium.com/max/778/1*2P9fgC7BUATQNgNCXP3UXA.png" /></figure><p>Note ¹</p><p>Category — Ecossistema de Aprendizagem Ética, iniciativa que reúne <a href="https://www.linkedin.com/pulse/reforma-tribut%C3%A1ria-do-consumo-compreendendo-pela-de-oliveira-junior-jp4ff/?trackingId=mMtdMijXSNuu7U82rh8vqA%3D%3D">livro</a>, plataforma digital, cursos, palestras e tutoria acadêmica em uma experiência contínua de aprendizagem. O objetivo é ensinar a Reforma Tributária do Consumo e demonstrar, na prática, como a Inteligência Artificial pode ser aplicada de forma ética, transparente e responsável na produção e disseminação do conhecimento</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c374d1df2919" width="1" height="1" alt=""><hr><p><a href="https://medium.com/jungletronics/por-tr%C3%A1s-do-categoric-o-caminho-secreto-de-cada-requisi%C3%A7%C3%A3o-c374d1df2919">Por Trás do Categoric: O Caminho Secreto de Cada Requisição</a> was originally published in <a href="https://medium.com/jungletronics">Jungletronics</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>