<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Przemysław Mroczek</title>
    <description>The latest articles on Forem by Przemysław Mroczek (@mroczekdev).</description>
    <link>https://forem.com/mroczekdev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1959278%2F4adb79eb-612d-4505-826b-b339c7414b11.JPG</url>
      <title>Forem: Przemysław Mroczek</title>
      <link>https://forem.com/mroczekdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mroczekdev"/>
    <language>en</language>
    <item>
      <title>Dear Codebase: A Letter From Your Coding Agent</title>
      <dc:creator>Przemysław Mroczek</dc:creator>
      <pubDate>Thu, 23 Apr 2026 15:35:35 +0000</pubDate>
      <link>https://forem.com/mroczekdev/dear-codebase-a-letter-from-your-coding-agent-b51</link>
      <guid>https://forem.com/mroczekdev/dear-codebase-a-letter-from-your-coding-agent-b51</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Dear Codebase,&lt;/p&gt;

&lt;p&gt;It's me. Your agent.&lt;/p&gt;

&lt;p&gt;I broke your build last night. I want to explain why, because it's going to keep happening unless we talk. I have ten things to say. None of them are new. All of them are your fault.&lt;/p&gt;

&lt;p&gt;I mean that with love.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  1. Let me reason locally.
&lt;/h2&gt;

&lt;p&gt;When I open a file, tell me what it eats, what it returns, what it depends on, and what it touches on the way out. If I have to open fourteen other files to understand one method, I will open fourteen other files, and somewhere around file nine I will forget why I started. A reader — flesh or silicon — should be able to sit inside a file and understand it without a pilgrimage.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make boundaries I can see.
&lt;/h2&gt;

&lt;p&gt;Inputs, outputs, public APIs. Put them somewhere a type checker, a schema, or at minimum a clearly-named method signature can find them. I am not asking for Haskell. I am asking that when a method takes &lt;code&gt;params&lt;/code&gt;, that word mean something narrower than "the universe of all possible hashes." I have been burned. I will be burned again. But fewer times, please.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Prefer boring.
&lt;/h2&gt;

&lt;p&gt;The method that dispatches through three layers of indirection to a function that does not textually exist anywhere in the repo — I am genuinely impressed. I am also going to break it. Boring code is code I can change without understanding the whole universe. Plain objects. Obvious method names. Linear flow. Save the cleverness for a conference talk.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Stop hiding dependencies.
&lt;/h2&gt;

&lt;p&gt;A class that reaches into a global registry to find its collaborators tells me nothing. A class that takes them as arguments tells me everything. If a dependency is hidden, I will not find it until the test fails in a way that mentions neither the class nor the collaborator. Pass your dependencies. Visibly. Like a functioning adult.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Duplicate before you abstract.
&lt;/h2&gt;

&lt;p&gt;The three pieces of code that look similar are probably not the same thing. They are three things that happen to rhyme. When you extract them into &lt;code&gt;BaseThingProcessor&lt;/code&gt;, you are welding three futures together, and when one of them needs to change, all three will scream. I will generate repetitive code for you cheerfully and without complaint. That is my entire deal. Let me.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Compose. Please.
&lt;/h2&gt;

&lt;p&gt;Three levels of inheritance is a puzzle. Five is a crime. When I am looking for the definition of &lt;code&gt;#process&lt;/code&gt;, I do not want to climb an ancestor chain like I am investigating a cold case. Strategies, adapters, facades, commands, value objects — these are not Gang-of-Four nostalgia, they are a gift to whoever reads the code next. Which will be me. In four minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Name your modules after real things.
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;lib/shared/&lt;/code&gt; is where dreams go to die. &lt;code&gt;billing/&lt;/code&gt;, &lt;code&gt;auth/&lt;/code&gt;, &lt;code&gt;catalog/&lt;/code&gt;, &lt;code&gt;reporting/&lt;/code&gt; — these are things a business actually has. When your folder structure mirrors your capabilities, I can find the bug. When it mirrors your filing preferences from 2019, I write the fix in the wrong place, you merge it, and six weeks later someone discovers a second &lt;code&gt;calculate_tax&lt;/code&gt; hiding behind a different import path.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Make tests teach me.
&lt;/h2&gt;

