<?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/tags/containers/</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>Wed, 06 May 2026 06:00:00 +0300</lastBuildDate><atom:link href="https://ounapuu.ee/tags/containers/index.xml" rel="self" type="application/rss+xml"/><item><title>How I self-host this blog at home with a dynamic IPv4 address, IPv6 prefix, and a dash of Wireguard</title><link>https://ounapuu.ee/posts/2026/05/06/self-host-at-home/</link><pubDate>Wed, 06 May 2026 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2026/05/06/self-host-at-home/</guid><description>I think I've finally figured it out. For now.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2026/05/06/self-host-at-home/media/cover_hu_a0e9c758122adadf.jpg" width="1200" height="630" alt="How I self-host this blog at home with a dynamic IPv4 address, IPv6 prefix, and a dash of Wireguard" /><p>Networking has long been my Achilles heel. I know the very basics, but the more complex areas of networking have been a
bit puzzling to me. By the time I figured out how IPv4 works, I found IPv6 and that my ISP supports it.</p>
<p>Back to square one.</p>
<p>That didn&rsquo;t stop me from learning some bits, and after 8+ years of self-hosting as a hobby, I&rsquo;ve settled on a setup that
works for me and overcomes common residential internet connection nuances, such as dynamic IPv4 addresses and changing
IPv6 prefixes. I&rsquo;m sharing these tips and tricks with the goal of helping out other hobbyists out there that happen to
share a similar stack.</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>My ISP is polite enough to provide a public IPv4 address, and allowing incoming traffic is a toggle in their online
self-service. Not perfect, but at least you can do it. However, they charge about 6 EUR a month for the static IP
address service, which I am not willing to pay out of principle.</p>
<p>They also support IPv6, which is great, and they provide you a whole <code>/56</code> slice of it to play with using IPv6 prefix
delegation. Unfortunately they have configured the lease time for the prefix to be incredibly short: 26 <strong>minutes</strong>!</p>
<p>A router reboot or short power outage usually results in the IPv4 address and IPv6 prefix changing, which is really
annoying as my services become unavailable for a short time.</p>
<h2 id="dynamic-dns">
  <a class="heading-anchor" href="#dynamic-dns">Dynamic DNS<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>A common way to overcome the dynamic IP address limitation is to sign up with a provider to set up a DNS record that
changes whenever your home IP address changes. My domain registrar does not have this as a feature, and I&rsquo;m not
interested in using a different provider, so I went in a different direction and built a home-grown script that does the
same thing.</p>
<p>Initially, this script relied on a public service that tells you what your IP address is, and based on that I could
check if things have changed and I need to update my DNS record. This approach has one glaring catastrophic failure mode
though: that provider could lie to you one day and now you&rsquo;ve pointed your DNS records at the attackers&rsquo; servers. :)</p>
<p>I ignored that failure mode for a while, but once I learned about the effectiveness of LLM-based tooling, I decided to
give it a go and to build a better solution that takes into account my setup and requirements, while at the same time
saving me from the frustration of troubleshooting and debugging this in a late evening. I&rsquo;m still very limited on
available free time, so optimizing for that is a priority for me.</p>
<p>My main networking gear runs OpenWRT, and it supports running shell scripts periodically in a crontab. The router has
two WAN interfaces, one for IPv4 and one for IPv6. It already knows what IP address and prefix have been assigned to it,
so I don&rsquo;t have to rely on an external service provider for finding this out.</p>
<p>Handling IPv4 addresses is simple: check the IPv4 address of the WAN interface. Query your existing DNS records, diff
it, and if it has changed, push an update in a separate API call. Super simple!</p>
<p>With IPv6, the approach is slightly different. Instead of the WAN interface, I have to get the IPv6 address of the
target machine, and make sure that it&rsquo;s routable over the public internet. When you&rsquo;ve checked your network settings in
an IPv6 network, you may have noticed a lot of different IP addresses there, with lots of letters thrown into the mix.</p>
<p>Here&rsquo;s an example from the machine that is serving you this blog (likely out of date though!):</p>
<pre tabindex="0"><code>    inet6 fdb3:6dad:6dce::f41/128 scope global dynamic noprefixroute 
    inet6 fdb3:6dad:6dce:0:2e0:4cff:fe0c:9ddb/64 scope global noprefixroute 
    inet6 2001:7d0:856c:4000::f41/128 scope global dynamic noprefixroute 
    inet6 2001:7d0:856c:4000:2e0:4cff:fe0c:9ddb/64 scope global dynamic noprefixroute 
    inet6 fe80::2e0:4cff:fe0c:9ddb/64 scope link noprefixroute 
