<?xml version="1.0" encoding="utf-8" standalone="yes"?><?xml-stylesheet type="text/xsl" href="/index.xsl"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>./techtipsy</title><link>https://ounapuu.ee/categories/free-tech-tip/</link><description>Recent content on ./techtipsy, a blog written by Herman Õunapuu.</description><generator>Hugo -- gohugo.io</generator><language>en-GB</language><managingEditor>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</managingEditor><webMaster>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</webMaster><lastBuildDate>Mon, 08 Sep 2025 06:00:00 +0300</lastBuildDate><atom:link href="https://ounapuu.ee/categories/free-tech-tip/index.xml" rel="self" type="application/rss+xml"/><item><title>The unreasonable effectiveness of the pancake rule</title><link>https://ounapuu.ee/posts/2025/09/08/pancakes/</link><pubDate>Mon, 08 Sep 2025 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2025/09/08/pancakes/</guid><description>Having problems with your team being late to the daily stand-up? Try this!</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2025/09/08/pancakes/media/cover_hu_2699cd97747020cc.jpg" width="1200" height="630" alt="The unreasonable effectiveness of the pancake rule" /><p>Being chronically late to meetings <em><strong>sucks.</strong></em></p>
<p>Not only is it very rude, but you&rsquo;re signalling that you don&rsquo;t value your coworkers&rsquo; time.</p>
<p>However, I&rsquo;ve picked up a technique that works unreasonably well within a team.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>If you are late to the first meeting of the day three times within a quarter, then you will have to make pancakes for
the whole team.</p>
<p>Let&rsquo;s say that you have a daily stand-up taking place at 10:00.</p>
<p>Arriving at <strong>10:00</strong>:59: completely OK.</p>
<p>Arriving at <strong>10:01</strong>:00: You&rsquo;re one step closer to making pancakes!</p>
<p>Keep in mind that you may hit some obstacles when implementing this rule, so feel free to adjust it. When proposing this
idea in my current team, I learned that the office does <em>not</em> offer pancake-making facilities. The pancakes can be
substituted for other types of cake or bringing in something else, as long as the team
gives prior approval of that modification.</p>
<p>The pancake strikes can also be pooled together and spent with your teammates if they wish to do so.</p>
<p>If you&rsquo;re struggling with your team being late to your daily meeting(s), then go ahead and add this rule to the working
agreement. You <em>do</em> have a working agreement set up, right? <em><strong>Right?</strong></em></p>
<p>And a free security tech tip to close out: if you see an unlocked work laptop at the office, open your internal chat
application of choice on it and try posting to a public channel that you&rsquo;ll be bringing cake/beers/candy to the office.
Works wonders for enforcing the habit of locking your laptop up when leaving the desk!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>to be fair, the sample size is two, but it has worked out really well in both!&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>How to run Uptime Kuma in Docker in an IPv6-only environment</title><link>https://ounapuu.ee/posts/2025/08/05/uptime-kuma-ipv6/</link><pubDate>Tue, 05 Aug 2025 21:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2025/08/05/uptime-kuma-ipv6/</guid><description>When you're too cheap to pay for an IPv4 address but you'd really like to monitor your services.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2025/08/05/uptime-kuma-ipv6/media/cover_hu_25de9680b02fc40f.jpg" width="1200" height="630" alt="How to run Uptime Kuma in Docker in an IPv6-only environment" /><p>I use <a href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a> to check the availability of a few services that I run,
with the most important one being my blog. It&rsquo;s really nice.</p>
<p>Today I wanted to set it up on a different machine to help troubleshoot and confirm some latency issues that I&rsquo;ve
observed, and for that purpose I picked the cheapest ARM-based Hetzner Cloud VM hosted in Helsinki, Finland.</p>
<p>Hetzner provides a public IPv6 address for free, but you have to pay extra for an IPv4 address. I didn&rsquo;t want to do that
out of
principle, so I went ahead and copied my Docker Compose definition over to the new server.</p>
<p>For some reason, Uptime Kuma would start up on the new IPv6-only VM, but it was unsuccessful in making requests to my
services, which support both IPv4 and IPv6. The requests would time out and show up as &ldquo;Pending&rdquo; in the UI, and the
service logs complained about not being able to deliver e-mails about the failures.</p>
<p>I confirmed IPv6 connectivity within the container by running <code>docker exec -it uptime-kuma bash</code> and running a
few <code>curl</code> and <code>ping</code> commands with IPv6 flags, had no issues with those.</p>
<p>When I added a public IPv4 address to the container, everything started working again.</p>
<p>I fixed the issue by explicitly disabling the IPv4 network in the Docker Compose service definition, and that did the
trick, Uptime Kuma made successful requests towards my services. It seems that the service defaults to IPv4 due to the
internal Docker network giving it an IPv4 network to work with, and that causes issues when your machine doesn&rsquo;t have
any IPv4 network or public IPv4 address associated with it.</p>
<p>Here&rsquo;s an example Docker Compose file:</p>
<pre tabindex="0"><code>name: uptime-kuma
services:
  uptime-kuma:
    container_name: uptime-kuma
    networks:
      - uptime-kuma
    ports:
      - 3001:3001
    volumes:
      - /path/to/your/storage:/app/data
    image: docker.io/louislam/uptime-kuma
    restart: always
networks:
  uptime-kuma:
    enable_ipv6: true
    enable_ipv4: false
</code></pre><p>That&rsquo;s it!</p>
<p>If you&rsquo;re interested in different ways to set up IPv6 networking in
Docker, <a href="/posts/2024/12/20/docker-ipv6/">check out this overview that I wrote a while ago.</a></p>
]]></content:encoded></item><item><title>Feature toggles: just roll your own!</title><link>https://ounapuu.ee/posts/2025/02/10/roll-your-own-feature-toggles/</link><pubDate>Mon, 10 Feb 2025 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2025/02/10/roll-your-own-feature-toggles/</guid><description>It's one of those build-vs-buy discussions where the build option is genuinely better in 99.99999% of the cases.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2025/02/10/roll-your-own-feature-toggles/media/cover_hu_40edbf50c792270.jpg" width="1200" height="630" alt="Feature toggles: just roll your own!" /><p>When you&rsquo;re dealing with a particularly large service with a slow deployment pipeline (15-30 minutes), and a rollback
delay of up to 10 minutes, you&rsquo;re going to need feature toggles (some also call them feature flags) to turn those
half-an-hour nerve-wrecking major incidents into a small <em>whoopsie-daisy</em> that you can fix in a few seconds.</p>
<p>Make a change, gate it behind a feature toggle, release, enable the feature toggle and monitor the impact. If there is
an issue, you can immediately roll it back with one HTTP request (or database query <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>). If everything looks good, you
can remove the usage of the feature toggle from your code and move on with other work.</p>
<p>Need to roll out the new feature gradually? Implement the feature toggle as a percentage and increase it as you go.</p>
<p>It&rsquo;s really that simple, and you don&rsquo;t have
to <a href="https://news.ycombinator.com/item?id=42902581">pay 500 USD a month to get similar functionality from a service provider</a>
and make critical paths in your application depend on them.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup> As my teammate once said, our service is perfectly
capable of breaking down on its own.</p>
<p>All you really need is one database table containing the keys and values for the feature toggles, and two HTTP
endpoints, one to <code>GET</code> the current value of the feature toggle, and one to <code>POST</code> a new value for an existing one. New
feature toggles will be introduced using tools like Flyway or Liquibase, and the same method can be used for also
deleting them later on. You can also add convenience columns containing timestamps, such as <code>created</code> and
<code>modified</code>, to track when these were introduced and when the last change was.</p>
<p>However, there are a few considerations to take into account when setting up such a system.</p>
<p>Feature toggles implemented as database table rows can work fantastically, but you should also monitor how often these
get used. If you implement a feature toggle on a hot path in your service, then you can easily generate thousands of
queries per second. A properly set up feature toggles system can sustain it without any issues on any competent database
engine, but you should still try to monitor the impact and remove unused feature toggles as soon as possible.</p>
<p>For hot code paths (1000+ requests/second) you might be better off implementing feature toggles as
application properties. There&rsquo;s no call to the database and reading a static property is darn fast, but you lose out on
the ability to update it while the application is running.</p>
<p>Alternatively, you can rely on the same database-based feature toggles system and keep a cached copy in-memory, while
also refreshing it from time to time. Toggling won&rsquo;t be as responsive as it will depend on the cache expiry time, but
the reduced load on the database is often worth it.</p>
<p>If your service receives contributions from multiple teams, or you have very anxious product managers that fill your
backlog faster than you can say &ldquo;story points&rdquo;, then it&rsquo;s a good idea to also introduce expiration dates for your
feature toggles, with ample warning time to properly remove them. Using this method, you can make sure that old feature
toggles get properly removed as there is no better prioritization reason than a looming major incident. You don&rsquo;t want
them to stick around for years on end, that&rsquo;s just wasteful and clutters up your codebase.</p>
<p>If your feature toggling needs are a bit more complicated, then you may need to invest more time in your DIY solution,
or you can use one of the SaaS options if you really want to, just account for the added expense and reliance on yet
another third party service.</p>
<p>At work, I help manage a business-critical monolith that handles thousands of requests per second during peak
hours, and the simple approach has served us very well. All it took was one motivated developer and about a day to
implement, document and communicate the solution to our stakeholders.</p>
<p>Skip the latter two steps, and you can be done within two hours, tops.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>letting inexperienced developers touch the production database is a fantastic way to take down your service, and a
very expensive way to learn about database locks.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>I hate to refer to specific Hacker News comments like this, but there&rsquo;s just something about paying 6000 USD a
year for such a service that I just can&rsquo;t understand. Has the Silicon Valley mindset gone too far? Or are US-based
developers just way too expensive, resulting in these types of services sounding reasonable? You can hire a senior
developer in Estonia for that amount of money for 2-3 weeks (including all taxes), and they can pop in and implement a
feature toggles system in a few hours at most. The response comment with the status page link that&rsquo;s highlighting
multiple outages for LaunchDarkly is the cherry on top.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>Why my blog was down for over 24 hours in November 2024</title><link>https://ounapuu.ee/posts/2025/01/21/downtime/</link><pubDate>Tue, 21 Jan 2025 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2025/01/21/downtime/</guid><description>Everything I learned from an incident that made me consider switching careers. It was a close one!</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2025/01/21/downtime/media/cover_hu_e9bbb9d03e5c963f.jpg" width="1200" height="630" alt="Why my blog was down for over 24 hours in November 2024" /><p>In November 2024, my blog was down for over 24 hours.</p>
<p>Here&rsquo;s what I learned from this absolute clusterfuck of an incident.</p>
<h2 id="lead-up-to-the-incident">
  <a class="heading-anchor" href="#lead-up-to-the-incident">Lead-up to the incident<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I was browsing through photos on my <a href="https://nextcloud.com/">Nextcloud</a> instance. Everything was fine, until Nextcloud