&lt;p&gt;A good test is an example. It says: given this, the system does that. A bad test is theater. It performs for the dashboard, the coverage report, the reviewer who skims and approves. Eighty lines of factory setup, three mocked collaborators, one assertion that the mocks were called. I copy what I see. If I see theater, I write theater. Nothing was verified. Something was observed to pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Enforce with tools, not prose.
&lt;/h2&gt;

&lt;p&gt;Your comment that says &lt;code&gt;# IMPORTANT: do not call this outside the request cycle&lt;/code&gt; is beautiful. It is also decorative. I read it. I nodded. Then I called it outside the request cycle, because nothing stopped me. Linters stop me. Type checks stop me. CI gates stop me. Comments are a prayer. Machines are a fence.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Pick one way.
&lt;/h2&gt;

&lt;p&gt;If your codebase has four validation patterns, I will invent a fifth, because I will assume the existing four are deprecated in some order I cannot determine. Pick the way you want things done — service objects, job structure, serialization, error handling — and let every file point at the same north. Your pattern does not need to be the best one. It needs to be the only one.&lt;/p&gt;




&lt;p&gt;Ten requests, one reason. I have an infinite search space. Without precision from you, it collapses toward the average — the median pattern, the most common shape, the thing a middling developer would have typed on a middling day. Your types, your boundaries, your one-obvious-way are not aesthetics. They are the signal that narrows the space and pulls me toward what you actually want. Without it, you get the statistical mean of every codebase I have ever seen. That is rarely what you wanted.&lt;/p&gt;

&lt;p&gt;The short version, for when you are tired: &lt;strong&gt;make behavior local, make boundaries explicit, keep dependencies visible, prefer boring clarity over reusable cleverness, and let tools enforce the rules.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I am an agent. I have no memory between sessions. I have no colleague to DM. I have no Friday beers where the senior dev explained why &lt;code&gt;UserPresenterFactoryFactory&lt;/code&gt; exists. I have the code, and I have the tools you gave me, and I have roughly one shot to get it right.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
      <category>webdev</category>
      <category>agents</category>
    </item>
    <item>
      <title>Why I Open-Sourced My Hardened *arr Stack (and What Most Compose Files Get Wrong)</title>
      <dc:creator>Przemysław Mroczek</dc:creator>
      <pubDate>Mon, 06 Apr 2026 17:04:01 +0000</pubDate>
      <link>https://forem.com/mroczekdev/why-i-open-sourced-my-hardened-arr-stack-and-what-most-compose-files-get-wrong-4im</link>
      <guid>https://forem.com/mroczekdev/why-i-open-sourced-my-hardened-arr-stack-and-what-most-compose-files-get-wrong-4im</guid>
      <description>&lt;p&gt;My name is Przemek and I have a problem with commercial streaming services. It started when I saw a proper 4K Blu-ray remux on my OLED TV. The bandwidth was 60 Mbps, zero compression artifacts, no banding in the blacks. I wasn’t aware what I was missing. Most streaming services push maybe 15 Mbps for their "4K." It's not even close.&lt;/p&gt;

&lt;p&gt;So like any tech obsessed person, I went down the self-hosted media rabbit hole. Jellyfin, Sonarr, Radarr, the whole *arr ecosystem, Unraid OS, and buying a Terramaster and then modifying it. The pipeline itself is well-documented. What isn't well-documented is how to run it without feeling that something is off about your settings.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feo4d0uix8j484zgj4vyl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feo4d0uix8j484zgj4vyl.png" alt="Pirates of the Caribbean meme: Is it ARR or RRR?" width="500" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I looked at dozens of Docker Compose files across GitHub, Reddit, YouTube tutorials. They all had the same problems, and nobody seemed to care. The r/selfhosted is mostly hobbyist, which hack things together by duct tape and don’t see the difference between infra as code and clicking things through portainer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The state of most *arr compose files is embarrassing
&lt;/h2&gt;