</code></pre><p>The two relevant ones are the ones that start with <code>2001:</code>, others are link-local or accessible over the local network
only. The shorter one consists of the IPv6 prefix part, and then the unique bit at the end is a predictable suffix that
the host gets. The other one also works, but is as far as I understand randomly generated and more difficult to predict
when we get around to next sections.</p>
<p>I know that there is probably a better way to do this, but I wanted to keep things simple enough so that I can
troubleshoot them if needed. It may be possible to trigger this updater script on events that WAN and WAN6 interfaces
send, but I have not validated this theory.</p>
<p>There are many different ways to find the IPv6 address of a particular host, so the script I have just tries multiple
approaches to find the one that we&rsquo;re looking for.</p>
<p><a href="/posts/2026/05/06/self-host-at-home/media/ddns.sh">Here&rsquo;s the script</a> in case you&rsquo;re interested in setting up something similar. It reads credentials from
an .env file and is built around <a href="https://api.zone.eu/">the Zone API</a>. On OpenWRT, the only dependency that you need to
install is <code>curl</code>, which to my surprise was not part of the default packages list, probably to save on space.</p>
<p>One lesson I learned from a previous iteration of the script: if you trigger DNS record updates every minute, then Zone
will actually reach out to you via e-mail telling you to cut that shit out, politely. It was just one missing <code>if</code>
statement, and yet it caused some frustration to engineers far away. Sorry!</p>
<h2 id="predictable-ip-addresses">
  <a class="heading-anchor" href="#predictable-ip-addresses">Predictable IP addresses<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>It&rsquo;s a good idea to set up static IP addresses for your hosts, both for IPv4 addresses and IPv6 prefix delegation via
DUID-s.</p>
<p>The OpenWRT GUI LuCI makes it quite simple, just set the addresses as static on the landing page for the hosts
that you are interested in forwarding traffic to, and you&rsquo;re done!</p>
<p>My recommendation here is to also set a predictable IPv6 suffix, otherwise all your IPv6 traffic rules may break once
again due to this nuance.</p>
<p>I like to make that host number the same for both IPv4 and IPv6, quick example:</p>
<ul>
<li><code>192.168.1.2</code></li>
<li><code>2001:7d0:854f:8e00::2</code></li>
</ul>
<p>Here&rsquo;s a configuration snippet example from <code>/etc/config/dhcp</code>, look for the <code>hostid</code> option:</p>
<pre tabindex="0"><code>config host
        option name &#39;mycoolserver&#39;
        option ip &#39;192.168.1.69&#39;
        list mac &#39;12:34:56:78:90:AB&#39;
        option duid &#39;yourduidgoeshere&#39;
        option hostid &#39;69&#39;
</code></pre><p>Apply with <code>service dnsmasq restart</code>.</p>
<p>In LuCI, as of OpenWRT 25.12, look for &ldquo;IPv6 token&rdquo;.</p>









<figure class="center">
  <a href="/posts/2026/05/06/self-host-at-home/media/ipv6token.png">
    <img src="/posts/2026/05/06/self-host-at-home/media/ipv6token_hu_556676a81dfcb972.webp"
     width="1000"
     height="103"
     loading="lazy"
     decoding="async"
     alt="Set an IPv6 token for a predictable IPv6 suffix.">

  </a>
  <figcaption class="center">Set an IPv6 token for a predictable IPv6 suffix.</figcaption>
</figure>

<p>Note that due to a bug, it doesn&rsquo;t seem to be possible to set a numeric IPv6 token via GUI, which is why you will need
to add it manually in CLI using the above approach.</p>
<h2 id="port-forwards-traffic-rules-potato-potahtoh">
  <a class="heading-anchor" href="#port-forwards-traffic-rules-potato-potahtoh">Port forwards, traffic rules, potato, potahtoh<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>Whenever you want to make a local machine accessible on the internet for IPv4, the solution is simple: set up a port