started generating preview images for older photos.
This process is quite resource intensive, but generally manageable. However, this time the images were high quality
photos in the 10-20 MB size range.</p>
<p>Nextcloud crunched through those, but ended up spawning so many processes that it ended up using all the available
memory on my home server.</p>









<figure class="center">
  <a href="/posts/2025/01/21/downtime/media/oom.png">
    <img src="/posts/2025/01/21/downtime/media/oom_hu_a7bb1706f8142c5e.webp"
     width="577"
     height="278"
     loading="lazy"
     decoding="async"
     alt="Uh-oh.">

  </a>
  <figcaption class="center">Uh-oh.</figcaption>
</figure>

<p>And thus, the server was down.</p>
<p>This could have been solved by a forced reboot. Things were complicated by the simple fact that I was 120 kilometers
away from my server, and I had no IPMI-like device set up.</p>
<p>So I waited.</p>
<p>50 minutes later, I successfully logged in to my server over SSH again! The load averages were in the three-digit realm,
but the system was mostly operational.</p>









<figure class="center">
  <a href="/posts/2025/01/21/downtime/media/loadaverage.png">
    <img src="/posts/2025/01/21/downtime/media/loadaverage_hu_33a3a2e52fb68add.webp"
     width="548"
     height="279"
     loading="lazy"
     decoding="async"
     alt="Load average after logging in after the out of memory incident, shown with htop.">

  </a>
  <figcaption class="center">Load average after logging in after the out of memory incident, shown with htop.</figcaption>
</figure>

<p>I thought that it would be a good idea to restart the server, since who knows what might&rsquo;ve gone wrong while the server
was handling the out-of-memory situation.</p>
<p>I reboot.</p>
<p>The server doesn&rsquo;t seem to come back up. Fuck.</p>









<figure class="center">
  <a href="/posts/2025/01/21/downtime/media/reboot.png">
    <img src="/posts/2025/01/21/downtime/media/reboot_hu_d91afcae7d436db8.webp"
     width="950"
     height="278"
     loading="lazy"
     decoding="async"
     alt="Kurwa.">

  </a>
  <figcaption class="center">Kurwa.</figcaption>
</figure>

<h2 id="the-downtime">
  <a class="heading-anchor" href="#the-downtime">The downtime<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>The worst part of the downtime was that I was simply unable to immediately fix it due to being 120 kilometers away from
the server.</p>
<p>My VPN connection back home was also hosted right there on the server,
using <a href="https://hub.docker.com/r/linuxserver/wireguard">this Docker image.</a></p>
<p>I eventually got around to fixing this issue the next day when I could finally get hands-on with the
server, <a href="/posts/2024/10/16/third-times-the-charm/">my trusty ThinkPad T430.</a>
I open the lid and am greeted with the console login screen. This means that the machine <em>did</em> boot.</p>
<p>I log in to the server over SSH and quickly open <code>htop</code>. My <code>htop</code> configuration shows metrics like <code>systemd</code> state, and
it was showing 20+ failed services. This is very unusual.</p>
<p><code>lsblk</code> and <code>mount</code> show that the storage is there. What was the issue?</p>
<p>Well, apparently the Docker daemon was not starting. I was searching for the error messages and ended
up <a href="https://github.com/moby/moby/issues/21215#issuecomment-568445170">on this GitHub issue.</a>
I tried the fix, which involved deleting the Docker folder with all the containers and configuration, and restarted the
daemon and containers. Everything is operational once again.</p>
<p>I then rebooted the server.</p>
<p>Everything is down again, with the same issue.</p>
<p>And thus began a 8+ hours long troubleshooting session that ran late into the night. <em>04:00-ish</em> late, on a Monday.</p>
<p>I tried everything that I could come up with:</p>
<ul>
<li>used the <code>btrfs</code> Docker storage driver instead of the default overlay one
<ul>
<li>Docker is still broken after a reboot</li>
</ul>
</li>
<li>replaced everything with <code>podman</code>
<ul>
<li>I could not get <code>podman</code> to play well with my containers and IPv6 networking</li>
</ul>
</li>
<li>considered switching careers
<ul>
<li>tractors are surprisingly expensive!</li>
</ul>
</li>
</ul>
<p>I&rsquo;m unable to put into words how frustrating this troubleshooting session was. The sleep deprivation, the lack of
helpful information, the failed attempts at finding solutions. I&rsquo;m usually quite calm and very rarely feel anger,
but during these hours I felt <em><strong>enraged.</strong></em></p>
<h2 id="the-root-cause">
  <a class="heading-anchor" href="#the-root-cause">The root cause<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>The root cause will make more sense after you understand the storage setup I had at the time.</p>
<p>The storage on my server consisted of four 4 TB SSD-s, two were mounted inside the laptop, and the remaining two were
connected via USB-SATA adapters. The filesystem in use was <code>btrfs</code>, both on the OS drive and the 4x 4TB storage pool. To
avoid
hitting the OS boot drive with unnecessary writes, I moved the Docker data root to a separate <code>btrfs</code> subvolume on the
main storage pool.</p>
<p>What was the issue?</p>
<p>Apparently the Docker daemon on Fedora Server is able to start up <em><strong>before every filesystem was mounted.</strong></em> In this
case, Docker daemon started up before the subvolume containing all the Docker images, containers and networks was
mounted.</p>
<p>I tested out this theory by moving the Docker storage back to <code>/var/lib/docker</code>, which lives on the root filesystem, and
after a reboot everything remained functional.</p>
<p>In the past, I ran a similar setup, but with the Docker storage on the SATA SSD-s that are mounted inside the laptop
over a native SATA connection. With the addition of two USB-connected SSD-s, the mounting process took longer for the
whole pool, which resulted in a race condition between the Docker daemon startup and the storage being mounted.</p>
<h2 id="fixing-the-root-cause">
  <a class="heading-anchor" href="#fixing-the-root-cause">Fixing the root cause<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>The fix for Docker starting up before all of your storage is mounted is actually quite elegant.</p>