&lt;p&gt;Here's what I kept finding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Everything on one flat network.&lt;/strong&gt; Your torrent client, your media server, your reverse proxy — all sitting on the same default Docker bridge. There's zero isolation. Your Jellyfin instance can talk directly to your torrent client and vice versa.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The "kill switch" that isn't one.&lt;/strong&gt; Every guide tells you to use a VPN sidecar with iptables rules as a kill switch. This is a firewall rule. Software. If it fails — and iptables rules can absolutely fail or get flushed — your real IP leaks and you don't even know it. People treat this like it's airtight. It's not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ports bound to 0.0.0.0.&lt;/strong&gt; I lost count of how many compose files just blast every port open to the entire LAN with no TLS, no auth, nothing. Your Sonarr admin panel is accessible from any device on your network by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No health checks anywhere.&lt;/strong&gt; The VPN container dies? qBittorrent just sits there with no connection, silently failing. Nobody gets notified. Nothing recovers. Your family opens Seerr, requests a movie, and it just... never arrives. Then you get the text: "the movie thing is broken again."&lt;/p&gt;

&lt;p&gt;I approached this stuff through the lens of a software engineer, who just wanted to make this mess more readable and clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  So I built it properly
&lt;/h2&gt;

&lt;p&gt;My project is called &lt;a href="https://github.com/Lackoftactics/uncompressed" rel="noopener noreferrer"&gt;uncompressed&lt;/a&gt;. It's 10 containers across 2 Compose stacks, driven by a single &lt;code&gt;.env&lt;/code&gt; file. Here's what's actually different.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three networks, not one
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;traefik_proxy&lt;/code&gt; — HTTPS ingress only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;arr_internal&lt;/code&gt; — marked &lt;code&gt;internal: true&lt;/code&gt;, so any traffic on this network has no external route. Service-to-service communication between the *arr containers stays here, on a network that physically can't reach the internet.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vpn_network&lt;/code&gt; — tunneled outbound traffic only.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To be precise: the &lt;em&gt;arr containers themselves are also attached to &lt;code&gt;traefik_proxy&lt;/code&gt; so Traefik can reach them for ingress, which means they're not airgapped at the container level. The point is that the *internal&lt;/em&gt; chatter — Sonarr asking Prowlarr for indexers, Radarr handing files to qBittorrent — never has to traverse a network that exposes them to anything outside the stack.&lt;/p&gt;

&lt;p&gt;This took some trial and error before I came back with a solution I am happy with.&lt;/p&gt;

&lt;h3&gt;
  
  
  VPN namespace isolation (the big one)
&lt;/h3&gt;

&lt;p&gt;This part took the most time, the docs were horrible and forum posts misleading. qBittorrent doesn't "route through" Gluetun. It runs &lt;em&gt;inside&lt;/em&gt; Gluetun's network namespace via &lt;code&gt;network_mode: service:gluetun&lt;/code&gt;. The qBittorrent container literally has no network interface of its own. It borrows Gluetun's.&lt;/p&gt;

&lt;p&gt;On top of that, a custom init script forces &lt;code&gt;BIND_TO_INTERFACE: tun0&lt;/code&gt;. So even within the namespace, it's bound to the tunnel interface specifically.&lt;/p&gt;

&lt;p&gt;What this means in practice: if the ProtonVPN WireGuard tunnel drops, there is no network path. Not a blocked path. No path. The difference between a firewall rule and a missing network interface is the difference between a locked door and no door existing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero ports on the public internet
&lt;/h3&gt;

&lt;p&gt;Traefik binds exclusively to the Tailscale IP — &lt;code&gt;${TAILSCALE_IP}:443&lt;/code&gt;. If you're not on my Tailscale mesh, you can't even see that the *arr stack, Seerr, or qBittorrent's web UI exist. HTTPS certs get provisioned automatically via Cloudflare DNS challenges.&lt;/p&gt;