forward to that particular machine, and you&rsquo;re done! It&rsquo;s a common enough flow for people who&rsquo;ve set up game servers and
the like, and well understood by more novice users.</p>
<p>With IPv6, port forwards don&rsquo;t help. You&rsquo;ll have to check one tab over at &ldquo;Traffic rules&rdquo; in OpenWRT GUI.</p>
<p>It&rsquo;s a common misconception that by using IPv6 you are exposing everything to the world as each device gets its own IPv6
address, but turns out that this is not the case in most common setups. By default, OpenWRT forwards only a few types of
traffic to IPv6 hosts, such as ICMP packets that make <code>ping</code> work between devices over IPv6 across the public internet.
If you are interested in allowing IPv6 clients to access services on your local server that has an IPv6 address, you
will have to explicitly allow it by adding a new traffic rule.</p>
<p>There&rsquo;s one issue with this approach that a lot of users seem to run into: if the IPv6 prefix changes, then all my
traffic rules that are pointing to a particular host are automatically broken!</p>
<p>Luckily there is a clever workaround implemented on OpenWRT that bypasses this issue. Assuming that you followed the
previous step and set yourself up with a predictable IPv6 suffix, when setting up a traffic rule, set the target device
up as <code>::69/-64</code>, just replace <code>69</code> with your actual suffix. The IPv6 prefix can now change, but the ports that you&rsquo;ve
made accessible on this specific host will remain working.</p>
<p>At this point, you should be all set with a reasonably well working setup where you&rsquo;ve handled the issues with dynamic
IPv4 and IPv6 prefix, and you can access your services over the public internet even when things happen.</p>
<h2 id="limitations">
  <a class="heading-anchor" href="#limitations">Limitations<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>One issue that this setup has is the fact that DNS change propagation takes time. Usually clients will pick up the new
records within 5 minutes, but in my professional career I&rsquo;ve seen some clients take up to 24 hours or longer to finally
start sending traffic to the new DNS record. Whenever your IP address changes, there will be a mini-outage. Not
catastrophic if you&rsquo;re just hosting hobby projects and personal services at home, but I wouldn&rsquo;t host anything
mission-critical in such a setup.</p>
<p>When your OpenWRT device is as underpowered as mine, then you may notice that the TLS encryption overhead when <code>curl</code>
-ing around can be significant. I have set my dynamic DNS script to run every 5 minutes, and it shows up on the CPU
usage graphs on my router.</p>









<figure class="center">
  <a href="/posts/2026/05/06/self-host-at-home/media/graph.png">
    <img src="/posts/2026/05/06/self-host-at-home/media/graph_hu_e39ff7b6e73083d4.webp"
     width="956"
     height="383"
     loading="lazy"
     decoding="async"
     alt="CPU usage graph on my router showing the scheduled dynamic DNS script doing work.">

  </a>
  <figcaption class="center">CPU usage graph on my router showing the scheduled dynamic DNS script doing work.</figcaption>
</figure>

<h2 id="wireguard-all-the-things">
  <a class="heading-anchor" href="#wireguard-all-the-things">Wireguard all the things!<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 know that Tailscale is a popular method of connecting up your devices and making your personal services privately
accessible over that, which significantly reduces your attack surface. Being behind a few updates or not being vigilant
enough is less of an issue compared to exposing your services over the public internet.</p>
<p>You don&rsquo;t necessarily need Tailscale for that though! If you just need a way to access your services over a private and
secure network, then setting up a dedicated mini PC or single-board computer is a very good starting point. Let it be
the server, allow traffic to move between the clients over the Wireguard interface, and you&rsquo;re all set!</p>
<p>Alternatively, if you have an OpenWRT router, then you can do it right there, but I found the GUI management setup to be
a bit clunky compared to deploying the plain configuration files to clients. When I <em>did</em> do that test, I discovered
quickly that my router and its single ARM CPU core with no cryptography extensions is too slow for managing my Wireguard
network, with speeds topping out at 20 Mbit/s. <strong>20.</strong> The LattePanda IOTA can easily saturate its gigabit link, as does
the ThinkPad T430, and even devices like the Orange Pi Zero can handle a theoretical maximum of about 240 Mbit/s over
Wireguard measured using <a href="https://github.com/cyyself/wg-bench">wg-bench</a>.</p>
<p>My current Wireguard host is <a href="/posts/2026/04/04/lattepanda/">the LattePanda V1, the most unstable computer in my fleet.</a>
With a USB adapter, it can push almost half a gigabit of traffic over Wireguard.</p>
<p>If you&rsquo;re like me, and you like hosting your services over Docker or Podman, then instead of listening on ports for all
interfaces on your containers (default behaviour when setting up port forwards), I recommend listening only on the
Wireguard interface. This makes the service only accessible over Wireguard, meaning that you only need to set up one
port forward and traffic rule to connect to the Wireguard network, and then you have access to all of your services. The
attack surface is significantly reduced, the whole Wireguard solution is stable and very small, and unless you leak your
private key, you are reasonably secure!</p>
<p>Here&rsquo;s a snippet from a compose file showcasing how to set this up for IPv4 and IPv6:</p>
<pre tabindex="0"><code>ports:
  - 10.69.69.12:2283:2283
  - &#34;[fded:abba:acca::12]:2283:2283&#34;