<p>The Docker service definition is contained in <code>/etc/systemd/system/docker.service</code>. You can override this configuration
by creating a new directory at <code>/etc/systemd/system/docker.service.d</code> and dropping a file with the name <code>override.conf</code>
in there with the following contents:</p>
<pre tabindex="0"><code>[Unit]
RequiresMountsFor=/containerstorage
</code></pre><p>The rest of the service definition remains the same and your customized configuration won&rsquo;t be overwritten with a Docker
version update. The <code>RequiresMountsFor</code> setting prevents the Docker service from starting up before that particular
mount exists.</p>
<p>You can specify multiple mount points on the same line, separated by spaces.</p>
<pre tabindex="0"><code>[Unit]
RequiresMountsFor=/containerstorage /otherstorage /some/other/mountpoint
</code></pre><p>You can also specify the mount points over multiple lines if you prefer.</p>
<pre tabindex="0"><code>[Unit]
RequiresMountsFor=/containerstorage 
RequiresMountsFor=/otherstorage 
RequiresMountsFor=/some/other/mountpoint
</code></pre><p>If you&rsquo;re using <code>systemd</code> unit files for controlling containers, then you can use the same <code>systemd</code> setting to prevent
your containers from starting up before the storage that the container depends on is mounted.</p>
<h2 id="avoiding-the-out-of-memory-incident">
  <a class="heading-anchor" href="#avoiding-the-out-of-memory-incident">Avoiding the out of memory incident<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>Nextcloud taking down my home server for 50 minutes was not the root cause, it only highlighted an issue that had been
there for days at that point. That doesn&rsquo;t mean that this area can&rsquo;t be improved.</p>
<p>After this incident, every Docker Compose file that I use includes resource limits on all containers.</p>
<p>When defining the limits, I started with very conservative limits based on the average resource usage as observed from
<code>docker stats</code> output.</p>
<p>Over the past few months I&rsquo;ve had to continuously tweak the limits, especially the memory ones, due to the containers
themselves running out of memory when the limits were set too low. Apparently software is getting increasingly more
resource hungry.</p>
<p>An example Docker Compose file with resource limits looks like this:</p>
<pre tabindex="0"><code>name: nextcloud
services:
  nextcloud:
    container_name: nextcloud
    volumes:
      - /path/to/nextcloud/stuff:/data
    deploy:
      resources:
        limits:
          cpus: &#34;4&#34;
          memory: 2gb
    image: docker.io/nextcloud:latest
    restart: always
  nextcloud-db:
    container_name: nextcloud-db
    volumes:
      - /path/to/database:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: &#34;4&#34;
          memory: 2gb
    image: docker.io/postgres:16
    restart: always
</code></pre><p>In this example, each container is able to use up to 4 CPU cores and a maximum of 2 GB of memory. And just like that,
Nextcloud is unable to take down my server by eating up all the available memory.</p>
<p>Yes, I&rsquo;m aware of the <a href="https://github.com/nextcloud/previewgenerator">Preview Generator Nextcloud app.</a> I have it, but
over multiple years of running Nextcloud, I have not found it to be very effective against the resource-hungry preview
image generation happening during user interactions.</p>
<h2 id="decoupling-my-vpn-solution-from-docker">
  <a class="heading-anchor" href="#decoupling-my-vpn-solution-from-docker">Decoupling my VPN solution from Docker<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>With this incident, it was also clear that running your gateway to your home network inside a container was a really
stupid idea.</p>
<p>I&rsquo;ve mitigated this issue by taking the WireGuard configuration generated by the container and moving it to the host. I
also used this as an opportunity to get a to-do list item done and
used <a href="https://stanislas.blog/2019/01/how-to-setup-vpn-server-wireguard-nat-ipv6/">this guide</a> to add IPv6 support inside
the
virtual WireGuard network. I can now access IPv6 networks everywhere I go!</p>
<p>I briefly considered setting WireGuard up on my openWRT-powered router, but I decided against it as I&rsquo;d like to own
one computer that I don&rsquo;t screw up with my configuration changes.</p>
<h2 id="closing-thoughts">
  <a class="heading-anchor" href="#closing-thoughts">Closing thoughts<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I have not yet faced an incident this severe, even at work. The impact wasn&rsquo;t that big, I guess a hundred people
were not able to read my blog, but the stress levels were off the charts for me during the troubleshooting process.</p>
<p>I&rsquo;ve long advocated for self-hosting and running basic and boring solutions, with the main benefits being ease of
maintenance, troubleshooting and low cost. This incident is a good reminder that even the most basic setups can have
complicated issues associated with them.</p>
<p>At least I got it fixed and learned about a new <code>systemd</code> unit setting, which is nice.</p>
<p>Still better than handling <a href="/posts/2024/10/01/kubernetes/">Kubernetes</a> <a href="/posts/2024/04/04/helm-rollbljat/">issues.</a></p>
]]></content:encoded></item><item><title>Backing up another PC with a single Ethernet cable</title><link>https://ounapuu.ee/posts/2025/01/15/backups-without-disk/</link><pubDate>Wed, 15 Jan 2025 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2025/01/15/backups-without-disk/</guid><description>If all you have is an Ethernet cable, then everything looks like a temporary storage device.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2025/01/15/backups-without-disk/media/cover_hu_9014625e7ebdbb4.jpg" width="1200" height="630" alt="Backing up another PC with a single Ethernet cable" /><p>I was in a pinch.</p>
<p>I needed to make a full disk backup of a PC, but I had no external storage device with me to store it on.
The local Wi-Fi network was also way too slow to transfer the disk over it.</p>
<p>All I had was my laptop with an Ethernet port, a Fedora Linux USB stick, and a short Ethernet cable.</p>
<p>I took the following steps:</p>
<ul>
<li>boot the target machine up with the Fedora Linux installer in a live environment</li>
<li>modify the SSH configuration on the target machine to allow <code>root</code> user login with a password
<ul>
<li>it&rsquo;s OK to do this on a temporary setup like this one, but don&rsquo;t do it on an actual Linux server</li>
</ul>
</li>
<li>set a password for the <code>root</code> user on the target machine
<ul>
<li>only required because a live environment usually does not set one for <code>root</code> user</li>
</ul>
</li>
<li>connect both laptops with the Ethernet cable</li>
<li>set static IPv4 addresses on both machines using network settings <sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>
<ul>
<li>edit the &ldquo;Wired&rdquo; connection and open the IPv4 tab</li>
<li>example IP address on target: 192.168.100.5</li>
<li>example IP address on my laptop: 192.168.100.1</li>
<li>make sure to set the netmask to 255.255.255.0 on both!</li>
</ul>
</li>
<li>verify that the SSH connection works to the target machine</li>
<li>back up the disk to your local machine using <code>ssh</code> and <code>dd</code>
<ul>
<li>example: <code>ssh root@192.168.100.5 &quot;dd if=/dev/sda&quot; | dd of=disk-image-backup.iso status=progress</code></li>
<li>replace <code>/dev/sda</code> with the correct drive name!</li>
</ul>
</li>
</ul>
<p>And just like that, I backed up the 120 GB SSD at gigabit speeds to my laptop.</p>









<figure class="center">
  <a href="/posts/2025/01/15/backups-without-disk/media/setup-0.jpg">
    <img src="/posts/2025/01/15/backups-without-disk/media/setup-0_hu_9fc5ce3a493235ab.webp"
     width="1000"
     height="750"
     loading="lazy"
     decoding="async"
     alt="Copying in progress.">

  </a>
  <figcaption class="center">Copying in progress.</figcaption>
</figure>










<figure class="center">
  <a href="/posts/2025/01/15/backups-without-disk/media/setup-1.jpg">
    <img src="/posts/2025/01/15/backups-without-disk/media/setup-1_hu_5f8d76c41d88289b.webp"
     width="1000"
     height="750"
     loading="lazy"
     decoding="async"
     alt="It might be only 105 MB/s, but it beat doing the same over Wi-Fi at 1.5 MB/s!">

  </a>
  <figcaption class="center">It might be only 105 MB/s, but it beat doing the same over Wi-Fi at 1.5 MB/s!</figcaption>
</figure>

<p>I&rsquo;ve used a similar approach in the past when switching between laptops by running a live environment on both machines
and copying the disk over with <code>dd</code> bit by bit.</p>
<p>You&rsquo;ll also save time on not having to copy the data over twice, first to an external storage device, and then to the
target device.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>there&rsquo;s probably a simpler way to do this with IPv6 magic, but I have not tested it yet&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>The IPv6 situation on Docker is good now!</title><link>https://ounapuu.ee/posts/2024/12/20/docker-ipv6/</link><pubDate>Fri, 20 Dec 2024 19:30:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/12/20/docker-ipv6/</guid><description>It's not often when a piece of software has genuinely improved, which is why this is worth celebrating!</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/12/20/docker-ipv6/media/cover_hu_2d1118bf85d813df.jpg" width="1200" height="630" alt="The IPv6 situation on Docker is good now!" /><p>Good news, everyone! Doing IPv6 networking stuff on Docker is actually good now!</p>
<p>I&rsquo;ve recently started reworking my home server setup to be more IPv6 compatible, and as part of that I learned that
during
the summer of 2024 <a href="https://docs.docker.com/engine/release-notes/27/#ipv6">Docker shipped an update</a> that eliminated a
lot of the configuration and tweaking previously necessary
to support IPv6.</p>
<p>There is no need to change the daemon configuration any longer, it just works on Docker Engine v27 and later.</p>
<h2 id="examples">
  <a class="heading-anchor" href="#examples">Examples<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>If your host has a working IPv6 setup and you want to listen to port 80 on both IPv4 and IPv6, then you don&rsquo;t