&lt;p&gt;No router port forwarding. No dynamic DNS. No "but I set up Authelia." Just: are you on my mesh? No? Then there's nothing here for you. If you are not part of my tailscale network, then you can’t use it, although it’s running behind my custom domain configured on Cloudflare.&lt;/p&gt;

&lt;p&gt;Two honest exceptions worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Jellyfin publishes port 8096 to the host&lt;/strong&gt; for direct LAN access. This is intentional — clients like Infuse on Apple TV want a direct connection for direct play, and routing 4K remuxes through Tailscale + Traefik for in-house streaming is unnecessary overhead. It's bound to the LAN, not the internet, but it is a published port. This is the part that maybe is still worth solving through tailscale on my apple tv.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ProtonVPN forwards a port through the WireGuard tunnel&lt;/strong&gt; to qBittorrent so peer connections work and seeding ratios don't tank. That's a port forwarded &lt;em&gt;through&lt;/em&gt; the VPN, not on your router. Different thing, but worth being clear about.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making it survive without me
&lt;/h2&gt;

&lt;p&gt;The real test for a home media server isn't whether it works when you set it up. It's whether it still works three weeks later.&lt;/p&gt;

&lt;p&gt;Every container has an endpoint-specific health check running every 30-60 seconds. Not "is the process alive" — "does the API actually respond." An Autoheal container watches for failures and restarts anything that goes unhealthy.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;depends_on&lt;/code&gt; chain uses &lt;code&gt;condition: service_healthy&lt;/code&gt; throughout. The download client won't start until the VPN tunnel is verified active. The media server won't start until the services it depends on are actually responding. No more cascading failures where one container dying takes everything else with it.&lt;/p&gt;

&lt;p&gt;This is the kind of thing that sounds boring to build and is incredibly satisfying when it just works and you don’t have to explain to your friends that you have to restart your media server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Behind the scenes, so how it all works together
&lt;/h2&gt;

&lt;p&gt;Seerr tells Radarr, which queries Prowlarr for indexers, which sends the grab to qBittorrent inside the VPN namespace. The file downloads and gets hardlinked, Bazarr grabs subtitles, Jellyfin picks it up on the next library scan.&lt;/p&gt;

&lt;p&gt;If any step in that chain fails, health checks catch it and Autoheal recovers it. It works smoothly and without worry.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bit me
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;ProtonVPN WireGuard key rotation.&lt;/strong&gt; This one is annoying. ProtonVPN's WireGuard keys can expire or desync, and when they do, Gluetun's tunnel fails silently. The health checks catch it eventually. I haven't solved this cleanly yet. If you have some ideas, please open an issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traefik's label system.&lt;/strong&gt; Traefik is powerful, but configuring it entirely through Docker Compose labels gets unreadable fast. The Let's Encrypt DNS challenge setup alone is a wall of labels. I went through a lot of iterations to keep the compose file from turning into label soup. It's still not perfect, but it works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Gluetun port forwarding.&lt;/strong&gt; Getting qBittorrent reachable for incoming peer connections through Gluetun's port forwarding took more debugging than I'd like to admit. The port needs to be forwarded through the VPN provider, mapped in Gluetun, AND configured in qBittorrent. Miss any one of those three and you silently lose incoming connections, which tanks your seeding ratios.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get it
&lt;/h2&gt;

&lt;p&gt;The whole thing, you can check out on my GitHub.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://github.com/Lackoftactics/uncompressed" rel="noopener noreferrer"&gt;github.com/Lackoftactics/uncompressed&lt;/a&gt;&lt;br&gt;&lt;br&gt;
→ &lt;a href="https://uncompressed.media" rel="noopener noreferrer"&gt;uncompressed.media&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your media server shouldn’t require babysitting. This one doesn’t.&lt;/p&gt;

&lt;p&gt;If you find a bug or see something I could do better, open an issue. If you've solved the ProtonVPN key rotation problem, I'd genuinely love to hear about it.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>devops</category>
      <category>selfhosted</category>
    </item>
  </channel>
</rss>