</code></pre><p>Want to make the service available over Wireguard <em>and</em> over the local network directly? Just add those to the list!
Note that if your local address changes and you don&rsquo;t update it in the compose file, your container will refuse to start
up as it cannot listen to the interface any longer, but you can mitigate that with the static IP addresses step.</p>
<p>When you are going with this route, it is unlikely but still possible that by the time the container starts up, the
Wireguard interface is not yet up. To resolve this, you can use <code>systemd</code> to set Wireguard up as a dependency that you
will have to wait for before the container starts up.</p>
<p>I manage my Wireguard connection with <code>wg-quick@interfacename</code> service. You can set up a <code>systemd</code> override for Docker,
or if you manage your Docker/Podman services via systemd, then you can set it up per-service using this pattern:</p>
<pre tabindex="0"><code># /etc/systemd/system/myimmichserver.service.d/override.conf
[Unit]
Requires=wg-quick@interfacename.service
After=wg-quick@interfacename.service
</code></pre><p>By the way, <code>systemd</code> overrides are also really useful for ensuring that your storage that your containers rely on is
properly mounted. If my service requires the path <code>/immich</code> to be available and mounted, add something like this:</p>
<pre tabindex="0"><code>BindsTo=immich.mount
After=immich.mount
</code></pre><p>If you unmount the mount point, it will also properly bring down the service. The service won&rsquo;t start if the mount point
is missing. I&rsquo;ve had the issue with containers seeing blank mount points more times than I&rsquo;d like to admit, and this has
eliminated this issue for me.</p>
<p><code>systemd</code> has received a lot of hate online, and I don&rsquo;t think it&rsquo;s fair. The ease with which you can set up
dependencies on your system, set up resource limits, make services more restricted to improve the security posture is
great and allows me to and avoid all sorts of failure modes. Production services that I&rsquo;m responsible for make use of
these <code>systemd</code> features, with great results.</p>
<p>For services that need to be public, such as Nextcloud and its public shareable links, this approach won&rsquo;t work,
obviously, but for things that only you and your family members use, this is a viable approach.</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>This setup works well enough for me to confidently host my blog and self-hosted services off of it. I&rsquo;ve hit a lot of
paper cuts and frustrations along the way, but after following this guide, you don&rsquo;t have to do the same.</p>
<p>Yes, VLAN-s are intentionally missing from this guide. I&rsquo;ll get to them eventually, maybe by Q4 2037 given my lack of
free time.</p>
<p>And no, IPv6 isn&rsquo;t complicated, it&rsquo;s just different from what everyone is used to. If we started out with IPv6 right
from the get-go, <a href="https://www.ietf.org/archive/id/draft-thain-ipv8-00.html">we wouldn&rsquo;t be having dumb arguments online.</a></p>
]]></content:encoded></item><item><title>I built the worst Jellyfin media server</title><link>https://ounapuu.ee/posts/2026/01/16/worst-media-server/</link><pubDate>Fri, 16 Jan 2026 06:00:00 +0200</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2026/01/16/worst-media-server/</guid><description>This experiment is brought to you by absurd memory prices.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2026/01/16/worst-media-server/media/cover_hu_dd22e2bc1424d67c.jpg" width="1200" height="630" alt="I built the worst Jellyfin media server" /><p>LattePanda V1. <a href="/posts/2023/02/28/lattepanda-v1/">It&rsquo;s been a while, huh?</a></p>
<p>I had it as a backup server <a href="/posts/2023/06/10/how-i-blew-up-my-backup-server/">(which I blew up)</a>.</p>
<p>Then it got promoted to&hellip; <a href="/posts/2024/12/11/wireguard-backup-fleet/">a backup server.</a> But
then <a href="/posts/2024/12/11/wireguard-backup-fleet/media/lattepanda-psu-failure.png">its PSU blew up.</a></p>
<p>Then it was waiting for some cool ideas at <a href="https://kaurpalang.com/">a potato enthusiast.</a></p>
<p>Now it&rsquo;s back in my hands.</p>
<p><a href="/posts/2025/11/18/lattepanda-iota/">Unlike its modern counterpart,</a> the LattePanda V1 is a flawed single board
computer.</p>
<p>It&rsquo;s slow.</p>
<p>It&rsquo;s unstable if you connect power hungry USB devices to it.</p>
<p>It needs some cooling, but I&rsquo;m too cheap to buy a properly designed heat sink for it.</p>
<p>It has display quirks.</p>
<p>It only has a slow 100 Mbps Ethernet link.</p>
<p>It only seems to work reliably on one side. Not even joking, the &ldquo;heat sink down&rdquo; configuration is the only one that
works for me.</p>
<p><a href="https://libreelec.tv/">LibreELEC</a> failed to play back any videos on it properly. The videos would play for 10 seconds,
and then it would hang, no matter the encoding or hardware acceleration settings.</p>
<p>With hardware prices being wonky again, I decided to give this board a last chance of being useful. If new hardware is
absurdly expensive, then it makes perfect sense to use what you have, no matter how slow or crummy it might be. Reduce,
reuse, and only <em>then</em> recycle.</p>
<p>That&rsquo;s when I decided to run a Jellyfin server off of it. Transcoding is out of the question, but serving media files
over the network should still be quick enough, right?</p>
<p>For this experiment, I
used <a href="/posts/2025/10/06/datablocks-white-label-drives/">one of the 18 TB hard drives that I&rsquo;ve covered earlier.</a>
Just the single one, no redundancy here. This one drive is probably about 10x the cost of the LattePanda V1 itself,
making it a perfectly reasonable choice.</p>
<p>For ease of troubleshooting, debugging and guaranteed eventual reinstallation, I put Fedora Server on an 128 GB USB 3.0
flash drive by Samsung. <a href="/posts/2024/12/02/linux-on-usb/">Risky move, I know,</a> but as you might have noticed, this whole
build is everything <em>but</em> reasonable. That left the eMMC storage as the perfect candidate for storing cache and service
related files.</p>
<p>Cooling is solved by a random server heat sink slapped on the bottom of the LattePanda, with a few critical
components like the CPU making contact with thick and soft thermal pads. The whole thing is fastened using velcro strips
that I cut to a thin size in the middle so that I can route it between the components on the PCB and within the heat
sink fins. The edges of the heat sink are covered with some painters&rsquo; tape that I had around to avoid shorting anything
out on the board, because those parts on the board contain all sorts of metal bits and pins that have power going
through them. I&rsquo;m actually quite happy with that mount!</p>