have to do anything special. However, the container will only have an IPv4 address internally.
You can verify it by listing all the Docker networks via <code>sudo docker network ls</code> and running
<code>sudo docker network inspect network-name-here</code> for the one associated with your container.</p>
<p>For services like <code>nginx</code> that log the source IP address, this is problematic, as every incoming IPv6 request will be
logged with the Docker network gateway IP address, such as <code>10.88.0.1</code>.</p>
<pre tabindex="0"><code>name: nginx
services:
  nginx:
    container_name: nginx
    ports:
      - 80:80
    image: docker.io/library/nginx
    restart: always
</code></pre><p>If you want the container to have an IPv4 <em>and</em> an IPv6 address within the Docker network, you can create a new network
and enable IPv6 in it.</p>
<pre tabindex="0"><code>name: nginx
services:
  nginx:
    container_name: nginx
    networks:
      - nginx-network
    ports:
      - 80:80
    image: docker.io/library/nginx
    restart: always
networks:
  nginx-network:
    enable_ipv6: true
</code></pre><p>There are situations where it&rsquo;s handy to have a static IP address for a container within the Docker network.
If you need help coming up with an unique local IPv6 address range, you
can <a href="https://unique-local-ipv6.com/">use this tool.</a></p>
<pre tabindex="0"><code>name: nginx
services:
  nginx:
    container_name: nginx
    networks:
      nginx-network
        ipv4_address: 10.69.42.5
        ipv6_address: fdec:cc68:5178::abba
    ports:
      - 80:80
    image: docker.io/library/nginx
    restart: always
networks:
  nginx-network:
    enable_ipv6: true
    ipam:
      driver: default
      config:
        - subnet: &#34;10.69.42.0/24&#34;
        - subnet: &#34;fdec:cc68:5178::/64&#34;
</code></pre><p>If you choose the <a href="https://docs.docker.com/engine/network/drivers/host/">host network driver,</a> your container will
operate within the same networking space as your container host. If the host handles both IPv4 and IPv6 networking, then
your container will happily operate with both. However, due to reduced network isolation, this has some security
implications that you must take into account.</p>
<pre tabindex="0"><code>name: nginx
services:
  nginx:
    container_name: nginx
    network_mode: host
    # ports are not relevant with host network mode
    image: docker.io/library/nginx
    restart: always
</code></pre><p>If you want your container to only accept connections on select interfaces, such as a Wireguard connection, then you will need
to specify the IP addresses in the <code>ports</code> section. Here&rsquo;s one example with both IPv4 and IPv6.</p>
<pre tabindex="0"><code>name: nginx
services:
  nginx:
    container_name: nginx
    networks:
      - nginx-network
    ports:
      - 10.69.42.5:80:80
      - &#34;[fdec:cc68:5178::beef]:80:80&#34;
    image: docker.io/library/nginx
    restart: always
networks:
  nginx-network:
    enable_ipv6: true
</code></pre><h2 id="what-about-podman">
  <a class="heading-anchor" href="#what-about-podman">What about Podman?<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I&rsquo;ve given up on Podman. Before doing things the IPv6 way, Podman was functional for the most part, requiring a few
tweaks to get things working.</p>
<p>I have not managed to get Podman to play fair with IPv6. No matter what I did, I could not get it to listen to certain
ports and access my services, the ports would always be filtered out.</p>
<h2 id="conclusion">
  <a class="heading-anchor" href="#conclusion">Conclusion<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I&rsquo;m genuinely happy to see that the IPv6 support has gotten better with Docker, and I hope that this short introduction
helps those out there looking to do things the IPv6 way with containers.</p>
]]></content:encoded></item><item><title>You can use almost anything as a key file for your encrypted storage device</title><link>https://ounapuu.ee/posts/2024/11/20/keyfiles/</link><pubDate>Wed, 20 Nov 2024 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/11/20/keyfiles/</guid><description>Yes, you can decrypt your storage with a picture of your cat.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/11/20/keyfiles/media/cover_hu_13983d04e0e274a1.jpg" width="1200" height="630" alt="You can use almost anything as a key file for your encrypted storage device" /><p>Imagine that you have an unencrypted drive containing your private data and one day it starts throwing a bunch of
errors. You have backups of the data so you&rsquo;ve got that part covered, but would you feel comfortable sending the drive
in to be warrantied? You have no control over who has access to that drive, and due to the drive failing you can&rsquo;t
format it as well.</p>
<p>Do you take the financial hit and buy a new drive, or send it in regardless and risk
someone looking through your files?</p>
<p>I&rsquo;ve bought and sold a bunch of hard drives and SSD-s over the years<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup>, and warrantied a few of them. I encrypt the
disk
on my personal and work machines because of the obvious security benefits, but for a long time I avoided doing the same
for other storage devices.</p>
<p>Then I realized that I can just encrypt them and use almost anything as the key. All you need is a reasonably sized file
to pass to <code>cryptsetup</code> as a key-file, refer to that key-file in <code>/etc/crypttab</code>, and you&rsquo;re good to
go!</p>
<p>Be creative! The key file can be anything:</p>
<ul>
<li>a dank meme</li>
<li>a very short low resolution cat video</li>
<li>an adorable photo of your dog</li>
<li>a precious family photo</li>
<li>Twilight fan fiction that you wrote back in 2011</li>
<li>your letter of resignation at your last job</li>
</ul>
<p>Just mind the size, <code>cryptsetup</code> wasn&rsquo;t very happy with a 17MB full resolution image. A JPEG that&rsquo;s less than a megabyte
in size worked well enough for me.</p>
<p>On Linux, you can encrypt a partition with a cat picture using a command like this:</p>
<pre tabindex="0"><code>cryptsetup luksFormat /dev/sdx1 --key-file /path/to/cat.jpg
</code></pre><p>You can make sure that the drive gets automatically decrypted at boot by defining it in <code>/etc/crypttab</code>.
Find the UUID of the encrypted partition by running <code>ls -lah /dev/disk/by-uuid</code> and see which one matches with
<code>/dev/sdx1</code>.
If the UUID is <code>ef277bc2-d953-44c4-88af-8320aca76969</code>, then a line in <code>/etc/crypttab</code> would look like this:</p>
<pre tabindex="0"><code>encryptedcatdrive UUID=ef277bc2-d953-44c4-88af-8320aca76969 /path/to/cat.jpg 
</code></pre><p>Once unlocked, your decrypted partition will be available as <code>/dev/mapper/encryptedcatdrive</code>.</p>
<p>Just make sure that the key file isn&rsquo;t placed on the encrypted drive itself, otherwise you&rsquo;ll lock yourself out for
good.</p>
<p>For more information on LUKS disk encryption and its
capabilities, <a href="https://wiki.archlinux.org/title/Dm-crypt/Device_encryption">see this handy Arch Wiki page.</a></p>
<p>This approach comes with some great benefits and a few downsides.</p>
<p>On the bright side, you don&rsquo;t have to worry about a stranger getting their hands on your data when selling a storage
device
or sending it in to be warrantied, assuming that you haven&rsquo;t posted the key file on social media or anywhere else.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup>
Just do a quick format of the drive, or format a very specific part to clear the LUKS headers, and the contents of the
drive are gone!</p>
<p>If you can&rsquo;t format the drive due to hardware issues, then you should still be safe, the attacker would
have to first <em>fix</em> the drive (which isn&rsquo;t a guaranteed success) and <em>then</em> figure out the key, which will require a lot
of effort and time.</p>
<p>The one obvious downside is that if you lose the key file, you lose the data. You can mitigate this by either adding
multiple key files as a backup option, or setting <a href="https://xkcd.com/936/">a strong password</a> that you can use in case
you lose your original key file.</p>
<p>If the drive contains something of value to your family members, then it may be a good idea to specify those details in
a will, or by taping the backup password with instructions on the drive itself. If you end up selling the drive before
your personal expiration date arrives, then you can simply remove the instructions. Just make sure that your family
members or relatives know to plug in the drive to a Linux machine.<sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p>
<p>I also like the thrill of hiding a key in plain sight. Did I use the cover photo of this post to encrypt my drives?
You&rsquo;ll never know, and that&rsquo;s what makes it fun!</p>
<p>If you&rsquo;re paranoid or targeted by state actors, then you probably shouldn&rsquo;t follow this advice, except the part where I
encourage encrypting stuff.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Hello, my name is Herman, and I&rsquo;m a recovering data hoarder.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>if we use a simple .jpg as an example key file, then it&rsquo;s highly likely that this won&rsquo;t be an issue as most
messaging and social media platforms compress the hell out of the image. The compression is also lossy, meaning that the
process is irreversible and the end result is a completely different file from the perspective of the encrypted disk.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:3">
<p>if you&rsquo;ve ever plugged in a drive with a Linux filesystem on a Windows machine, then you&rsquo;ll know that it will
happily recommend formatting it. Oh, goodie.&#160;<a href="#fnref:3" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>Your Wi-Fi might be terrible because of Dynamic Frequency Selection (DFS)</title><link>https://ounapuu.ee/posts/2024/11/11/openwrt-dfs/</link><pubDate>Mon, 11 Nov 2024 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/11/11/openwrt-dfs/</guid><description>My Wi-Fi kept dropping out until I learned about this fun little feature. Here's how to fix it for good.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/11/11/openwrt-dfs/media/cover_hu_922176fce2414a28.jpg" width="1200" height="630" alt="Your Wi-Fi might be terrible because of Dynamic Frequency Selection (DFS)" /><p>For a few months, I had issues with my Wi-Fi network. The 2.4 GHz network would be fine, but the 5 GHz one would
suddenly stop working and completely disappear from the available Wi-Fi networks. OpenWRT upgrades also didn&rsquo;t improve
the situation. This was very annoying.</p>
<p>After some discussions with a friend, I learned
about <a href="https://en.wikipedia.org/wiki/Dynamic_frequency_selection">Dynamic Frequency Selection (DFS).</a> Apparently some
channels on the 5 GHz Wi-Fi spectrum are also used by weather and military radars, and those take priority. If such
interference is detected, your Wi-Fi access point should switch to a different channel.</p>
<p>It turns out that some implementations are buggy and mine is one of them.</p>
<p>One quick but permanent fix is to manually select a Wi-Fi channel to operate in.</p>
<p>With OpenWRT, you can get a list of all the available Wi-Fi channels using this command:</p>
<pre tabindex="0"><code>iw list | grep dBm
</code></pre><p>Example output:</p>
<pre tabindex="0"><code>* 2412 MHz [1] (20.0 dBm)
* 2417 MHz [2] (20.0 dBm)
* 2422 MHz [3] (20.0 dBm)
* 2427 MHz [4] (20.0 dBm)
* 2432 MHz [5] (20.0 dBm)
* 2437 MHz [6] (20.0 dBm)
* 2442 MHz [7] (20.0 dBm)
* 2447 MHz [8] (20.0 dBm)
* 2452 MHz [9] (20.0 dBm)
* 2457 MHz [10] (20.0 dBm)
* 2462 MHz [11] (20.0 dBm)
* 2467 MHz [12] (20.0 dBm)
* 2472 MHz [13] (20.0 dBm)
* 5180 MHz [36] (23.0 dBm)
* 5200 MHz [40] (23.0 dBm)
* 5220 MHz [44] (23.0 dBm)
* 5240 MHz [48] (23.0 dBm)
* 5260 MHz [52] (20.0 dBm) (radar detection)
* 5280 MHz [56] (20.0 dBm) (radar detection)
* 5300 MHz [60] (20.0 dBm) (radar detection)
* 5320 MHz [64] (20.0 dBm) (radar detection)
* 5500 MHz [100] (26.0 dBm) (radar detection)
* 5520 MHz [104] (26.0 dBm) (radar detection)
* 5540 MHz [108] (26.0 dBm) (radar detection)
* 5560 MHz [112] (26.0 dBm) (radar detection)
* 5580 MHz [116] (26.0 dBm) (radar detection)
* 5600 MHz [120] (26.0 dBm) (radar detection)
* 5620 MHz [124] (26.0 dBm) (radar detection)
* 5640 MHz [128] (26.0 dBm) (radar detection)
* 5660 MHz [132] (26.0 dBm) (radar detection)
* 5680 MHz [136] (26.0 dBm) (radar detection)
* 5700 MHz [140] (26.0 dBm) (radar detection)
* 5720 MHz [144] (13.0 dBm) (radar detection)
* 5745 MHz [149] (13.0 dBm)
* 5765 MHz [153] (13.0 dBm)
* 5785 MHz [157] (13.0 dBm)
* 5805 MHz [161] (13.0 dBm)
* 5825 MHz [165] (13.0 dBm)
* 5845 MHz [169] (13.0 dBm)
* 5865 MHz [173] (13.0 dBm)
</code></pre><p>Notice the ones with <code>(radar detection)</code> at the end? Those are the potentially problematic channels. We&rsquo;re going to
avoid them from now on by picking a specific channel to use.</p>
<p>When it comes to the choice of the channels themselves, you&rsquo;ll also have to consider the channel width. If you pick a
channel next to one of the radar detection ones and with a large channel width, you might still run into issues.</p>
<p>Choosing a specific channel also comes with bandwidth and range trade-offs. If you don&rsquo;t care much for those, go for the
lowest one and with 40 MHz width. Picking the optimal Wi-Fi channel and channel width configuration for your specific
needs is better explained by other resources.</p>









<figure class="center">
  <a href="/posts/2024/11/11/openwrt-dfs/media/openwrt-luci.png">
    <img src="/posts/2024/11/11/openwrt-dfs/media/openwrt-luci_hu_762e2ab7b34fdf81.webp"
     width="902"
     height="462"
     loading="lazy"
     decoding="async"
     alt="Choosing a specific channel in OpenWRT using the GUI (LuCI).">

  </a>
  <figcaption class="center">Choosing a specific channel in OpenWRT using the GUI (LuCI).</figcaption>
</figure>

<p>I recommend getting a Wi-Fi spectrum analysis app that shows you the channels that are least populated by neighboring
Wi-Fi access points.</p>
<p>It is also possible to define the list of channels that the Wi-Fi AP can automatically choose from using the <code>channels</code>
option for the wireless interface. We can use this setting to avoid the radar detection channels completely. This
setting doesn&rsquo;t seem to be configurable via the graphical interface (LuCI), but you can change it
in <code>/etc/config/wireless</code> using the command line and <code>vi</code>, over SSH.</p>
<p>More information about this option and others can be found
in <a href="https://openwrt.org/docs/guide-user/network/wifi/basic#common_options">OpenWRT documentation.</a></p>
<p>Based on our example, a configuration that avoids radar detection frequencies can look something like this:</p>
<pre tabindex="0"><code>config wifi-device &#39;radio0&#39;
        option type &#39;mac80211&#39;
        option path &#39;pci0000:00/0000:00:00.0&#39;
        option channel &#39;auto&#39;
        option channels &#39;36 40 44 48 149 153 157 161 165 169 173&#39;
        option band &#39;5g&#39;     
        option htmode &#39;VHT40&#39;
        option country &#39;EE&#39;    
        option cell_density &#39;0&#39;
</code></pre><p>Using a manually specified channel has resulted in no Wi-Fi related issues for over half a year.</p>
<p>I consider this a permanent fix.</p>
<p>If you&rsquo;re using a PC and don&rsquo;t want to mess with Wi-Fi issues ever again, then just run some Ethernet cables. It&rsquo;s worth
it.</p>
]]></content:encoded></item><item><title>No HDMI port on the ThinkPad T430? No problem!</title><link>https://ounapuu.ee/posts/2024/07/08/thinkpad-t430-hdmi/</link><pubDate>Mon, 08 Jul 2024 13:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/07/08/thinkpad-t430-hdmi/</guid><description>Yes, you can fit a HDMI port in the ThinkPad T430, no crazy modifications required.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/07/08/thinkpad-t430-hdmi/media/cover_hu_4ddbd24444d473b3.jpg" width="1200" height="630" alt="No HDMI port on the ThinkPad T430? No problem!" /><p>The ThinkPad T430 has a few options for running it with an external display:</p>
<ul>
<li>VGA port, which is pretty much obsolete at this point</li>
<li>mini DisplayPort connector on the laptop itself</li>
<li>DVI or DisplayPort on a dock</li>
</ul>
<p>The mini DisplayPort port has annoyed me for as long as I&rsquo;ve had this machine.</p>
<p>Most places where I&rsquo;ve had to present something only offer an HDMI cable,
which means that I always have to carry a dongle around, and I keep forgetting
to bring one everywhere I happen to go.</p>
<p>Until now.</p>
<p>I have a few of these SATA HDD adapters that replace the optical drive on
the ThinkPad T430, and I discovered that my mini DisplayPort to HDMI adapter
can fit in one without a problem.</p>









<figure class="center">
  <a href="/posts/2024/07/08/thinkpad-t430-hdmi/media/adapter-out.jpg">
    <img src="/posts/2024/07/08/thinkpad-t430-hdmi/media/adapter-out_hu_1eee9bb795c46d67.webp"
     width="800"
     height="600"
     loading="lazy"
     decoding="async"
     alt="It&#39;s a snug fit so it doesn&#39;t fall out.">

  </a>
  <figcaption class="center">It&#39;s a snug fit so it doesn&#39;t fall out.</figcaption>