<figure class="center">
  <a href="/posts/2026/01/16/worst-media-server/media/lattepanda-temps.png">
    <img src="/posts/2026/01/16/worst-media-server/media/lattepanda-temps_hu_84c5210842d31066.webp"
     width="1000"
     height="683"
     loading="lazy"
     decoding="async"
     alt="All things considered, the janky cooling setup is holding up very well. The 100 degree peaks are sensor/measurement
errors.">

  </a>
  <figcaption class="center">All things considered, the janky cooling setup is holding up very well. The 100 degree peaks are sensor/measurement
errors.</figcaption>
</figure>

<p>I&rsquo;ve been experimenting with Podman again after Docker kept doing weird things with the v29 release, and I&rsquo;ve been happy
with the results, so that&rsquo;s what I used on the LattePanda V1 as well. I slapped Jellyfin on it, threw in some test files
and gave it a go.</p>
<p>Navigating the UI feels a bit slow at times, but it&rsquo;s not really noticeable on an LG smart TV with a really laggy user
interface.</p>
<p>Actual video playback that requires no transcoding works quite well, at least for smaller media sizes.</p>
<p>Technically, you can do transcoding on this and even utilize the tiny little integrated GPU in it, but the results are
not usable. With smaller files it might be usable, but in one test I saw 7-8 FPS transcoding speeds and the server
struggling to keep up, with CPU usage locked at 100%.</p>
<p>If we ignore all the downsides, then the LattePanda V1 can actually be a usable media server. Serving files off of a big
drive does not require a lot of resources, and for that the LattePanda V1 is a solid choice. It also uses only a few
watts of power on its own, so you can keep it on 24/7 guilt-free. In this build, the hard drive itself is actually the
most power hungry component by a long shot (about 2/3 of the total power budget).</p>
<p>The 2 GB of memory is <em>juuuuuuuust</em> enough for this setup.</p>









<figure class="center">
  <a href="/posts/2026/01/16/worst-media-server/media/lattepanda-memory-usage.png">
    <img src="/posts/2026/01/16/worst-media-server/media/lattepanda-memory-usage_hu_ad0b4de22af0ece1.webp"
     width="1000"
     height="683"
     loading="lazy"
     decoding="async"
     alt="Totally usable.">

  </a>
  <figcaption class="center">Totally usable.</figcaption>
</figure>