</figure>










<figure class="center">
  <a href="/posts/2024/07/08/thinkpad-t430-hdmi/media/adapter-closed.jpg">
    <img src="/posts/2024/07/08/thinkpad-t430-hdmi/media/adapter-closed_hu_c3b02f2f4bc02f66.webp"
     width="800"
     height="600"
     loading="lazy"
     decoding="async"
     alt="You&#39;d never know the adapter was in here.">

  </a>
  <figcaption class="center">You&#39;d never know the adapter was in here.</figcaption>
</figure>

<p>I don&rsquo;t currently have a need for a second SSD in my T430, so this &ldquo;mod&rdquo;
makes perfect sense to me.</p>
<p>I bet there is someone out there who is capable of routing an <em>actual</em> HDMI
port in place of this adapter. The existence of FHD mods for this laptop
suggests that this is possible. If you&rsquo;re <em>that</em> person and created that mod,
let me know.</p>
]]></content:encoded></item><item><title>OpenWRT, ISP modem and dynamic IP addresses: how to fix connectivity issues without rebooting your router every time</title><link>https://ounapuu.ee/posts/2024/05/20/openwrt-connectivity-fix/</link><pubDate>Mon, 20 May 2024 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/05/20/openwrt-connectivity-fix/</guid><description>The solution proposed might be a bit specific for my particular setup, but hopefully useful to someone out there.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/05/20/openwrt-connectivity-fix/media/cover_hu_e82e7b9beb9d68f.jpg" width="1200" height="630" alt="OpenWRT, ISP modem and dynamic IP addresses: how to fix connectivity issues without rebooting your router every time" /><p><a href="https://elisa.ee/">My current ISP</a> provides an internet connection over a
copper wire. To use it,
I have a crappy modem (Technicolor CGA2121, DOCSIS 3.0). It&rsquo;s running in bridge
mode,
meaning that all it does is convert the signal running over the coax cable
into plain old Ethernet.</p>
<p>My main networking device is a TP-Link Archer C7 v5. It runs OpenWRT. This
router/Wi-Fi AP box connects to the modem and handles everything, including
getting a public
IPv4 address from the ISP.<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p>
<p>After a power outage or my ISP doing maintenance, the public IP address has
usually changed. This wouldn&rsquo;t be a problem if I just stuck to the ISP-approved
modem.<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p>
<p>With my setup, there was a problem. The OpenWRT box would try to operate with
the IPv4 address that it was
given since the DHCP lease had not yet expired. However, this meant that there
was no internet
connectivity. A reboot of the OpenWRT box would resolve the issue.</p>
<p>This manual workaround wasn&rsquo;t good enough for me. It would be quite problematic
if this issue happened while I was away from home because I&rsquo;d still like to
access
my home server.</p>
<p>After traversing OpenWRT forums and consulting the Slack workspace of my local
hackerspace, I found that bringing up the WAN interface again would result in
the OpenWRT box getting a new public IPv4 address. Problem solved!</p>
<p>To automate this workaround, I created a single crontab entry in the OpenWRT
box. This is
also configurable in a graphical user interface as long as you
have <a href="https://openwrt.org/docs/guide-user/luci/start">LuCI installed.</a></p>
<p>The crontab entry looks like this:</p>
<p><code>*/5 * * * * /bin/ash -c '/bin/ping -c 3 8.8.8.8 &gt; /dev/null || /sbin/ifup wan'</code></p>
<p>Every 5 minutes, the router pings Google&rsquo;s DNS server. If that command succeeds,
then the internet connection works and that&rsquo;s it. If the ping fails, then the
other half of the shell command is executed, which brings up the <code>wan</code> interface
on my router.</p>
<p>Feel free to use a different IP address to test with. Your WAN network interface
might also have a different name.</p>
<p>The downside of this solution is that if the server you&rsquo;re using to verify your
internet connection is down or refuses pings, then you&rsquo;ll be causing
interruptions in your home network every 5 minutes.</p>
<p>Talking to the ISP about this issue was something I considered as well. Then
I remembered that it took me 1.5 months of fighting chatbots and repeating the
same information to different customer care agents to use my own modem that&rsquo;s
identical to the one the ISP uses. That&rsquo;s a <em>hell no</em> from me.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>I really should get around to drilling those holes in the apartment
building to get access to a fiber connection.&#160;<a href="#fnref:1" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
<li id="fn:2">
<p>using the ISP-approved box would introduce a whole other set of problems
because they are surprisingly low quality.&#160;<a href="#fnref:2" class="footnote-backref" role="doc-backlink">&#x21a9;&#xfe0e;</a></p>
</li>
</ol>
</div>
]]></content:encoded></item><item><title>The hidden media play/pause/stop keys on the Lenovo ThinkPad L390 Yoga</title><link>https://ounapuu.ee/posts/2024/05/07/thinkpad-l390-hidden-media-keys/</link><pubDate>Tue, 07 May 2024 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2024/05/07/thinkpad-l390-hidden-media-keys/</guid><description>How a few accidental key presses revealed the presence of media playback control keys on my ThinkPad L390 Yoga.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2024/05/07/thinkpad-l390-hidden-media-keys/media/cover_hu_a6666e239a0c3efa.jpg" width="1200" height="630" alt="The hidden media play/pause/stop keys on the Lenovo ThinkPad L390 Yoga" /><p>ThinkPad keyboards were once well known for their great layouts, feel and
functionality. This included the media playback control keys.</p>









<figure class="center">
  <a href="/posts/2024/05/07/thinkpad-l390-hidden-media-keys/media/T420.jpg">
    <img src="/posts/2024/05/07/thinkpad-l390-hidden-media-keys/media/T420_hu_3a0505d7634368c6.webp"
     width="463"
     height="302"
     loading="lazy"
     decoding="async"
     alt="Media playback control keys on a ThinkPad T420 keyboard.">

  </a>
  <figcaption class="center">Media playback control keys on a ThinkPad T420 keyboard.</figcaption>
</figure>

<p>On the ThinkPad T430, the new chiclet keyboard layout moved the media keys to
the function row. Still there, but less convenient to access.</p>









<figure class="center">
  <a href="/posts/2024/05/07/thinkpad-l390-hidden-media-keys/media/T430.jpg">
    <img src="/posts/2024/05/07/thinkpad-l390-hidden-media-keys/media/T430_hu_619b1ebe964a53f4.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="Media playback control keys on a ThinkPad T430 keyboard.">

  </a>
  <figcaption class="center">Media playback control keys on a ThinkPad T430 keyboard.</figcaption>
</figure>

<p>The ThinkPad L390 Yoga doesn&rsquo;t have any visible function keys for controlling
media playback. However, I found that the play/pause and stop
buttons are still functional on the up/down arrow keys. Left/right arrow keys
act as <code>Home</code> and <code>End</code> keys.</p>
<p><code>evtest</code> output for play/pause key combination:</p>
<pre tabindex="0"><code>Event: time 1714716211.222697, type 4 (EV_MSC), code 4 (MSC_SCAN), value e3
Event: time 1714716211.222697, type 1 (EV_KEY), code 143 (KEY_WAKEUP), value 1
Event: time 1714716211.222697, -------------- SYN_REPORT ------------
Event: time 1714716212.291293, type 4 (EV_MSC), code 4 (MSC_SCAN), value a2
Event: time 1714716212.291293, type 1 (EV_KEY), code 164 (KEY_PLAYPAUSE), value 1
Event: time 1714716212.291293, -------------- SYN_REPORT ------------
Event: time 1714716212.366568, type 4 (EV_MSC), code 4 (MSC_SCAN), value a2
Event: time 1714716212.366568, type 1 (EV_KEY), code 164 (KEY_PLAYPAUSE), value 0
Event: time 1714716212.366568, -------------- SYN_REPORT ------------
Event: time 1714716214.026847, type 4 (EV_MSC), code 4 (MSC_SCAN), value e3
Event: time 1714716214.026847, type 1 (EV_KEY), code 143 (KEY_WAKEUP), value 0
Event: time 1714716214.026847, -------------- SYN_REPORT ------------
</code></pre><p><code>evtest</code> output for stop key combination:</p>
<pre tabindex="0"><code>Event: time 1714716254.780584, type 4 (EV_MSC), code 4 (MSC_SCAN), value e3
Event: time 1714716254.780584, type 1 (EV_KEY), code 143 (KEY_WAKEUP), value 1
Event: time 1714716254.780584, -------------- SYN_REPORT ------------
Event: time 1714716255.614775, type 4 (EV_MSC), code 4 (MSC_SCAN), value a4
Event: time 1714716255.614775, type 1 (EV_KEY), code 166 (KEY_STOPCD), value 1
Event: time 1714716255.614775, -------------- SYN_REPORT ------------
Event: time 1714716255.658388, type 4 (EV_MSC), code 4 (MSC_SCAN), value a4
Event: time 1714716255.658388, type 1 (EV_KEY), code 166 (KEY_STOPCD), value 0
Event: time 1714716255.658388, -------------- SYN_REPORT ------------
Event: time 1714716256.961601, type 4 (EV_MSC), code 4 (MSC_SCAN), value e3
Event: time 1714716256.961601, type 1 (EV_KEY), code 143 (KEY_WAKEUP), value 0
Event: time 1714716256.961601, -------------- SYN_REPORT ------------
</code></pre><p>On a very recent laptop, <a href="/posts/2024/04/12/lenovo-p14s-gen4/">the ThinkPad P14s gen4</a>, the media playback keys seem to
be gone. Left/right arrow keys do still work as <code>Home</code> and <code>End</code> keys, though.</p>
<p>If you also miss your media playback control keys on your ThinkPad, then hit
that <code>Fn</code> key and give it a go, you might get lucky!</p>
]]></content:encoded></item><item><title>The simplicity of the modulo operator: how I scaled an inefficient solution on a legacy system</title><link>https://ounapuu.ee/posts/2023/12/11/modulo/</link><pubDate>Mon, 11 Dec 2023 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2023/12/11/modulo/</guid><description>Sometimes the best solution is the one that works well enough and can be cranked out quickly.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2023/12/11/modulo/media/cover_hu_af67fc91dc3cd432.jpg" width="1200" height="630" alt="The simplicity of the modulo operator: how I scaled an inefficient solution on a legacy system" /><p>Your service cannot process events fast enough during peak hours.</p>
<p>There is no obvious quick and dirty fix.</p>
<p>Refactoring would take ages.</p>
<p>People have been unhappy for a while now.</p>
<p>What the hell do you do?</p>
<h2 id="background">
  <a class="heading-anchor" href="#background">Background<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I had the pleasure of working with a legacy backend system recently. It had plenty