<p>As of writing this post, it has been running for about a week, and it&rsquo;s been fine. I intend to keep it running in a
drawer for as long as possible just to see what will die first.</p>
<p>Will it be the eMMC storage?</p>
<p>The USB flash drive holding the operating system?</p>
<p>The expensive hard drive?</p>
<p>The power supply?</p>
<p>The thin velcro strips holding the cooling together?</p>
<p>My patience?</p>
<p>Don&rsquo;t worry, I have backups. <em>Well</em>, backups of the important bits. I&rsquo;m ready to lose
data <a href="/posts/2026/01/14/raid0/">again.</a></p>
<h2 id="2026-01-22-update">
  <a class="heading-anchor" href="#2026-01-22-update">2026-01-22 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>Okay, it was <em>too</em> bad of an idea.</p>
<p>The LattePanda V1 would occasionally just&hellip; stop.</p>









<figure class="center">
  <a href="/posts/2026/01/16/worst-media-server/media/lattepanda-is-kill.png">
    <img src="/posts/2026/01/16/worst-media-server/media/lattepanda-is-kill_hu_26193b745dd0bf59.webp"
     width="1000"
     height="507"
     loading="lazy"
     decoding="async"
     alt="Oops, it died.">

  </a>
  <figcaption class="center">Oops, it died.</figcaption>
</figure>

<p>From what I gathered by accident, it&rsquo;s likely that the USB port containing the operating system would flake out and
result in the system not being able to run any tools that are not in memory. I tried to move the installation to the
eMMC drive, but after failing multiple times due to the display not working, or the system deciding to shut down
randomly, I gave up on it. For now.</p>
<p>Guess I&rsquo;ll have to use it in a solar-powered website project. ¯\_(ツ)_/¯</p>
]]></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>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>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>Steam local network game transfers are a game-changer</title><link>https://ounapuu.ee/posts/2023/09/11/steam-cache/</link><pubDate>Mon, 11 Sep 2023 06:00:00 +0300</pubDate><author>ihavesomethoughtsonyourblog@ounapuu.ee (Herman Õunapuu)</author><guid>https://ounapuu.ee/posts/2023/09/11/steam-cache/</guid><description>Setting up a Steam LAN cache has never been this easy, so I went ahead and did some testing with different configurations.</description><content:encoded><![CDATA[<img src="https://ounapuu.ee/posts/2023/09/11/steam-cache/media/cover_hu_8677bb4c48010087.jpg" width="1200" height="630" alt="Steam local network game transfers are a game-changer" /><p>Steam recently launched a new feature: <a href="https://help.steampowered.com/en/faqs/view/46BD-6BA8-B012-CE43">local network game transfers.</a></p>
<p>The idea is simple: if you have a game downloaded on another PC and you&rsquo;re both on the same local network, then Steam can download game data
from that PC, avoiding the need to download the game over public internet. Using this method you can reduce your internet
usage and enjoy faster download times.</p>
<p>I think this feature is absolutely brilliant. There are many homes out there, even in developed countries, where the
internet connection sucks. The speed might be capped at something very slow, such as 10 Mbit/s down, or you might
have a bandwidth cap in place. Perhaps you have multiple gaming PC-s and you want to avoid downloading 100+ gigabytes
worth of files over the internet to avoid hitting your bandwidth cap. In those situations being able to download your games to a PC that has a bunch of storage in it
makes a lot of sense.</p>
<p>This isn&rsquo;t the only way to achieve the same goal as <a href="https://lancache.net/">projects like LanCache exist.</a> However, with
Steam the setup is so simple that anyone who knows how to install Steam could achieve a similar result. The only caveat is that
you&rsquo;re limited to games installed through Steam, LanCache supports many other services.</p>
<p>Here are some ideas on how you can set up your own Steam cache.</p>
<h2 id="using-an-old-laptop">
  <a class="heading-anchor" href="#using-an-old-laptop">Using an old laptop<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 you have an older laptop around and it has a gigabit Ethernet connection, then that can be a good candidate for
setting up a Steam cache.</p>
<p>The hardware I picked for this test is a ThinkPad T430. It has a 4 core CPU in it, 16 GB of RAM and plenty of ways
to attach storage to it. With storage I tried two approaches: using an older 1TB 2.5&quot; SATA HDD, and using
two Samsung 870 EVO 1TB SATA SSD-s in RAID0.</p>









<figure class="center">
  <a href="/posts/2023/09/11/steam-cache/media/image0.jpg">
    <img src="/posts/2023/09/11/steam-cache/media/image0_hu_ceb50a4db84a6f9c.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="It&#39;s a Steam cache. See, it says so right on the lid!">

  </a>
  <figcaption class="center">It&#39;s a Steam cache. See, it says so right on the lid!</figcaption>