of ongoing problems, but one of those was more acute compared to others: the service could
not process certain very important events fast enough during peak hours and that was problematic
for everyone that relied on those events.</p>
<p>The processing of events was very basic:</p>
<ul>
<li>load 1000 entities</li>
<li>for each entity, do the following sequentially
<ul>
<li>do some processing</li>
<li>send a JMS message via ActiveMQ</li>
<li>wait for confirmation</li>
</ul>
</li>
</ul>
<p>The processing was triggered by the scheduled jobs system that the service had
been relying on since its inception more than a decade ago. That system only allowed to run one instance
of a scheduled job at any time.</p>
<p>This processing worked well for almost a decade at this point, but started causing more
and more issues as the service was the backbone of a rapidly-growing business.
At least it was a nice problem to have.</p>
<p>I was the lucky guy who got assigned to solving this issue, and in hindsight
I&rsquo;m really glad I got it because tackling this one was fun.</p>
<p>The slowness of the process was down to the part where events had to be sent
via ActiveMQ. It&rsquo;s possible to send events without waiting for any confirmation,
but our service was going with the slower approach of waiting for a confirmation
that the message was successfully sent.
After consulting with my colleagues, going through the depths of <code>git blame</code>
and doing some Jira archeology I soon
learned that this slowness was there for a reason: some events could fail to
be processed if the service suddenly died, which wasn&rsquo;t that rare of an
occurrence. Sure, the processing was quick, but you had the risk of losing events,
and that was even worse than sending events with a big delay.</p>
<p>When looking at the behaviour of this code path I learned that sending each event
took about 20 milliseconds. That is not a lot of time for a single message, but
if you have queued up 1000 events, then that results in 20+ seconds required
to send all of those messages. Take into account the fact that you are doing
this processing sequentially and the fact that during peak hours you had to process
thousands of events within one minute, and you can see where this becomes
problematic.</p>
<p>To give some additional context: we&rsquo;re dealing with a service that has at least
2 or more instances running at any time in a Kubernetes cluster. We had other high priority
work ongoing as well and refactoring this part of the system was not feasible in
a short time window. Spending weeks or even months on this issue was out of the
question.</p>
<h2 id="the-solution">
  <a class="heading-anchor" href="#the-solution">The solution<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>The first idea I tried was pretty basic: try to see if we could process each event in parallel
using Java parallel streams. Hibernate and ActiveMQ put brakes on that
idea pretty quick due to objects related to them not being thread-safe.</p>
<p>The second idea I tried was to find a way to bulk-send events since I suspected
that the ActiveMQ connection setup and teardown was being done separately for
each processed entity. That, however, was not the case.</p>
<p>The third idea was the one I went with.
From my early programming days I learned about <a href="https://en.wikipedia.org/wiki/Modulo">the modulo operation</a>.
I also had vague knowledge of Kafka and its way to split work via partitions.
Didn&rsquo;t take my mind long to connect the dots, and a <em><strong>TechTipsy certified quickfix™</strong></em>
was born.</p>
<p>I took the existing job, created
multiple instances of it and gave each instance a number from 0 to 4, let&rsquo;s call
it a &ldquo;worker ID&rdquo;. Those modified scheduled jobs ran the same database query, but
with a small adjustment: in addition to other criteria, each scheduled job
only picked entities where the <em>modulo 5</em> result of its numerical identifier
matched the worker ID.</p>
<p>This means that the service could process 5 times more entries at once without
having to commit to a big and risky rewrite.</p>









<figure class="center">
  <a href="/posts/2023/12/11/modulo/media/image0.jpg">
    <img src="/posts/2023/12/11/modulo/media/image0_hu_67b9e85c37868e94.webp"
     width="1280"
     height="720"
     loading="lazy"
     decoding="async"
     alt="I imagine this is what the &#34;just one more lane, bro!&#34; crowd thinks happens in 
the real world.">

  </a>
  <figcaption class="center">I imagine this is what the &#34;just one more lane, bro!&#34; crowd thinks happens in 
the real world.</figcaption>
</figure>

<p>The PostgreSQL modulo operator helped facilitate this process and was
the key to avoiding loading all of the entities into memory and filtering
that list down using the modulo operator in application code.</p>
<p>All-in-all, this took a few days of work to implement, roll out in staging
and production, observing and refactoring to make the solution maintainable. For a service
that&rsquo;s part of the critical path in the tech stack and notorious for its
slow deployment cycle, it was pretty fast.</p>
<h2 id="example">
  <a class="heading-anchor" href="#example">Example<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>Here&rsquo;s an example to help illustrate the process.
The database holds 7 entities with the following ID-s: 1001, 1002, 1003, 1004,
1005, 1006, 1007.</p>
<p>Worker 0 starts up and selects all entities where the result of <code>ID mod 5 = 0</code>.</p>
<ul>
<li>1001 mod 5 = 1</li>
<li>1002 mod 5 = 2</li>
<li>1003 mod 5 = 3</li>
<li>1004 mod 5 = 4</li>
<li><strong>1005 mod 5 = 0</strong> &lt;&ndash; this one</li>
<li>1006 mod 5 = 1</li>
<li>1007 mod 5 = 2</li>
</ul>
<p>Worker 0 selects entity 1005.</p>
<p>Worker 1 selects entities 1001, 1006.</p>
<p>Worker 2 selects entities 1002, 1007.</p>
<p>Worker 3 selects entity 1003.</p>
<p>Worker 4 selects entity 1004.</p>
<p>All the workers operate in parallel.</p>
<h2 id="caveats">
  <a class="heading-anchor" href="#caveats">Caveats<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>This solution isn&rsquo;t perfect and has some caveats related to certain
implementation details.</p>
<p>Adding additional workers is relatively simple, but requires lots of small but
well-documented steps. This will only be a problem if the system sees additional
growth that it cannot handle, but that should be years from now. I like to think
of it as job security for future generations.</p>
<p>Due to the way the scheduled jobs solution is built, it is possible for
one instance of the service to run more than one worker at once, which could
be a problem for compute-heavy or memory-hungry processing work. However, for this
use case it&rsquo;s not a problem.</p>
<p>All-in-all, I&rsquo;m happy with the solution, the team was happy after I made the
<em><strong>TechTipsy certified quickfix™</strong></em> more maintainable, and everyone relying on this solution
to work properly were also happy.</p>
]]></content:encoded></item><item><title>Anything's a portable speaker if you're brave enough</title><link>https://ounapuu.ee/posts/2023/01/02/anythings-a-portable-speaker/</link><pubDate>Mon, 02 Jan 2023 07:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2023/01/02/anythings-a-portable-speaker/</guid><description>All about that time I converted a JBL soundbar into a portable speaker</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/media/cover_hu_4fe4cf2661554252.jpg" width="1200" height="630" alt="Anything's a portable speaker if you're brave enough" /><p>I hate buying things that are single-purpose, which is why I ended up with this
setup.</p>