</figure>

<p>The hard drive based solution isn&rsquo;t something I recommend, unless you plan on running two or more in a RAID-like setup.
The Steam cache use case seems to have hit the limits of the hard drive and that limited the transfer speeds
significantly.</p>
<p>The SSD-based setup was great! You might want to run something other than a RAID0 setup if you want to avoid a drive
failure taking your whole Steam library with you, I ran with this setup mainly to see where the limits are. The Steam
library is also something you can easily redownload, should things go really wrong.</p>
<p>One benefit of setting up an older laptop as a Steam cache is that you can take it with you to a place that has great
internet connectivity, download your games there and bring it back home with you. That&rsquo;s exactly what I did when
performing testing, I went to my local hackerspace, started the downloads, and came back half a day later to find
that over a terabyte worth of games had been downloaded. Doing the same on a slower home network would have taken days.
I guess the idea of <a href="https://en.wikipedia.org/wiki/IP_over_Avian_Carriers">IP over avian carriers</a>,
<a href="https://www.jeffgeerling.com/blog/2023/pigeon-still-faster-internet">or over humans</a>,
isn&rsquo;t dead yet.</p>
<p>When downloading the games, I noticed that Steam was hitting the CPU hard. Even on a decent 4-core Intel i7-3820QM the
CPU was often running at 100%. This meant that download speeds were usually around 500-700 Mbit/s, which is still good,
but nowhere near saturating the gigabit link that the laptop had.</p>









<figure class="center">
  <a href="/posts/2023/09/11/steam-cache/media/image1.jpg">
    <img src="/posts/2023/09/11/steam-cache/media/image1_hu_67e371a1c61477b.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="The CPU is really struggling here.">

  </a>
  <figcaption class="center">The CPU is really struggling here.</figcaption>
</figure>

<p>Now that I had all the games downloaded, it was time to test the performance. The maximum transfer speeds were around
400-500 Mbit/s over my local network. I used my Steam Deck over an USB-C dock to start the downloads and picked the
internal NVMe SSD as the download target to avoid any storage speed bottlenecks. I would have liked to see speeds
close to 1 Gbit/s, but these speeds are still a massive improvement if your network speeds are something like
50 Mbit/s down, that&rsquo;s a 10x improvement!</p>









<figure class="center">
  <a href="/posts/2023/09/11/steam-cache/media/image2.jpg">
    <img src="/posts/2023/09/11/steam-cache/media/image2_hu_a89d48392fef01f8.webp"
     width="1067"
     height="800"
     loading="lazy"
     decoding="async"
     alt="Typical transfer speeds observed with this setup.">

  </a>
  <figcaption class="center">Typical transfer speeds observed with this setup.</figcaption>
</figure>

<p>If you&rsquo;re worried about the fact that <a href="https://learn.microsoft.com/en-us/lifecycle/products/windows-10-home-and-pro">Windows 10 is going to lose support in October 2025</a>
and your Steam cache PC is not supported by Windows 11, then don&rsquo;t worry, you can do the same thing over Linux.</p>
<h2 id="using-a-virtual-machine">
  <a class="heading-anchor" href="#using-a-virtual-machine">Using a virtual machine<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 you already have a NAS or a home server running, then you can create a new VM, add a bunch of storage to it,
setup the OS and Steam and you&rsquo;re good to go!</p>
<p>I did most of the testing on a Windows 10 VM and a 1TB HDD attached over USB 3.0. I setup the VM using a bog-standard
QEMU/KVM setup and set everything up through <code>virt-manager</code> in Linux. The only noteworthy part is that I set up the
network interface as a <code>macvtap</code> device, resulting in the VM showing up on my local home network as if it was a separate
PC. That step was taken to avoid any issues with the Steam instances on different PC-s not being able to transfer data
directly between each other.</p>
<p>My server runs on the <a href="/posts/2022/01/17/asrock-x300-future-of-desktops/">ASRock Deskmini X300</a>. It has plenty of CPU
performance, but during testing I ran into performance issues with the hard drive again. When I added SSD-based storage
to the VM, I found that the performance was great and quite similar to what I saw on the laptop.</p>
<p>The downside with the Windows 10 VM setup was the CPU usage. While the VM was idling, my server saw a noticeable
increase in idle CPU usage, utilizing about 10-20% of my CPU at all times. During transfers the VM was using most of
my CPU cores.</p>
<p>I did testing on a Linux VM to see if those are more efficient compared to a Windows 10 VM. The answer is obviously
&ldquo;yes&rdquo;, but there is one issue: I could not get Steam to work. At the time of testing, Steam had recently released a
bigger visual overhaul and I suspect that it might have something to do with those issues. The VM has no GPU
acceleration and opening Steam would result in &ldquo;Loading user data&rdquo; popup showing up, but nothing else happening.
I tried Ubuntu Desktop 22.04, Fedora Linux 38, and even Flatpak installations of Steam. All of them ran into the same
exact issue. There are probably ways to set Steam to not require GPU acceleration without opening its settings view
graphically, but I did not bother with that.</p>
<p>If you like to think in virtual machines, then this is a good option. However, it&rsquo;s not something I stuck with due to
the inefficient resource usage that a Windows 10 VM exhibited.</p>
<h2 id="using-containers">
  <a class="heading-anchor" href="#using-containers">Using containers<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 is the solution that I am currently running.</p>
<p>I recently found the <a href="https://docs.linuxserver.io/images/docker-webtop">LinuxServer.io webtop Docker images.</a> These
containers allow you to run various Linux desktop environments and distros on the same machine. You can connect to them
using the integrated KasmVNC solution over a browser. You can even use your GPU on the container host machine to improve
the video rendering performance in the container!</p>









<figure class="center">
  <a href="/posts/2023/09/11/steam-cache/media/image3.png">
    <img src="/posts/2023/09/11/steam-cache/media/image3_hu_5e25b945f3eec876.webp"
     width="1280"
     height="697"
     loading="lazy"
     decoding="async"
     alt="Fedora XFCE desktop running in a container.">

  </a>
  <figcaption class="center">Fedora XFCE desktop running in a container.</figcaption>
</figure>

<p>I picked the <code>fedora-xfce</code> option because Fedora is great, and the XFCE desktop environment is not that resource
intensive. I also utilized <a href="https://github.com/linuxserver/docker-mods/tree/universal-package-install">the linuxserver.io package install mod</a>
to install Steam on container startup. For networking, I created a separate Docker network using the <code>macvlan</code> driver to
achieve something similar to the VM setup, resulting in the container showing up as a separate machine on the local
network. Inside the container I set up Steam to start on startup using standard GUI tools that XFCE provides for this
purpose.</p>
<p>I really like this setup. I can easily point the Steam downloads to a larger storage pool and make use of all the CPU
power that Steam needs to perform the transfers.</p>
<p>During testing the limitation seems to have been my Steam Deck. I could still not saturate the 1 Gbit/s link.</p>
<p>The KasmVNC solution is quite nice and performant. The fact that I could use it in the browser makes it very convenient
to use as well.</p>
<p>The CPU usage during idle is very small, except for when you accidentally leave the Steam window open with animated
content being displayed. During file transfers the CPU gets a beating, but it&rsquo;s nothing the AMD Ryzen 7 5700G can&rsquo;t handle.</p>
<p>For storage I eventually ended up using a 1TB Lexar NVMe SSD. That&rsquo;s not enough to download all the games I have in my
Steam library, but at least I can download those games that take 20+GB of space and not have to worry about downloading
those over a slower internet connection.</p>
<p>I have not ended up using this setup too much yet, but I imagine that with games growing in size this is going to become
much more relevant for my Steam Deck. And if I were to move to the countryside where the only internet connection
available is a mobile 4G connection capped at 10 Mbit/s down, then this setup would be fantastic to have.</p>
<h2 id="tech-tips">
  <a class="heading-anchor" href="#tech-tips">Tech tips<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 you intend to run a similar setup, I recommend enabling &ldquo;Scheduled updates&rdquo; to run during the night. I set mine up
to auto-update games between 00:00 and 06:00, which is where I&rsquo;m likely going to sleep and not notice that game downloads
are taking place. Steam is notorious for hogging all of the bandwidth on the network and this nifty feature helps
avoid disturbing other users on the network.</p>
<p>If you&rsquo;re performing the initial mass download of your games but would like for it to take place during scheduled times, start downloading
all of the games and then restart your machine. When Steam starts up again, you&rsquo;ll notice that all your games are
queued up nicely to download during your scheduled updates time slot.</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 setup is not ideal and <a href="https://help.steampowered.com/en/faqs/view/46BD-6BA8-B012-CE43">the Steam support page highlights it well.</a>
For example, this setup does not work in fully offline situations as internet connectivity is required for the initial
setup of the local network game transfer. I recommend giving that page a read to understand the requirements and technical specifics of this setup.</p>
]]></content:encoded></item></channel></rss>