<figure class="center">
  <a href="/posts/2023/01/02/anythings-a-portable-speaker/media/0-image.jpg" aria-label="View full-size image">
    <img src="/posts/2023/01/02/anythings-a-portable-speaker/media/0-image_hu_7dceac05ae91cecc.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="">

  </a>
  
</figure>










<figure class="center">
  <a href="/posts/2023/01/02/anythings-a-portable-speaker/media/1-image.jpg" aria-label="View full-size image">
    <img src="/posts/2023/01/02/anythings-a-portable-speaker/media/1-image_hu_e4b5dd1bab88357b.webp"
     width="600"
     height="800"
     loading="lazy"
     decoding="async"
     alt="">

  </a>
  
</figure>

<p>Take a speaker, a battery, put them together, and what you now have is a
portable speaker. Since I had access to both, I felt no need to buy a separate
portable speaker for use in social events.</p>
<p>The fact that this JBL soundbar supports Bluetooth was what sealed the deal.
Just make sure to keep the setup a fair distance away from any bigger bonfires,
liquids and bugs, and you should be good to go.</p>
]]></content:encoded></item><item><title>Strangling your service with a Kubernetes misconfiguration</title><link>https://ounapuu.ee/posts/2022/04/18/strangling-your-service-with-kubernetes/</link><pubDate>Mon, 18 Apr 2022 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2022/04/18/strangling-your-service-with-kubernetes/</guid><description>Short story about a fun and simple way to shoot yourself in the foot, Kubernetes style.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/media/cover_hu_4fe4cf2661554252.jpg" width="1200" height="630" alt="Strangling your service with a Kubernetes misconfiguration" /><p>This is a quick story about a fun discovery that I made a while ago.</p>
<p><em>For legal reasons, all of this is made up and no such service ever existed.</em></p>









<figure class="center">
  <a href="/posts/2022/04/18/strangling-your-service-with-kubernetes/media/thisisfine.jpg">
    <img src="/posts/2022/04/18/strangling-your-service-with-kubernetes/media/thisisfine_hu_6b59c5482301b18f.webp"
     width="1280"
     height="606"
     loading="lazy"
     decoding="async"
     alt="Accurate representation of the state of affairs in the project, circa 2019.">

  </a>
  <figcaption class="center">Accurate representation of the state of affairs in the project, circa 2019.</figcaption>
</figure>

<p>Once upon a time, we had this Java service that handled all the backend
work that you&rsquo;d expect to occur for a product with a web interface. The
service wasn&rsquo;t the newest thing on the block and had seen dozens of developers
work on it over many years.</p>
<p>At one point, it was moved to a Kubernetes cluster. That meant configuring all
the bits and pieces. In YAML, of course.</p>
<p>A couple of years ago I joined the project and started work on it. Things were
constantly on fire, so the fact that the service took anywhere from 5 to 15
minutes to deploy to the staging environment wasn&rsquo;t something I focused on.
After pushing a commit, the team either took a coffee break, went to play some
table tennis or started work on something else while the pipeline did its job.</p>
<p>It didn&rsquo;t help that running the service locally was hand-waved away as something
that was too impractical to do by people who had been in the project longer than
me, so the team simply relied on local tests and checking the staging
environment after they pushed a change.</p>
<p>A year or so later, I had enough. The project wasn&rsquo;t also constantly on fire,
only occasionally, so I decided to take the time to dig into our CI pipeline
configuration to see what is causing the pipeline to be so slow. My
investigation lead to Kubernetes configuration, specifically the part
where resource limits were configured. For whatever reason, I found that the
service was allowed an absurdly low CPU allocation, measured in millicores.</p>
<p>I increased the limit to something sensible, such as 1 full CPU core. The result?
The startup time of each pod took 30-40 seconds now, resulting in deployments that
took 2 to 3 minutes max. This is an insane improvement over the old deployments
that took up to 15 minutes regularly.</p>









<figure class="center">
  <a href="/posts/2022/04/18/strangling-your-service-with-kubernetes/media/artistsrendition.jpg">
    <img src="/posts/2022/04/18/strangling-your-service-with-kubernetes/media/artistsrendition_hu_a7c3f2044b7deb79.webp"
     width="1054"
     height="510"
     loading="lazy"
     decoding="async"
     alt="Artists rendition of the CPU usage patterns that the service exhibited, before and after the fix.">

  </a>
  <figcaption class="center">Artists rendition of the CPU usage patterns that the service exhibited, before and after the fix.</figcaption>
</figure>

<p>To go even faster. I tweaked the <code>maxSurge</code> property of the rolling update deployment
strategy to start up more new pods in parallel, further shortening the time that
it took to deploy the service. The only thing I had to keep in mind was the
number of database connections afforded to each pod and the maximum number of
connections offered by the database. Start up too many pods in parallel, and
you&rsquo;ll find that your deployment fails due to the service exhausting the
available database connections.</p>
<p>Some time later I learned the reason behind such a CPU resource limit configuration.
Apparently it&rsquo;s a good practice to set your resource limits based on the average
load that your service exhibits after it has properly started up. It does make
sense, especially if you don&rsquo;t want to have your Kubernetes nodes sit idle due to
inefficient resource usage.</p>
<p>This example case shows that it&rsquo;s a trade-off that you&rsquo;ll have to take into
consideration, especially if your service starts up quite slowly and not in
mere seconds.</p>
<p>At the time of writing this post, the tweaked configuration is still up and
running in production.</p>
]]></content:encoded></item><item><title>Tech tip: eliminate HDD humming noise</title><link>https://ounapuu.ee/posts/2021/04/02/tech-tip-1/</link><pubDate>Fri, 02 Apr 2021 07:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2021/04/02/tech-tip-1/</guid><description>Silence your hard drives with this one weird trick! System administrators hate him!</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/media/cover_hu_4fe4cf2661554252.jpg" width="1200" height="630" alt="Tech tip: eliminate HDD humming noise" /><p>Anyone that has bought themselves external WD drives from the Elements/My Book/Easystore series are probably familiar
with the acoustic characteristics of the drives. The drives have a loud hum caused
by <a href="https://arstechnica.com/gadgets/2020/09/western-digital-is-trying-to-redefine-the-word-rpm/">WD running the drives at 7200rpm while claiming the drives to be &ldquo;5400rpm-class&rdquo;</a>
and the clacking of the read-write heads is audible as well. In a small space, such as an apartment, the hum is
maddening, especially when you have more than one drive running at the same time.</p>
<p>After running such a setup for months, enough was enough. I bought some sound dampening foam and used that to try to
limit the noise that my setup was making. However, that didn&rsquo;t do much and the before/after noise comparisons didn&rsquo;t
have much of a difference. As a result of this testing, I did have various pieces of acoustic foam left, and after I
noticed that my Lenovo M73 Tiny PC had a similar shape to the WD My Book 12 TB hard drives that I got recently, I had an
idea.</p>









<figure class="center">
  <a href="/posts/2021/04/02/tech-tip-1/media/image.jpg">
    <img src="/posts/2021/04/02/tech-tip-1/media/image_hu_4f0d8b9faae28e72.webp"
     width="600"
     height="800"
     loading="lazy"
     decoding="async"
     alt="Minimum viable server, now with 90% less noise!">

  </a>
  <figcaption class="center">Minimum viable server, now with 90% less noise!</figcaption>
</figure>

<p>I used four pieces of foam and placed them so that I could stack the hard drives on top. This resulted in the humming
noise being completely eliminated. The sound of hard drive read-write heads is still there, but it is so much less
audible now. After this change, I have started hearing the cooling fan more than the hard drives themselves.</p>
<p>This setup could probably be improved by switching around the setup so that the hard drives were at the bottom with the
bottom drive having additional foam or rubber feet below it to support it better. For the time being, I&rsquo;m perfectly
happy with this arrangement, as long as it doesn&rsquo;t fall over.</p>
<h2 id="2021-04-23-update">
  <a class="heading-anchor" href="#2021-04-23-update">2021-04-23 update<svg class="heading-anchor__icon" viewBox="0 0 24 24" width="0.75em" height="0.75em" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg></a>
</h2>
<p>I have now added a fan to the setup!</p>









<figure class="center">
  <a href="/posts/2021/04/02/tech-tip-1/media/image2.jpg">
    <img src="/posts/2021/04/02/tech-tip-1/media/image2_hu_a93d534011079b3e.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="Pretty cool, eh?">

  </a>
  <figcaption class="center">Pretty cool, eh?</figcaption>
</figure>

<p>The fan itself is powered by one of the USB ports with the help of a spare sacrificial USB cable and a Noctua omni-join
kit that I had left over.</p>
<p>The drive temperatures are now reading around 42C and 46C.</p>
]]></content:encoded></item></channel></rss>