<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.1/modules/content/"><channel><title>Aitor Alonso</title><link>https://aalonso.dev/</link><description>I'm a product-minded senior software engineer that turns complex problems into elegant solutions. Experienced in backend, frontend, and mobile development, my expertise is in Elixir, Golang, and Node.js.</description><language>en-us</language><atom:link href="https://aalonso.dev/blog/" rel="self" type="application/rss+xml"/><item><title>How to monitor a remote NUT UPS server with a custom watchdog script</title><link>https://aalonso.dev/blog/2026/how-to-monitor-a-remote-nut-ups-server-with-a-custom-watchdog-script/</link><pubDate>Sat, 25 Apr 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2026/how-to-monitor-a-remote-nut-ups-server-with-a-custom-watchdog-script/</guid><category>linux</category><category>proxmox</category><category>synology</category><category>tutorial</category><description>Learn how to replace nut-monitor with a custom watchdog script to have more control on shutdown conditions. Useful for custom NUT servers as the one from Synology.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2026/how-to-monitor-a-remote-nut-ups-server-with-a-custom-watchdog-script/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><blockquote>
<p><strong>If you don&rsquo;t care about the context, you can <a
  href="#the-solution-a-small-custom-watchdog-script">jump directly to the final solution</a>.</strong></p>
</blockquote>
<p>A couple of weeks ago I started noticing something very annoying in my homelab. My mini PC running <a
  href="https://aalonso.dev/blog/tags/proxmox/">Proxmox VE</a>, which is connected to the same UPS as my <a
  href="https://aalonso.dev/blog/tags/synology/">Synology NAS</a>, was force-shutting down every single time there was a brief power blip, even when the battery was still at 99%. To make things worse, once the mini PC went down, it wouldn&rsquo;t come back up. As soon as I rebooted it, it would shut itself down again within seconds, and the only way to stop the loop was to physically unplug its ethernet cable and boot it in isolation from the network. Not exactly what I want from a headless box in a rack.</p>
<p>In this article I&rsquo;ll walk you through how I debugged this, what the actual root cause was so you won&rsquo;t lose your time (spoiler: it&rsquo;s because the custom UPS server Synology DSM uses), and how I ended up replacing <code>nut-monitor</code> with a tiny custom watchdog script that finally made my homelab behave the way I wanted during power outages. Let&rsquo;s dive in!</p>
<h2 id="my-setup">
My setup
</h2>
<p>First let me describe my setup to let you understand my context:</p>
<ul>
<li>I have a <strong>Synology NAS DS920+</strong> with a CyberPower UPS physically connected to it over USB. Synology DSM runs its own NUT server (<code>upsd</code>) and acts as the <em>primary</em> in the NUT topology.</li>
<li>Then I have a <strong>Beelink Ryzen mini PC</strong> running <strong>Proxmox VE</strong> on the same LAN and plugged into the same UPS. It was running <code>nut-monitor</code> as a <em>slave</em> (or <em>secondary</em>, in newer NUT terminology), configured to read status from the Synology&rsquo;s UPS server over the network.</li>
</ul>
<p>The relevant <code>MONITOR</code> line in <code>/etc/nut/upsmon.conf</code> on the mini PC looked like this (the IP of the Synology is 192.168.1.7):</p>
<pre><code class="language-bash">MONITOR ups@192.168.1.7 1 monuser secret slave
</code></pre>
<p>This is a very common homelab layout: one machine owns the UPS physically, others watch it over the network. It&rsquo;s also the officially recommended pattern, so in theory everything should just work.</p>
<h2 id="the-symptom">
The symptom
</h2>
<p>The bug was as follows. Whenever there was a 2 second power blip at home (someone plugging in a high-draw appliance, for example), my Synology would keep running as if nothing happened (its battery threshold for shutdown is set to <code>When battery is low</code>, and we never got close to that), but my mini PC would immediately shut itself off. Checking <code>journalctl</code> on the mini PC afterwards, I&rsquo;d see entries from <code>nut-monitor</code> saying:</p>
<pre><code class="language-bash">ups@192.168.1.7: forced shutdown in progress
</code></pre>
<p>And as I mentioned, the most annoying part was that rebooting didn&rsquo;t fix it. Every time I brought the mini PC back up, <code>nut-monitor</code> would reconnect to the Synology UPS server, then shut down again within seconds. The only workaround I had was to unplug the ethernet cable, boot the machine, log in, and then plug the ethernet back in. Not a solution for a headless server.</p>
<h2 id="what-the-forced-shutdown-actually-means">
What the forced shutdown actually means
</h2>
<p>That <code>forced shutdown in progress</code> message is very specific. In NUT, every UPS has a <code>ups.status</code> field that can contain a set of flags. The ones that matter for this article are:</p>
<ul>
<li><code>OL</code> (On Line): UPS is running on mains power.</li>
<li><code>OB</code> (On Battery): UPS is running on battery because mains power is out.</li>
<li><code>LB</code> (Low Battery): battery is critically low and the UPS is about to die.</li>
<li><code>FSD</code> (Forced Shutdown): <strong>the UPS server is telling all its slaves to shut down right now</strong>, regardless of battery level.</li>
</ul>
<p>When <code>nut-monitor</code> on a slave sees <code>FSD</code> in the UPS status, it triggers a local shutdown. That&rsquo;s by design, and it&rsquo;s meant to coordinate graceful shutdowns between the primary and its slaves during a real power outage.</p>
<p>So the question became: <em class="text-accent">why was the Synology broadcasting <code>FSD</code> on a two-second power blip, when it wasn&rsquo;t shutting itself down?</em></p>
<h2 id="discovering-the-root-cause">
Discovering the root cause
</h2>
<p>Disabled the <code>ups-monitor</code> service on the mini PC (to avoid it shutting down as soon as it connects) and plugged back in the ethernet cable again. Then, I took a look at the UPS status:</p>
<pre><code class="language-bash">upsc ups@192.168.1.7
</code></pre>
<p>I could still see <code>ups.status: FSD OL</code> hours after the power outage. <code>OL</code> means the UPS is back on mains already, but <code>FSD</code> was still set. That&rsquo;s when it clicked: Synology DSM&rsquo;s NUT implementation is more aggressive than a vanilla <code>upsd</code>. <strong>On any power event, it asserts <code>FSD</code> to its slaves almost immediately, before even evaluating its own battery threshold.</strong> And it takes a while to clear the flag afterwards, which also explains the post-reboot shutdown loop: my mini PC would come up, see the leftover <code>FSD</code> on the UPS status, and shut itself down again.</p>
<p>This is a known quirk of Synology&rsquo;s NUT server, discussed in several homelab forums, and it&rsquo;s not something you can turn off in DSM.</p>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    The quick workaround to clear a stuck <code>FSD</code> flag is to go to <strong>DSM → Control Panel → Hardware &amp; Power → UPS</strong> and toggle the UPS network server off and back on. That reinitializes the NUT daemon internal state and the flag goes away. Keep this in mind if you ever get stuck in the post-reboot shutdown loop.
  </div>
</div>

<h2 id="why-notifyflag-fsd--does-not-help">
Why <code>NOTIFYFLAG FSD ...</code> does not help
</h2>
<p>The first thing I tried was to look for a way to tell <code>nut-monitor</code> to just ignore <code>FSD</code>. NUT has a <code>NOTIFYFLAG</code> mechanism in <code>/etc/nut/upsmon.conf</code> that lets you configure what happens on each event, and also a <code>NOTIFYCMD</code> hook to run your own script:</p>
<pre><code class="language-bash">NOTIFYFLAG FSD     SYSLOG+WALL
NOTIFYFLAG ONBATT  SYSLOG+WALL+EXEC
NOTIFYFLAG LOWBATT SYSLOG+WALL+EXEC
NOTIFYFLAG ONLINE  SYSLOG+WALL+EXEC
NOTIFYCMD &quot;/usr/local/bin/nut-notify.sh&quot;
</code></pre>
<p>So I wrote a dummy <code>nut-notify.sh</code> that literally did <code>echo &quot;This is a test&quot;</code> and nothing else, just to test the script was actually ran when it should. But it didn&rsquo;t work. The mini PC still shut down on FSD. Why?</p>
<p>Because <strong><code>upsmon</code> has FSD handling hardcoded</strong>. <code>NOTIFYFLAG</code> and <code>NOTIFYCMD</code> are a notification hook, not a shutdown interceptor. Internally, when <code>upsmon</code> sees <code>FSD</code> in the UPS status it runs its own shutdown sequence immediately, completely bypassing any custom command you may have plugged in. There is no way to turn this off, short of patching NUT itself.</p>
<p>At that point it was clear I couldn&rsquo;t make <code>nut-monitor</code> play nice with my Synology. I needed to replace it with something I fully control.</p>
<h2 id="the-solution-a-small-custom-watchdog-script">
The solution: a small custom watchdog script
</h2>
<p>The plan was simple: disable <code>nut-monitor</code> entirely, and write a bash script that polls the UPS status every 30 seconds via <code>upsc</code>, and only triggers a shutdown if the conditions I actually care about are met. Specifically:</p>
<ul>
<li>The UPS reports <code>OB</code> and battery charge is at or below 30% (my own threshold, independent of what the UPS thinks).</li>
<li>Or as a safety net:
<ul>
<li>The UPS reports <code>OB LB</code> (on battery, low battery), which is a legitimate low battery situation.</li>
<li>The connection with the Synology UPS server was lost for a while.</li>
</ul>
</li>
</ul>
<p>The <code>FSD</code> flag is ignored. Only if at least one of the three above conditions is meet the mini PC will shut down. This is the key. And to handle real outages where the Synology itself or my network goes offline, the script also counts consecutive communication failures and shuts down if the UPS server stays unreachable for too long.</p>
<p>So let&rsquo;s proceed with this solution. First, write a watchdog script to <code>/usr/local/bin/ups-watchdog.sh</code>:</p>
<pre><code class="language-bash">#!/bin/bash

UPS=&quot;ups@192.168.1.7&quot;
MIN_BATTERY=30
CHECK_INTERVAL=30
# The Synology NAS takes 5-6 min to reboot, so that's why I set 15 attempts prior to shutdown, which equals
# to 7.5 min at a polling ratio of 30s. I don't want the mini pc to shutdown just because DSM is updating.
COMMS_LOST_THRESHOLD=15

comms_failures=0

while true; do
    UPS_DATA=$(timeout 10 upsc &quot;$UPS&quot; 2&gt;/dev/null)
    STATUS=$(echo &quot;$UPS_DATA&quot; | grep &quot;^ups.status:&quot; | awk '{print $2}')
    CHARGE=$(echo &quot;$UPS_DATA&quot; | grep &quot;^battery.charge:&quot; | awk '{print $2}')

    if [[ -z &quot;$STATUS&quot; ]]; then
        comms_failures=$((comms_failures + 1))
        echo &quot;UPS watchdog: cannot reach UPS server (failure $comms_failures/$COMMS_LOST_THRESHOLD)&quot;

        if [[ &quot;$comms_failures&quot; -ge &quot;$COMMS_LOST_THRESHOLD&quot; ]]; then
            echo &quot;UPS watchdog: UPS server unreachable for too long, shutting down as precaution&quot;
            /sbin/shutdown -h now &quot;UPS server unreachable&quot;
        fi

        sleep &quot;$CHECK_INTERVAL&quot;
        continue
    fi

    comms_failures=0
    echo &quot;UPS watchdog: status=$STATUS charge=$CHARGE%&quot;

    # Shut down on LB flag from UPS
    if [[ &quot;$STATUS&quot; == *&quot;OB&quot;* ]] &amp;&amp; [[ &quot;$STATUS&quot; == *&quot;LB&quot;* ]]; then
        echo &quot;UPS watchdog: LB flag set on battery, shutting down&quot;
        /sbin/shutdown -h now &quot;UPS low battery&quot;
    # Shut down on charge threshold while on battery
    elif [[ &quot;$STATUS&quot; == *&quot;OB&quot;* ]] &amp;&amp; [[ -n &quot;$CHARGE&quot; ]] &amp;&amp; [[ &quot;$CHARGE&quot; -le &quot;$MIN_BATTERY&quot; ]]; then
        echo &quot;UPS watchdog: battery at ${CHARGE}% on battery power, shutting down&quot;
        /sbin/shutdown -h now &quot;UPS battery critical&quot;
    fi

    sleep &quot;$CHECK_INTERVAL&quot;
done
</code></pre>
<p>Note that the script uses <code>OB</code> and <code>LB</code> as shutdown triggers but never <code>FSD</code>. That&rsquo;s intentional. <code>FSD</code> being asserted on its own is not a reason for my mini PC to die, and since we got rid of <code>nut-monitor</code>, nothing on this machine will ever react to that flag again.</p>
<p>Okay, once we have it, make it executable:</p>
<pre><code class="language-bash">sudo chmod +x /usr/local/bin/ups-watchdog.sh
</code></pre>
<p>And wrap it in a systemd service at <code>/etc/systemd/system/ups-watchdog.service</code>:</p>
<pre><code class="language-ini">[Unit]
Description=UPS Battery Watchdog
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/ups-watchdog.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
</code></pre>
<p>Finally, stop and disable <code>nut-monitor</code>, mask it to prevent it from being started from package updates, and then enable and start the new custom watchdog script:</p>
<pre><code class="language-bash">sudo systemctl stop nut-monitor
sudo systemctl disable nut-monitor
sudo systemctl mask nut-monitor
sudo systemctl daemon-reload
sudo systemctl enable ups-watchdog
sudo systemctl start ups-watchdog
</code></pre>
<p>Note: if you also have a <code>nut-client</code> service running, you can also stop and disable it as well since it&rsquo;s not needed anymore:</p>
<pre><code class="language-bash">sudo systemctl stop nut-client
sudo systemctl disable nut-client
sudo systemctl mask nut-client
</code></pre>
<h2 id="verifying-it-works">
Verifying it works
</h2>
<p>After deploying the script, I did two tests to make sure the new setup behaved correctly.</p>
<p><strong>Test 1: brief power outage.</strong> I pulled the UPS plug from the wall for about 40 seconds and then plugged it back in. Before, this would instantly kill the mini PC. Now, the logs from <code>journalctl</code> for the script shows the <code>OB</code> status briefly, then goes back to <code>OL</code>, and the mini PC stays happily up:</p>
<pre><code class="language-text">UPS watchdog: status=OL charge=100%
UPS watchdog: status=OB charge=99%
UPS watchdog: status=OL charge=99%
</code></pre>
<p><strong>Test 2: Synology reboot.</strong> I triggered a reboot on the Synology to simulate a DSM update. The mini PC lost contact with the UPS server for about 5 minutes, counted 10 consecutive failures, and then got its connection back, all without shutting itself down:</p>
<pre><code class="language-text">UPS watchdog: status=OL charge=100%
UPS watchdog: cannot reach UPS server (failure 1/15)
UPS watchdog: cannot reach UPS server (failure 2/15)
...
UPS watchdog: cannot reach UPS server (failure 10/15)
UPS watchdog: status=OL charge=100%
</code></pre>
<p>Which was exactly the behavior I wanted.</p>
<h2 id="wrapping-up">
Wrapping up
</h2>
<p>And that&rsquo;s the full story of how I finally stopped my Proxmox mini PC from randomly shutting itself down on every power blip. The lesson I take from this is that <code>nut-monitor</code> is a great tool for well-behaved UPS topologies, but when your primary is a Synology (with its aggressive <code>FSD</code> behavior), the hardcoded slave shutdown logic gets in the way. In that case, replacing it with a small polling script you fully control is, in my opinion, the simplest and most reliable solution. A bit of bash goes a long way.</p>
<p>If you run a similar homelab setup and have been fighting the same battle, I hope this saves you a few hours of head-scratching. Until next time!</p>
]]></content:encoded></item><item><title>How to configure Bazzite as a headless streaming gaming PC to play remotely in your LAN</title><link>https://aalonso.dev/blog/2026/how-to-configure-bazzite-as-a-headless-streaming-gaming-pc/</link><pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2026/how-to-configure-bazzite-as-a-headless-streaming-gaming-pc/</guid><category>gaming</category><category>linux</category><category>networking</category><category>remote</category><category>tutorial</category><description>Learn how to configure your PC with Bazzite Linux as a headless streaming gaming PC to play remotely on any device in your LAN.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2026/how-to-configure-bazzite-as-a-headless-streaming-gaming-pc/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>I have a Linux gaming PC running <a
  href="https://bazzite.gg/"rel="noopener noreferrer" target="_blank">Bazzite</a> that I use exclusively for gaming. It sits in a corner of my house with just the power and the ethernet cable, and I connect to it remotely from my TV, my laptop, my iPad, or whatever device is closest to me at the moment. No monitor, no keyboard, no mouse attached to it. Just a box that boots up, waits for me, and streams games over my local network.</p>
<p>Getting to this point was not complicated, but it required a few pieces to come together: a streaming server, a client app, a dummy HDMI plug, and a way to log in without a screen. In this article, I&rsquo;ll walk you through how I set it all up, so you can do the same with your own PC.</p>
<h2 id="how-is-this-possible">
How is this possible?
</h2>
<p>I use two different pieces of free open-source software to make this magic work. Sunshine and Moonlight.</p>
<p><a
  href="https://github.com/LizardByte/Sunshine"rel="noopener noreferrer" target="_blank">Sunshine</a> is an open-source, self-hosted game streaming server. It runs on your gaming PC and captures whatever is on the screen (your desktop, a game, etc.) encodes it using your GPU&rsquo;s hardware encoder, and streams it over the network with very low latency. It supports AMD, Intel, and NVIDIA GPUs, and you can configure it through a web interface.</p>
<p><a
  href="https://moonlight-stream.org/"rel="noopener noreferrer" target="_blank">Moonlight</a> is an open-source game streaming client that connects to Sunshine. It originally started as an implementation of NVIDIA&rsquo;s GameStream protocol, but with Sunshine as the host, it works with any GPU. Moonlight is available on pretty much every platform you can think of: Windows, macOS, Linux, Android, iOS, Apple TV, and even some smart TVs. It supports up to 4K resolution with HDR and up to 120 FPS on compatible clients.</p>
<p>The idea is simple: Sunshine runs on your gaming PC (the server), Moonlight runs on the device you want to play from (the client), and they talk to each other over your LAN. The result is that you can use your PC and play your PC games from your couch, your bed, or anywhere in your house, with a controller in your hands (or mouse and keyboard if you want) and a screen in front of you that is not your PC monitor.</p>
<h2 id="setting-up-sunshine-on-bazzite">
Setting up Sunshine on Bazzite
</h2>
<p>Bazzite makes trivially easy to set up sunshine. It comes pre-installed, so you just need to enable it. Bazzite uses <a
  href="https://docs.bazzite.gg/Installing_and_Managing_Software/ujust/"rel="noopener noreferrer" target="_blank"><code>ujust</code></a> as a command runner for common system tasks, and setting up Sunshine is one of them:</p>
<pre><code class="language-bash">ujust setup-sunshine
</code></pre>
<p>This will open an interactive menu where you can choose to enable or disable the Sunshine service. Select <code>Enable</code>, and Sunshine will start as a systemd daemon that launches automatically on every boot.</p>
<p>Once enabled, you can access the Sunshine web interface with the same ujust command, but selecting the option <code>Open Portal</code>, or just going to <code>https://localhost:47990</code> from a browser on the same machine (or via Moonlight later). The first time you open it, you&rsquo;ll be asked to set up a username and password for the Sunshine admin panel. After that, you can configure your streaming settings from there, like encoder type, bitrate, and resolution. Personally, I didn&rsquo;t configure anything here and left all defaults untouched.</p>
<div
  class="border-l-4 border-yellow-500 dark:border-yellow-400 bg-yellow-50 dark:bg-yellow-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-yellow-800 dark:text-yellow-200">
    

    Warning
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-yellow-900 dark:text-yellow-100">
    Make sure you set strong credentials for the Sunshine web interface. Anyone on your network who can reach port 47990 could potentially access your streaming settings.
  </div>
</div>

<p>I also recommend setting a static local IP address in your Bazzite PC. Although it is not necessary, it might simplify things later, like in case your PC doesn&rsquo;t get auto-discovered by the Moonlight clients. There are multiple ways to setup this, including using the UI like going to System Settings in your desktop environment. All options are valid, and also out of scope for this article, as explaining them would make the article too large. You can always make a quick google search if you want.</p>
<h2 id="setting-up-wake-on-lan-on-bazzite">
Setting up Wake on LAN on Bazzite
</h2>
<p>I highly recommend also enabling Wake on Lan (WoL) on your Bazzite PC that you will use to stream games. WoL lets you power on your PC remotely by sending a special network packet (aka. &ldquo;magic packet&rdquo;) from any device on your LAN. Moonlight clients already has this feature, so you could power on your PC from any moonlight client you are willing to use.</p>
<p>To enable it, your network card needs to support WoL, and it needs to be enabled in the BIOS. Most modern motherboards support it, but the option might be disabled by default. Check your BIOS settings under something like <code>Power Management</code> or <code>Wake Up Event Setup</code>, and enable <code>Wake on LAN</code> or <code>Resume by PCI-E Device</code>.</p>
<p>Once that&rsquo;s done, Bazzite makes the software side easy with another <code>ujust</code> command:</p>
<pre><code class="language-bash">ujust toggle-wol
</code></pre>
<p>This will detect your active Ethernet interface automatically and show you the current status. You&rsquo;ll get an interactive menu with these options:</p>
<ul>
<li><strong>Enable</strong> — turns on WoL for the current session and creates a udev rule so it persists across reboots.</li>
<li><strong>Force-Enable</strong> — creates a systemd service that forces WoL on at every boot. Use this if the regular enable doesn&rsquo;t stick.</li>
<li><strong>Disable</strong> — turns off WoL and removes the persistence rules.</li>
</ul>
<p>I&rsquo;d recommend starting with <code>Enable</code>, and if you find that WoL stops working after a reboot, switch to <code>Force-Enable</code>. In my case on a MSI motherboard, it was enough to use just the first option.</p>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    Wake on LAN only works over Ethernet, not Wi-Fi. Your gaming PC needs to be connected to your router with a cable.
  </div>
</div>

<h2 id="setting-up-moonlight-on-your-devices">
Setting up Moonlight on your devices
</h2>
<p>Moonlight is available on basically every platform. I use it on my iPhone, my iPad, my MacBook, and my XBOX Series X. The setup process is the same everywhere:</p>
<ol>
<li><strong>Install Moonlight</strong> go to the <a
  href="https://moonlight-stream.org/"rel="noopener noreferrer" target="_blank">official moonlight website</a> to know from where download the client. For phones and tablets, it&rsquo;s usually the default app store.</li>
<li><strong>Open Moonlight</strong>, and it will automatically scan your local network for Sunshine hosts. Your Bazzite PC should appear in the list. If it doesn&rsquo;t appear, introduce the IP of your PC manually.</li>
<li><strong>Select your PC</strong>, and Moonlight will ask you to enter a PIN. This PIN will also appear on the Sunshine web interface (or on the PC&rsquo;s screen if one is connected). Enter the PIN in Sunshine to pair the devices.</li>
<li><strong>That&rsquo;s it.</strong> Once paired, you can launch your desktop or any game directly from Moonlight.</li>
</ol>
<p>In Moonlight&rsquo;s settings, you can adjust the stream resolution, frame rate, bitrate, and video codec. Here are the settings I&rsquo;d recommend as a starting point:</p>
<ul>
<li><strong>Resolution</strong>: match your client display or go with 1440p for a good balance.</li>
<li><strong>Frame rate</strong>: 60 FPS is the sweet spot for most setups.</li>
<li><strong>Video codec</strong>: I left this in auto, but if your hardware is fairly old and can only choose between HEVC (H.265) and H.264, chose HEVC (H.265) if your GPU and client support it. It gives better image quality at the same bitrate compared to H.264.</li>
<li><strong>Bitrate</strong>: if your local network is not a bottleneck, push it to 50–80 Mbps for excellent quality. In my case with a good network (ethernet cat. 5e or Wi-fi 6) the default bitrates from Moonlight work nice.</li>
</ul>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    A fun thing I discovered: if you set the Moonlight client resolution higher than your PC&rsquo;s desktop resolution (for example, streaming at 4K while the desktop runs at 1440p), Sunshine will upscale the captured frames before encoding. The result looks noticeably sharper than streaming at the native lower resolution, because the upscaling happens on the GPU before compression rather than on the client side after decompression. So if your network can handle it, try bumping up the Moonlight resolution one step above your game resolution.
  </div>
</div>

<h2 id="the-dummy-hdmi-plug">
The dummy HDMI plug
</h2>
<p>Here&rsquo;s where things get interesting if you want a truly headless setup. When you disconnect the monitor from your PC, the GPU detects that no display is attached and reports no available outputs. On Linux with KDE Wayland (the Desktop Environment I&rsquo;m using), this means the desktop compositor (KWin) has nothing to render to, so it either fails to start or enters a fallback state. And if there&rsquo;s no desktop session, Sunshine has nothing to capture and stream.</p>
<p>I found this out the hard way. Everything was working great with the monitor connected, but the moment I unplugged it and tried to connect via Moonlight, it just kept saying the computer was offline. Checking the logs confirmed the issue:</p>
<pre><code class="language-txt">sddm-greeter-qt6: There are no outputs - creating placeholder screen
amdgpu: Cannot find any crtc or sizes
</code></pre>
<p>The solution is surprisingly low-tech: a <strong>dummy HDMI plug</strong>. It&rsquo;s a small passive dongle (about the size of a USB flash drive) that you plug into your GPU&rsquo;s HDMI port. Inside, it has a tiny chip with EDID data that tells the GPU &ldquo;hey, there&rsquo;s a monitor here.&rdquo; The GPU believes it, creates a real display output, and everything works as if a monitor were connected.</p>
<p>You can find them on Amazon for about €5–8. Search for &ldquo;HDMI dummy plug 4K&rdquo; and pick any well-reviewed one. Make sure it advertises 4K (3840×2160) resolution, so that KDE and Sunshine offer you proper resolution options (some cheaper ones only report 1080p, which would limit your streaming quality).</p>
<p>I went with an HDMI dummy plug specifically because my GPU (an AMD Radeon RX 5700 XT) has HDMI 2.0b, which supports 4K at 60Hz, more than enough for Moonlight streaming. I also wanted to keep the DisplayPort outputs free in case I ever want to connect a real monitor again. But if you prefer DisplayPort, those dummy plugs exist too and work the same way.</p>
<p>Once the dummy plug is in, the headless boot looks exactly like a normal boot with a monitor. KDE starts properly, Sunshine captures the desktop, and Moonlight connects without any issues.</p>
<h2 id="going-fully-headless-logging-in-without-a-screen">
Going fully headless: logging in without a screen
</h2>
<p>With the dummy plug solving the display problem, there&rsquo;s one more challenge: how do you get past the login screen without a keyboard and a monitor? Here, you have a few different options, depending on your security concerns, and how much do you want to complicate things.</p>
<h3 id="the-easy-option-auto-login">
The easy option: auto-login
</h3>
<p>The simplest approach is to configure not password for your user, and configure SDDM (KDE&rsquo;s display manager) to auto-login. You can create a file at <code>/etc/sddm.conf.d/autologin.conf</code> with:</p>
<pre><code class="language-ini">[Autologin]
User=yourusername
Session=plasma
Relogin=true
</code></pre>
<p>This makes SDDM log you in automatically at boot. <code>Relogin=true</code> means it will also re-login if the session crashes, which is important for a headless machine that needs to recover on its own.</p>
<p>The downside? As your user has not password, someone with physical access to the machine could access your desktop with just plugin in a monitor and a mouse and keyboard. For many people this is perfectly fine, but I couldn&rsquo;t leave it like that. Like, what I have the bad luck that someone breaks into my house and steals the whole PC? I have Steam with my credit card there, and also I&rsquo;m logged into my email there.</p>
<h3 id="the-yubikey-option-physical-token-authentication">
The YubiKey option: physical token authentication
</h3>
<p>I have a <a
  href="https://www.yubico.com/products/"rel="noopener noreferrer" target="_blank">YubiKey</a>, a small hardware security key, and it turns out you can use it as a login token for KDE. The idea is that instead of typing a password, you plug the YubiKey into a USB port and SDDM authenticates you automatically via FIDO2/U2F. No typing, no screen needed.</p>
<p>On Bazzite, the <code>pam-u2f</code> package is already installed (you can verify with <code>rpm -q pam-u2f</code>). Setting it up involves two steps:</p>
<p><strong>1. Register your YubiKey:</strong></p>
<pre><code class="language-bash">mkdir -p ~/.config/Yubico
pamu2fcfg -o pam://localhost -i pam://localhost &gt; ~/.config/Yubico/u2f_keys
</code></pre>
<p>Touch the key when it blinks. This generates the credential file that PAM will use to verify your identity.</p>
<p><strong>2. Configure PAM for SDDM and the KDE screen locker:</strong></p>
<p>Add the following line <strong>before</strong> the <code>auth substack password-auth</code> line in both <code>/etc/pam.d/sddm</code> and <code>/etc/pam.d/kde</code>:</p>
<pre><code class="language-bash">auth        sufficient    pam_u2f.so authfile=/home/yourusername/.config/Yubico/u2f_keys cue origin=pam://localhost appid=pam://localhost
</code></pre>
<p>The <code>sufficient</code> keyword means: if the YubiKey authenticates successfully, skip the password prompt entirely. If the key isn&rsquo;t present, fall through to the normal password prompt. You never lose the ability to type your password as a fallback.</p>
<p>With this setup, my boot flow looks like this:</p>
<ol>
<li>Power on the PC (via Wake on LAN or the power button).</li>
<li>A wait a minute, as I know it takes 40-50s to my PC to get into login screen.</li>
<li>I plug in the YubiKey, I touch it a few times until it blinks → SDDM detects it and logs me in.</li>
<li>Sunshine starts capturing the desktop → I connect from Moonlight on any device.</li>
</ol>
<p>Someone finding the PC would need my YubiKey or my password to get in. And the YubiKey is always with me.</p>
<p>The downside? While the setup is headless, I need to wake up and walk to the computer to plug the YubiKey and touch it to login. With the auto-login option, I can just sit on my sofa or lay on my bed, boot it up remotely with WoL, and start playing once it boots.</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>And that&rsquo;s it. With Sunshine on Bazzite, and a Moonlight client on whatever device you want, you can use your gaming PC to stream games anywhere in your LAN. You get a fully silent gaming PC that you can wake up, log into, and play from anywhere in your house. If you want to go a extra step, you could buy a dummy HDMI plug and make the whole setup headless. Do you want more security? Add a YubiKey for secure headless login.</p>
<p>Happy gaming!</p>
]]></content:encoded></item><item><title>How to configure Pi-hole for local DNS resolution in your LAN</title><link>https://aalonso.dev/blog/2026/how-to-configure-pihole-for-local-dns-resolution-in-your-lan/</link><pubDate>Sat, 28 Feb 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2026/how-to-configure-pihole-for-local-dns-resolution-in-your-lan/</guid><category>networking</category><category>raspberry-pi</category><category>tutorial</category><description>Learn how to configure Pi-hole as a local DNS server so every device in your LAN can resolve custom domains like myservice.home.arpa without touching each device's hosts file.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2026/how-to-configure-pihole-for-local-dns-resolution-in-your-lan/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>This article is a companion to my previous one on <a
  href="https://aalonso.dev/blog/2026/how-to-use-caddy-as-a-reverse-proxy-for-your-services/">how to use Caddy as a reverse proxy for your services</a>. In that article, I set up Caddy so that services are reachable via custom local domains like <code>myservice.home.arpa</code>, but for those domains to actually work, every device on your LAN needs to be able to resolve them — and that&rsquo;s exactly what we tackle here. In today&rsquo;s article I&rsquo;ll explain how I use Pi-hole as a local DNS server to achieve network-wide resolution for those custom domains without touching every device&rsquo;s <code>hosts</code> file. Let&rsquo;s dive in!</p>
<h2 id="why-local-domains-and-why-homearpa">
Why local domains and why <code>.home.arpa</code>?
</h2>
<p>Before jumping into the setup, let me quickly explain why this is useful and why I use <code>.home.arpa</code> as my local TLD.</p>
<p>When you self-host services, you usually access them by typing their local IP address and port directly in the browser, like <code>http://192.168.1.100:8080</code>. This works, but it&rsquo;s ugly, hard to remember, and doesn&rsquo;t play well with HTTPS (certificates are usually tied to domain names, not bare IPs, although <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/">I explain how to generate certificates for bare IPs with your own CA</a> in another article). The natural solution is to assign readable domain names to your services, like <code>myservice.home.arpa</code>, which it&rsquo;s not only easier to remember, but gives you more flexibility if for whatever reason you need to migrate it to a different hardware over a different IP address.</p>
<p>As for the TLD choice: <code>.home.arpa</code> is the <a
  href="https://datatracker.ietf.org/doc/html/rfc8375"rel="noopener noreferrer" target="_blank">officially reserved</a> special-use domain for residential LANs. Using it ensures you will never accidentally conflict with a real public domain now or in the future, which is a real risk if you just invent something like <code>.local</code> or <code>.home</code>. <code>.local</code> in particular is already reserved for mDNS (Bonjour/Avahi), so you must avoid it. Stick with <code>.home.arpa</code> and you&rsquo;ll be safe.</p>
<h2 id="installing-pi-hole">
Installing Pi-hole
</h2>
<p>Pi-hole is a network-level DNS server and ad-blocker. It&rsquo;s designed to run on a <a
  href="https://aalonso.dev/blog/tags/raspberry-pi/">Raspberry Pi</a> (hence the name), but it will happily run on any Linux machine, including a VM or a Docker container. For this tutorial, I&rsquo;ll assume you&rsquo;re installing it on a Debian-based system (Raspberry Pi OS, Debian, Ubuntu&hellip;).</p>
<p>Pi-hole provides an <a
  href="https://docs.pi-hole.net/main/basic-install/"rel="noopener noreferrer" target="_blank">official one-liner installer</a>:</p>
<pre><code class="language-bash">curl -sSL https://install.pi-hole.net | bash
</code></pre>
<p>The installer is interactive and will guide you through the configuration. A couple of things to keep in mind during the setup:</p>
<ul>
<li>When asked for the <strong>upstream DNS provider</strong>, choose any public resolver you trust (I use Cloudflare&rsquo;s <code>1.1.1.1</code> and <code>1.0.0.1</code>). Pi-hole will forward queries it can&rsquo;t answer locally to this provider.</li>
<li>When asked for the <strong>network interface</strong>, select the one connected to your LAN.</li>
<li>Take note of the <strong>static IP address</strong> your Pi-hole machine will use. The installer will remind you about this, but it&rsquo;s crucial: if your Pi-hole&rsquo;s IP changes, everything breaks. Make sure to either set a static IP on the machine itself, or create a DHCP reservation for it on your router.</li>
</ul>
<p>Once the installer finishes, it will print the URL and credentials to access the Pi-hole web admin panel. Keep them safe!</p>
<h2 id="adding-simple-local-dns-records">
Adding simple local DNS records
</h2>
<p>This is the core of what we want to achieve. Pi-hole includes a built-in local DNS resolver that lets you define custom DNS records, mapping domain names to local IPs. Open the Pi-hole web admin panel (by default at <code>http://&lt;pihole-ip&gt;/admin</code>) and log in. Then navigate to <strong>Settings → Local DNS Records</strong>. You will see a simple form with two fields: <strong>Domain</strong> and <strong>IP Address</strong>.</p>
<p>For example, if I have a Caddy reverse proxy running on a machine with IP <code>192.168.1.100</code>, and I want <code>myservice.home.arpa</code> to point to it, I would fill in:</p>
<ul>
<li><strong>Domain:</strong> <code>myservice.home.arpa</code></li>
<li><strong>IP Address:</strong> <code>192.168.1.100</code></li>
</ul>
<p>Click <strong>Add</strong> and that&rsquo;s it. The record is immediately active, no restart needed. You can add as many records as you want, one per service or server. In my homelab, for example, I have records for my NAS, my Proxmox nodes, and each of the services I expose through Caddy.</p>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    Pi-hole also supports <strong>CNAME records</strong> under <strong>Local DNS → CNAME Records</strong>. CNAMEs are useful when multiple service domains should point to the same machine. For example, if both <code>serviceA.home.arpa</code> and <code>serviceB.home.arpa</code> live behind the same reverse proxy at <code>192.168.1.100</code>, you could create a single DNS A record for <code>proxy.home.arpa → 192.168.1.100</code>, and then two CNAME records: <code>serviceA.home.arpa → proxy.home.arpa</code> and <code>serviceB.home.arpa → proxy.home.arpa</code>. This way, if the proxy IP ever changes, you only update one DNS A record.
  </div>
</div>

<h2 id="adding-complex-local-dns-records">
Adding complex local DNS records
</h2>
<p>But what about wildcard domains? For example, if you want <code>*.home.arpa</code> to resolve to the same IP, so you can have <code>service1.home.arpa</code>, <code>service2.home.arpa</code>, etc without adding a record for each one? Pi-hole&rsquo;s built-in local DNS doesn&rsquo;t support wildcards, but there&rsquo;s a simple workaround: you can use <code>dnsmasq</code> directly, which is the DNS server Pi-hole uses under the hood. Just SSH into your Pi-hole machine and create a new file under <code>/etc/dnsmasq.d/</code> with a <code>.conf</code> extension, for example <code>/etc/dnsmasq.d/99-homelab.conf</code>, and add the following content:</p>
<pre><code class="language-bash"># Specific subdomain overrides
address=/myservice.home.arpa/192.168.1.100

# Wildcard for everything else
address=/home.arpa/192.168.1.120
</code></pre>
<p>With the above configuration, <code>myservice.home.arpa</code> will resolve to <code>192.168.1.100</code>, while any other subdomain under <code>home.arpa</code> (like <code>service1.home.arpa</code>, <code>nas.home.arpa</code>, etc) will resolve to <code>192.168.1.120</code>. After saving the file, we need to configure pi-hole to load the custom dnsmasq configuration files, and then restart it so the file is actually loaded. We can do that with the following commands:</p>
<pre><code class="language-bash">sudo pihole-FTL --config misc.etc_dnsmasq_d true
sudo systemctl restart pihole-FTL
</code></pre>
<p>Alternatively, you can go to the Pi-hole admin panel, then <strong>Settings → Advanced</strong> (in the upper right corner) <strong>→ All settings</strong> (back in the lateral menu) <strong>→ Miscellaneous</strong> and checking the checkbox under <strong>misc.etc_dnsmasq_d</strong>. Then, again, restart Pi-hole to load the new configuration.</p>
<h2 id="making-your-devices-use-pi-hole-as-their-dns-server">
Making your devices use Pi-hole as their DNS server
</h2>
<p>Defining the DNS records is only half the work. The other half is telling your devices to actually query Pi-hole instead of the default DNS server assigned by your ISP. There are two ways to do this.</p>
<ul>
<li>
<p><strong>Option 1: Configure it on your router (recommended)</strong> - This is the best approach because it&rsquo;s network-wide: every device that connects to your LAN (laptops, phones, smart TVs, IoT devices&hellip;) will automatically use Pi-hole as their DNS server without any extra configuration on each device. Every router is different, but the setting you&rsquo;re looking for is usually under <strong>DHCP settings</strong> or <strong>LAN settings</strong>, and is often labeled <strong>DNS server</strong> or <strong>Primary DNS</strong>. Set the value to the <strong>static IP of your Pi-hole machine</strong>.</p>
</li>
<li>
<p><strong>Option 2: Configure it per device</strong> - If you can&rsquo;t or don&rsquo;t want to change your router&rsquo;s settings, you can configure each device individually to point to Pi-hole. The downside is obvious: you need to repeat this on every device, and any new device that joins your network won&rsquo;t automatically benefit from it.</p>
<ul>
<li>On <strong>macOS</strong>, go to <strong>System Settings → Network</strong>, select your active connection (Wi-Fi or Ethernet), click <strong>Details</strong>, and under the <strong>DNS</strong> tab add your Pi-hole&rsquo;s IP as a DNS server. To avoid breaking internet connection when you are outside of your LAN (for example, if you are using a MacBook on the go), you might want to set up a <a
  href="https://support.apple.com/en-us/105129"rel="noopener noreferrer" target="_blank">network location</a>, so that your Mac uses Pi-hole only when connected to your home network.</li>
<li>On other systems like Windows and Linux, the process is similar, and very often can be configured directly in the network settings. Just look for the DNS server configuration and set it to your Pi-hole&rsquo;s IP. An in case you are using a portable device (like a laptop), be sure to configure it to use Pi-hole only when connected to your home network, otherwise you might have DNS resolution issues when outside of your LAN.</li>
</ul>
</li>
</ul>
<h2 id="verifying-it-works">
Verifying it works
</h2>
<p>Once you&rsquo;ve configured at least one device to use Pi-hole as its DNS, it&rsquo;s time to verify everything works correctly. The quickest way is to use <code>nslookup</code> or <code>dig</code> from a terminal on that device.</p>
<p>For example, to verify that <code>myservice.home.arpa</code> resolves correctly:</p>
<pre><code class="language-bash">nslookup myservice.home.arpa
</code></pre>
<p>You should see an output like this:</p>
<pre><code class="language-text">Server:		192.168.1.10
Address:	192.168.1.10#53

Name:	myservice.home.arpa
Address: 192.168.1.100
</code></pre>
<p>Here, <code>192.168.1.10</code> is your Pi-hole, and <code>192.168.1.100</code> is the IP you configured in the DNS record. If you see that, your local DNS resolution is working! You can also verify it from a browser: open <code>http://myservice.home.arpa</code> and, if the service is running and listening on that IP, it should load. Of course, if you&rsquo;ve set up <a
  href="https://aalonso.dev/blog/2026/how-to-use-caddy-as-a-reverse-proxy-for-your-services/">TLS with Caddy</a> and <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/">your own CA</a> as I described in my previous articles, you can use <code>https://myservice.home.arpa</code> and get a green padlock too.</p>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    The Pi-hole admin panel also shows the DNS query log in real time under <strong>Query Log</strong>. This is a great place to confirm your devices are sending queries to Pi-hole, and to troubleshoot any resolution issues. If you see your queries there and the status is <code>OK</code>, you&rsquo;re all set.
  </div>
</div>

<h2 id="bonus-using-your-own-certificate-to-secure-pi-hole-admin-panel-with-https">
Bonus: Using your own certificate to secure pi-hole admin panel with HTTPS
</h2>
<p>If you also want to use your own self-signed certificate from your own CA to secure the Pi-hole admin panel with HTTPS, you can jus <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#using-the-ca-to-generate-a-new-certificate-from-the-csr">follow the instructions on my CA article to generate a certificate for the Pi-hole</a>. Then, once you have your separated <code>server.key</code> and <code>server.crt</code> files, you can just merge them into a pem file with <code>cat server.key server.crt &gt; pihole.pem</code>, and copy that <code>pihole.pem</code> file into the Pi-hole machine under <code>/etc/pihole/pihole.pem</code>. Then, ensure the files permissions are correct:</p>
<pre><code class="language-bash"># In the pihole machine
cd /etc/pihole
sudo chown pihole:pihole pihole.pem
sudo chmod 600 pihole.pem
</code></pre>
<p>Then, configure Pi-hole to use that certificate by editing the <code>/etc/pihole/pihole.toml</code> file</p>
<pre><code class="language-diff">  [webserver.tls]
    # Path to the TLS (SSL) certificate file.
    #
    # All directories along the path must be readable and accessible by the user running
    # FTL (typically 'pihole'). This option is only required when at least one of
    # webserver.port is TLS. The file must be in PEM format, and it must have both,
    # private key and certificate (the *.pem file created must contain a 'CERTIFICATE'
    # section as well as a 'RSA PRIVATE KEY' section).
    #
    # The *.pem file can be created using `cp server.crt server.pem &amp;&amp; cat server.key &gt;&gt;
    # server.pem` if you have these files instead
    #
    # Allowed values are:
    #     A valid TLS certificate file (*.pem)
-   cert = &quot;/etc/pihole/tls.pem&quot;
+   cert = &quot;/etc/pihole/pihole.pem&quot;
</code></pre>
<p>And finally, restart Pi-hole to apply the changes:</p>
<pre><code class="language-bash">sudo systemctl restart pihole-FTL
</code></pre>
<h2 id="bonus-best-adblock-list-to-remove-ads-and-protect-your-lan-against-malware">
Bonus: best adblock list to remove ads and protect your LAN against malware
</h2>
<p>For blocking ads, malware, cookie banners and other annoyances on my LAN I&rsquo;m using a single adblock list: <a
  href="https://github.com/hagezi/dns-blocklists"rel="noopener noreferrer" target="_blank">HaGeZi&rsquo;s Pro DNS Blocklist</a>. <strong>I highly recommend HaGeZi&rsquo;s lists</strong> for a simple and easy single-list installation that will avoid you the hassle and problems of managing multiple lists. The list is updated frequently, and personally, the <a
  href="https://github.com/hagezi/dns-blocklists"rel="noopener noreferrer" target="_blank">Multi Pro variant</a> works perfectly for me, with not false positives so far. I recommend you to take your time to fully read the <code>Readme</code> of the GitHub repo to understand what each list contains. Higher tier list already includes the lower tier ones (for example, Multi Pro++ includes Multi Pro, which also includes Multi Normal, etc).</p>
<h2 id="wrapping-up">
Wrapping up
</h2>
<p>That&rsquo;s everything you need to have a clean, network-wide local DNS setup at home; that not only resolves local domain names like <code>*.home.arpa</code> to the correct IPs, but also removes annoying ads from your devices and protect your network against malware. Until next time!</p>
]]></content:encoded></item><item><title>How to use Caddy as a reverse proxy for your services</title><link>https://aalonso.dev/blog/2026/how-to-use-caddy-as-a-reverse-proxy-for-your-services/</link><pubDate>Fri, 30 Jan 2026 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2026/how-to-use-caddy-as-a-reverse-proxy-for-your-services/</guid><category>docker</category><category>networking</category><category>tutorial</category><description>In this article I explain how to configure caddy as a reverse proxy for your services (eg. Docker) with TLS encryption and HTTPS connection.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2026/how-to-use-caddy-as-a-reverse-proxy-for-your-services/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>I have a few docker services deployed on a Beelink mini PC at home that I usually access from my LAN. Because all services are deployed in the same machine, they are listening to different ports (say 3000, 5000, 8080, etc), but that&rsquo;s ugly and not friendly for other non-tech people at home that also use them, so I set up caddy as a reverse proxy. This not only allows me to provide an easy to remember URL like <code>myservice.home.arpa</code>, but also ensures we can use TLS secured connections with HTTPS. In today&rsquo;s article, I would explain how did I achieve this at home. Let&rsquo;s dive in!</p>
<h2 id="installing-caddy">
Installing Caddy
</h2>
<p>First, we need to install caddy. For that, it&rsquo;s better that you follow the <a
  href="https://caddyserver.com/docs/install"rel="noopener noreferrer" target="_blank">official instructions for your system</a>, that also will be more up to date than this article in case something change. In my case, I&rsquo;m using Debian 13 Trixie, so at the time of writing all I did was:</p>
<pre><code class="language-bash">sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl gnupg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
</code></pre>
<h2 id="obtaining-and-storing-the-certificates-for-tlshttps">
Obtaining and storing the certificates for TLS/HTTPS
</h2>
<p>Now caddy is installed, but prior to start using it with TLS encryption, we need to plan how to manage the certificates we&rsquo;ll use for that. First thing we need is to know the domain we&rsquo;ll use to connect to the service, and once we have that clear, we have two main options:</p>
<ul>
<li>
<p>You could use <a
  href="https://letsencrypt.org/"rel="noopener noreferrer" target="_blank">Let&rsquo;s Encrypt</a> and ACME challenges to automatically obtain and renew trusted certificates. For that, the domain you&rsquo;ll use must be a public one (you own and use a domain bought at a register), and ports 80 and 443 must be open externally, plus other basic requirements explained in <a
  href="https://caddyserver.com/docs/automatic-https"rel="noopener noreferrer" target="_blank">Caddy official documentation for automatic HTTPS</a>.</p>
</li>
<li>
<p>If you like me, are using local domains such as <code>myservice.home.arpa</code> and only accessing them from your LAN, your best option is to use a self-signed certificate. Caddy already generates one while installing, but to better control it and to simplify the management of multiple services you might have in your homelab, I recommend you to <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/">generate your own using your own CA. I have an article about it</a> that covers the topic in better detail. One advantage of using your own CA instead of the default certificate generated by Caddy is that once trusted in your devices, it doesn&rsquo;t matter what/where else you deploy (a <a
  href="https://aalonso.dev/blog/tags/kubernetes/">kubernetes</a> cluster, something from a <a
  href="https://aalonso.dev/blog/tags/synology/">Synology</a>, whatever in another server or a <a
  href="https://aalonso.dev/blog/tags/raspberry-pi/">Raspberry PI</a>, etc), they will be always be trusted. For this tutorial, I will use my own self-signed certificated obtained from my own CA in the way I explain in that article.</p>
</li>
</ul>
<p>So continuing with the manual, self-signed generated certificate files. You should have a public certificate (e.g. <code>myservice.crt</code>) and its private key or secret (e.g. <code>myservice.key</code>). Now, let&rsquo;s install them so we can use them we caddy. We&rsquo;ll copy them into the <code>/etc/caddy/certs/</code> folder (create it if it doesn&rsquo;t exist), then, provide them with the correct file permissions.</p>
<pre><code class="language-bash">sudo chmod 644 /etc/caddy/certs/myservice.crt
sudo chmod 600 /etc/caddy/certs/myservice.key
sudo chown -R caddy:caddy /etc/caddy/certs/*
</code></pre>
<p>With the above commands we are indicating those files belong to the <code>caddy</code> user, created automatically as part of the installation process done by caddy. This is the user that will execute our caddy server. Also, for better security, we are indicating that the certificate private key is only accessible by that user <code>caddy</code>. Once this is done, we can proceed with the final configuration for the reverse proxy.</p>
<h2 id="configuring-the-reverse-proxy-setting-up-tlshttps">
Configuring the reverse proxy setting up TLS/HTTPS
</h2>
<p>Most, if not all, of caddy configuration is made on its main configuration file under <code>/etc/caddy/Caddyfile</code>. Setting up a reverse proxy with Caddy is fairly simple. For example, imagine I want to configure a reverse proxy for the service <code>myservice</code> under the URL <code>myservice.home.arpa</code> using TLS with the previous certificates to achieve an HTTPS connection. The service is running on that same machine on docker and listening to port <code>8000</code> All I need to do is to add the following to the <code>Caddyfile</code></p>
<pre><code class="language-txt">myservice.home.arpa {
	tls /etc/caddy/certs/myservice.crt /etc/caddy/certs/myservice.key

	reverse_proxy http://localhost:8000 {
		header_down Referrer-Policy &quot;strict-origin-when-cross-origin&quot;
	}
}
</code></pre>
<p>And that&rsquo;s it. Now, the final thing you need to do is to restart caddy to start using the new configuration, which you can do in Debian with <code>sudo systemctl restart caddy</code>. Once caddy restarts, you reverse proxy should be working and you should be able to access your service under <code>https://myservice.home.arpa</code> from your LAN. Things are easy with caddy, right?</p>
<div
  class="border-l-4 border-yellow-500 dark:border-yellow-400 bg-yellow-50 dark:bg-yellow-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-yellow-800 dark:text-yellow-200">
    

    Warning
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-yellow-900 dark:text-yellow-100">
    <p>Note that, because in this tutorial I used a local domain (<code>.home.arpa</code>), in order to access that URL from your device, you need to configure the DNS for it. Otherwise, if trying to access it from your browser you will get a <code>DNS_PROBE_POSSIBLE</code> error. Similar DNS errors will occur if trying to access it via CLI or programmatically. Explaining how to proper configure DNS is outside of the scope of this article, as it&rsquo;s not related with caddy nor its reverse proxy capabilities. However, I have another article in which <a
  href="https://aalonso.dev/blog/2026/how-to-configure-pihole-for-local-dns-resolution-in-your-lan">I explain how to achieve this network-wide for your entire LAN with a custom DNS server like Pi-hole</a>. A simpler approach, but that needs to be configure on each client device, is to just update your <code>hosts</code> file (usually under <code>/etc/hosts</code> in UNIX like systems), adding at the bottom the static IP of your server (where caddy is running) and the domain used for your service, like this:</p>
<pre><code># Caddy is running on my Beelink Mini PC with static IP 192.168.1.100
192.168.1.100	myservice.home.arpa
</code></pre>
  </div>
</div>

]]></content:encoded></item><item><title>How to install Debian on ProxmoxVE with LUKS encrypted root partition that auto-unlocks on boot</title><link>https://aalonso.dev/blog/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/</link><pubDate>Sun, 28 Dec 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/</guid><category>linux</category><category>proxmox</category><category>security</category><category>tutorial</category><description>I'll show you how to create a Debian VM in ProxmoxVE, with the root partition encrypted with LUKS that automatically unlocks and mounts at boot.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>At the beginning of the year, I wrote a similar article on <a
  href="https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/">how to achieve this on a regular PC using TPM 2.0</a>, while installing Manjaro (ArchLinux based) on my main desktop. However, what about a VM? When hardware is virtualized, things are a bit different. In this article, I&rsquo;ll show you what you need to create a Debian VM in ProxmoxVE, with the root partition encrypted with LUKS that automatically unlocks and mounts at boot. Let&rsquo;s dig in!</p>
<h2 id="configuring-proxmoxve">
Configuring ProxmoxVE
</h2>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    I won&rsquo;t cover here how to download the Debian ISO and provide it to Proxmox. For that, better to reference the official <a
  href="https://www.debian.org/distrib/"rel="noopener noreferrer" target="_blank">Debian page</a> to download the ISO, and the <a
  href="https://forum.proxmox.com/threads/how-to-install-the-first-vm-from-iso-images.105206/"rel="noopener noreferrer" target="_blank">Proxmox forum</a> for instructions on adding it to Proxmox (a bit old thread, but the steps remain the same in Proxmox VE v9). In this article, I&rsquo;ll just focus on what we need for LUKS auto-unlock.
  </div>
</div>

<p>First, we need to provide the virtualized hardware to make this auto-unlock and auto-mount thing to work. For that, like in my previous article, we&rsquo;ll leave it to TPM 2.0. We can emulate a TPM device with Proxmox. For that, when creating the VM, under the &ldquo;System&rdquo; tab, we need to configure two important settings.</p>
<ul>
<li>We must define <code>OVMF (UEFI)</code> as the BIOS. This is because TPM will only work with UEFI BIOS.</li>
<li>We must obviously enable TPM, in its 2.0 version, by checking the <code>Add TPM</code> checkbox and indicating the correct version.</li>
</ul>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/proxmox-vm-system-settings.png"
    alt="Create Virtual Machine in Proxmox, System Settings"width="1440"height="1078"
    loading="eager"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/proxmox-vm-system-settings.png"
    data-alt="Create Virtual Machine in Proxmox, System Settings"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>You can configure where to store the EFI and the TPM. For the sake of this tutorial, I kept things simple and selected the default <code>local-lvm</code> storage on my ProxmoxVE installation.</p>
<p>An additional change from default you might notice: I&rsquo;m using <code>q35</code> as the <em>Machine</em> setting, instead of the default <code>i440fx</code>. This is because <code>q35</code> is modern, has better PCIe support, and works great with Debian. <code>i440fx</code> is there for legacy hardware or legacy device emulation, which is not what I need. It will work anyway, but it&rsquo;s always better to use the modern and well-supported option if you don&rsquo;t have an specific reason to stick with the legacy options.</p>
<p>Also, enabling <em>Qemu Agent</em> allows for better communication between the host and the virtualized machine.</p>
<h2 id="installing-debian">
Installing Debian
</h2>
<p>Installing Debian is straight-forward nowadays. After booting up the installation ISO, I choose the default CLI install (plain <code>Install</code> option) just because I&rsquo;m more familiarized with it and feels quicker to me, but the same can be achieved with the GUI too.</p>
<p>Go to the usual language and locale settings, host, users creation, etc., until you reach the <em>Partition disks</em> step. Here, we&rsquo;ll select <code>Guided - use entire disk and set up encrypted LVM</code>.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partitioning-method.png"
    alt="Debian partitioning method"width="2552"height="778"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partitioning-method.png"
    data-alt="Debian partitioning method"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>If asked for a partition scheme, I recommend you to either select <code>All files in one partition</code> or <code>Separate /home partition</code>, but do not separate <code>/var</code> and <code>/tmp</code> or you will suffer from low disk space problems, specially if using docker later. I chose <code>All files in one partition</code>.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partitioning-scheme.png"
    alt="Debian partitioning scheme"width="2556"height="858"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partitioning-scheme.png"
    data-alt="Debian partitioning scheme"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Then, provide the encryption passphrase. This is the passphrase used to encrypt the data on your root partition. It&rsquo;s (it should be) different from your root and user passwords, and it&rsquo;ll be later stored in the TPM for the auto-unlock and auto-mount to work. In the next step, accept the proposed partition scheme and continue by writing changes to disk. Note that the installer generated separated <code>/boot</code> and <code>/boot/efi</code> partitions, needed for auto-unlock feature to work <a
  href="https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/#prerequisites">as explained on my previous article</a>.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partition-table.png"
    alt="Debian partition table"width="2560"height="1250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-install-debian-on-proxmox-with-luks-encrypted-root-partition-that-auto-unlocks-on-boot/debian-partition-table.png"
    data-alt="Debian partition table"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Next, Debian will start installing. I&rsquo;m using a <code>net-install</code> ISO, so it will fetch all updated packages from the network and I&rsquo;ll get an up-to-date system after installation. From here, let&rsquo;s just wait for the installation to finish and then reboot the VM.</p>
<h2 id="enabling-auto-unlock-and-auto-mount-of-root-partition">
Enabling auto-unlock and auto-mount of root partition
</h2>
<p>Now that Debian is installed, once the VM reboots, it will load GRUB and start loading Debian. However, due to the disk being LUKS encrypted, we need to manually unlock it this time. Once we configure everything, it will automatically unlock on next restarts.</p>
<p>So open the VM directly from Proxmox web GUI, to land on the VNC interface that allows us to interact with the VM like a regular PC. You will see a prompt asking for the disk encryption passphrase. Something like: <code>Please unlock disk sda3_crypt:</code>. Introduce the passphrase to allow the VM continue booting.</p>
<p>Once the VM fully booted, it&rsquo;s time to configure the auto-unlock. Log in as root from the VNC, or access via SSH if you enabled it during Debian installation. Then, we&rsquo;ll install the required packages:</p>
<pre><code class="language-bash">apt install clevis clevis-luks clevis-tpm2 tpm2-tools clevis-initramfs
</code></pre>
<p>Now, let&rsquo;s find our encrypted root partition and configured it to automatically unlock on boot. First, let&rsquo;s search for it with <code>lsblk</code>:</p>
<pre><code class="language-bash">root@debian:~# lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT
NAME                     SIZE FSTYPE      MOUNTPOINT
sda                       32G
├─sda1                   976M vfat        /boot/efi
├─sda2                   977M ext4        /boot
└─sda3                  30.1G crypto_LUKS
  └─sda3_crypt          30.1G LVM2_member
    ├─debian--vg-root   28.5G ext4        /
    └─debian--vg-swap_1  1.6G swap        [SWAP]
sr0                      754M iso9660
</code></pre>
<p>In my case, I can see that <code>sda3</code> is the encrypted root partition, as it contains the <code>crypto_LUKS</code> tag. So let&rsquo;s configure it to auto-unlock with <code>clevis</code>. For that run:</p>
<pre><code class="language-bash">clevis luks bind -d /dev/sda3 tpm2 '{&quot;pcr_bank&quot;:&quot;sha256&quot;,&quot;pcr_ids&quot;:&quot;7&quot;}'
</code></pre>
<p>A quick explanation on the options I used:</p>
<ul>
<li><code>&quot;pcr_ids&quot;:&quot;7&quot;</code> means we want to use PCR ID 7. It will seal the LUKS key against the UEFI settings and the <a
  href="https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot"rel="noopener noreferrer" target="_blank">Secure Boot</a> policy using the TPM 2.0 chip. So if the UEFI or Secure Boot settings are modified, the TPM will compute different PCR values and decryption will fail. This gives protection against <a
  href="https://en.wikipedia.org/wiki/Evil_maid_attack"rel="noopener noreferrer" target="_blank">evil maid attacks</a>.</li>
<li><code>&quot;pcr_bank&quot;:&quot;sha256&quot;</code> means to use SHA256 as the algorithm to be used for the PCR bank. If not specified, it defaults to SHA1, which is less secure, and in my particular case for my Proxmox VE and host hardware, it&rsquo;s unsupported, so I would get an error if I don&rsquo;t specify it.</li>
</ul>
<p>So execute the <code>clevis</code> command, and when prompted for <code>existing LUKS password</code>, introduce the disk encryption passphrase. And that&rsquo;s it. Now, you can reboot your VM and it will automatically unlock the root partition and boot normally. I hope it helps!</p>
]]></content:encoded></item><item><title>How to use GMail as SMTP server for Proxmox VE 9</title><link>https://aalonso.dev/blog/2025/how-to-use-gmail-as-smtp-server-for-proxmox/</link><pubDate>Sun, 30 Nov 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-use-gmail-as-smtp-server-for-proxmox/</guid><category>proxmox</category><category>snippets</category><category>tutorial</category><description>If you want to configure email notifications from Proxmox quick and easy, here I tell you everything you need to know.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-use-gmail-as-smtp-server-for-proxmox/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Hello everyone! This time I just want to share with you how I configured sending email notifications from my <a
  href="https://aalonso.dev/blog/tags/proxmox/">Proxmox</a> server. This way, I&rsquo;ll get an email with different notifications, like backups, jobs and tasks.</p>
<p><strong>I&rsquo;m using a GMail account I created just for this single purpose,</strong> so the steps I&rsquo;ll share in the following paragraphs are using GMail SMTP servers and settings. However, the steps to use another SMTP server should be the same, just changing some key configuration values, so you can take it as a general guide with some care. Let&rsquo;s jump into it!</p>
<h2 id="configuring-postfix">
Configuring postfix
</h2>
<p>Postfix is a popular open-source Mail Transfer Agent (MTA) used to send, receive, and deliver email on Unix-like systems. That&rsquo;s the tool we&rsquo;ll use to send our mails from Proxmox. First, let&rsquo;s ensure we have all the dependencies we need. As root, run:</p>
<pre><code class="language-bash">apt install libsasl2-modules postfix-pcre
</code></pre>
<p>Then, let&rsquo;s modify some config files. First, let&rsquo;s indicate <code>postfix</code> to use GMail SMTP server.</p>
<pre><code class="language-ini"># Add the following settings to /etc/postfix/main.cf
# Update existing values if keys exist

relayhost = smtp.gmail.com:587
smtp_use_tls = yes
smtp_sasl_auth_enable = yes
smtp_sasl_security_options =
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_tls_CAfile = /etc/ssl/certs/Entrust_Root_Certification_Authority.pem
smtp_header_checks = pcre:/etc/postfix/smtp_header_checks

# Comment the following line out, or just delete it
# mydestination = $myhostname, localhost.$mydomain, localhost
</code></pre>
<p>Now, let&rsquo;s provide <code>postfix</code> the credentials for the server. We do it by generating a <code>/etc/postfix/sasl_passwd</code> file with the email address and the password of the GMail account, in the shape <code>&lt;server&gt;:&lt;port&gt; &lt;email_address&gt;:&lt;password&gt;</code>. The password will likely be your google account password, or an <a
  href="https://support.google.com/mail/answer/185833?hl=en"rel="noopener noreferrer" target="_blank">application password</a> if you set up 2FA.</p>
<pre><code class="language-ini"># File /etc/postfix/sasl_passwd
smtp.gmail.com:587 your_email_address@gmail.com:your_password
</code></pre>
<p>And the last file we&rsquo;ll create: provide correct SMTP headers to Postfix, so the sender details will appear correctly on your destination email client. This includes a custom name for the sender (in the example, just <code>My Hostname - Proxmox</code>, but it can be anything), and the sending email address between angle brackets (<code>&lt; &gt;</code>).</p>
<pre><code class="language-ini"># File: /etc/postfix/smtp_header_checks
+ /^From:.*/ REPLACE From: &quot;My Hostname - Proxmox&quot; &lt;your_email_address@gmail.com&gt;
</code></pre>
<p>Finally, we need to set the correct permissions to the files, update postfix maps, and restart postfix.</p>
<pre><code class="language-bash">chmod 600 /etc/postfix/sasl_passwd
postmap /etc/postfix/sasl_passwd

chmod 600 /etc/postfix/smtp_header_checks
postmap /etc/postfix/smtp_header_checks

systemctl restart postfix.service
</code></pre>
<p>And that&rsquo;s all for postfix!</p>
<h2 id="testing-it">
Testing it
</h2>
<p>To test the mail notifications, form Proxmox VE GUI, go to <em>Datacenter &gt; Notifications</em>. There should be an existing <em>&ldquo;Notification Target&rdquo;</em> of type <code>sendmail</code> to notify the <code>root</code> user in its configured email address (set up during ProxmoxVE installation). By selecting that notification target and clicking on <em>&ldquo;Test&rdquo;</em> button we should be able to test it.</p>
<div
  class="border-l-4 border-cyan-500 dark:border-cyan-400 bg-cyan-50 dark:bg-cyan-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-cyan-800 dark:text-cyan-200">
    

    Note
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-cyan-900 dark:text-cyan-100">
    If it doesn&rsquo;t work on the first try, test it in an incognito window in your browser. I have find Proxmox VE to sometimes cache some settings and weird-behaving after changes with the CLI.
  </div>
</div>

<p>At this point you should get a test email on your inbox. If it&rsquo;s not there, although it should not happen because we are using GMail SMTP server which have a good reputation, take a look at your SPAM folder, just in case such an automated test email triggered some SPAM rules. Also, if you are using the same email address as the sender and receiver, it might not appear in your inbox.</p>
<h2 id="alternative-approach">
Alternative approach
</h2>
<p>I cannot verify it because I didn&rsquo;t try this way, but it should be possible to make the same configuration we did for Postfix to use GMail SMTP server just from the Proxmox VE web GUI. For that, you should only need to add a new <em>Notification Target</em> of type <code>SMTP</code>. But as said, I didn&rsquo;t try it, so take it with a pinch of salt.</p>
]]></content:encoded></item><item><title>How to automatize your backups with Systemd cronjobs, with logs and health-checks</title><link>https://aalonso.dev/blog/2025/how-to-automatize-your-backups-with-systemd-cronjobs-with-logs-and-health-checks/</link><pubDate>Sat, 25 Oct 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-automatize-your-backups-with-systemd-cronjobs-with-logs-and-health-checks/</guid><category>linux</category><category>snippets</category><category>tutorial</category><description>I'll show you how to take advantage of Systemd capabilities to schedule backups tasks with logs and integrate Healthchecks.io to ping us on failure.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-automatize-your-backups-with-systemd-cronjobs-with-logs-and-health-checks/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Hello everyone! Today I wan&rsquo;t to share with you all how I set up my backup scripts/tasks in Linux, with the help of <code>Systemd</code>. As most of you might know if you have more than a few months on using Linux, <a
  href="https://systemd.io/"rel="noopener noreferrer" target="_blank">Systemd</a> provides a system and service manager that runs as PID 1 and starts the rest of the system. Is the standard in most Linux distributions out there, including Debian based ones (the most used on servers worldwide).</p>
<p>We can take advantage of its capabilities to implement <strong>robust cron jobs</strong> that run our backup tasks or scripts, and we will also get <strong>out of the box logs management</strong> via <code>journalctl</code>. Let&rsquo;s do it!</p>
<h2 id="a-working-duo-service-and-timer">
A working duo: service and timer
</h2>
<p>We need two <code>systemd</code> pieces to make this work: a systemd <code>service</code>, that will be the one in charge of executing the task or script, and a systemd <code>timer</code>, that is the one triggering the execution of the service on a schedule. Let&rsquo;s take a quick look to both of them and I&rsquo;ll comment them later. As an example, during the rest of the article, I will use a simple <code>rsync</code> command as a backup tool.</p>
<pre><code class="language-systemd"># File under /etc/systemd/system/rsync-backup.service
[Unit]
Description=Rsync backup
After=network.target

[Service]
Type=oneshot
ExecStart=/root/.local/backup.sh
</code></pre>
<pre><code class="language-systemd"># File under /etc/systemd/system/rsync-backup.timer
[Unit]
Description=Timer to make a rsync backup every Monday

[Timer]
OnCalendar=Mon 02:30:00
Persistent=true

[Install]
WantedBy=timers.target
</code></pre>
<p>So we defined our service <code>rsync-backup.service</code> that executes the actual backup script (<code>/root/.local/backup.sh</code>), and a timer <code>rsync-backup.timer</code> that ensures the service is executed every Monday at 2:30 AM. To enable the timer, we must execute the following commands:</p>
<pre><code class="language-bash">systemctl daemon-reload
systemctl enable --now rsync-backup.timer # Chose your real timer name here!
</code></pre>
<p>Now, the timer should be visible if we execute <code>systemctl list-timers</code> and the end of the table shown by that command.</p>
<p>If you want to manually run the backup at an arbitrary time, you could just execute the actual backup script under <code>/root/.local/backup.sh</code> (in this particular example, but I recommend you to do <code>systemctl start rsync-backup.service</code> instead, so this way, we get the log management with <code>journalctl</code> out of the box.</p>
<p>Finally, note that the service type is <code>oneshot</code>. This means, the service will execute the script under <code>ExecStart</code> and finish once the script exits. During that time, if you check the service with <code>systemctl status</code> it will show as &ldquo;activating&rdquo;. You might be tempted to use <code>simple</code> type instead of <code>oneshot</code>. The differences are mostly on parallel executions when multiple <code>ExecStart</code> are added, with doesn&rsquo;t matter for our use case, but here is a <a
  href="https://stackoverflow.com/a/39050387/13370153"rel="noopener noreferrer" target="_blank">nice explanation from Stack Overflow</a> in case you are curious.</p>
<h2 id="adding-health-checks">
Adding health-checks
</h2>
<p>How do we add health checks then? I use <a
  href="https://healthchecks.io/"rel="noopener noreferrer" target="_blank">Healthchecks.io</a> for that, which is a simple and free to use (up to 20 jobs) service that works like a gem. I&rsquo;m in love with it. How it works is simple: they provide an endpoint you must hit with <code>curl</code> or whatever to indicate your job executed. You can even provide more info like how long did the execution took, or even any possible exit error code.</p>
<p>I won&rsquo;t guide on how setup the check in <a
  href="https://healthchecks.io/"rel="noopener noreferrer" target="_blank">Healthchecks.io</a> itself, as I find the tool very straightforward and they have their own support articles in case you need them. So let&rsquo;s jump directly to an example of a health check configured there for our backup script.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-automatize-your-backups-with-systemd-cronjobs-with-logs-and-health-checks/healthchecks.png"
    alt="Healthcheck.io example configuration"width="2748"height="308"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-automatize-your-backups-with-systemd-cronjobs-with-logs-and-health-checks/healthchecks.png"
    data-alt="Healthcheck.io example configuration"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>A quick explanation on the configuration shown on the image:</p>
<ul>
<li>We defined the name of the task in <a
  href="https://healthchecks.io/"rel="noopener noreferrer" target="_blank">Healthchecks.io</a> as &ldquo;Rsync-backup&rdquo;. The check mark is green because I already did my first health check while testing my script.</li>
<li>The ping URL is the one we must hit with <code>curl</code>. It&rsquo;s a random UUID they will provide to you.</li>
<li>Now, in the integrations, is where we setup how we want to be notified on errors. I earlier configured it to notify me via email and telegram.</li>
<li>Then, we set when the cron is expected to run, and a grace time period. In this example, I indicated the cron must run Mondays at 2:30 AM (<code>30 2 * * 1</code> in <a
  href="https://crontab.guru/#30_2_*_*_1"rel="noopener noreferrer" target="_blank">crontab language</a>), and that it should not last more than 30 minutes from that time to ping. Therefore, if 30 minutes past 2:30 AM on Monday a ping has not being received, we&rsquo;ll be notified on our configured channels.</li>
<li>Finally, there are some quick metric about the last ping (9 hours ago in my case) and how long did the script execution took (7 minutes, more on this next). This will be populated after your first ping.</li>
</ul>
<p>Nice! So we configured <a
  href="https://healthchecks.io/"rel="noopener noreferrer" target="_blank">Healthchecks.io</a> and enabled our first health check. How do we integrate it in our backup task? If you remember, we were using the systemd service to execute a backup script <code>/root/.local/backup.sh</code>. Let&rsquo;s take a look to it:</p>
<pre><code class="language-bash">#! /usr/bin/env bash

HEALTHCHECKS_URL=https://hc-ping.com/1a2fdfaf-cbf7-4c37-8d87-e7e013e27e3b

curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL/start
rsync -aAHXv --delete /etc /mnt/backup
curl -fsS -m 10 --retry 5 -o /dev/null $HEALTHCHECKS_URL/$?
</code></pre>
<p>A simple script as you can see. What this does is hitting our health check endpoint with <code>start</code> path param, which indicates <a
  href="https://healthchecks.io/"rel="noopener noreferrer" target="_blank">Healthchecks.io</a> our execution started. This is useful for the duration metrics, but also for resetting the grace time period. Then, the actual backup task is executed: in this example, a simple <code>rsync</code> command. Finally, we hit our health check endpoint again, this time sending the exit code of the <code>rsync</code> command as path param. This way, we&rsquo;ll register a successful execution if sending a value of zero, and we&rsquo;ll register (and be notified) of an error if sending something different.</p>
<p>Note: don&rsquo;t forget to provide execution permissions to your script with <code>chmod +x</code> 😉</p>
<h2 id="checking-logs-after-executions">
Checking logs after executions
</h2>
<p>Did the task failed and you want to know what happened? Easy enough! With <code>systemd</code> we get logs management out of the box. Just run <code>journalctl -u rsync-backup.service</code> (or whatever name your backup service has) and all your logs will be there to be checked!</p>
<h2 id="whats-next">
What&rsquo;s next?
</h2>
<p>Due to <code>Systemd</code> strong capabilities, we could expand and improve the above service and timer, like adding retries in case of errors. There is so much things we can do with Systemd, so as always on Linux, hack and experiment to find your way!</p>
]]></content:encoded></item><item><title>How to upload and renew certificates on both Synology DSM and Proxmox VE</title><link>https://aalonso.dev/blog/2025/how-to-upload-and-renew-certificates-on-both-synology-dsm-and-proxmox-ve/</link><pubDate>Sat, 27 Sep 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-upload-and-renew-certificates-on-both-synology-dsm-and-proxmox-ve/</guid><category>linux</category><category>proxmox</category><category>security</category><category>synology</category><description>After explaining how to set your own CA in last article, here I'll explain how to renew and upload those certificates in Synology DSM, Proxmox VE and Caddy.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-upload-and-renew-certificates-on-both-synology-dsm-and-proxmox-ve/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>In the last article, I wrote <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/">how to create your own Certificate Authority to provide certs for your self-hosted services</a>, which is very useful when you have tons of servers, VMs and services deployed in your homelab, as I do. The backbone of my homelab is a <a
  href="https://aalonso.dev/blog/tags/synology/">Synology</a> NAS server and a <a
  href="https://aalonso.dev/blog/tags/proxmox/">Proxmox</a> cluster, along with some docker services here and there. I&rsquo;m using reverse proxies to access my docker containers with HTTPS: caddy for the ones hosted on Proxmox, and the built-in reverse-proxy from DSM for those hosted on the Synology.</p>
<p>Today, I&rsquo;ll show you particularities of each platform, and how to upload and renew existing certificates in Synology DSM, Proxmox Ve and Caddy; while maintaining the trust in the new certs thanks to the CA we created in the previous article. Let&rsquo;s go!</p>
<h2 id="renewing-and-updating-certs-in-synology-dsm">
Renewing and updating certs in Synology DSM
</h2>
<p>As everything with Synology, we can renew the certificate directly from DSM. Following are the instructions for DSM 7.2, and the official instructions from Synology Knowledge Center can be found <a
  href="https://kb.synology.com/en-us/DSM/help/DSM/AdminCenter/connection_certificate"rel="noopener noreferrer" target="_blank">here</a>.</p>
<ol>
<li>Go to <strong>Control Panel</strong> &gt; <strong>Security</strong> &gt; <strong>Certificate</strong>.</li>
<li>Select the desired certificate.</li>
<li>Select <strong>Renew certificate</strong> from the <strong>Action</strong> drop-down menu, and click <strong>Next</strong>. A new private key and certificate signing request will be created.</li>
<li>Click <strong>Renew certificate</strong> to retrieve your new private key and certificate signing request (CSR). You can use the new signing request to reapply for another certificate authority signed certificate.</li>
</ol>
<p>Then, you can <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#using-the-ca-to-generate-a-new-certificate-from-the-csr">use that CSR to renew your certificate using your own CA</a>.</p>
<h2 id="renewing-and-updating-certs-in-proxmox-ve">
Renewing and updating certs in Proxmox VE
</h2>
<p>In contrast with Synology, Proxmox VE doesn&rsquo;t have a built in mechanism to generate a CSR for certificate renewal, and we must do it manually:</p>
<ol>
<li>On a terminal, <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#requesting-the-ca-a-new-certificate-for-our-service">generate a new private key and a CSR</a>.</li>
<li>Then, <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#using-the-ca-to-generate-a-new-certificate-from-the-csr">use that CSR to issue a new certificate</a>.</li>
<li>Finally, import the new generated certificate and the private key to Proxmox VE by going in the webgui to <strong>Node name</strong> &gt; <strong>Certificates</strong> &gt; <strong>Upload Custom Certificate</strong>.</li>
<li>The previous old certificate should be overwritten. If not, you can manually delete it by selecting it and the clicking on <strong>Delete Custom Certificate</strong>.</li>
</ol>
<p>Now, just reload the Proxmox VE webgui and the new cert will be in use.</p>
<h2 id="renewing-and-updating-certs-in-a-web-server-like-caddy-and-nginx-or-apache">
Renewing and updating certs in a web server like Caddy (and Nginx or Apache)
</h2>
<p>Caddy requires to be configured manually, usually via an SSH session, but the steps are very similar to what we already know:</p>
<ol>
<li>As with Proxmox VE, on a terminal, <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#requesting-the-ca-a-new-certificate-for-our-service">generate a new private key and a CSR</a>.</li>
<li>Then, <a
  href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/#using-the-ca-to-generate-a-new-certificate-from-the-csr">use that CSR to issue a new certificate</a>.</li>
<li>Now, SSH into your caddy server, and check the configuration at <code>/etc/caddy/Caddyfile</code> to check from where is the server is reading the certs from. You will do the same for Apache or Nginx by looking to their respective configuration files.</li>
<li>In my case, in caddy, they are being read from <code>/etc/caddy/certs/</code> as I can read in the configuration:</li>
</ol>
<pre><code class="language-text">tls /etc/caddy/certs/server.crt /etc/caddy/certs/server.key
</code></pre>
<ol start="5">
<li>So just replace those files with the key and cert newly generated from the CA.</li>
<li>Don&rsquo;t forget to assign the correct permissions to both files:</li>
</ol>
<pre><code class="language-bash">chmod 644 /etc/caddy/certs/server.crt
chmod 600 /etc/caddy/certs/server.key
chown -R caddy:caddy /etc/caddy/certs/*
</code></pre>
<ol start="7">
<li>Finally, restart or reload your webserver to take the new certificates with <code>systemctl reload caddy</code> or <code>caddy reload</code> depending on how you installed it.</li>
</ol>
]]></content:encoded></item><item><title>How to create your own Certificate Authority to provide certs for your self-hosted services</title><link>https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/</link><pubDate>Thu, 28 Aug 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/</guid><category>linux</category><category>proxmox</category><category>security</category><category>synology</category><description>I'll show you how to create your own Certificate Authority (CA) to sign your own certificates, trust them, renew them, and use them in proxmox and Synology.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-create-your-own-certificate-authority-to-provide-certs-for-your-self-hosted-services/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><h2 id="some-context">
Some context
</h2>
<p>In the last years, my homelab has grown exponentially. I started with a Raspberry Pi zero back in University, just as a playground with nothing special in there, and nowadays it consists of a <a
  href="https://aalonso.dev/blog/tags/synology/">Synology</a> NAS server, a four Raspberry PI 4 cluster, and a <a
  href="https://aalonso.dev/blog/tags/proxmox/">Proxmox</a> cluster with two nodes backed by some Beelink Mini PCs. And of course, all of this hardware serves multiple self-hosted services that are currently very integrated in my daily workflow and other aspects of my life.</p>
<p>Most of my services are only accessible from my LAN, and I want them to stay like that. I can access them via their local IP <code>192.168.X.X</code>, or using a local domain name like <code>myservice.home.arpa</code>. And of course, I would like to access them via secure HTTPS connections, just in case. I think nowadays is it possible to use auto-managed Let&rsquo;s encrypt certificates with those, either using Cloudflare tunneling, Tailscale, or other tools. But for me personally, using your own CA for self-signing certificates is much easier to setup, and also more fun. At the end of the day, those are local services on my homelab, so I don&rsquo;t need complex solutions, and I want to learn and experiment.</p>
<h2 id="why-creating-our-own-ca">
Why creating our own CA?
</h2>
<p>I also want, for the services I use the most, to keep things stable and avoid service interruptions, failures, and cumbersome manual tasks. When you generate a self-signed certificate, you must manually trust it in your client devices. What happens when you have N services? Without your own CA, you have N different certificates, and you must trust the N different certificates into your M different devices. And each time one of them expires and you generate a new one, you must trust it again.</p>
<p>This problem is already solved, that is one of the reasons why Certificate Authorities (CA) exist! If you create your own CA and use it to generate and sign all of your self-signed certificates, you only need to trust the CA, and that&rsquo;s it!</p>
<ul>
<li>So an existing certificate expires and you renew it? No problem! Your devices already trust it.</li>
<li>So you need to add a new IP or a new domain name to an existing service certificate? No problem! You won&rsquo;t need to share and trust the new certificate elsewhere.</li>
</ul>
<p>So, how do we achieve this?</p>
<h2 id="creating-our-own-ca">
Creating our own CA
</h2>
<p>We can create our own CA with <code>openssl</code> from the terminal. The commands I share here works in UNIX like system like Linux and MacOS. I&rsquo;m not sure if they will work as they are in Windows, but shouldn&rsquo;t be so much different, and the main concepts are the same.</p>
<p>First what comes first. Let&rsquo;s generate the private key for our CA. I recommend using a minimum length of 4096 bytes.</p>
<pre><code class="language-bash">openssl genrsa -out rootCA.key 4096
</code></pre>
<p>Now, let&rsquo;s use it to generate the root certificate for our CA. I&rsquo;ll set an expiration date in ten years from now, but feel free to use whatever value you prefer.</p>
<pre><code class="language-bash">openssl req -x509 -new -key rootCA.key -sha256 -days 3650 -out rootCA.pem
</code></pre>
<p>It will ask for the certificate data, I usually only fill the <code>Country Name</code> (<code>ES</code> in my case) and the <code>Organization Name</code> and <code>Common Name</code> with the same value of <code>My own CA</code>.</p>
<p>Congratulations, now you have your own CA! From now on, we will use this newly generated root certificate and key to generate a sign any certificate we need for our homelab. Keep those safe and don&rsquo;t lose them! I usually store them as a <code>.zip</code> into my 1Password vault, along with the <code>rootCA.srl</code> file (we&rsquo;ll generate it later). You can use any password manager or store them as an encrypted <code>.zip</code> file or whatever in any public cloud or hosting service you usually use.</p>
<p>Now, as explained earlier, you only need to trust the CA root certificate, and all certificates generated from it will be automatically trusted. Every device differs a little bit, but I&rsquo;ll show you next how to trust it on Apple devices.</p>
<h3 id="trusting-the-ca-root-certificate-in-iphone-ipad-and-mac">
Trusting the CA root certificate in iPhone, iPad and Mac
</h3>
<p><strong>Trusting the CA root certificate in MacOS</strong> is fairly easy. Just double click on the <code>rootCA.pem</code> file and the Keychain app will open. Important! Don&rsquo;t confuse it with the new Passwords app introduced in (<em>I think</em>) MacOS 15 Sequoia. Once on the Keychain app, look for it (it should appear under the <code>Common Name</code>, in my case <code>My own CA</code>), double click again, and in the window it opens select &ldquo;Trust&rdquo; &gt; &ldquo;When using this certificate&rdquo; &gt; &ldquo;Trust always&rdquo;.</p>
<p><strong>On iPhone and iPad</strong> it requires a little bit more of work. Currently tested on iOS and iPadOS versions 18. If in the future we have a <em>new iPadOS</em> more <em>computer like</em> things can differ a little. Anyway, just send the <code>rootCA.pem</code> to your device via Airdrop or whatever you prefer, and open it. It will ask you where do you want to install it. Select &ldquo;this iPhone&rdquo; or &ldquo;this iPad&rdquo;. Then, go to Settings and you&rsquo;ll see the new profile ready to install, just bellow your name from your Apple account. Click there, accept the warning about the cert being untrusty (we generated it) and install it. That&rsquo;s everything? Well, no. <strong>The cert is now installed, but Safari and other browsers like Chrome won&rsquo;t trust it yet.</strong> We need a last step. We must go to Settings &gt; General &gt; About &gt; (scroll down to the end) Certificate trust settings. There, you will see our new installed root certificate and a deactivated toggle. Flip the toggle, accept the new warning, and voilà.</p>
<h2 id="requesting-the-ca-a-new-certificate-for-our-service">
Requesting the CA a new certificate for our service
</h2>
<p>To generate certificates for our services, we need to create a <em>Certificate Signing Request</em> (CSR) for them. Think about it like the CA is the Government, the service is you, and you fill a form (the CSR) to request anything. Like any request, as you will do with your Government, you need to sign it. So first, we need to generate a private key for our service/server. This is the same as we did initially for the CA key.</p>
<pre><code class="language-bash">openssl genrsa -out server.key 4096
</code></pre>
<p>Now, we create and sign with the server key the CSR for our CA.</p>
<pre><code class="language-bash">openssl req -new -key server.key -out server.csr
</code></pre>
<p>Again, it will ask you some basic data for the future certificate. Like before, I just set the <code>Country Name</code>, <code>Organization Name</code> (in my case to <code>My Homelab</code>) and <code>Common Name</code> (in my case, to the service or server name, like <code>My Service</code>). The rest of fields, including challenge password, are left empty.</p>
<h2 id="using-the-ca-to-generate-a-new-certificate-from-the-csr">
Using the CA to generate a new certificate from the CSR
</h2>
<p>For better security and improved support, we&rsquo;ll use an X.509 extensions file to define some parameters for the new certificate. Create a file <code>v3.ext</code> and update the values under <code>[alt_names]</code> with your own:</p>
<pre><code class="language-txt">authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names

[alt_names]
DNS.1 = myservice.home.arpa
IP.1 = 192.168.1.123
</code></pre>
<p>Is it possible to include more DNS names or IPs. Just add extra entries like <code>DNS.2</code> or <code>IP.2</code> and so on. Also, it&rsquo;s possible to use wildcards to generate a certificate valid for all subdomains, like <code>DNS.2 = *.myserver.home.arpa</code>. If you use wildcards, ensure you are using it under the <code>DNS.2</code> entry, and leave the <code>DNS.1</code> for the root domain like <code>DNS.1 = myserver.home.arpa</code>.</p>
<p>Next, let&rsquo;s use our CA to generate a new certificate from the CSR, using the above extensions file. I will use SHA-256 to sign it and set a validity period of 825 days, as <a
  href="https://support.apple.com/en-us/103769"rel="noopener noreferrer" target="_blank">apple won&rsquo;t trust certificates longer than that</a>, and me and my family use Apple devices. Feel free to set a longer validity period (like another 10 years) if this doesn&rsquo;t care to you.</p>
<pre><code class="language-bash">openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial \
        -out server.crt -days 825 -sha256 -extfile v3.ext
</code></pre>
<p>Now you will have two new files in your directory:</p>
<ul>
<li><code>server.crt</code> is the new certificate for your service/server that you can use along with the earlier generated <code>server.key</code>. Just upload them to your server or service and configure it to start using them.
<ul>
<li>I also wrote instructions on <a
  href="https://aalonso.dev/blog/2025/how-to-upload-and-renew-certificates-on-both-synology-dsm-and-proxmox-ve/">how to upload and renew certificates on both Synology DSM and Proxmox VE</a> in another article. Take a look if you use any of them, you might find it useful.</li>
</ul>
</li>
<li><code>rootCA.srl</code> stores the next serial number your CA will assign when signing a new certificate. Each time you sign a certificate with your CA, OpenSSL reads and updates this file, ensuring that every certificate has a unique serial number (which is required for security and validity). <strong>You must keep this <code>rootCA.srl</code> in the same directory as your <code>rootCA.key</code> and <code>rootCA.pem</code> every time you generate a new certificate with this CA. Deleting or losing this file can cause duplicate serial numbers, which can invalidate your CA or cause certificate management issues.</strong></li>
</ul>
<h2 id="renewing-server-certificates">
Renewing server certificates
</h2>
<p>To renew a server certificates, for example because it&rsquo;s near its expiry date, you will use a CSR again. You could theoretically reuse the same CSR you used for your initial certificate, and or the same server private key, but it&rsquo;s better for security to generate a new private key for the server and use it to create a new Certificate Signing Request.</p>
<pre><code class="language-bash">openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr
</code></pre>
<p>And then, sign it with the CA. This time, we do not use the <code>-CAcreateserial</code> option and instead relies on the existing <code>rootCA.srl</code> file with <code>-CAserial</code>.</p>
<pre><code class="language-bash">openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key \
        -CAserial rootCA.srl -out server.crt -days 825 -sha256 -extfile v3.ext
</code></pre>
<h2 id="renewing-ca-certificates">
Renewing CA certificates
</h2>
<p>And what about renewing the CA certificates without invalidating previous generated certs using it? If we want all existing certs to remain trusted, we just need to generate a new CA root certificate using the same private key we used the first time. In fact, the command is exactly the same:</p>
<pre><code class="language-bash">openssl req -x509 -new -key rootCA.key -sha256 -days 3650 -out rootCA.pem&quot;
</code></pre>
<p>And that&rsquo;s it. Simple, and you keep full control. The only <em>cons</em> if that you need to remember to update the certs before they expiring. It&rsquo;s a good idea to write you down a reminder somewhere that will notify you in time (I write myself a reminder in <a
  href="https://www.todoist.com/"rel="noopener noreferrer" target="_blank">Todoist</a> to notify 30 days prior expiration). If you want, you could also automatize it, by writing an small service, CLI or API that will receive a CSR and will provide new certs for you. Kinda like how Let&rsquo;s Encrypt works, but <em>homemade</em>.</p>
]]></content:encoded></item><item><title>What is the "Ship, Show, Ask" Git strategy</title><link>https://aalonso.dev/blog/2025/what-is-the-ship-show-ask-git-strategy/</link><pubDate>Sat, 26 Jul 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/what-is-the-ship-show-ask-git-strategy/</guid><category>agile</category><category>engineering</category><category>productivity</category><category>programming</category><description>In this article, I'll explain what is the "Ship, Show, Ask" git strategy, that I personally find very useful on mature teams.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/what-is-the-ship-show-ask-git-strategy/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Hello everyone! This month, I want to talk you about the <em>&ldquo;Ship, Show, Ask&rdquo;</em> git strategy. I use it with my teammates at <a
  href="https://cabify.com/"rel="noopener noreferrer" target="_blank">Cabify</a>, and I find it to be a wonderful way to maintain a good pace and individual productivity, while at the same time, ensuring a continuos code delivery and transparency and communication with the rest of the team.</p>
<p>Has you ever had a pull request or merge request blocked for days waiting for review? Do you find difficult for you or your team to review every piece of code in time? Then, <em>&ldquo;Ship, Show, Ask&rdquo;</em> could be a good fit for you or your team. Only note that, for this git strategy to work, two things must be truth in your team and your code:</p>
<ul>
<li><strong>You have a mature team,</strong> where everybody knows everybody and trust everybody (how they code, how they work).</li>
<li><strong>You have a good automatic test suite</strong> and checks/regression tests that are automatically run in the pipeline prior to deploying any new code.</li>
</ul>
<p>without the above, <em>&ldquo;Ship, Show, Ask&rdquo;</em> won&rsquo;t never work. Why? With this strategy, you won&rsquo;t always open a Pull/Merge Request. And when you do, you won&rsquo;t always wait for your colleagues to review it prior to merge. So yes, a mature team and good and sufficient automatic checks are a must.</p>
<p>Okay, after this disclaimer, let&rsquo;s dive into it!</p>
<h2 id="what-is-">
What is <em>&ldquo;Ship, Show, Ask&rdquo;</em>?
</h2>
<p><em>&ldquo;Ship, Show, Ask&rdquo;</em> is a branching strategy that combines the idea of creating Pull Requests with the ability to keep shipping changes quickly. It was <a
  href="https://martinfowler.com/articles/ship-show-ask.html"rel="noopener noreferrer" target="_blank">introduced by Rousan Wilsenach on Martin Fowler&rsquo;s blog.</a></p>
<p>The changes we make in the repository are categorised into three types:</p>
<ul>
<li><strong>Ship</strong>: Merged into the main branch without review.</li>
<li><strong>Show</strong>: Opens a Pull Request but merges immediately without review.</li>
<li><strong>Ask</strong>: Opens a Pull Request to discuss changes before merging.</li>
</ul>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ship-show-ask.png"
    alt="Ship Show Ask diagram"width="1920"height="1080"
    loading="eager"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ship-show-ask.png"
    data-alt="Ship Show Ask diagram"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>The three possible categories for your changes give the strategy its name. Now, let&rsquo;s see each of them in detail and why. When should you use each of them?</p>
<h2 id="ship">
Ship
</h2>
<p>Ship means <strong>making a change directly to the main branch.</strong> We don&rsquo;t expect code reviews, <strong>we go straight to production</strong> (though tests or checks will still run automatically by the CI before deployment to prevent errors).</p>
<p>The commit goes directly to production, bypassing the usual Pull Request.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ship.png"
    alt="Ship diagram"width="1515"height="455"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ship.png"
    data-alt="Ship diagram"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p><strong>Intended for:</strong></p>
<ul>
<li>Adding a small change using an established pattern.</li>
<li>Fixing a simple bug caused by an error.</li>
<li>Documentation updates.</li>
<li>Small code improvements based on team feedback.</li>
<li>Adding new tests to prevent errors.</li>
</ul>
<h2 id="show">
Show
</h2>
<p>Here, we use a Pull Request, but we don&rsquo;t expect manual code reviews. Instead, we rely on automated tests, coverage checks, and code validation without requiring another person&rsquo;s input. We create a Pull Request <strong>as a way to document the change and validate that automated tests pass</strong>, but don&rsquo;t wait for team feedback.</p>
<p>This doesn&rsquo;t mean conversations about the code won&rsquo;t happen. They&rsquo;ll just occur <em>after</em> merging. The goal is to keep work flowing forward with minimal blockers while still leaving room to discuss and improve development practices. The team is notified when a Pull Request is created, reviews it later, and provides any necessary feedback. The key is that the Pull Request remains easily identifiable and separate.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/show.png"
    alt="Show diagram"width="1920"height="1080"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/show.png"
    data-alt="Show diagram"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p><strong>Intended for:</strong></p>
<ul>
<li>Necessary bug fixes that leave a record for learning.</li>
<li>Small code improvements or refactors.</li>
<li>Adding new features following agreed structures.</li>
<li>New functionality with automated tests.</li>
</ul>
<h2 id="ask">
Ask
</h2>
<p>This category is similar to <em>Show</em>, but <strong>here we <em>do</em> wait for team feedback before merging.</strong> We do this because there&rsquo;s some uncertainty: maybe the solution is complex, we don&rsquo;t know how to implement it, or there are doubts. The goal is to keep the branch alive for as little time as possible to avoid blocking others.</p>
<p>With <em>Ask</em>, we expect the team to review the Pull Request before merging. <strong>We&rsquo;re not waiting for an approval, we&rsquo;re just starting a conversation to resolve uncertainty.</strong> This is important to remark: choosing <em>Ask</em> doesn&rsquo;t mean we&rsquo;re waiting for colleagues&rsquo; approval, but just to open a discussion before merging due to blockers or doubts. We may not need a full review, and if we do, we should clarify that.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ask.png"
    alt="Ask diagram"width="1920"height="1080"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/what-is-the-ship-show-ask-git-strategy/ask.png"
    data-alt="Ask diagram"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p><strong>Intended for:</strong></p>
<ul>
<li>Large pieces of code requiring assistance.</li>
<li>Doubts about implementation or code quality.</li>
<li>Uncertainty about what we&rsquo;re doing.</li>
<li>Waiting for external dependencies before merging.</li>
</ul>
<h2 id="the-rules-of-">
The Rules of <em>&ldquo;Ship, Show, Ask&rdquo;</em>
</h2>
<p>Naturally, as stated at the beginning of this article, following this strategy requires some ground rules:</p>
<ol>
<li>A good, robust, and sufficient automated test suite.</li>
<li>A reliable CI/CD system that ensures the main branch is always deployable, and that prevents unwanted errors in production.</li>
<li>Branches are as small as possible, short-lived, and always branched directly from main.</li>
<li>Trust in the team and strong development practices like pair programming, seniority, and above all, accountability. The developer decides their change&rsquo;s category. <em>With great power (merging your own Pull Requests) comes great responsibility (not breaking production).</em></li>
<li>The team has moved past individual ego, trusts each other, and accepts that the main branch doesn&rsquo;t need perfect code (as long as automated tests pass).</li>
</ol>
<h2 id="final-thoughts">
Final Thoughts
</h2>
<p>I want to close this article as I started it, defending all the good about this strategy. I believe the core idea behind it is to empower individuals, give the team autonomy, and treat everyone equally. It also speeds up development and enables continuous delivery. But achieving this requires a lot of trust, which not all teams may have initially. Because of that, <strong>being able to apply this inside a team is an achievement in its own</strong>, as it&rsquo;s a proof that the team has reached a mature state, without individual egos and with trust in each others. A truly sane work environment.</p>
<p>I&rsquo;m <strong>proud</strong> to be using it from nine to five Mondays through Fridays, and I&rsquo;m glad for my teammates. Keep it going!</p>
]]></content:encoded></item><item><title>How to make a reading progress top bar with both React and Vue</title><link>https://aalonso.dev/blog/2025/how-to-make-a-reading-progress-top-bar-with-both-react-and-vue/</link><pubDate>Sat, 28 Jun 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-make-a-reading-progress-top-bar-with-both-react-and-vue/</guid><category>snippets</category><category>tutorial</category><category>typescript</category><description>In this article, I share some code snippets to create a reading progress top bar like the one I have in my blog for both React and Vue.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-make-a-reading-progress-top-bar-with-both-react-and-vue/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Hello everyone, today I just want to share two code snippets for those that might find it useful. I have always like how the reading progress bar look in the blogs, so I build my own one for my own tech blog (this blog!). Each article in my blog has the same behavior, with a reading bar in the top of the page, the header, that fills as you scroll down. You can check how it works by scrolling down in this page.</p>
<p>My blog was initially built on top of Next.js with React, but later I moved to Nuxt.js and Vue, so I had to rewrite my component, and that&rsquo;s why I have both versions.</p>
<p>Okay, let&rsquo;s stop talking, here is the code!</p>
<h2 id="react-component">
React component
</h2>
<p>Here is the React component:</p>
<pre><code class="language-tsx">// File: src/components/ProgressBar.tsx
import React, { FC, useEffect, useRef } from &quot;react&quot;

type ProgressBarProps = {
  enabled?: boolean
}

const ProgressBar: FC&lt;ProgressBarProps&gt; = ({ enabled = false }) =&gt; {
  const progressBar = useRef&lt;HTMLDivElement&gt;(null)

  const updateProgressBar = () =&gt; {
    const winScroll = document.body.scrollTop || document.documentElement.scrollTop
    const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
    const scrolled = (winScroll / height) * 100
    if (progressBar &amp;&amp; progressBar.current) progressBar.current.style.width = scrolled + &quot;%&quot;
  }

  useEffect(() =&gt; {
    if (progressBar &amp;&amp; progressBar.current) progressBar.current.style.width = &quot;0&quot;
    if (enabled &amp;&amp; typeof window !== typeof undefined) {
      window.addEventListener(&quot;scroll&quot;, updateProgressBar)
      return () =&gt; window.removeEventListener(&quot;scroll&quot;, updateProgressBar)
    }
  }, [enabled])

  return (
    &lt;div className=&quot;w-full h-0.5 bg-transparent&quot;&gt;
      &lt;div className=&quot;w-0 h-0.5 bg-txt-color-bright&quot; ref={progressBar}&gt;&lt;/div&gt;
    &lt;/div&gt;
  )
}

export default ProgressBar
</code></pre>
<p>I was using it back then as a part of the header, but was only visible on the articles, like so:</p>
<pre><code class="language-tsx">// File: src/components/Header.tsx
import ProgressBar from &quot;./ProgressBar&quot;

const Header: FC = () =&gt; {
  return (
    &lt;header className=&quot;fixed top-0 w-full z-20&quot;&gt;
      &lt;nav className=&quot;border-b border-border-color&quot;&gt;{/*...*/}&lt;/nav&gt;
      &lt;ProgressBar enabled={router.pathname === ROUTES.article} /&gt;
    &lt;/header&gt;
  )
}

export default Header
</code></pre>
<h2 id="vue-component">
Vue component
</h2>
<p>And here is the current Vue version as the date of writing this article:</p>
<pre><code class="language-vue">&lt;!-- File: components/article-progress-bar.vue --&gt;
&lt;template&gt;
  &lt;div class=&quot;w-full h-0.5 bg-transparent pb-1&quot;&gt;
    &lt;div ref=&quot;progressBar&quot; class=&quot;w-0 h-0.5 bg-primary&quot; /&gt;
  &lt;/div&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
import { ref, onMounted, onUnmounted } from &quot;vue&quot;

const progressBar = ref&lt;HTMLDivElement | null&gt;(null)

const updateProgressBar = () =&gt; {
  const winScroll = document.body.scrollTop || document.documentElement.scrollTop
  const height = document.documentElement.scrollHeight - document.documentElement.clientHeight
  const scrolled = (winScroll / height) * 100
  if (progressBar.value) progressBar.value.style.width = scrolled + &quot;%&quot;
}

if (progressBar.value) progressBar.value.style.width = &quot;0&quot;

onMounted(() =&gt; {
  if (typeof window !== &quot;undefined&quot;) {
    window.addEventListener(&quot;scroll&quot;, updateProgressBar)
  }
})

onUnmounted(() =&gt; {
  if (typeof window !== &quot;undefined&quot;) {
    window.removeEventListener(&quot;scroll&quot;, updateProgressBar)
  }
})
&lt;/script&gt;
</code></pre>
<p>And again, I&rsquo;m using it directly in the header:</p>
<pre><code class="language-vue">&lt;!-- File: components/header-component.vue --&gt;
&lt;template&gt;
  &lt;header
    class=&quot;fixed inset-x-0 top-0 z-[1] px-4 sm:px-10 backdrop-blur-sm bg-base-100/80 dark:bg-base-100/80 max-w-6xl mx-auto&quot;
  &gt;
    &lt;nav class=&quot;py-2 md:py-6&quot;&gt;
      &lt;!-- ... --&gt;
    &lt;/nav&gt;
    &lt;ArticleProgressBar v-if=&quot;route.path.match(/^\/blog\/\d{4}\/[^/]+\/?$/)&quot; /&gt;
  &lt;/header&gt;
&lt;/template&gt;

&lt;script setup lang=&quot;ts&quot;&gt;
const route = useRoute()
&lt;/script&gt;
</code></pre>
<p>That&rsquo;s all. Short and to the point! I hope it helps you make your own reading progress bar for your site!</p>
]]></content:encoded></item><item><title>3-2-1 Backup strategy. What is it and why is it important</title><link>https://aalonso.dev/blog/2025/3-2-1-backup-strategy-what-is-it-and-why-is-it-important/</link><pubDate>Thu, 29 May 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/3-2-1-backup-strategy-what-is-it-and-why-is-it-important/</guid><category>hardware</category><category>security</category><category>synology</category><description>Today I'm here to explain what the 3-2-1 backup strategy is and why is important if you have your data hosted at home.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/3-2-1-backup-strategy-what-is-it-and-why-is-it-important/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>If you are like me, and you like to play around with hardware and computers, or just want more privacy for your data (all those important documents like tax reports, health documents, family embarrassing photos, etc.), you probably have your own cloud at home, typically with a <a
  href="https://en.wikipedia.org/wiki/Network-attached_storage"rel="noopener noreferrer" target="_blank">NAS</a>. Whether you&rsquo;ve built your own home NAS using TrueNAS, <a
  href="https://aalonso.dev/blog/tags/synology/">Synology</a>, or a Raspberry Pi with a bunch of external drives, one thing&rsquo;s certain: you care about your data. You&rsquo;ve probably invested time in organizing your media library, or you have important files you don&rsquo;t want to lose.</p>
<p>To really protect your data from any possible incident, you need to follow a backup strategy. And there is a particular backup strategy that fits great either for big corporations like Google or Microsoft as well as small individual users with a server at home like you and me: <strong>the 3-2-1 backup strategy.</strong></p>
<h2 id="but-im-already-protecting-my-data-with-raid-156">
But I&rsquo;m already protecting my data with RAID 1/5/6!
</h2>
<p>You are not! Redundant <a
  href="https://en.wikipedia.org/wiki/RAID"rel="noopener noreferrer" target="_blank">RAID</a> configurations (like RAID 1, RAID 5 and RAID 6) are not a backup strategy, just a way to avoid disruption in the typical and inevitable case of a disk stop working. RAID works by duplicating your data (RAID 1) between disks, or breaking it into parts and distributing it among multiple disks along with a way to recover missing parts from a parity disk (RAID 5) or disks (RAID 6).</p>
<p>This will avoid you losing any data if one disk (or two, if using RAID 6) breaks at the same time, but is not a proper backup strategy because you could lose your data in any of the following scenarios:</p>
<ul>
<li><strong>Multiple disks breaks at the same time.</strong> Even if it seems unlikely, this can really happens. For example, you bought all your disks at once, and they might be from the same manufacturing batch. IF that batch had a problem they can fail at the same time. Or you could get a power spike at home that breaks multiple disk at the same time.</li>
<li><strong>Your home or your NAS burns down</strong>. If your disks are ashes you won&rsquo;t be able to recover your data from them.</li>
<li><strong>Someone breaks into your home and steals your NAS</strong>. If you do not longer have the disk, you won&rsquo;t be able to recover the data from them either. If someone stealing your NAS and accessing your data concerns you, you might want to encrypt the disk at rest. <a
  href="https://kb.synology.com/en-global/WP/Synology_Volume_Encryption_White_Paper/2"rel="noopener noreferrer" target="_blank">Synology has Volume Encryption for that</a>, which I set in my Synology.</li>
<li>You get ransomware and your disk are encrypted and all your data gone for good. You need to start over.</li>
</ul>
<p>Okay, so now that we are in the same page and we understand why a good backup strategy is important, let&rsquo;s move on.</p>
<h2 id="what-is-3-2-1-backup-strategy">
What is 3-2-1 backup strategy?
</h2>
<p>Luckily for all of us, 3-2-1 backup strategy is very easy to understand and follow.</p>
<ul>
<li>Keep 3 copies of your data.</li>
<li>Keep those copies on at least 2 different storage medias.</li>
<li>Keep 1 copy off-site.</li>
</ul>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/3-2-1-backup-strategy-what-is-it-and-why-is-it-important/3-2-1-backup-rule.png"
    alt="3-2-1 Backup strategy diagram"width="798"height="443"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/3-2-1-backup-strategy-what-is-it-and-why-is-it-important/3-2-1-backup-rule.png"
    data-alt="3-2-1 Backup strategy diagram"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>You could think at first glance that this is difficult and expensive for small users, and that is more oriented to business system, but not at all. Let me show you how I approach 3-2-1 backup strategy a home for my Synology NAS!</p>
<h3 id="keep-3-copies-of-your-data">
Keep 3 copies of your data
</h3>
<p>You must keep three copies of your data, not necessarily three backups! I have.</p>
<ul>
<li>1 copy on my NAS. The data itself as we are using it day to day
<ul>
<li>Additionally, I have snpashots of most important folders, that are took every night at midnight using <a
  href="https://www.synology.com/en-global/dsm/feature/snapshot_replication"rel="noopener noreferrer" target="_blank">Synology Snapshot Replication</a> and taking advantage of my btfs volumes.</li>
</ul>
</li>
<li>1 copy on an external NAS grade hard drive. An old drive I was using on the NAS before. Again, every day at 2:00 AM a backup of the data is made to that drive via USB using <a
  href="https://www.synology.com/en-global/dsm/feature/hyper_backup"rel="noopener noreferrer" target="_blank">Hyper backup</a></li>
<li>1 copy on the cloud, specifically on <a
  href="https://c2.synology.com/en-global/storage/overview"rel="noopener noreferrer" target="_blank">Synology C2</a>, where I create a backup every day at 4:00 AM using also Hyper Backup.</li>
</ul>
<p>So you see, three copies! Four if you take into account the snapshots, that I particularly don&rsquo;t consider myself a backup, but more a quick undo for human errors.</p>
<h3 id="keep-those-copies-on-at-least-2-different-storage-medias">
Keep those copies on at least 2 different storage medias
</h3>
<p>I also do!</p>
<ul>
<li>1 copy is in the NAS, along with the snapshots.</li>
<li>1 copy is in an external drive. Along it could be debatable if it a different media, as it&rsquo;s also a mechanical hard drive, and very similar to those I&rsquo;m using on the NAS. And in my particular case, because the USB is always plugged to the Synology NAS for convenience.</li>
<li>But if any doubt, having the copy on Synology C2 makes it a totally different storage media.</li>
</ul>
<h3 id="keep-1-copy-offsite">
Keep 1 copy offsite
</h3>
<p>And bingo! The backup I have in the cloud, using Synology C2, prepares me for any major disaster that could happen. Obviously, this is more like a last option scenario. Likely, I should be able to restore my data form any other of my backups (snapshots or the external drive). The cloud/offsite options is only for major disasters.</p>
<p>Obviously, this is the more expensive to maintain backup of all I have, and it requires for my case, ~250€ every year for 3 TB with data deduplication. I&rsquo;m storing there 5,5 TB worth of real data, compressed and deduplicated, in several backups versions with a custom retention policy. I&rsquo;m using for that data around a 80% of the 3 TB.</p>
<h2 id="follow-the-3-2-1-backup-strategy-your-own-way">
Follow the 3-2-1 backup strategy your own way
</h2>
<p>You don&rsquo;t need to follow my example! The above was just an illustration of how I approach it. I understand having a cloud provider to backup terabytes of data is not cheap, and sometimes this is a stopper for a lot of people. But remember, you should think on what kind of data you have, and what is important to you, and what do you want to protect and what not. Perhaps, you have tons of movies, that you can always download or ask a friend if you lose them. Or you just want to backup some important PDFs and a few photos. You can always upload those, probably a few GBs, to regular cloud provides like Google Drive, One Drive, iCloud or Proton from a synched folder in your server or computer.</p>
<p>The only thing important is tho follow the 3-2-1 rules, as we saw above. Just three rules that should protect your data of any incident you could face. How do you apply them is up to you, as long as you fulfill all of them for the data you never ever want to lose.</p>
]]></content:encoded></item><item><title>How to clone a disk image to a Synology NAS over LAN with Clonezilla</title><link>https://aalonso.dev/blog/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/</link><pubDate>Sat, 26 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/</guid><category>linux</category><category>synology</category><category>tutorial</category><category>windows</category><description>Step-by-step instructions on how to create or clone a disk image from a PC to a Synology NAS server using Clonezilla, transmitting the image over LAN.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>After clean installing my PC with both Linux Mint and Windows 11, and after the initial configuration to my taste and installing my essential apps, I decided I wanted to create a disk image, so next time (or I really mess up) won&rsquo;t take so much time. For that task, I decided to use the old and reliable tool that is the defacto standard for disk imaging and barebones restores among IT techs: <a
  href="https://clonezilla.org/"rel="noopener noreferrer" target="_blank">Clonezilla</a>.</p>
<p>As you might already now, I also have a <a
  href="https://aalonso.dev/blog/tags/synology/">Synology NAS</a> server at home. So I wanted to store there the disk images, as I already have my NAS setup with proper redundancies and backups strategies.</p>
<p>This article intends to be very brief and straight forward. Just a reminder of what I need to do the next time I want to image a disk, and I hope it will also help other people with the same problem. First, let&rsquo;s focus on what we need to setup in the Synology for this to work, then, we&rsquo;ll take a look on the actual step-by-step instructions on Clonezilla.</p>
<h2 id="preparing-the-synology-nas-sftp-server">
Preparing the Synology NAS SFTP server
</h2>
<p>First, in order to be able to send the cloned image directly from Clonezilla to the Synology NAS over the network, we need to enable both SSH and SFTP server in Synology. This is just checking a checkbox in the DSM Control Panel app. <a
  href="https://kb.synology.com/en-af/DSM/help/DSM/AdminCenter/system_terminal"rel="noopener noreferrer" target="_blank">SSH settings</a> are under Control Panel &gt; Terminal &amp; SNMP &gt; Terminal, while <a
  href="https://kb.synology.com/en-af/DSM/help/DSM/AdminCenter/file_ftp_sftp"rel="noopener noreferrer" target="_blank">SFTP settings</a> are under Control Panel &gt; File Services &gt; FTP.</p>
<p>Apart from that, I personally created a shared folder called <code>Clonezilla</code> (with capital letter &lsquo;C&quot;). I configured it as a compressed folder and disabled recycle bin, as I would store cold data (disk image backups) and I don&rsquo;t need recycle bin functionality (I already have <code>btrfs</code> snapshots and other backups if I accidentally delete it). I gave my own user (the same I would use to connect to the Synology from Clonezilla) read and writte permissions. The rest of the settings are default, you don&rsquo;t need to give &ldquo;NFS permissions&rdquo; to the folder or any other kind of permissions.</p>
<p>Now, let&rsquo;s start with the proper disk imaging.</p>
<h2 id="imaging-the-disk-with-clonezilla">
Imaging the disk with Clonezilla
</h2>
<p>First, we obviously need to prepare a bootable USB with Clonezilla. I won&rsquo;t enter into too much detail here. Clonezilla have their own <a
  href="https://clonezilla.org//clonezilla-live.php"rel="noopener noreferrer" target="_blank">instructions</a> on this, and is the same that when installing Linux or Windows.</p>
<h3 id="initial-steps-and-synology-connection">
Initial steps and Synology connection
</h3>
<p>So we boot up the Clonezilla Live Image (I selected the default option <code>Clonezilla Live (VGA 800x600)</code>) and then we:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/clonezilla-grub.png"
    alt="Booting up Clonezilla"width="1024"height="288"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/clonezilla-grub.png"
    data-alt="Booting up Clonezilla"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<ol>
<li>Configure the language and keyboard (I&rsquo;ll obviously use English language options for this guide).</li>
<li>Start Clonezilla.</li>
<li>Select <code>device-image</code> mode.</li>
<li>Select <code>ssh_server</code> as destination of the backup.</li>
<li>To configure the network, I just relied on the <code>dhcp</code> auto option.</li>
<li>Then, introduce the local IP of your Synology NAS. Probably something like <code>192.168.1.X</code></li>
<li>Introduce the SSH port of your Synology SSH server. By default it&rsquo;s <code>22</code>.</li>
<li>Then, introduce your Synology username.</li>
<li>Now, indicate the folder where we&rsquo;ll store the backed up disk image. In my case, I put <code>/Clonezilla</code>
<ul>
<li><strong>IMPORTANT:</strong> you don&rsquo;t need to indicate the volume to your folder or full Linux path in the Synology. Trying to provide a path like <code>/volume1/Clonezilla</code> will end in a &ldquo;No such file or directory error&rdquo; when connecting to the server in the next step.</li>
<li>Also, ensure you allow to connect via SSH <strong>using password</strong> to your Synology NAS server. Otherwise, if you only allow access via SSH keys, Clonezilla won&rsquo;t be able to connect to the server and you will get an error.</li>
</ul>
</li>
<li>Finally, introduce your user password for the SSH connection to establish.</li>
</ol>
<h3 id="actual-backup-disk-image-options">
Actual backup, disk image options
</h3>
<p>Once the connection is established, it&rsquo;s time to start with the disk imaging. I personally stick with the <code>Beginner</code> mode, as I don&rsquo;t need anything fancy and I want to avoid possible mistakes. Here is how it follows:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/beginner-mode.png"
    alt="Selecting beginner mode in Clonezilla"width="1024"height="250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-clone-a-disk-image-to-synology-nas-over-lan-with-clonezilla/beginner-mode.png"
    data-alt="Selecting beginner mode in Clonezilla"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<ol start="0">
<li>As said, select <code>Beginner</code> mode.</li>
<li>I want to clone the full disk, so I select <code>savedisk</code> option, but it would also be possible to just image partitions with <code>saveparts</code> option.
<ul>
<li>Note: I will continue this guide with <code>savedisk</code>, and I&rsquo;m unsure how it changes if only cloning partitions.</li>
</ul>
</li>
<li>Select what physical drive you want to image (in case you have more than one).</li>
<li>Give a name to the backup, or accept the default one provided by Clonezilla.</li>
<li>In my case, the Linux Mint partition was LUKS encrypted, so it gave me an option to introduce the LUKS password and clone the disk in an efficient manner regarding final image size.</li>
<li>Regarding compression, I selected <code>z9p</code> that overall, as far as I googled, seems to have better performance, both in final image size and compression speed. <code>z1p</code> seems more like a legacy options for very old hardware.</li>
<li>Then, for some systems like Linux, it asks you if you want to automatically check/repair the <strong>source filesystem</strong> prior to making the image. If you are sure the source filesystem works fine, you can skip it with <code>sfsck</code> as I did. Otherwise, you migh want to use another option.</li>
<li>Once the image is created, you can check if it&rsquo;s restorable. I heavily encourage that, as it&rsquo;s very quick and will save your ass in the future if you need the image and then you realize it doesn&rsquo;t work. Checking the image now will prevent problems in the future, as any possible problem will be highlighted and you can take action now and be ready for the future.</li>
<li>Clonezilla asks you if you want to encrypt the disk image. In my case, I selected not to (<code>senc</code>), as I already have my Sinology drive encrypted at rest, and I don&rsquo;t worry about anyone accessing the file when the NAS is up and running (I&rsquo;m the only one with permissions in the <code>Clonezilla/</code> folder, and also the only one with SSH and SFTP access).</li>
<li>Finally, you need to indicate to Clonezilla what to do after the image is created. I choose <code>-p choose</code> as I want to go back to my computer and see the status of everything, but you can tell it to automatically poweroff or reboot on finish.</li>
<li>Now, let it create the disk image, it might take a while. In my case, for a 100-200 GB installation on a 1 TB NVMe it took between 10-20 minutes on my hardware (time differs between systems, Linux and Windows).
<ul>
<li>Note: initially it might say something like &ldquo;X hours remaining&rdquo;, but in my case, it dropped after a while.</li>
</ul>
</li>
</ol>
<p>And that&rsquo;s it. You should be able to see your disk image in your Synology NAS server. I hope this article helped you. Until next time!</p>
]]></content:encoded></item><item><title>How to configure an Xbox One controller with dongle to work with Linux</title><link>https://aalonso.dev/blog/2025/how-to-configure-an-xbox-one-controller-with-dongle-to-work-with-linux/</link><pubDate>Sat, 29 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-configure-an-xbox-one-controller-with-dongle-to-work-with-linux/</guid><category>linux</category><category>gaming</category><category>tutorial</category><description>Today I'll show you how to configure an Xbox One controller with a dongle to work with Linux and play with it on Steam.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-configure-an-xbox-one-controller-with-dongle-to-work-with-linux/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>I&rsquo;m a Linux user at heart, but I also like to play video games. So at the beginning of the year I decided to drop Windows 10 on my desktop, as it was about to reach its end of life for official support, and move to Manjaro Linux. One of the first things I did was to install Steam, and configure my old Xbox One controller to work with it.</p>
<p>I&rsquo;ve been using an Xbox One controller with a dongle to play on Windows, and I wanted the same experience on Linux. I would just plug the dongle into my computer and start playing. The dongle I use is an old model like the one in the picture below. Microsoft released a new model later, that is less bulky. I guess these instructions will work for the new model as well, but I haven&rsquo;t tested it.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-configure-an-xbox-one-controller-with-dongle-to-work-with-linux/dongle.png"
    alt="Xbox One controller dongle"width="740"height="416"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-configure-an-xbox-one-controller-with-dongle-to-work-with-linux/dongle.png"
    data-alt="Xbox One controller dongle"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<h2 id="two-options-to-configure-the-controller">
Two options to configure the controller
</h2>
<p>There are two drivers out there to make the dongle work with Linux. One is <a
  href="https://github.com/medusalix/xow"rel="noopener noreferrer" target="_blank">xow</a>, a mature yet old (not necessary a bad thing!) driver that has been around for a while. The other one is <a
  href="https://github.com/medusalix/xone"rel="noopener noreferrer" target="_blank">xone</a>, made by the same author, but is newer and also supports Xbox Series X/S controllers and audio/led control with the controller.</p>
<p>I was unable to make the <code>xone</code> driver work with my controller, so I used the <code>xow</code> driver. Following now, I&rsquo;ll indicate how I configured the controller to work with my system (Manjaro Linux, based on Arch Linux) and play with it on Steam. If you are interested in the <code>xone</code> driver, you can check the installation instructions directly in the repository. I linked both of them above.</p>
<h2 id="installing-and-configuring-the-xow-driver">
Installing and configuring the xow driver
</h2>
<p>First, we need to install the packages required by <code>xow</code>.</p>
<pre><code class="language-bash">sudo pacman -S curl cabextract libusb
</code></pre>
<p>Now, we need to clone the repository and run the installation script.</p>
<pre><code class="language-bash">cd /tmp
git clone https://github.com/medusalix/xow
cd xow
make BUILD=RELEASE
sudo make install
</code></pre>
<p>We also need to install the firmware for the controller. Once <code>xow</code> is installed, it also installs a script in our path to get the controller firmware. So we can execute it as follows from anywhere.</p>
<pre><code class="language-bash">sudo xow-get-firmware.sh
</code></pre>
<p>Lastly, <code>xow</code> will also install a systemd service to handle the dongle. It should be already started, but we can ensure it by running the following command.</p>
<pre><code class="language-bash">sudo systemctl enable xow
sudo systemctl restart xow
</code></pre>
<p>At this point, we should be able to plug the dongle into our computer and recognized, but we still need to pair the controller with the dongle, even if it was paired with the dongle in another computer before (or in a previous Windows installation like my case).</p>
<h2 id="connecting-and-pairing-the-controller">
Connecting and pairing the controller
</h2>
<p>First, let&rsquo;s connect the dongle to the computer. If everything works you should see under the logs for the daemon that the dongle was initialized.</p>
<pre><code class="language-bash">systemctl status xow
# Or alternatively for the logs only
# journalctl -u xow -f
</code></pre>
<p>In my case, it shows the following output:</p>
<pre><code class="language-text">● xow.service - Xbox One Wireless Dongle Driver
     Loaded: loaded (/etc/systemd/system/xow.service; enabled; preset: disabled)
     Active: active (running) since Sat 2025-02-01 16:05:31 CET; 2h 38min ago
 Invocation: 5704f28070f64b2eb864b645f1f9cdb9
   Main PID: 981 (xow)
      Tasks: 4 (limit: 38251)
     Memory: 1.1M (peak: 2.3M)
        CPU: 28ms
     CGroup: /system.slice/xow.service
             └─981 /usr/local/bin/xow

feb 01 16:05:31 manjaro systemd[1]: Started Xbox One Wireless Dongle Driver.
feb 01 16:05:35 manjaro xow[981]: 2025-02-01 16:05:35 INFO  - xow v0.5-36-gd335d60 ©Severin v. W.
feb 01 16:05:35 manjaro xow[981]: 2025-02-01 16:05:35 INFO  - Waiting for device...
feb 01 18:43:04 manjaro xow[981]: 2025-02-01 18:43:04 INFO  - Wireless address: XX:XX:XX:XX:XX:XX
feb 01 18:43:05 manjaro xow[981]: 2025-02-01 18:43:05 INFO  - Dongle initialized
</code></pre>
<p>Also, you can check the device is recognized by running <code>lsusb</code>. This is also a good place to start debugging if you have any issues. In my case, it shows the following (apart from other devices):</p>
<pre><code class="language-text">Bus 001 Device 005: ID 045e:02e6 Microsoft Corp. Xbox Wireless Adapter for Windows
</code></pre>
<p>If everything is working, you should be able to pair the controller with the dongle. To do this, as always, first long-press the pairing button on the dongle, see the white light of the dongle blinking. Then, long-press the pairing button on the controller. The Xbox logo should start blinking on the controller, and once the pairing is successful, the light will turn on steadily.</p>
<p>However, I had some problems when pairing the controller initially, let&rsquo;s see them, so hopefully I can save you some time if you have the same issue.</p>
<h2 id="troubleshooting">
Troubleshooting
</h2>
<p>When pairing the controller, dongle light was blinking, as so the controller light was blinking too. However, the controller light was not turning on steadily after the pairing was successful and the controller was not paired.</p>
<p>Checking the logs for the <code>xow</code> service, I saw the following:</p>
<pre><code class="language-text">INFO  - Pairing enabled
terminate called after throwing an instance of 'InputException'
what():  Error opening device: Permission denied
</code></pre>
<p>The old known permissions issue in Linux. It seemed that <code>xow</code> didn&rsquo;t have the proper permissions to access the input devices. What I had to do to fix it was:</p>
<ol>
<li>First, stop the <code>xow</code> service.</li>
</ol>
<pre><code class="language-bash">sudo systemctl stop xow
</code></pre>
<ol start="2">
<li>Then, copy the udev rules to the correct location. They are under the <code>install</code> directory of the repository.</li>
</ol>
<pre><code class="language-bash">sudo cp install/udev.rules /etc/udev/rules.d/99-xow.rules
</code></pre>
<ol start="3">
<li>Reload the udev rules.</li>
</ol>
<pre><code class="language-bash">sudo udevadm control --reload-rules
sudo udevadm trigger
</code></pre>
<ol start="4">
<li>Start the <code>xow</code> service again.</li>
</ol>
<pre><code class="language-bash">sudo systemctl start xow
</code></pre>
<p>And that&rsquo;s it. After these changes, I repeated the pairing process, and it worked like a charm this time. Hope this helps you if you have the same issue.</p>
]]></content:encoded></item><item><title>How to automatically mount LUKS encrypted Linux root partition on boot with TPM 2.0</title><link>https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/</link><pubDate>Sat, 22 Feb 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/</guid><category>linux</category><category>security</category><category>tutorial</category><description>How to configure an Arch Linux based distribution (Manjaro) to automatically mount a LUKS encrypted Linux root partition on boot thanks to TPM 2.0.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>The two main operating systems for desktop/laptop computers out there, Windows and MacOS, allows you to encrypt your hard drive to protect your data from physical theft. And they do it in a way that is transparent for the end user. That is, you don&rsquo;t need to do anything special to encrypt your hard drive, you just need to enable it in the system settings or during the installation process. And once it&rsquo;s enabled, you won&rsquo;t note it. Everything works as usual. You&rsquo;ll power on your computer, and you will be presented with the login screen as always.</p>
<p>They achieve this by using a technology called <a
  href="https://en.wikipedia.org/wiki/BitLocker"rel="noopener noreferrer" target="_blank">BitLocker</a> for Windows and <a
  href="https://en.wikipedia.org/wiki/FileVault"rel="noopener noreferrer" target="_blank">FileVault</a> for MacOS. Linux distributions are not far behind, and you can encrypt your root partition with <a
  href="https://en.wikipedia.org/wiki/Linux_Unified_Key_Setup"rel="noopener noreferrer" target="_blank">LUKS</a> (Linux Unified Key Setup). However, the end user experience is not the same, as, by default, you&rsquo;ll be asked for the password of the LUKS partition when you power on your computer, even before of showing the login screen (when you will likely be asked for a password again, this time for your user account).</p>
<p>So how do Windows and MacOS achieve this? They store the key to decrypt the disc volume in a secure chip. There is a standard for this called <a
  href="https://en.wikipedia.org/wiki/Trusted_Platform_Module"rel="noopener noreferrer" target="_blank">Trusted Platform Module</a> (TPM), that most Intel and AMD CPUs out there support. You can check an article I wrote last month about <a
  href="https://aalonso.dev/blog/2025/how-to-enable-tpm-support-for-old-computers-in-bios/">how to check TPM support and enable it on your computer</a>. If you are curious, Apple implements this in their own T2 security chip.</p>
<p>So can we use the same technology to automatically mount the LUKS encrypted root partition on boot? Yes, we can! Let&rsquo;s see how to do it.</p>
<h2 id="prerequisites">
Prerequisites
</h2>
<p>You will need to accomplish with the following prerequisites first:</p>
<ul>
<li><a
  href="https://aalonso.dev/blog/2025/how-to-enable-tpm-support-for-old-computers-in-bios/">TPM 2.0 must be supported and enabled on your computer</a>.</li>
<li>A separated unencrypted <code>/boot</code> partition (apart from the usual separated <code>/boot/efi</code> partition if you are using UEFI).</li>
<li>A LUKS encrypted root partition.
<ul>
<li>Although this tutorial is focused on automatically mounting a single LUKS encrypted root partition on boot, this will work for any number of LUKS encrypted partitions, like having <code>/</code> and <code>/home</code> on two different LUKS encrypted partitions. You just need to configure all the partitions to be mounted on boot as we&rsquo;ll see later.</li>
</ul>
</li>
</ul>
<p>Note that a <strong>separated and unencrypted</strong> <code>/boot</code> partition is required. This is because your computer will need to be able to access the <code>/boot</code> partition, so it can load the kernel and initramfs, so that it can then load the encryption key from the TPM 2.0 chip. Otherwise, your computer will not be able to boot without asking for the password to even load GRUB or whatever bootloader you are using.</p>
<h2 id="a-security-note">
A security note
</h2>
<p>Some people will argue that having an unencrypted <code>/boot</code> partition is a security risk, and that it opens the door to a lot of potential attacks. For example, if an attacker gains physical access to your computer, they would build a malicious kernel and initramfs, so you will boot your computer with a compromised system that could be used to steal your information or spy on you. Like everything with security, you need to understand what is your threat model. What do I mean?</p>
<p>I&rsquo;m writing this tutorial for people like me: people that have a computer at home, in a trusted environment, that just want to protect their data from physical theft. Like a burglar breaking into your house, and stealing your computer. I might store some sensitive information on my computer (passwords, private keys, financial information, etc.), that I don&rsquo;t want to end up in the hands of a burglar. I don&rsquo;t worry about a malicious actor gaining physical access to my computer, as I trust the people around me.</p>
<p>Again, <strong>understand your threat model</strong>, and decide if this is something you want to do. If you don&rsquo;t trust your environment, you might want to fully encrypt your system and manually unlock it on every boot, either with a password or with a FIDO2 key. In that case, this tutorial might not be for you.</p>
<h2 id="automatically-mount-the-luks-encrypted-root-partition-on-boot">
Automatically mount the LUKS encrypted root partition on boot
</h2>
<blockquote>
<p>Note: As indicated in the prerequisites section of this article, I&rsquo;ll assume you already have an encrypted LUKS partition, that you are probably unlocking manually with a password. I won&rsquo;t explain how to create a LUKS encrypted partition in this article. You can easily encrypt your root partition when installing any Linux distribution in the installation wizard.</p>
</blockquote>
<p>First, we need to install the required packages to make all of this work. I&rsquo;m using <a
  href="https://manjaro.org/"rel="noopener noreferrer" target="_blank">Manjaro</a> an european Arch Linux based distribution, so the packages will be slightly different if you are using a non-Arch Linux based distribution like Debian, Fedora or Ubuntu.</p>
<p>We&rsquo;ll install the packages to interact with the TPM 2.0 chip, and <a
  href="https://wiki.archlinux.org/title/Clevis"rel="noopener noreferrer" target="_blank">clevis</a> to bind the LUKS encrypted partitions to the TPM 2.0 chip. To make the initramfs automatically mount the LUKS encrypted partitions on boot from the TPM 2.0 chip, we&rsquo;ll install the <code>mkinitcpio-clevis-hook</code> hook. This is provided as an AUR package, so we&rsquo;ll need to use a package manager that supports AUR, like <code>yay</code>.</p>
<pre><code class="language-bash">yay -S clevis clevis-luks tpm2-tss mkinitcpio-clevis-hook tpm2-tools
</code></pre>
<p>Now, we need to add the clevis hooks to the <code>HOOKS</code> variable in the <code>/etc/mkinitcpio.conf</code> file. The hook <strong>must be added before the <code>encrypt</code> hook</strong>, as indicated in the <a
  href="https://wiki.archlinux.org/title/Clevis#Mkinitcpio_hook"rel="noopener noreferrer" target="_blank">Arch Linux wiki</a>. In my case, a fresh installation of Manjaro, it looks like this:</p>
<pre><code class="language-bash">HOOKS=(base udev autodetect microcode kms modconf block keyboard keymap consolefont plymouth clevis encrypt filesystems)
</code></pre>
<p>Now, let&rsquo;s recreate the initramfs.</p>
<pre><code class="language-bash">sudo mkinitcpio -P
</code></pre>
<p>Lastly, let&rsquo;s bind the LUKS encrypted partitions to the TPM 2.0 chip using the <code>clevis</code> tool. First, we need to know what is the encrypted partition that we want to bind with Luks. For that, we can use the <code>lsblk</code> command to list all the block devices and their partitions. This is the output of my system:</p>
<pre><code class="language-text">NAME                                          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINTS
nvme0n1                                       259:0    0 931,5G  0 disk
├─nvme0n1p1                                   259:1    0   300M  0 part  /boot/efi
├─nvme0n1p2                                   259:2    0     1G  0 part  /boot
└─nvme0n1p3                                   259:3    0 930,2G  0 part
  └─luks-bc7b9680-6567-45c4-a87a-f73e7ed0245e 254:0    0 930,2G  0 crypt /home
                                                                         /var/log
                                                                         /var/cache
                                                                         /
</code></pre>
<p>So in my case, the encrypted partition is <code>/dev/nvme0n1p3</code>. Now, let&rsquo;s bind it to the TPM 2.0 chip. We&rsquo;ll indicate to clevis that we want to use the TPM 2.0 chip, and that we want to use the PCR ID 7. PCR ID 7 will seal the LUKS key against the UEFI settings and the <a
  href="https://wiki.archlinux.org/title/Unified_Extensible_Firmware_Interface/Secure_Boot"rel="noopener noreferrer" target="_blank">Secure Boot</a> policy using the TPM 2.0 chip. So if the UEFI or Secure Boot settings are modified, the TPM will compute different PCR values and decryption will fail. This gives protection against <a
  href="https://en.wikipedia.org/wiki/Evil_maid_attack"rel="noopener noreferrer" target="_blank">evil maid attacks</a>.</p>
<pre><code class="language-bash">sudo clevis luks bind -d /dev/nvme0n1p3 tmp2 '{&quot;pcr_ids&quot;: &quot;7&quot;}'
</code></pre>
<p>And that&rsquo;s it! The partition is now bound to the TPM 2.0 chip, and will be automatically mounted on boot. You don&rsquo;t need to do anything else, nor modify anything under <code>/etc/fstab</code> nor <code>/etc/crypttab</code>. Just for the records, my <code>/etc/crypttab</code> file looks like this:</p>
<pre><code class="language-text"># &lt;name&gt;                                  &lt;device&gt;                                  &lt;password&gt; &lt;options&gt;
luks-bc7b9680-6567-45c4-a87a-f73e7ed0245e UUID=bc7b9680-6567-45c4-a87a-f73e7ed0245e            none
</code></pre>
<p>You can check the status of the binding with the following command:</p>
<pre><code class="language-bash">sudo clevis luks list -d /dev/nvme0n1p3
</code></pre>
<p>You should see something like this:</p>
<pre><code class="language-text">1: tpm2 '{&quot;hash&quot;:&quot;sha256&quot;,&quot;key&quot;:&quot;ecc&quot;,&quot;pcr_bank&quot;:&quot;sha1&quot;,&quot;pcr_ids&quot;:&quot;7&quot;}'
</code></pre>
<p>Now, let&rsquo;s reboot your computer and see if it works. You should see the login screen as usual, and you won&rsquo;t be asked for the password to decrypt the LUKS partition, just the credentials for your user account. The same user experience as Windows and MacOS!</p>
]]></content:encoded></item><item><title>How to enable TPM support for old computers in BIOS</title><link>https://aalonso.dev/blog/2025/how-to-enable-tpm-support-for-old-computers-in-bios/</link><pubDate>Sat, 25 Jan 2025 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2025/how-to-enable-tpm-support-for-old-computers-in-bios/</guid><category>hardware</category><category>tutorial</category><category>windows</category><description>In this article, I'll show you how to enable TPM on your old computer, so you can install Windows 11 without having to upgrade your hardware.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2025/how-to-enable-tpm-support-for-old-computers-in-bios/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>If you are like me and the <a
  href="https://gs.statcounter.com/os-version-market-share/windows/desktop/worldwide"rel="noopener noreferrer" target="_blank">~63% of Windows users</a> in the world as of December 2024, you are probably still using Windows 10 on your computer. And the reason why you have not upgraded to Windows 11 yet is probably because your computer apparently doesn&rsquo;t support it. You&rsquo;ll probably have a not too old computer, let&rsquo;s say 5 years old components (like in my case), but still, Windows Update shows your computer is not compatible with Windows 11. How can this be?</p>
<p>Windows 11 requirements are not a crazy thing: 1 GHz or faster processor with at least 2 cores and 4 GB of RAM, so most of the computers out there should be compatible. The problem is that Windows 11 requires a <a
  href="https://en.wikipedia.org/wiki/Trusted_Platform_Module"rel="noopener noreferrer" target="_blank">Trusted Platform Module 2.0 (TPM 2.0)</a> to be present and enabled on your computer. This is a hardware security chip that is used to store cryptographic keys and other security-related data, used mainly to encrypt the hard drive and secure the boot process.</p>
<p>Does that mean that you need to upgrade your hardware, that otherwise meets the requirements by far and it&rsquo;s just a few years old, just to install Windows 11? Probably not. Big chances are that your motherboard or processor has a TPM 2.0 chip, but it&rsquo;s disabled by default. In this article, I&rsquo;ll show you how to enable it on your computer.</p>
<h2 id="does-my-motherboard-support-tpm-20">
Does my motherboard support TPM 2.0?
</h2>
<p>Microsoft has been requiring hardware manufacturers to support TPM 2.0 since at least 10 years ago, in order to certify the components for Windows. And even before, some manufacturers have been adding it to their products. However, as in my case, it might be disabled by default in your BIOS.</p>
<p>For example, in my particular case, I have a MSI motherboard, and the TPM 2.0 chip is disabled by default, but already supported by all MSI motherboards since the Intel 100 series and AMD 300 series. As an example to illustrate how much hardware manufacturers have been supporting TPM 2.0, here is a table with the series of motherboards and processors that support it specifically for MSI motherboards:</p>
<table>
  <thead>
      <tr>
          <th>Series</th>
          <th>Chipset</th>
          <th>CPU</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Intel 500 Series</td>
          <td>Intel Z590 / B560 / H510</td>
          <td>Intel 10th / 11th Gen</td>
      </tr>
      <tr>
          <td>Intel 400 Series</td>
          <td>Intel Z490 / B460 / H410</td>
          <td>Intel 10th / 11th Gen</td>
      </tr>
      <tr>
          <td>Intel 300 Series</td>
          <td>Intel Z390 / Z370 / B365 / B360 / H370 / H310</td>
          <td>Intel 8th / 9th Gen</td>
      </tr>
      <tr>
          <td>Intel 200 Series</td>
          <td>Intel Z270 / B250 / H270</td>
          <td>Intel 6th / 7th Gen</td>
      </tr>
      <tr>
          <td>Intel 100 Series</td>
          <td>Intel Z170 / B150 / H170 / H110</td>
          <td>Intel 6th / 7th Gen</td>
      </tr>
      <tr>
          <td>Intel X299</td>
          <td>Intel X299</td>
          <td>Intel Core X-series 10000/9000/78xx</td>
      </tr>
      <tr>
          <td>AMD 500 Series</td>
          <td>AMD X570 / B550 / A520</td>
          <td>AMD 5000/4000/3000 / Ryzen 5/3/2/1</td>
      </tr>
      <tr>
          <td>AMD 400 Series</td>
          <td>AMD X470 / B450</td>
          <td>AMD 4000/3000 / Ryzen 4/3/2/1</td>
      </tr>
      <tr>
          <td>AMD 300 Series</td>
          <td>AMD X370 / B350 / A320</td>
          <td>AMD 3000 / Ryzen 3/2/1</td>
      </tr>
      <tr>
          <td>AMD X399 / TRX40 Series</td>
          <td>AMD X399 / TRX40</td>
          <td>AMD Threadripper 1000/2000/3000</td>
      </tr>
  </tbody>
</table>
<p>So as you can see, an older hardware doesn&rsquo;t mean that it doesn&rsquo;t support TPM 2.0. It just means that you might have to enable it in your BIOS. If your motherboard is not from MSI, you might need to check your manufacturer website or your motherboard manual to find out if it supports TPM 2.0. To continue with this article, let&rsquo;s see how to enable it on an MSI motherboard as this is the one I have.</p>
<h2 id="enabling-tpm-20-on-your-old-hardware">
Enabling TPM 2.0 on your old hardware
</h2>
<p>All you have to do, on an MSI motherboard, is to go to the <code>Security</code> tab, and then you will find the TPM settings under <code>Trusted Computing</code> submenu. You have to enable <code>Security Device Support</code> feature, and set one of the following options:</p>
<ul>
<li>For Intel motherboards, set <code>TPM Device Selection</code> to <code>PTT</code>.</li>
</ul>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-enable-tpm-support-for-old-computers-in-bios/enable-tpm-intel.jpg"
    alt="Enable TPM 2.0 for an Intel MSI motherboard"width="960"height="720"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-enable-tpm-support-for-old-computers-in-bios/enable-tpm-intel.jpg"
    data-alt="Enable TPM 2.0 for an Intel MSI motherboard"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<ul>
<li>For AMD motherboards, set <code>AMD fTPM Switch</code> to <code>AMD CPU fTPM</code>.</li>
</ul>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2025/how-to-enable-tpm-support-for-old-computers-in-bios/enable-tpm-amd.jpg"
    alt="Enable TPM 2.0 for an AMD MSI motherboard"width="960"height="720"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2025/how-to-enable-tpm-support-for-old-computers-in-bios/enable-tpm-amd.jpg"
    data-alt="Enable TPM 2.0 for an AMD MSI motherboard"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Then, press <code>F10</code> key to save the changes and exit. After doing this, you should be able to install Windows 11 on your computer.</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>As you can see, most of the old computers out there should be compatible with TPM 2.0, even if initially it seems like they don&rsquo;t support it from Windows. You might just have to enable the TPM 2.0 feature in your BIOS, and you will be ready to go.</p>
<p>Now, you can install Windows 11 on your computer, or go the other way around, and install <a
  href="https://aalonso.dev/blog/2025/how-to-automatically-mount-luks-encrypted-linux-root-partition-on-boot/">Linux with a LUKS encrypted root partition that automatically unlocks on boot</a>. The choice is yours, will you take the blue pill or the red pill?</p>
]]></content:encoded></item><item><title>How to use UUID v7 on PostgreSQL with Ecto in Elixir</title><link>https://aalonso.dev/blog/2024/how-to-use-uuid-v7-postgresql-ecto-elixir/</link><pubDate>Fri, 27 Dec 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-to-use-uuid-v7-postgresql-ecto-elixir/</guid><category>elixir</category><category>phoenix</category><category>programming</category><description>In this article, I'll show you how to use UUID v7 with Ecto and PostgreSQL without external dependencies.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-to-use-uuid-v7-postgresql-ecto-elixir/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Since UUID v7 came officially out, I&rsquo;ve always tried to use it for primary keys on all new projects and databases. However, due to it being so new, it&rsquo;s not supported by default on a lot of databases and libraries.</p>
<p>As I&rsquo;m working recently, both professionally and personally, with Elixir and PostgreSQL, I&rsquo;ll show you how do I use UUID v7 on PostgreSQL with Ecto in Elixir. Fully integrated with Ecto library and without the need for any external library. You will forget that you are using UUID v7 under the hood, because once configured,everything will be like working with the good old UUID v4.</p>
<p>Let&rsquo;s get started!</p>
<h2 id="defining-our-uuidv7-ectotype-module">
Defining our UUIDv7 <code>Ecto.Type</code> module
</h2>
<p>In order to fully integrate UUID v7 with Ecto, we need to define a custom <code>Ecto.Type</code> module. This module will be responsible for converting the UUID v7 to a binary format and vice versa. In my case, I created a file under <code>lib/my_app/uuid.ex</code> as follows:</p>
<pre><code class="language-elixir">defmodule MyAPP.UUID do
  @moduledoc &quot;&quot;&quot;
  A UUID v7 implementation and `Ecto.Type` for Elixir.

  Based on the RFC for the version 7 UUID: [RFC 9562](https://datatracker.ietf.org/doc/rfc9562/).

  Borrowed from https://github.com/martinthenth/uuidv7

  ## Usage

  In the database schema, change primary key attribute from `:binary_id` to `MyApp.UUID`:

  def MyApp.Schemas.User do
  @primary_key {:id, MyApp.UUID, autogenerate: true}
  end
  &quot;&quot;&quot;

use Ecto.Type

@typedoc &quot;&quot;&quot;
A hex-encoded UUID string.
&quot;&quot;&quot;
@type t :: &lt;&lt;\_::288&gt;&gt;

@typedoc &quot;&quot;&quot;
A raw binary representation of a UUID.
&quot;&quot;&quot;
@type raw :: &lt;&lt;\_::128&gt;&gt;

@doc false
@spec type() :: :uuid
def type, do: :uuid

defdelegate cast(value), to: Ecto.UUID

defdelegate cast!(value),
to: Ecto.UUID

defdelegate dump(value), to: Ecto.UUID

defdelegate dump!(value), to: Ecto.UUID

defdelegate load(value), to: Ecto.UUID

defdelegate load!(value), to: Ecto.UUID

@doc false
@spec autogenerate() :: t()
def autogenerate, do: generate()

@doc &quot;&quot;&quot;
Generates a version 7 UUID.
&quot;&quot;&quot;
@spec generate() :: t()
def generate, do: cast!(bingenerate())

@doc &quot;&quot;&quot;
Generates a version 7 UUID based on the timestamp (ms).
&quot;&quot;&quot;
@spec generate(non_neg_integer()) :: t()
def generate(milliseconds), do: milliseconds |&gt; bingenerate() |&gt; cast!()

@doc &quot;&quot;&quot;
Generates a version 7 UUID in the binary format.
&quot;&quot;&quot;
@spec bingenerate() :: raw()
def bingenerate, do: :millisecond |&gt; System.system_time() |&gt; bingenerate()

@doc &quot;&quot;&quot;
Generates a version 7 UUID in the binary format based on the timestamp (ms).
&quot;&quot;&quot;
@spec bingenerate(non*neg_integer()) :: raw()
def bingenerate(milliseconds) do
  &lt;&lt;u1::12, u2::62, *::6&gt;&gt; = :crypto.strong_rand_bytes(10)
  &lt;&lt;milliseconds::48, 7::4, u1::12, 2::2, u2::62&gt;&gt;
end

@doc &quot;&quot;&quot;
Extracts the timestamp (ms) from the version 7 UUID.
&quot;&quot;&quot;
@spec timestamp(t() | raw()) :: non*neg_integer()
def timestamp(&lt;&lt;milliseconds::48, 7::4, *::76&gt;&gt;), do: milliseconds
def timestamp(&lt;&lt;\_::288&gt;&gt; = uuid), do: uuid |&gt; dump!() |&gt; timestamp()
end

</code></pre>
<p>Now that we defined our <code>Ecto.Type</code> module, we can use it on our database schemas.</p>
<h2 id="using-our-custom-ectotype-on-our-ecto-schemas">
Using our custom <code>Ecto.Type</code> on our Ecto schemas
</h2>
<p>Let&rsquo;s say we have a <code>User</code> schema with a primary key of type <code>UUIDv7</code>. We can define it as follows:</p>
<pre><code class="language-elixir">defmodule MyApp.Schemas.User do
  @moduledoc &quot;&quot;&quot;
  User schema.
  &quot;&quot;&quot;

  use Ecto.Schema

  @type t() :: %__MODULE__{
          id: MyApp.UUID.t(),
          email: String.t(),
          hashed_password: String.t(),
        }

  @primary_key {:id, MyApp.UUID, autogenerate: true}
  @foreign_key_type MyApp.UUID
  schema &quot;users&quot; do
    field :email, :string
    field :hashed_password, :string, redact: true

    timestamps(type: :utc_datetime)
  end
end
</code></pre>
<p>Nice! Now, let&rsquo;s take a look at how to configure the repository to automatically use our custom UUIDv7 <code>Ecto.Type</code> module on our database migrations.</p>
<h2 id="configuring-the-repository-and-database-migrations">
Configuring the repository and database migrations
</h2>
<p>We need to update our repository configuration, usually under <code>config/config.exs</code>, to use our custom UUIDv7 <code>Ecto.Type</code> module for autogenerated primary keys and IDs.</p>
<pre><code class="language-elixir">config :my_app,
  ecto_repos: [MyApp.Repo],
  generators: [
    timestamp_type: :utc_datetime,
    binary_id: {MyApp.UUID, :generate, []},
    migration_primary_key: {:id, :uuid, autogenerate: true},
    migration_timestamps: [type: :utc_datetime]
  ]
</code></pre>
<p>Then, our autogenerated database migrations created with <code>mix ecto.gen.migration</code> should look like this:</p>
<pre><code class="language-elixir">defmodule MyApp.Repo.Migrations.CreateUsersTables do
  use Ecto.Migration

  def change do
    execute &quot;CREATE EXTENSION IF NOT EXISTS citext&quot;, &quot;&quot;

    create table(:users, primary_key: false) do
      add :id, :uuid, primary_key: true
      add :email, :citext, null: false

      timestamps(type: :utc_datetime)
    end

    create unique_index(:users, [:email])
  end
end
</code></pre>
<p>And that&rsquo;s it! Now, you can use UUID v7 on PostgreSQL with Ecto in Elixir without the need for any external library.</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>As you can see, it&rsquo;s very easy to use UUID v7 on PostgreSQL with Ecto in Elixir. You can forget that you are using UUID v7 under the hood, because once configured, everything will be like working with the good old UUID v4. Also, not needing any external library is a huge plus, as you are in control of the implementation and you can easily understand how it works under the hood. You remove potential security risks, potential deps problems, and you can easily integrate it with other libraries and tools that expect UUID v4.</p>
<p>I hope this helps you to start using UUID v7 on PostgreSQL with Ecto in Elixir. Happy coding!</p>
]]></content:encoded></item><item><title>How to seamlessly manage AWS credentials using 1Password CLI</title><link>https://aalonso.dev/blog/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/</link><pubDate>Wed, 27 Nov 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/</guid><category>aws</category><category>productivity</category><category>tutorial</category><description>A guide to manage AWS credentials seamlessly with 1Password, including MFA, by integrating the 1Password CLI with the AWS CLI.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>If you, like me, interact with AWS a lot via the CLI, you are probably getting tired of the CLI continuously asking you for your credentials or the OTPs generated by your MFA device. I use 1password to manage all my credentials, and although they provide a shortcut to retrieve credentials if you have the desktop app installed (Cmd+Shift+Space on my Mac), it is still a bit cumbersome to continuously searching for the OTPs.</p>
<p>As an engineer and developer, I like to automate things as much as possible. So I was happy when a coworker told me that the <a
  href="https://developer.1password.com/docs/cli"rel="noopener noreferrer" target="_blank">1password CLI</a> could be used to retrieve AWS credentials. This way, I can use my 1password vault to manage all my credentials, and the CLI to retrieve the temporary credentials when I need them.</p>
<p>This guide explains how to integrate the 1password CLI with the AWS CLI to retrieve your AWS credentials from 1password, including the MFA token, automatically for you. You&rsquo;ll only need to authorize the operation when prompted, which usually will just require your fingerprint on your computer fingerprint reader, reducing a lot the manual work.</p>
<h2 id="before-you-start">
Before you start
</h2>
<p>Before you configure your local environment, ensure:</p>
<ul>
<li>You have a valid AWS account.</li>
<li>You have installed the <a
  href="https://developer.1password.com/docs/cli/get-started/"rel="noopener noreferrer" target="_blank">1password CLI</a>.</li>
<li>You have installed the <a
  href="https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-getting-started.html"rel="noopener noreferrer" target="_blank">AWS CLI</a>.
<ul>
<li>
<p><strong>Version 2.0 or higher is required.</strong> You can check your AWS CLI version by running:</p>
<pre><code class="language-bash">aws --version
</code></pre>
</li>
</ul>
</li>
</ul>
<h2 id="configure-main-aws-credential-retrieval-from-1password">
Configure main AWS credential retrieval from 1password
</h2>
<p>First, let&rsquo;s see how to work normally with AWS CLI without having to store your credentials in your computer.</p>
<p>First, create a new item in 1password with the following fields:</p>
<ul>
<li>AWS access key.</li>
<li>AWS secret access key.</li>
</ul>
<p>You can use the same item to store your AWS user/password and also the MFA generator (OTP codes). Make sure the name of the item is something unique on your 1password items, and it&rsquo;s easy to remember (something like <code>aws credentials john.doe@email.com</code> is just perfect).</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/1password_item.png"
    alt="1password item example"width="978"height="886"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/1password_item.png"
    data-alt="1password item example"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Check that you can access your AWS account element from the command line:</p>
<pre><code class="language-bash"># Replace &lt;Your 1password item&gt; with the name of the item you created in the previous step
op item get '&lt;Your 1password item&gt;' --format json | jq '.fields[]|.label'
</code></pre>
<p>Following the previous example, the command should be:</p>
<pre><code class="language-bash">op item get 'aws credentials john.doe@email.com' --format json | jq '.fields[]|.label'
</code></pre>
<p>1Password will ask you for permission to check your 1password account. You can configure the amount of time you want to be asked for permission on the &ldquo;developer settings&rdquo; section of the 1password app.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/developer_settings.png"
    alt="1password developer settings"width="1524"height="526"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/developer_settings.png"
    data-alt="1password developer settings"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Check the command output. It will enumerate the different fields the 1password entry has. It should be like this (it depends on your item layout).</p>
<pre><code class="language-txt">&quot;username&quot;
&quot;password&quot;
&quot;notesPlain&quot;
&quot;one-time password&quot;
&quot;access key&quot;
&quot;secret access key&quot;
</code></pre>
<p>Let&rsquo;s try to obtain the json snippet needed by <code>aws-cli</code>:</p>
<pre><code class="language-bash">op item get '&lt;1password item name&gt;' --format json --fields 'label=access key','label=secret access key' | jq '{ &quot;Version&quot; : 1, &quot;AccessKeyId&quot;: &quot;\(.[] | select(.label==&quot;access key&quot;) | .value )&quot;, &quot;SecretAccessKey&quot;: &quot;\(.[] | select(.label==&quot;secret access key&quot;) | .value )&quot;}'
</code></pre>
<p>The output should be something like this:</p>
<pre><code class="language-json">{
  &quot;Version&quot;: 1,
  &quot;AccessKeyId&quot;: &quot;XXXXXXXXXXXXXXXXXXXX&quot;,
  &quot;SecretAccessKey&quot;: &quot;XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&quot;
}
</code></pre>
<p>Now, edit your <code>~/.aws/credentials</code> file and add the following lines:</p>
<pre><code class="language-ini">[1password]
credential_process = sh -c &quot;op item get '&lt;1password item name&gt;' --format json --fields 'label=access key','label=secret access key' | jq '{ \&quot;Version\&quot; : 1, \&quot;AccessKeyId\&quot;: \&quot;\\(.[] | select(.label==\&quot;access key\&quot;) | .value )\&quot;, \&quot;SecretAccessKey\&quot;: \&quot;\\(.[] | select(.label==\&quot;secret access key\&quot;) | .value )\&quot;}'&quot;
</code></pre>
<p>Make sure you replace <code>&lt;1password item name&gt;</code> with the name of the item you created in the previous step. Also, make sure you comment or remove the <code>aws_access_key_id</code> and <code>aws_secret_access_key</code> lines from your <code>~/.aws/credentials</code> file. Also, you must take care of using the correct fields in your 1password item. In my example, I have the access key and secret access key in the fields <code>access key</code> and <code>secret access key</code>.</p>
<p>Check you can access your AWS account element from the command line:</p>
<pre><code class="language-shell">aws sts get-caller-identity --profile 1password
</code></pre>
<p>the output should be something like this:</p>
<pre><code class="language-json">{
  &quot;UserId&quot;: &quot;xxxxxxxxxxxxxxxxxxxx&quot;,
  &quot;Account&quot;: &quot;xxxxxxxxxxxx&quot;,
  &quot;Arn&quot;: &quot;arn:aws:iam::xxxxxxxxxxxx:john/john.doe@email.com&quot;
}
</code></pre>
<h2 id="configure-mfa-retrieval-from-1password">
Configure MFA retrieval from 1password
</h2>
<p>Now, let&rsquo;s configure AWS CLI to retrieve the OTP for 2FA from 1password. Other tools such as <code>kubectl</code> use AWS CLI under the hood, so this will also configure your MFA retrieval for other tools (like the already mentioned <code>kubectl</code> if you work with kubernetes).</p>
<p>Additionally, the plugin we are going to install will allow you to use two new commands: <code>open-console</code> and <code>set-env</code> that helps a lot to manage your AWS access.</p>
<p>First, check your <code>~/.aws/config</code> file. You should have something like this:</p>
<pre><code class="language-ini">[default]
region = us-east-1 # Or your default region
output = json

[profile 1password]
region = us-east-1 # Or your default region
output = json

[profile example-profile]
role_arn= # ...
source_profile=1password
mfa_serial = # ...
region = us-east-1 # Or your default region
</code></pre>
<p>Test you have access to that profile:</p>
<pre><code class="language-bash">aws sts get-caller-identity --profile example-profile
</code></pre>
<p>The output should be something like this:</p>
<pre><code class="language-json">{
  &quot;UserId&quot;: &quot;xxxxxxxxxxxxxxxxxxxx&quot;,
  &quot;Account&quot;: &quot;xxxxxxxxxxxx&quot;,
  &quot;Arn&quot;: &quot;arn:aws:sts::xxxxxxxx:assumed-role/IAM_ROLE_NAME/xxxxxxxx&quot;
}
</code></pre>
<p>Now, clone the <a
  href="https://github.com/srlobo/awscli-plugin-op"rel="noopener noreferrer" target="_blank">awscli-plugin-op</a> repository from my coworker. In my case, I cloned it directly inside a new <code>plugins/</code> folder inside <code>~/.aws/</code>:</p>
<pre><code class="language-bash">cd ~/.aws
mkdir plugins
cd plugins
git clone https://github.com/srlobo/awscli-plugin-op.git
</code></pre>
<p>The result directory structure should be like this:</p>
<pre><code class="language-txt">~/.aws/
 |-- plugins/
 |    |-- awscli-plugin-op/
 |    |   |-- README.md
 |    |   |-- awscli_plugin_op/
 |    |   |   |-- *.py
</code></pre>
<p>Then, configure the plugin:</p>
<ol>
<li>Add the following lines to the end of your <code>~/.aws/config</code> file:</li>
</ol>
<pre><code class="language-ini">[plugins]
op_totp = awscli_plugin_op
# Replace with the absolute path to the cloned repo
cli_legacy_plugin_path = /Users/john.doe/.aws/plugins/awscli-plugin-op
</code></pre>
<ol start="2">
<li>Also, in the same file, add the <code>op_identity</code> property on each profile you want to use the plugin with (usually you want to use it on all the profiles):</li>
</ol>
<pre><code class="language-ini">op_identity = &lt;your 1password item name&gt;
# Without quotes. Following the previous example:
# op_identity = aws credentials john.doe@email.com
</code></pre>
<p>Your <code>~/.aws/config</code> file should look like this:</p>
<pre><code class="language-ini">[default]
region = us-east-1
output = json

[profile 1password]
region = us-east-1
output = json
op_identity = &lt;your 1password item name&gt;

[profile example-profile]
role_arn= # ...
source_profile=1password
mfa_serial = # ...
region = us-east-1
op_identity = &lt;your 1password item name&gt;

[plugins]
op_totp = awscli_plugin_op
cli_legacy_plugin_path = /Users/john.doe/.aws/plugins/awscli-plugin-op
</code></pre>
<p>With the <code>[plugins]</code> section, you are telling the AWS CLI to use the plugin. Then, each <code>op_identity</code> attribute is telling the plugin what 1password item hold the MFA data. Note the <code>op_identity</code> field is literal, don&rsquo;t put quotes around it.</p>
<p>Finally, test the new configuration:</p>
<pre><code class="language-bash">aws sts get-caller-identity --profile example-profile
</code></pre>
<p>If you have configured the plugin correctly, the system will ask you for the 1password password or just for the biometric authentication.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/biometric_request.png"
    alt="1Pasword biometric authentication request"width="804"height="655"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/how-to-seamlessly-manage-aws-credentials-using-1password-cli/biometric_request.png"
    data-alt="1Pasword biometric authentication request"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>The output should be something like this:</p>
<pre><code class="language-json">{
  &quot;userid&quot;: &quot;xxxxxxxxxxxxxxxxxxxx&quot;,
  &quot;account&quot;: &quot;xxxxxxxxxxxx&quot;,
  &quot;Arn&quot;: &quot;arn:aws:sts::xxxxxxxx:assumed-role/IAM_ROLE_NAME/xxxxxxxx&quot;
}
</code></pre>
<p>Congratulations! You have successfully configured your AWS CLI to retrieve your credentials from 1password!</p>
]]></content:encoded></item><item><title>How to use erlang applications in elixir and how to translate them to elixir code.</title><link>https://aalonso.dev/blog/2024/how-to-use-erlang-applications-in-elixir-and-how-to-translate-them-to-elixir-code/</link><pubDate>Sun, 27 Oct 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-to-use-erlang-applications-in-elixir-and-how-to-translate-them-to-elixir-code/</guid><category>elixir</category><category>programming</category><category>tutorial</category><description>In this article, I'll show you how to use erlang applications in your elixir codebase, and also how to translate them to elixir code.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-to-use-erlang-applications-in-elixir-and-how-to-translate-them-to-elixir-code/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>As you might already know, Elixir is built on top of the Erlang Virtual Machine (BEAM). This means that Elixir and Erlang share the same underlying runtime system, allowing for seamless interoperability between the two languages. Elixir code compiles to Erlang bytecode, which runs on the BEAM, making it possible to use Erlang libraries and modules directly within Elixir projects.</p>
<p>This interoperability allows Elixir developers to leverage the vast ecosystem of Erlang libraries and applications without the need for translation or rewriting. You can directly use existing Erlang applications in your Elixir code by calling their functions using the atom syntax. Let&rsquo;s see how it works.</p>
<h2 id="using-erlang-applications-directly-in-elixir">
Using Erlang applications directly in Elixir
</h2>
<p>To call an Erlang function from Elixir, you use the syntax <code>:module.function(args)</code>. The colon before the module name tells Elixir that you&rsquo;re referring to an Erlang module. This works out of the box for modules included in the Erlang standard library.</p>
<p>For example, if you want to use the <code>base64</code> module from Erlang&rsquo;s standard library, you can do:</p>
<pre><code class="language-elixir">encoded = :base64.encode(&quot;Hello, World!&quot;)
decoded = :base64.decode(encoded)
</code></pre>
<p>Now, let&rsquo;s see how to use an Erlang application that is not included in the Erlang standard library. Erlang applications are typically started automatically when your Elixir application starts, as long as they are listed in your <code>mix.exs</code> file under the <code>:extra_applications</code> key. For example:</p>
<pre><code class="language-elixir">def application do
  [
    extra_applications: [:logger, :crypto]
  ]
end
</code></pre>
<p>This ensures that the Erlang <code>crypto</code> application is started along with your Elixir application. After that, you can use its functions in your Elixir code. Here&rsquo;s an example of using the <code>crypto</code> module to generate a random string:</p>
<pre><code class="language-elixir"># Generate 16 random bytes
random_bytes = :crypto.strong_rand_bytes(16)

# Convert the bytes to a hexadecimal string
random_string = Base.encode16(random_bytes, case: :lower)

IO.puts(&quot;Random string: #{random_string}&quot;)
</code></pre>
<p>This code generates a random 16-byte string and encodes it as a lowercase hexadecimal string. The <code>:crypto.strong_rand_bytes/1</code> function is an Erlang function from the <code>crypto</code> application, called directly from Elixir code.</p>
<p>But what if you want to directly translate Erlang code to Elixir code? Well, Elixir and Erlang are not 100% compatible, but they are very similar, so you can translate Erlang code to Elixir with a few changes. Let&rsquo;s see how it works.</p>
<h2 id="translating-erlang-code-to-elixir-code">
Translating Erlang code to Elixir code
</h2>
<p>Translating Erlang code to Elixir is easy as long as you keep in mind the following things when transcoding Erlang to Elixir:</p>
<ul>
<li>Erlang atoms start with a lowercase letter, whereas Elixir atoms start with a colon (<code>:</code>). For example, <code>ok</code> in Erlang becomes <code>:ok</code> in Elixir.</li>
<li>Erlang variables start with an uppercase letter, whereas Elixir variables start with a lowercase letter. For example, <code>Socket</code> in Erlang becomes <code>socket</code> in Elixir.</li>
<li>Erlang modules are always referenced as atoms. For example, <code>gen_tcp</code> in Erlang becomes <code>:gen_tcp</code> in Elixir.</li>
<li>Function calls in Erlang use a colon (<code>:</code>) whereas function calls in Elixir always use a dot (<code>.</code>). For example, <code>gen_tcp:listen</code> in Erlang becomes <code>:gen_tcp.listen</code> in Elixir. (Replace the colon with a dot.)</li>
<li>Last, but no least, it&rsquo;s important to note that <strong>Erlang strings aren&rsquo;t the same as Elixir strings</strong>. In Erlang, a double-quoted string is a list of characters whereas in Elixir a double-quoted string is a sequence of bytes (a binary). Thus, double-quoted Erlang and Elixir strings aren&rsquo;t compatible. So if an Erlang function takes a string argument, you can&rsquo;t pass it an Elixir string. Instead, Elixir has a character list which you can create using single-quotes rather than double-quotes. For example, <code>'hello'</code> is a list of characters that&rsquo;s compatible with the Erlang string <code>&quot;hello&quot;</code>.</li>
</ul>
<p>Let&rsquo;s see how it works with an example. Let&rsquo;s translate the server example <a
  href="https://www.erlang.org/docs/26/man/gen_tcp"rel="noopener noreferrer" target="_blank">from the docs</a> for generic TCP/IP server in Erlang (<code>gen_tcp</code>) to Elixir code.</p>
<p>We start with the following Erlang code, that defines the <code>server</code> function:</p>
<pre><code class="language-elixir">server() -&gt;
    {ok, LSock} = gen_tcp:listen(5678, [binary, {packet, 0}, {active, false}]),
    {ok, Sock} = gen_tcp:accept(LSock),
    {ok, Bin} = gen_tcp:recv(Sock, 0),
    ok = gen_tcp:close(Sock),
    ok = gen_tcp:close(LSock),
    Bin.
</code></pre>
<p>Now, translated to Elixir code, it looks like this:</p>
<pre><code class="language-elixir">def server() do
    {:ok, lsock} = :gen_tcp.listen(5678, [:binary, packet: 0, active: false])
    {:ok, sock} = :gen_tcp.accept(lsock)
    {:ok, bin} = :gen_tcp.recv(sock, 0)
    :ok = :gen_tcp.close(sock)
    :ok = :gen_tcp.close(lsock)
    bin
end
</code></pre>
<p>Some notes about the changes made:</p>
<ul>
<li>When translating the first function call to <code>:gen_tcp.listen</code>, the arguments received by that function are translated as <code>[:binary, {:packet, 0}, {:active, false}])</code>, which can be simplified to a Keyword list in elixir, as Keyword list are just list of tuples.</li>
<li>Erlang instructions always ends with a comma. Think of them as the semicolon (<code>;</code>) in other languages such as C or Java. Since Elixir doesn&rsquo;t use any character to separate instructions in different lines you need to remove them.</li>
<li>The end of a function in Erlang is marked by a dot (<code>.</code>). In Elixir, it&rsquo;s marked by the <code>end</code> keyword.</li>
</ul>
<h2 id="conclusion">
Conclusion
</h2>
<p>And that&rsquo;s it! You now know how to use Erlang applications in your Elixir codebase, and also how to translate Erlang code to Elixir code. You can copy paste the above code in your Elixir project and run it.</p>
]]></content:encoded></item><item><title>How Phoenix LiveView works. A Beginner's Guide to understanding Phoenix LiveView.</title><link>https://aalonso.dev/blog/2024/how-phoenix-liveview-works-a-beginners-guide/</link><pubDate>Sun, 29 Sep 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-phoenix-liveview-works-a-beginners-guide/</guid><category>elixir</category><category>phoenix</category><category>programming</category><description>Learn how Phoenix LiveView works under the hood to provide interactive web applications rendered in the server.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-phoenix-liveview-works-a-beginners-guide/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>In the <em>old days</em> of web development, interacting with a web application meant reloading the entire page every time you wanted to update the user interface. This was slow, inefficient, and made for a poor user experience. Any interaction with the website, no matter how small it was, would require a round trip to the server, which would then generate a new HTML page and send it back to the client, usually a web browser. Then, the web browser would load the new page from zero, to show the updated content.</p>
<p>From a few years back to now, interactive web applications have been built using JavaScript frameworks like React, Angular, or Vue. These frameworks allow developers to build dynamic web applications that can update the user interface without reloading the page. The round trip to the server is still there, but only to fetch the data needed to update the user interface. Now, the server, instead of returning HTML, usually returns only the information needed to the frontend as a JSON document. The user interface is then updated using JavaScript, which is executed in the web browser. JavaScript then updates the DOM of the page and only the affected component changes, without requiring a full page load.</p>
<p>As you probably know by your experience as a developer, this approach does the work, but it has its own set of problems. The most common one is that you need to write and maintain two separate codebases: one for the frontend and one for the backend. This can be a problem because you need to keep both codebases in sync, which can be a source of bugs and inconsistencies. Also, you need to know two different programming languages and frameworks, which can be a barrier for some developers. Yes, you can use node for the backend, which will allow to use the same language for both codebases, but still even then some things has very different considerations.</p>
<h2 id="what-is-phoenix-liveview">
What is Phoenix LiveView?
</h2>
<p><a
  href="https://hexdocs.pm/phoenix_live_view/welcome.html"rel="noopener noreferrer" target="_blank">Phoenix LiveView</a> is an elixir library that provides a new approach to building interactive web applications that aims to solve these problems. It allows you to build interactive web applications using only Elixir and Phoenix, without writing a single line of JavaScript. It achieves that by using server-side rendering and WebSockets.</p>
<p>LiveViews run in a <a
  href="https://www.phoenixframework.org/"rel="noopener noreferrer" target="_blank">Phoenix</a> server, which can scale to handle millions of WebSocket connections, and it has built-in presence tracking so it knows who&rsquo;s connected, and a built-in PubSub system for broadcasting real-time updates to LiveViews. And all this rests on a rock-solid foundation of <a
  href="https://elixir-lang.org/"rel="noopener noreferrer" target="_blank">Elixir</a>, <a
  href="https://www.erlang.org/doc/system/design_principles.html"rel="noopener noreferrer" target="_blank">OTP</a>, and <a
  href="https://www.erlang.org/"rel="noopener noreferrer" target="_blank">Erlang</a>.</p>
<p>So it provides an unrivaled stack for building massively scalable, multi-user, interactive, real-time distributed web apps faster and with less code.</p>
<h2 id="how-does-phoenix-liveview-work">
How does Phoenix LiveView work?
</h2>
<p>A LiveView page is initially rendered as a static HTML page via a request-response cycle (the browser sending the habitual <code>GET</code> request to the server to load a page). Then a persistent WebSocket connection is automatically opened between the browser and a stateful <a
  href="https://hexdocs.pm/phoenix_live_view/welcome.html#what-is-a-liveview"rel="noopener noreferrer" target="_blank">LiveView process</a> running on the server. This WebSocket connection is opened by a JavaScript worker loaded on the initial page load and it&rsquo;s bundled with LiveView. We&rsquo;ll go back to it later.</p>
<p>And what about the interaction? Any interaction on the web application triggers events that are then pushed down the WebSocket to the LiveView process, which causes its state to change. After those state changes, the LiveView process only re-renders the parts of the page that are affected. Those HTML diffs are then sent back to the browser via the WebSocket as a response. Then, our JavaScript worker patches the DOM.</p>
<p>All this happens out of the box. So as the LiveView process receives events from the GUI, changes state, and re-renders, everything is automatically kept in sync without having to write any JavaScript or manage WebSockets.</p>
<h2 id="liveview-life-cycle">
LiveView Life Cycle
</h2>
<p>Let&rsquo;s walk through the life cycle of a LiveView application. When a user initially browses to the website, a regular HTTP GET request is sent to the server, and we have a live route
declared in the router for that.</p>
<pre><code class="language-elixir">defmodule MyAppWeb.Router do
  use Phoenix.Router
  import Phoenix.LiveView.Router

  scope &quot;/&quot;, MyAppWeb do
    live &quot;/counter&quot;, CounterLive
  end
end
</code></pre>
<p>That route indicates that the <code>CounterLive</code> module should be used to handle the request. The CounterLive module is a LiveView module that is responsible for rendering the page and handling events.</p>
<pre><code class="language-elixir">defmodule MyAppWeb.CounterLive do
  # In Phoenix v1.6+ apps, the line is typically: use MyAppWeb, :live_view
  use Phoenix.LiveView

  def render(assigns) do
    ~H&quot;&quot;&quot;
    &lt;div&gt;
      Current value: &lt;%= @counter %&gt;
      &lt;button phx-click=&quot;inc_counter&quot;&gt;Increment counter&lt;/button&gt;
    &lt;/div&gt;
    &quot;&quot;&quot;
  end

  def mount(_params, _session, socket) do
    counter = 0
    {:ok, assign(socket, :counter, counter)}
  end

  def handle_event(&quot;inc_counter&quot;, _params, socket) do
    {:noreply, update(socket, :counter, &amp;(&amp;1 + 1))}
  end
end
</code></pre>
<p>Then, in the <code>CounterLive</code> module, the <code>mount</code> callback is invoked, which assigns the initial state to the socket. In this case, sets its value to zero. Then <code>render</code> is automatically invoked with the state
that was assigned to the socket in <code>mount</code>, and a fully-rendered HTML page is sent back to the browser as a regular HTTP response. Handling the initial request in this way has a few important benefits.</p>
<ul>
<li>First, the response is super quick.</li>
<li>Second, you get a fully-rendered, meaningful HTML page, even if JavaScript is disabled in the browser. And so the initial LiveView page is search engine friendly.</li>
</ul>
<p>Nothing too surprising so far, but here&rsquo;s where things get interesting. When the initial page is loaded, it also loads a sliver of JavaScript. It&rsquo;s an <code>app.js</code>, which opens a persistent WebSocket connection to the server (do you remember I already told you about a JavaScript worker?). It&rsquo;s at this point that a stateful LiveView process is spawned. Mount is then invoked again, this time inside of the stateful process, and initializes the state of that process by assigning values to the socket. Then as you probably already guessed, render is also invoked again to render HTML content for that state. But this time, it&rsquo;s not the raw HTML that&rsquo;s pushed back to the browser over the WebSocket, as you might expect. It&rsquo;s actually something more intriguing and efficient.</p>
<p>Let&rsquo;s focus on this section of the HEEx template</p>
<pre><code class="language-elixir">~H&quot;&quot;&quot;
&lt;div&gt;
  Current value: &lt;%= @counter %&gt;
  &lt;button phx-click=&quot;inc_counter&quot;&gt;+&lt;/button&gt;
&lt;/div&gt;
&quot;&quot;&quot;
</code></pre>
<p>Since the assigned counter value is interpolated in this template, we have one dynamic value (<code>@counter</code>), a value that may or may not have changed, and the rest of the template is static. It will never change. So, LiveView splits the template into parts, the stuff that&rsquo;s dynamic and the stuff that&rsquo;s static. The dynamic values evaluates to <code>0</code> since that&rsquo;s the initial counter. You can think of this value as being at position or
index zero of the template, and all the static and dynamic parts are pushed to the browser over the WebSocket. Then the JavaScript provided by LiveView weaves the static and dynamic parts together.</p>
<p>Splitting the rendered content into static and dynamic parts really pays off when handling events. For example, when we click the button to increment the counter, an <code>inc_counter</code> event is pushed down the WebSocket to the LiveView process and gets handled by a matching <code>handle_event</code> callback. A new value of <code>counter + 1</code> is assigned to the socket, and whenever a LiveView state changes, the render function is automatically called. Since handling the on event only changed the counter value, only these template expressions need to be evaluated. If the LiveView had other pieces of state rendered by the template, they would only be evaluated if they changed when the counter was incremented.</p>
<p>So what&rsquo;s sent to the browser this time? Well, the static parts aren&rsquo;t sent again. They&rsquo;re already cached in the browser. Only the dynamic value that changed or the diffs get sent. So, as before, all the LiveView JS needs to do is zip the static and dynamic parts together, and then it uses the <a
  href="https://github.com/patrick-steele-idem/morphdom"rel="noopener noreferrer" target="_blank">morphdom</a> library to efficiently patch the DOM to increase the counter.</p>
<h2 id="but-how-will-this-scale">
But how will this scale?
</h2>
<p>It&rsquo;s a fair question, especially if you&rsquo;re new to the Elixir/Phoenix platform. Since each stateful LiveView runs in a separate process, depending on the number of users you could have thousands, hundreds of thousands, or even millions of processes. And all the communication happens over WebSockets.</p>
<p>A lot of systems would buckle under these conditions, but Elixir and Phoenix are uniquely suited for it. And by riding atop this battle-tested platform, LiveView is in a league of its own.</p>
<p><a
  href="https://www.phoenixframework.org/blog/the-road-to-2-million-websocket-connections"rel="noopener noreferrer" target="_blank">The Road to 2 Million Websocket Connections</a> has a lot of juicy details.</p>
]]></content:encoded></item><item><title>Linking or importing existing photo files to Synology Photos folder without duplicating space on BTRFS</title><link>https://aalonso.dev/blog/2024/linking-existing-photo-files-to-synology-photos-folder-without-duplicating-space-on-btrfs/</link><pubDate>Fri, 30 Aug 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/linking-existing-photo-files-to-synology-photos-folder-without-duplicating-space-on-btrfs/</guid><category>linux</category><category>synology</category><category>tutorial</category><description>If you want to add existing photo files to Synology Photos from another folder or a backup without actually coping or duplicating them, check this.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/linking-existing-photo-files-to-synology-photos-folder-without-duplicating-space-on-btrfs/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>If you, like me, have a Synology NAS and use Synology Photos to manage your photos, you might have faced the problem of adding existing photo files to Synology Photos from another folder in your NAS (different and outside of your Synology Photos folder) without actually coping or duplicating them, which will took more disk space. In this article, I&rsquo;ll show you exactly that, but you will need a volume with the BTRFS file system.</p>
<h2 id="context-what-was-my-problem-i-was-trying-to-solve">
Context, what was my problem I was trying to solve
</h2>
<p>I recently returned from a long trip to England and Scotland. I took tons of photos with my camera, as did my girlfriend with her phone. Back at home, I wanted to create a macro album of all the photos we took in <a
  href="https://www.synology.com/dsm/feature/photos"rel="noopener noreferrer" target="_blank">Synology Photos</a>, that is what we use at home for our memories together. They were thousands of photos (+5000!), and they were in tons of different folders. I like to separate photos I took with my camera in folders by day and location/activity, all inside my user&rsquo;s Synology Photos folder. The photos my girlfriend took with her phone are in her <code>MobileBackup</code> folder within her user space. I took a few photos with my phone too, and they are in my own <code>MobileBackup</code> folder.</p>
<p>We could manually select the photos and add them to a shared album, and even when you can select multiple photos from a folder to add to an album at once, you must select all of them folder by folder. You cannot select a folder and add all photos there to an album. So as I had a lot of folders with photos because of how I organize the ones took with the camera, it was a tedious process. And also, I have set Synology Photos to show RAW files from the camera, but I didn&rsquo;t those to appear in the macro album, so I would need to manually discriminate RAW files from JPEGs manually for thousands of photos, which is inviable.</p>
<p>So I decided to create a conditional album and select the folders in which I have all my camera photos, plus my <code>MobileBackup</code> folder and my girlfriend&rsquo;s <code>MobileBackup</code> folder, and filter it to do not include RAW files. And here is where the problem arises: I could not access my girlfriend&rsquo;s <code>MobileBackup</code> folder from my user space. Also, my girlfriend, that has a much simpler setup than me (all photos being <code>.heic</code> and being in the same folder, so could multi-select and add at once), could not manually add her photos to the album, as conditional albums doesn&rsquo;t allow manually adding photos.</p>
<p>So after a lot of Google searches to know what could I do to fix this problem, I found this <a
  href="https://www.reddit.com/r/synology/comments/vqcpes/importing_existing_photo_files_into_synology/"rel="noopener noreferrer" target="_blank">Reddit post</a>.</p>
<h2 id="what-didnt-work">
What didn&rsquo;t work
</h2>
<p>My initial idea, as a long time Linux nerd and user, was to use symlinks to link her files to my user space, that I should be able to read as we share a linux user group for that. But Synology Photos doesn&rsquo;t follow symlinks, so that didn&rsquo;t work.</p>
<p>As I saw in <a
  href="https://www.reddit.com/r/synology/comments/vqcpes/importing_existing_photo_files_into_synology/"rel="noopener noreferrer" target="_blank">the Reddit post from Arnout</a>, he tried also a bid mount. This achieves something similar, but requires the mount to be set up at boot every time (in <code>/etc/fstab</code> or in some boot scripts such as <code>rc.local</code>). This is not as clean, but can be used as a sort of a hack to fool applications which otherwise would not want to follow symlinks). However, this didn&rsquo;t work neither.</p>
<p>This leaves us with two possible solutions: hard links and BTRFS CoW (reflink), which are filesystem-specific features.</p>
<h2 id="the-solution">
The solution
</h2>
<p>I also had a BTRFS volume, so I decided to try the reflink method. What this does is that it &ldquo;duplicates&rdquo; the file, but it doesn&rsquo;t actually duplicate the data, it just creates a new metadata entry for the file, and the data is shared between the two files. Once one of the files get modified, then an actual copy is made and they stop sharing data. This is a feature of BTRFS, and it is called <a
  href="https://en.wikipedia.org/wiki/Copy-on-write"rel="noopener noreferrer" target="_blank">Copy-on-Write (CoW)</a>. This is the same feature that allows you to take snapshots of your filesystem without actually duplicating the data.</p>
<p>To achieve this, we can just use the <code>cp</code> command:</p>
<pre><code class="language-bash">cp --reflink=always -R /volume1/teammap/oldbackupfolder /volume1/photo/oldpictures
</code></pre>
<p>In my case, what I did was:</p>
<pre><code class="language-bash">cp --reflink=always -R \
  /volume1/homes/girlfriend/Photos/MobileBackup/iPhone/2024/08/ \
  /volume1/homes/myuser/Photos/Girlfriend_UK_Trip
</code></pre>
<p>Then, I was able to select <code>Girlfriend_UK_Trip</code> folder to the conditional album, and all photos were there, without actually duplicating the space taken.</p>
<h2 id="caveats">
Caveats
</h2>
<p>There a few caveats to know here:</p>
<ul>
<li>This only works with BTRFS volumes. If you don&rsquo;t have a BTRFS volume, you can&rsquo;t use this method. However, hard links might work, but I didn&rsquo;t test it.</li>
<li>Files need to be on the same volume. If you want to link files from different volumes, you will need to use a different method.</li>
<li>This won&rsquo;t be useful if files are being modified constantly, or might be modified in the future. If you modify one of the files in the original folder, the CoW in the photos folder will not get the changes, and it will occupy new extra space, as the data is not shared anymore.</li>
</ul>
<p>If this is something you can live with, then you can use this method to link or import existing photo files to Synology Photos folder without duplicating space on BTRFS. Otherwise, it might be worthy to study other solutions, such as a shared folder between users.</p>
<h2 id="now-what">
Now what?
</h2>
<p>That&rsquo;s all. Thanks for reading this article! If you, like me, are in love with your Synology NAS, you might find interesting other articles I wrote for it. <a
  href="https://aalonso.dev/blog/tags/synology/">Check them out!</a></p>
]]></content:encoded></item><item><title>Binary Search Trees: What are they and how they work</title><link>https://aalonso.dev/blog/2024/binary-search-trees-what-are-they-and-how-they-work/</link><pubDate>Sun, 28 Jul 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/binary-search-trees-what-are-they-and-how-they-work/</guid><category>engineering</category><category>patterns</category><category>programming</category><category>tutorial</category><description>A by example guide to understand binary search trees, their structure, how they work, and how to implement them (in Python).</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/binary-search-trees-what-are-they-and-how-they-work/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Binary Search Trees (BSTs) are a fundamental data structure in computer science. They provide efficient methods for storing, searching, and managing data. In this article, we&rsquo;ll explore what BSTs are, how they work, and how to implement them.</p>
<h2 id="what-is-a-binary-search-tree">
What is a Binary Search Tree?
</h2>
<p>A Binary Search Tree is a binary tree where each node has at most two children, referred to as the left child and the right child. For each node:</p>
<ul>
<li>The left subtree contains only nodes with values less than the node&rsquo;s value.</li>
<li>The right subtree contains only nodes with values greater than the node&rsquo;s value.</li>
</ul>
<p>This property makes BSTs useful for operations like searching, insertion, and deletion.</p>
<p>Okay, now that the basics are covered, let&rsquo;s see how to code and work with a BST! I&rsquo;ll use Python for this example, just because it&rsquo;s very popular and looks like pseudo-code, but the concepts are the same for other programming languages.</p>
<h2 id="code-definition">
Code definition
</h2>
<p>To create a BST data structure, we just need to model the <em>nodes</em> of the graph. Each node will have a value, and pointers to the left and right children, like so:</p>
<pre><code class="language-python">class Node:
    def __init__(self, value):
        self.value = value
        self.left = None
        self.right = None
</code></pre>
<p>So to create a tree, we just create the root node with a value:</p>
<pre><code class="language-python">tree = Node(10) # 10 is the root node
</code></pre>
<p>And that&rsquo;s it! Now, let&rsquo;s see how to implement the basic operations of a BST. We don&rsquo;t like anemic models, so we&rsquo;ll implement the insertion, search, and deletion operations as methods of the <code>Node</code> class. Let&rsquo;s go!</p>
<h2 id="basic-operations">
Basic Operations
</h2>
<p>As I like to learn by example, let&rsquo;s take a look at how to implement a BST in Python.</p>
<h3 id="insertion">
Insertion
</h3>
<p>To insert a value into a BST, we start at the root and compare the value to be inserted with the current node&rsquo;s value. If the value is less, we move to the left child; if greater, we move to the right child. We repeat this process until we find an empty spot where the new value can be inserted.</p>
<pre><code class="language-python">def insert(self, root, key):
    # If the root is None, create a new node with the key and return it
    if root is None:
        return Node(key)
    else:
        # If the key is already present in the tree, return the root
        if root.value == key:
            return root
        # If the key is greater than the root's value, insert it in the right subtree
        if root.value &lt; key:
            root.right = self.insert(root.right, key)
        # If the key is less than the root's value, insert it in the left subtree
        else:
            root.left = self.insert(root.left, key)
    # Return the root after insertion
    return root
</code></pre>
<p>Let&rsquo;s see this in action!</p>
<pre><code class="language-python">tree = Node(10)
tree.insert(tree, 5)
tree.insert(tree, 15)
tree.insert(tree, 3)
tree.insert(tree, 7)
tree.insert(tree, 12)
tree.insert(tree, 18)
</code></pre>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_1.svg"
    alt="The BST after inserting some nodes"width="400"height="200"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_1.svg"
    data-alt="The BST after inserting some nodes"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<h3 id="search">
Search
</h3>
<p>To search for a value in a BST, we start at the root and compare the value with the current node&rsquo;s value. If the value matches, we return the node. If the value is less, we search the left subtree; if greater, we search the right subtree. We repeat that algorithm until we find the node or the node or we end in a leaf node.</p>
<pre><code class="language-python">def search(self, root, key):
    # Base case: return the node if it's found or None if it's not
    if root is None or root.value == key:
        return root
    # If the key is greater than the root's value, search in the right subtree
    if root.value &lt; key:
        return self.search(root.right, key)
    # If the key is less than the root's value, search in the left subtree
    return self.search(root.left, key)
</code></pre>
<p>This will return the node we&rsquo;re looking for, or <code>None</code> if the node is not found.</p>
<pre><code class="language-python">node = tree.search(tree, 12)
print(node and node.value) # 12
</code></pre>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_2.svg"
    alt="Searching for value 12"width="400"height="200"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_2.svg"
    data-alt="Searching for value 12"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<h3 id="deletion">
Deletion
</h3>
<p>Deleting a node from a BST involves three main cases:</p>
<ol>
<li>The node to be deleted is a leaf node.</li>
<li>The node to be deleted has one child.</li>
<li>The node to be deleted has two children.</li>
</ol>
<p>If the node to be deleted is a leaf node, we just need to remove the node from the tree. However, if the node has one or two children, we need to handle the deletion differently. For a node with one child, we replace the node with its child. For a node with two children, we find the inorder successor (the smallest node in the right subtree), replace the node&rsquo;s value with the inorder successor&rsquo;s value, and then delete the inorder successor.</p>
<p>The inorder successor is the smallest node in the right subtree of the node to be deleted (i.e., the leftmost node in the right subtree, or the following node if we order them from lowest to highest).</p>
<pre><code class="language-python">def delete(self, root, key):
    # Base case: if the root is None, return None
    if root is None:
        return root
    # If the key to be deleted is smaller than the root's key,
    # then it lies in the left subtree
    if key &lt; root.value:
        root.left = self.delete(root.left, key)
    # If the key to be deleted is greater than the root's key,
    # then it lies in the right subtree
    elif key &gt; root.value:
        root.right = self.delete(root.right, key)
    # If the key is the same as the root's key, then this is the node
    # to be deleted
    else:
        # Node with only one child or no child
        if root.left is None:
            return root.right
        elif root.right is None:
            return root.left
        # Node with two children: Get the inorder successor
        # (smallest in the right subtree)
        temp = self.minValueNode(root.right)
        # Copy the inorder successor's content to this node
        root.value = temp.value
        # Delete the inorder successor
        root.right = self.delete(root.right, temp.value)
    return root

def minValueNode(self, node):
    # Loop to find the leftmost leaf
    current = node
    while current.left is not None:
        current = current.left
    return current
</code></pre>
<p>Let&rsquo;s see this in action, this time, with a bigger tree. This is how our BST initially looks like:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_3.svg"
    alt="BST prior to deleting"width="500"height="250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_3.svg"
    data-alt="BST prior to deleting"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Now, let&rsquo;s delete the node with value <code>5</code>.</p>
<pre><code class="language-python">tree = tree.delete(tree, 5)
</code></pre>
<p>The resulting tree is a follows:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_4.svg"
    alt="BST after deleting node &amp;ldquo;5&amp;rdquo;"width="500"height="250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_4.svg"
    data-alt="BST after deleting node &amp;ldquo;5&amp;rdquo;"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<h2 id="caveats">
Caveats
</h2>
<p>The efficiency of a BST depends on its balance. If the tree is unbalanced, the search, insertion, and deletion operations can take O(n) time, where n is the number of nodes in the tree. Deletions can affect the balance of the tree, but also insert orders. THe resulting tree will differ depending on the order of insertions. For example:</p>
<pre><code class="language-python">tree = Node(10)
tree.insert(tree, 5)
tree.insert(tree, 15)
tree.insert(tree, 3)
tree.insert(tree, 7)
tree.insert(tree, 12)
tree.insert(tree, 18)
tree.insert(tree, 6)
tree.insert(tree, 8)
tree.insert(tree, 2)
tree.insert(tree, 4)
</code></pre>
<p>will produce a tree like this:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_3.svg"
    alt="BST after inserting nodes in an specific order"width="500"height="250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_3.svg"
    data-alt="BST after inserting nodes in an specific order"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>But if we change the order of insertion like so:</p>
<pre><code class="language-python">tree = Node(10)
tree.insert(tree, 18)
tree.insert(tree, 15)
tree.insert(tree, 12)
tree.insert(tree, 8)
tree.insert(tree, 7)
tree.insert(tree, 6)
tree.insert(tree, 5)
tree.insert(tree, 4)
tree.insert(tree, 3)
tree.insert(tree, 2)
</code></pre>
<pre><code class="language-mermaid">graph TD;
A([10]) --&gt; B([18]);
A --&gt; C([7]);
B --&gt; D([15]);
B --&gt; L([X]);
D --&gt; E([12]);
D --&gt; M([X]);
C --&gt; F([8]);
C --&gt; G([5]);
F --&gt; N([X]);
G --&gt; H([6]);
G --&gt; I([2]);
H --&gt; O([X]);
I --&gt; J([4]);
I --&gt; K([3]);
J --&gt; P([X]);
K --&gt; Q([X]);

</code></pre>
<p>We end up with a quite different tree:</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_5.svg"
    alt="BST after inserting nodes in a different order"width="500"height="250"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/binary-search-trees-what-are-they-and-how-they-work/image_5.svg"
    data-alt="BST after inserting nodes in a different order"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Sometimes, it might be needed to balance the tree when there is a lot of unbalanced nodes, like in the last example. A simple way to improve the balance of a tree is the following:</p>
<pre><code class="language-python">def balanceBST(self, root):
    # Helper function to store the in-order traversal of the BST
    def storeInOrder(node, inorder_nodes):
        if not node:
            return
        storeInOrder(node.left, inorder_nodes)
        inorder_nodes.append(node.value)
        storeInOrder(node.right, inorder_nodes)

    # Helper function to build a balanced BST from sorted nodes
    def buildBalancedBST(nodes, start, end):
        if start &gt; end:
            return None
        mid = (start + end) // 2
        node = Node(nodes[mid])
        node.left = buildBalancedBST(nodes, start, mid - 1)
        node.right = buildBalancedBST(nodes, mid + 1, end)
        return node

    # Store the in-order traversal of the BST
    inorder_nodes = []
    storeInOrder(root, inorder_nodes)

    # Build and return the balanced BST
    return buildBalancedBST(inorder_nodes, 0, len(inorder_nodes) - 1)

</code></pre>
<h2 id="conclusion">
Conclusion
</h2>
<p>Binary Search Trees are a powerful tool for managing ordered data. They provide efficient algorithms for insertion, search, and deletion, making them a fundamental component in computer science. Understanding BSTs and their operations is crucial for any programmer looking to master data structures and algorithms.</p>
<p>All the code from this article can be found <a
  href="https://gist.github.com/tairosonloa/60fd4c5a9c5d28b7403fcadb1ddad0a6"rel="noopener noreferrer" target="_blank">on Github</a>.</p>
]]></content:encoded></item><item><title>A Beginner's Guide to Pointers in Go</title><link>https://aalonso.dev/blog/2024/a-beginners-guide-to-pointers-in-go/</link><pubDate>Sat, 29 Jun 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/a-beginners-guide-to-pointers-in-go/</guid><category>golang</category><category>programming</category><description>A beginner's guide to pointers in Go. Learn what pointers are, how to use them, and why they are useful. Just the basics!</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/a-beginners-guide-to-pointers-in-go/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>In Go, like the majority of programming languages (like C, C++, Rust, etc.), when you assign a value to a variable, that value is stored at a specific memory address in your computer.</p>
<p>You can use the reference operator <code>&amp;</code> to find out this address, as shown below:</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    var myVar int = 35

    fmt.Println(&amp;myVar) // Prints a memory address. Ex: 0xc000012028
}
</code></pre>
<p>Running this code on my computer, I get an output like <code>0xc000012028</code>, which represents the memory address of the variable <code>myVar</code> (displayed in hexadecimal). If you run this code on your own machine or in the <a
  href="https://go.dev/play/p/ZCvcJouK2Ob"rel="noopener noreferrer" target="_blank">Go playground</a>, you’ll likely see a different value.</p>
<h2 id="understanding-pointers-what-are-they">
Understanding pointers. What are they?
</h2>
<p>When you apply the <code>&amp;</code> operator to a variable, it returns a pointer. A pointer is simply a variable that holds the memory address of another variable. You can imagine pointers as <em>&ldquo;pointing to&rdquo;</em> a specific location in memory.</p>
<p>Pointers in Go have types, just like the variables they reference. For example, a pointer of type <code>*int</code> can only store the memory address of an <code>int</code> variable, and a pointer of type <code>*string</code> can only store the memory address of a <code>string</code> variable.</p>
<p>This may seem a bit complex, so let&rsquo;s clarify by modifying the previous example to assign the address of <code>myVar</code> to a new variable instead of printing it. To make it clear, I will use explicit variable declarations without type inference.</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    // Declare myVar variable with the type int and assign the value 35.
    var myVar int = 35


    // Declare myVarPtr variable with the type *int. Use the &amp; operator to
    // get a pointer to the myVar variable and assign it as the value.
    var myVarPtr *int = &amp;myVar

    fmt.Println(myVarPtr) // Prints a memory address. Ex: 0xc000012028
}
</code></pre>
<p>In this case, the variable <code>myVarPtr</code> is a pointer of type <code>*int</code>, holding the memory address of the <code>myVar</code> variable.</p>
<p>We can rewrite this using the more common <code>:=</code> shorthand for variable declarations, like this:</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    myVar := 35
    myVarPtr := &amp;myVar

    fmt.Println(myVarPtr)
}
</code></pre>
<h2 id="using-a-pointer-to-access-a-value-dereferencing">
Using a pointer to access a value (dereferencing)
</h2>
<p>The dereference operator <code>*</code> allows you to read or change the value stored at the address a pointer refers to. This is also called indirection.</p>
<p>Here&rsquo;s an example of how to use the dereference operator to read a value:</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    myVar := 35
    myVarPtr := &amp;myVar

    fmt.Println(myVarPtr)  // Prints a memory address. Ex: 0xc000012028
    fmt.Println(*myVarPtr) // Prints 35
}
</code></pre>
<p>In the above example, <code>myVarPtr</code> is a pointer to the <code>myVar</code> variable. By using <code>*myVarPtr</code>, we can access the value of <code>myVar</code> and print it out.</p>
<p>We can also use the dereference operator to modify the value at the memory address. For example:</p>
<pre><code class="language-go">package main

import &quot;fmt&quot;

func main() {
    myVar := 35
    myVarPtr := &amp;myVar

    *myVarPtr = 88     // Use the dereference operator to assign a new value
    fmt.Println(myVar) // Prints 88
}
</code></pre>
<h2 id="wrapping-up">
Wrapping up
</h2>
<p>That&rsquo;s the basics of pointers in Go. They are just variables that store memory addresses of other variables. You can use the reference operator <code>&amp;</code> to get the memory address of a variable and the dereference operator <code>*</code> to access or modify the value stored at that address. This is useful when you need to pass a variable by reference to a function or when you want to modify the value of a variable from another function, or when you want to avoid copying large data structures.</p>
]]></content:encoded></item><item><title>Public Money, Public Code: why government software should be open source</title><link>https://aalonso.dev/blog/2024/public-money-public-code-why-government-software-should-be-open-source/</link><pubDate>Sun, 26 May 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/public-money-public-code-why-government-software-should-be-open-source/</guid><category>opensource</category><category>programming</category><description>"Public Money, Public Code" is an FSFE initiative advocating that all tax-funded software should be FOSS for transparency, efficiency, and public benefit.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/public-money-public-code-why-government-software-should-be-open-source/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Hello everybody! As someone of you may already know, I&rsquo;m from the European Union, from Spain, and I&rsquo;m a big fan of free open-source software. Today, I want to talk about a very interesting initiative from the <a
  href="https://fsfe.org/index.en.html"rel="noopener noreferrer" target="_blank">Free Software Foundation Europe</a> (FSFE) called <a
  href="https://publiccode.eu/en/"rel="noopener noreferrer" target="_blank">&ldquo;Public Money, Public Code&rdquo;</a>. This initiative advocates that all software developed using public funds (paid from taxes) should be made available as Free and Open Source Software (FOSS).</p>
<p>European citizens will vote next 9th of June for the European Parliament, and I think it&rsquo;s a good time to talk about this initiative that could benefit all of us, not only in Europe but worldwide. You know that some tech-legal initiatives that start in the EU end coming or affecting to the US and the rest of the world too! Let&rsquo;s dive into why this is important and how it can benefit everyone.</p>
<h2 id="what-is-public-money-public-code">
What is &ldquo;Public Money, Public Code&rdquo;?
</h2>
<p>As said, it&rsquo;s all about making sure that software funded by the public is available to the public under free and open-source licenses. In other words, <strong>code paid by the people should be available to the people!</strong></p>
<h2 id="what-are-the-advantages">
What are the advantages?
</h2>
<p>So, what’s at the heart of this initiative? Here are the main points:</p>
<ul>
<li>
<p><strong>Transparency</strong>: With open-source software, anyone can look at the code. This means bugs and security issues get spotted and fixed faster. Plus, it makes government operations way more accountable.</p>
</li>
<li>
<p><strong>Cost-Efficiency</strong>: Sharing is caring, especially with code. Different government agencies can reuse and share software, cutting down on costs and avoiding repetitive spending.</p>
</li>
<li>
<p><strong>Innovation</strong>: Open-source encourages collaboration. Developers worldwide can pitch in, improving public software and creating better solutions.</p>
</li>
<li>
<p><strong>Sovereignty</strong>: Governments can break free from being dependent on big, proprietary software companies. This means more control over their digital tools and fewer worries about vendor lock-in.</p>
</li>
</ul>
<h2 id="why-should-you-care">
Why Should You Care?
</h2>
<p>Think about all the digital services we rely on daily – e-government, healthcare systems, public transportation apps, online education platforms. These run on software that needs to be secure and efficient. Proprietary software can be limiting and expensive, tying up government funds that could be better spent elsewhere.</p>
<p>Free and open-source software (FOSS) offers a brilliant alternative. It&rsquo;s all about open access: anyone can see, modify, and propose improvements the code. This openness doesn’t just save money; it creates a more resilient and adaptable tech ecosystem. Plus, it’s a win for democracy, giving citizens more insight into how their governments operate and how their data is handled.</p>
<h2 id="real-world-wins">
Real-World Wins
</h2>
<p>Several places have already jumped on the &ldquo;Public Money, Public Code&rdquo; bandwagon with great results:</p>
<ul>
<li>
<p><strong>Spain</strong>: During the COVID-19 pandemic, the Spanish government launched and app to track the spread and exposure to the virus by the Spanish citizens. The app used Bluetooth technology to track the proximity of the users to other users, and to link infected people with the people that have been in contact with them. As you can imagine, this raised a lot of privacy concerns, so the app was developed as open-source software, and the code was available for everyone to see it on <a
  href="https://github.com/radarcovid"rel="noopener noreferrer" target="_blank">GitHub</a></p>
</li>
<li>
<p><strong>Spain</strong>: The city of Barcelona has massively adopted open-source software. This switch has saved money and boosted local tech communities while giving the city more control over its digital infrastructure. (<a
  href="https://joinup.ec.europa.eu/collection/open-source-observatory-osor/news/public-money-public-code"rel="noopener noreferrer" target="_blank">Source</a>)</p>
</li>
<li>
<p><strong>France</strong>: The French government has been pushing for more free software in public administration. This move has made things more transparent and opened up more public participation in tech projects. (<a
  href="https://joinup.ec.europa.eu/collection/open-source-observatory-osor/news/new-action-plan-open-source-french-administration"rel="noopener noreferrer" target="_blank">Source</a>)</p>
</li>
<li>
<p><strong>Germany</strong>: Munich’s famous &ldquo;LiMux&rdquo; project, which switched the city’s systems to open-source software, showed both the potential and the challenges of such a transition. Despite some setbacks, it was a significant step towards digital independence. (<a
  href="https://joinup.ec.europa.eu/collection/open-source-observatory-osor/document/limux-it-evolution-open-source-success-story-never"rel="noopener noreferrer" target="_blank">Source 1</a>) (<a
  href="https://joinup.ec.europa.eu/collection/open-source-observatory-osor/document/limux-project-munich"rel="noopener noreferrer" target="_blank">Source 2</a>) (<a
  href="https://joinup.ec.europa.eu/collection/open-source-observatory-osor/document/limux-it-evolution"rel="noopener noreferrer" target="_blank">Source 3</a>)</p>
</li>
</ul>
<h2 id="the-roadblocks">
The Roadblocks
</h2>
<p>Switching to open-source isn’t a walk in the park. Governments often face pushback from big software vendors and have to deal with the headache of changing long-standing systems. Plus, there can be a steep learning curve for public sector IT departments to get up to speed with managing open-source solutions.</p>
<p>Also, those FOSS projects need to be maintained and updated, which can be a challenge for governments with limited resources. And in oposition to a normal FOSS project, the public sector has to deal with a lot of bureaucracy and regulations that can slow down the development process. It might be even possible that some projects are not allowed to directly receive contributions from the public, but we should aim for at least the code to be open and available for everyone to see and point out issues and possible improvements. So there is a big chance that those FOSS projects would be not as active as they should be.</p>
<p>But these challenges can be tackled. With the right planning, training, and support, governments can make the transition to open-source software a success. The benefits – in terms of cost savings, security, and innovation – are well worth the effort.</p>
<h2 id="looking-ahead">
Looking Ahead
</h2>
<p>The &ldquo;Public Money, Public Code&rdquo; initiative isn’t just a cool idea; it’s a blueprint for a more transparent, efficient, and fair digital future. As more governments see the benefits of going open-source, this movement is likely to gain even more momentum.</p>
<p>In short, if public money is funding software, the public should benefit from it. By adopting open-source principles, governments can ensure their digital infrastructure is secure, transparent, and works for everyone. As we continue to move into an increasingly digital world, this approach will be crucial for building trust and fostering innovation in public services. It’s not just about saving money – it’s about creating a better, more open digital world for all of us.</p>
<p>If you want to know more about this initiative, you can visit the <a
  href="https://publiccode.eu/en/"rel="noopener noreferrer" target="_blank">Public Money, Public Code website</a>. And if you think a change like this would be positive, I highly encourage you to sign the <a
  href="https://publiccode.eu/en/openletter/"rel="noopener noreferrer" target="_blank">Open Letter</a> and support this initiative. You don&rsquo;t need to be a European Citizen to show support on the initiative. Let&rsquo;s make sure that public money is used for the public good!</p>
]]></content:encoded></item><item><title>Programming "time": why it's so difficult to work with dates and times in software development</title><link>https://aalonso.dev/blog/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/</link><pubDate>Sat, 27 Apr 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/</guid><category>engineering</category><category>programming</category><description>Have you ever wondered why it's so difficult to work with time and dates in code? Do you want to know why and some curiosities? Then, continue reading.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Okay, raise your hand if you ever had a bug related to dates and times in your code. 🙋‍♂️</p>
<p>You raised your hand, right? Don&rsquo;t worry, me too. And I&rsquo;m pretty sure that almost every developer has already faced this kind of problem at least once in their career. But why is it so difficult to work with dates and times in software development? Why do we have so many problems with them? Today I&rsquo;ll show you some curiosities about time and dates that you probably didn&rsquo;t know. So, let&rsquo;s get started!</p>
<h2 id="assume-that-what-you-know-about-time-is-wrong">
Assume that what you know about time is wrong
</h2>
<p>I&rsquo;m sure you have had to manipulate dates and times in your code at some point. Something as simple as adding a number of days to a date, to set the expiration of an API token, for example.</p>
<p>Let&rsquo;s say you want to create a token that expires after one year. Although is not recommended to use such a long expiration time, let ignore the security implications, as I want to focus only in the date manipulation. You might be tempted to do something like this:</p>
<pre><code class="language-python">from datetime import datetime, timedelta

current_date = datetime.now()
expiration_date = current_date + timedelta(days=365)
print(expiration_date)
</code></pre>
<p>I&rsquo;m sorry to tell you that, if you are doing things that way, you are introducing a bug in your code. Why? Because you are assuming that a year has always 365 days.</p>
<p>If I execute the code above today, April 27, 2024, the expiration date will be April 27, 2025. But if I executed the same code the past year, the expiration date would be April 26, 2024. Why? Because 2024 is a leap year, so February had 29 days. Therefore, if you add 365 days to a date that is in a leap year, you will end up with a date that is one day before the expected date.</p>
<p>This might seem like a silly example, and you might be thinking <em>&ldquo;This doesn&rsquo;t happen in the real world. Any programmer will just add 1 year instead of 365 days to ensure the time library being used takes care of that&rdquo;.</em></p>
<p>Oh, really? Have you ever heard about leap day bugs? There are many examples of software that had bugs related to leap days. There are even people listing them on the internet. For example, <a
  href="https://newsletter.pragmaticengineer.com/p/happy-leap-day"rel="noopener noreferrer" target="_blank">Gergely Orosz did so in The Pragmatic Engineer</a>, and also, Matt Johnson-Pint has been recording <a
  href="https://codeofmatt.com/list-of-2024-leap-day-bugs/"rel="noopener noreferrer" target="_blank">leap day bugs for three different leap years</a>.</p>
<p>Just to name a few striking examples this year:</p>
<ul>
<li>The biggest airline in Colombia, Avianca, printed tickets for February 29, 2024, with the incorrect date of March 1, 2024.</li>
<li>Some EA multiplayer videogames could not be played if the player system date was set to February 29, 2024. A workaround to play that day was to set the system date to March 1, 2024.</li>
<li>Several payment terminals across the globe stopped to work on February 29, 2024.</li>
</ul>
<p>So did you said that that doesn&rsquo;t happen in the real world? That is not a valid argument. It happens, and it happens more that we would like to admit.</p>
<h2 id="not-really-assume-that-what-you-know-about-time-is-wrong">
Not, really, assume that what you know about time is wrong
</h2>
<p>&ldquo;Okay, it happens, but time normally behaves in the same way, those are just some minor exceptions I already know.&rdquo;_ you might be thinking now. Let me ask you: what are those exceptions? You are probably thinking the already mentioned lap years, and also the summer time changes (some countries change the time twice a year, moving the clock one hour forward or backward). Is that all? What if I tell you that the following is true:</p>
<ul>
<li>A day can have 30 hours.</li>
<li>The day after a Thursday is not always a Friday.</li>
<li>Not all leap years occur every 4 years.</li>
<li>Timezones shifts can be greater than +12 hours or lower than -12 hours.</li>
</ul>
<p>And <a
  href="https://gist.github.com/timvisee/fcda9bbdff88d45cc9061606b4b923ca"rel="noopener noreferrer" target="_blank">there are a lot more</a>. So please, <strong>if you want to become a better programmer, assume that what you know about time is wrong</strong>, and take special care when working with dates and times in your code. Always use battle-tested time manipulation libraries, and always test your code with different dates and times.</p>
<p>Now, let me explain the four examples I gave you:</p>
<h3 id="a-day-can-have-30-hours">
A day can have 30 hours
</h3>
<p>In Japan, times past midnight can also be counted past the 24 hour mark, usually when an associated activity spans across midnight. For example, bars or clubs may advertise as being open until &ldquo;30時&rdquo; (i.e. 6 am). <a
  href="https://en.wikipedia.org/wiki/Date_and_time_notation_in_Japan#Time"rel="noopener noreferrer" target="_blank">This is a cultural thing in Japan</a>, partly because the closing time is considered part of the previous business day.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/japan-30-hours.jpeg"
    alt="Sign in Japan indicating the opening hours of some clubs using a 30 hours clock"width="360"height="640"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/japan-30-hours.jpeg"
    data-alt="Sign in Japan indicating the opening hours of some clubs using a 30 hours clock"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<h3 id="the-day-after-a-thursday-is-not-always-a-friday">
The day after a Thursday is not always a Friday
</h3>
<p>On the last week of December 2011, the Samoan government decided to skip December 30, 2011, and move directly to December 31, 2011 from December 29, 2011. This was done to align the country&rsquo;s calendar with its main trading partners, Australia and New Zealand. That means, <a
  href="https://www.nytimes.com/2011/12/30/world/asia/samoa-to-skip-friday-and-switch-time-zones.html"rel="noopener noreferrer" target="_blank">people at Samoa went to bed on Thursday, and wake up next day on Saturday</a>. They had not Friday that week.</p>
<h3 id="not-all-leap-years-occur-every-4-years">
Not all leap years occur every 4 years
</h3>
<p>Okay, this is a tricky one. The Gregorian calendar has a leap year every 4 years. That effectively means, that every year divisible by 4 will be a leap year, right? Like 2024, 2028&hellip; 2100? No, because <a
  href="https://en.wikipedia.org/wiki/Century_leap_year"rel="noopener noreferrer" target="_blank">there are exceptions</a>. For example, the year 2000 was a leap year, but the year 1900 was not. Also, the year 2100 won&rsquo;t be a lap year neither. This is because the Gregorian calendar has a rule that states that years that are divisible by 100 are not leap years, unless they are also divisible by 400.</p>
<p>That&rsquo;s because a year is not exactly 365.25 days long, but 365.2425 days long. So, if we don&rsquo;t correct this, we would have an error of 0.75 days every 100 years (about 3 extra leap days in 400 years). By ensuring only century years divisible by 400 are leap years, we correct this error, as we are removing 3 leap days every 400 years (there are 3 out of 4 century years that are not being defined as leap years).</p>
<h3 id="timezones-shifts-can-be-greater-than-12-hours-or-lower-than--12-hours">
Timezones shifts can be greater than +12 hours or lower than -12 hours
</h3>
<p>Okay, we all know that working with timezones is a pain. But if a day has 24 hours (forget about Japan this time), and if we consider Greenwich meridian as UTC+0, then the maximum shift should be +12 hours or -12 hours, right? It doesn&rsquo;t have sense to have a UTC+14 timezone, right? Shouldn&rsquo;t that be UTC-10?</p>
<p>Well, let me introduce you to the <a
  href="https://www.timeanddate.com/time/zones/lint"rel="noopener noreferrer" target="_blank">Line Islands</a>. The Line Islands are a group of atolls in the central Pacific Ocean, south of Hawaii, that are part of Kiribati. The Line Islands are the only place on Earth where the time zone is UTC+14. That means that when it is 14:00 in Line Islands, it is 12:00 in the nearby Baker Island (UTC-12). That is a 26 hours difference! So here is another <em>bonus</em> falsehood about time (take it as a 2 by 1 offer): different places in Earth can have more than 24 hours of difference between them too! (and we are not taking into account summer time changes!).</p>
<p>Also, the Antarctica, been in a pole, <a
  href="https://en.wikipedia.org/wiki/Time_in_Antarctica"rel="noopener noreferrer" target="_blank">has a lot of timezones with shifts greater than 12 hours</a>. For example, the timezone of McMurdo Station, when Daylight Saving Time is in effect, is UTC+13.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/antarctica-timezones.png"
    alt="Map of approximate time zones on the continent of Antarctica"width="1852"height="1760"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/programming-time-why-it-is-so-difficult-to-work-with-dates-and-times-in-software-development/antarctica-timezones.png"
    data-alt="Map of approximate time zones on the continent of Antarctica"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Do you want more about timezones? <a
  href="https://www.bbc.com/news/science-environment-68722032"rel="noopener noreferrer" target="_blank">We might need to differentiate between Earth and Moon timezones soon</a>.</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>Time is a complex thing. It is not as simple as we might think. There are a lot of exceptions and special cases that we need to take into account when working with dates and times in software development. We, as developers, need to be aware of these exceptions and take special care when working with dates and times in our code. Always ensure that you are using battle-tested libraries to manipulate dates and times.</p>
<p>As a last defensive resource, ensure that you are testing your code with different dates and times. Take into account different time exceptions and special cases, like those I showed you today, when you are building your test scenarios. Until next <em>time</em>!</p>
]]></content:encoded></item><item><title>How to mount a Synology SHR1 disk on Linux (two disks volume - RAID1 with LVM)</title><link>https://aalonso.dev/blog/2024/how-to-mount-a-synology-shr1-disk-on-linux-2-disks-volume-raid1-with-lvm/</link><pubDate>Sun, 31 Mar 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-to-mount-a-synology-shr1-disk-on-linux-2-disks-volume-raid1-with-lvm/</guid><category>linux</category><category>synology</category><category>tutorial</category><description>Learn how to mount a Synology SHR1 disk on Linux (from a two disks volume - underlying RAID1 with LVM) and access its content.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-to-mount-a-synology-shr1-disk-on-linux-2-disks-volume-raid1-with-lvm/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><div
  class="border-l-4 border-blue-500 dark:border-blue-400 bg-blue-50 dark:bg-blue-800/20 p-4 rounded-r-lg my-6 not-prose"
>
  <div class="font-bold text-sm 2xl:text-base flex items-center gap-2 mb-2 text-blue-800 dark:text-blue-200">
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="size-5">
      <path
        d="m5.433 13.917 1.262-3.155A4 4 0 0 1 7.58 9.42l6.92-6.918a2.121 2.121 0 0 1 3 3l-6.92 6.918c-.383.383-.84.685-1.343.886l-3.154 1.262a.5.5 0 0 1-.65-.65Z"
      />
      <path
        d="M3.5 5.75c0-.69.56-1.25 1.25-1.25H10A.75.75 0 0 0 10 3H4.75A2.75 2.75 0 0 0 2 5.75v9.5A2.75 2.75 0 0 0 4.75 18h9.5A2.75 2.75 0 0 0 17 15.25V10a.75.75 0 0 0-1.5 0v5.25c0 .69-.56 1.25-1.25 1.25h-9.5c-.69 0-1.25-.56-1.25-1.25v-9.5Z"
      />
    </svg>

    Edited: <time datetime="2024-09-08">Sep 08, 2024</time>
  </div>
  <div class="prose dark:prose-invert prose-sm 2xl:prose-base max-w-full text-blue-900 dark:text-blue-100">
    This tutorial is about how to access a single disk from a Synology SHR1 volume of 1 or 2 disks
(RAID1 under the hood). If you have more disks or a different SHR configuration, Synology recently
released an <a
  href="https://kb.synology.com/en-us/DSM/tutorial/How_can_I_recover_data_from_my_DiskStation_using_a_PC"rel="noopener noreferrer" target="_blank">official tutorial on how to mount all disks on a PC to restore
data</a>.
  </div>
</div>

<h2 id="background-story">
Background story
</h2>
<p>I have a <a
  href="https://global.download.synology.com/download/Document/Hardware/DataSheet/DiskStation/20-year/DS920&#43;/enu/Synology_DS920_Plus_Data_Sheet_enu.pdf"rel="noopener noreferrer" target="_blank">Synology DS920+</a> NAS at home that I really love, and that I use for a tons of things. It holds family files (<a
  href="https://www.synology.com/en-us/dsm/feature/drive"rel="noopener noreferrer" target="_blank">Synology Drive</a>), family photos (<a
  href="https://www.synology.com/en-us/dsm/feature/photos"rel="noopener noreferrer" target="_blank">Synology Photos</a>), a Plex server, some Docker containers, and backups from other servers at home. Everything just works, and it has minimal maintenance. I cannot imagine my daily live without it anymore.</p>
<p>When I initially bought it back in 2020, I configured it with 2x 6TB disks in a SHR1 raid configuration. SHR1 is a Synology proprietary RAID configuration with redundancy of one disk (when you have at least two disks). Unlike traditional raid solutions like RAID1 or RAID5, that requires disks of the same size, or otherwise the available storage space is up to the size of the smaller disk, SHR1 is more flexible and allows you to add disks of different sizes to the volume and take advantage of the extra space.</p>
<p>How does SHR work? Under the hood, it uses LVM (Logical Volume Manager) to manage the disks and create different volumes with different sizes and raid configuration (it uses RAID1 when the volume takes two disks, and RAID 5 for 3+). If you are interested, you can read more about it in the <a
  href="https://kb.synology.com/en-global/DSM/tutorial/What_is_Synology_Hybrid_RAID_SHR"rel="noopener noreferrer" target="_blank">Synology Knowledge Center</a> and play with the <a
  href="https://www.synology.com/en-us/support/RAID_calculator?drives=8%20TB%7C8%20TB%7C6%20TB&amp;raid=SHR_1%7CRAID_5"rel="noopener noreferrer" target="_blank">Synology RAID Calculator</a>.</p>
<p>Recently, I bought two new 8TB disks, and over the years, a few fancy features where released by Synology (like full volume encryption), so I decided to, instead to add the new disk on the empty slots and expand the volume, to <em>reinstall</em> with a new encrypted SHR1 volume with 1x 6TB and 2x 8TB disks, leaving the spare 6TB disk for quick-recovery local backups. Again, under the hood, this means two LVM volumes, one with 6TB and RAID5 (accross the three disks), and another with 2TB and RAID1 (both 8TB disks).</p>
<p>So I did that, and restored system configuration and critical data from my <a
  href="https://c2.synology.com/en-us/storage/nas"rel="noopener noreferrer" target="_blank">Synology C2 cloud</a> backup with <a
  href="https://www.synology.com/en-us/dsm/feature/hyper_backup"rel="noopener noreferrer" target="_blank">Hyper Backup</a>. It took like a whole day for 2TB of data, but it worked like a charm. I restored data from the cloud backup because I took this opportunity to test my cloud backups setups, and with the assurance that I could restore my files (and other files not covered by the cloud backup) at any moment from the spare 6TB disks if something went wrong.</p>
<p>Until this moment, everything was fine. Then I tried mounting the old 6TB disk on my Linux desktop to access the files, and I had some trouble. I got one error after another, and I had to spend a few days investigating and trying different things to finally mount the disk and access the files. So I decided to write this article to help others that may have the same problem.</p>
<p>Please, if you try to do the same thing, <strong>ensure first you are able to access the files on the old disk from a Linux PC before you proceed with the new installation</strong>. I didn&rsquo;t do that, because I wrongly assumed that the disk would be mounted without any problem just out of the box. Luckily, I had a cloud backup of important files as I said, but I would have been freaked out if I didn&rsquo;t have it and just assumed that the files were safe on the old disk. Also, remember to always do a <a
  href="https://www.youtube.com/watch?v=u_77X-MlCnk"rel="noopener noreferrer" target="_blank">3-2-1 backup strategy</a>, with at least 3 copies of your data, 2 local but on different devices, and 1 offsite.</p>
<h2 id="how-to-mount-the-synology-shr1-raid1-disk-on-linux-and-access-its-content">
How to mount the Synology SHR1 (RAID1) disk on Linux and access its content
</h2>
<p>The first problem that I found was that modern Linux kernel (5.4+) doesn&rsquo;t support the old btrfs version used by Synology. To the date of writing this article (March 2024), Synology uses an old version of the Linux kernel (4.4.302+, under DSM 7.2.1), and includes some custom volume flags <a
  href="https://www.reddit.com/r/synology/comments/u6y5qm/has_anyone_found_a_solution_for_mounting_synology/"rel="noopener noreferrer" target="_blank">that are not supported by the mainline kernel</a>, leading to an error when trying to mount the disk (from <code>dmesg</code>):</p>
<pre><code class="language-txt">BTRFS critical (device sde6): corrupt leaf: root=1 block=31883264 slot=2, invalid root flags, have 0x400000000 expect mask 0x1000000000001
</code></pre>
<p>The solution I found was to setup a VM of Ubuntu Bionic Beaver, and install there a 4.15 kernel. I got the idea from a <a
  href="https://www.reddit.com/r/synology/comments/u6y5qm/comment/i5lk4jn/?utm_source=share&amp;utm_medium=web3x&amp;utm_name=web3xcss&amp;utm_term=1&amp;utm_content=share_button"rel="noopener noreferrer" target="_blank">comment</a> in the above linked Reddit post</p>
<pre><code class="language-bash"># Installing the kernel in ubuntu
sudo apt install linux-image-4.15.0-108-generic
# Reboot and select it on the grub menu, inside the advanced options submenu
</code></pre>
<p>Then, we can proceed to mount the disk.</p>
<pre><code class="language-bash">sudo mount /dev/sdb5 /mnt/synology
</code></pre>
<pre><code class="language-txt">mount: /mnt/synology: unknown filesystem type 'linux_raid_member'.
</code></pre>
<p>Here we got the first error. As the disk was part of a RAID1 filesystem (remember, SHR1 with two disks is RAID1 with LVM), I first need to create a software RAID to mount it. Let&rsquo;s try to assemble it.</p>
<pre><code class="language-bash">sudo mdadm --assemble --run /dev/md0 /dev/sdb5
</code></pre>
<pre><code class="language-txt">mdadm: /dev/sdb5 is busy - skipping
</code></pre>
<p>Second error. It says the disk is busy, but it&rsquo;s not even mounted. Let&rsquo;s see what&rsquo;s happening.</p>
<pre><code class="language-bash">sudo mdadm --examine /dev/sdb
</code></pre>
<pre><code class="language-txt">/dev/sdb:
  MBR Magic : aa55
  Partition[0] :   4294967295 sectors at            1 (type ee)
</code></pre>
<p>Okay, let&rsquo;s see what is using it. I&rsquo;ll type <code>ls /dev/md</code> on my terminal and then press <em>tab</em> and see what auto-completes. In my case, it was <code>/dev/md127</code>. Let&rsquo;s stop it and try to create again the software RAID at <code>/dev/md0</code></p>
<pre><code class="language-bash">sudo mdadm --stop /dev/md127
sudo mdadm --assemble --run /dev/md0 /dev/sdb5
</code></pre>
<pre><code class="language-txt">mdadm: /dev/md0 has been started with 1 drive (out of 2).
</code></pre>
<p>Our first success! We can ignore the warning about the missing disk, as we are only interested in mounting the disk to access the files. Let&rsquo;s try to mount it again.</p>
<pre><code class="language-bash">sudo mount /dev/md0 /mnt/synology
</code></pre>
<pre><code class="language-txt">mount: /mnt/synology: unknown filesystem type 'LVM2_member'.
</code></pre>
<p>Now that things were working we got another error. In that case, we found that our RAID1 disk was using LVM, which is how SHR works as I already explained. Let&rsquo;s see what LVM volumes are available using <code>vgs</code></p>
<pre><code class="language-bash">sudo vgs
</code></pre>
<pre><code class="language-txt">VG     #PV #LV #SN Attr   VSize VFree
vg1000   1   1   0 wz--n- 5.45t    0
</code></pre>
<p>Okay, it seems like we have a volume group named <code>vg1000</code>. Let&rsquo;s mount the logical volume there.</p>
<pre><code class="language-bash">sudo mount /dev/mapper/vg1000-lv /mnt/synology
</code></pre>
<p>Final success! The disk was successfully mounted and I could see all of its data in <code>/mnt/synology</code>. Phew!</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>From this experience, I learned a few things the hard way</p>
<ul>
<li>Don&rsquo;t expect that mounting a RAID1 disk will be as easy as mounting an external drive formatted with ext4 or nfts. It&rsquo;s not plug and play.</li>
<li>Especially, if it&rsquo;s not RAID1 per se. In my case, it was a SHR1, that also includes a new actor I never dealt with before, LVM.</li>
<li><strong>Always have a backup of your data.</strong> I had a cloud backup of important files, but I would have been freaked out if I didn&rsquo;t have it and just assumed that the files were safe on the old disk.</li>
</ul>
<p>I hope this article helps you to mount your Synology disk on Linux and access its content. Hopefully, you have wasted way less time than I did, until my article popped up on your search results. Lastly, today is March 31st (<a
  href="https://www.worldbackupday.com/en"rel="noopener noreferrer" target="_blank">Backup Day!</a>), so let me take this opportunity to remember you the importance of doing a good backup of your data. Have a nice day!</p>
]]></content:encoded></item><item><title>F.I.R.S.T. principles of testing</title><link>https://aalonso.dev/blog/2024/f-i-r-s-t-principles-of-testing/</link><pubDate>Sun, 25 Feb 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/f-i-r-s-t-principles-of-testing/</guid><category>agile</category><category>architectures</category><category>engineering</category><category>patterns</category><category>programming</category><description>The F.I.R.S.T principles serve as a foundational framework for effective code testing, that help to create robust and reliable code tests.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/f-i-r-s-t-principles-of-testing/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>Today, I want to do a brief introduction to the F.I.R.S.T principles of testing, similar to what I did in the article about the <a
  href="https://aalonso.dev/blog/2022/a-brief-introduction-to-solid-principles/">S.O.L.I.D principles of software design</a>.</p>
<p>The F.I.R.S.T principles serve as a foundational framework for effective code testing. These principles, which stand for <em>Fast, Isolated, Repeatable, Self-validating, and Thorough</em>, guide developers in creating robust and reliable tests to ensure the quality and integrity of their code.</p>
<p>Let&rsquo;s take a closer look at each of the F.I.R.S.T principles:</p>
<h2 id="fast">
Fast
</h2>
<p>One key tenet of the F.I.R.S.T principles is speed. Developers are encouraged to run unit tests swiftly and effortlessly throughout the development process. Regardless of the number of unit tests in place, they should execute rapidly, providing prompt feedback on the code&rsquo;s correctness.</p>
<p>If tests take too long to run, developers may be less inclined to run them frequently, or even adding new ones, leading to a decrease in the overall quality of the codebase. Fast tests are essential for maintaining a productive and efficient development workflow, and a robust and reliable codebase.</p>
<h2 id="isolated">
Isolated
</h2>
<p>The principle of isolation emphasizes the independence of each unit test. Tests should not rely on external factors or be influenced by the outcomes of other tests.</p>
<p>It&rsquo;s a good practice to follow the <a
  href="https://xp123.com/3a-arrange-act-assert/"rel="noopener noreferrer" target="_blank">3 A’s of testing: Arrange, Act, Assert</a> (also known as <a
  href="https://martinfowler.com/bliki/GivenWhenThen.html"rel="noopener noreferrer" target="_blank">Given, When, Then by Martin Fowler</a>), which defines a structure that helps maintain a clear and autonomous testing environment.</p>
<p>Basically, the test should first arrange the necessary preconditions and inputs, then act on the object or method under test, and finally assert that the expected results have occurred.</p>
<p>A unit test should only assert one logical outcome (test one single thing). Multiple asserts can be part of this logical outcome, as long as they all act on the state of the same object.</p>
<h2 id="repeatable">
Repeatable
</h2>
<p>Reproducibility is crucial in the F.I.R.S.T methodology. Tests should be consistent and deterministic, producing the same results regardless of the environment in which they are executed. Each test should establish its own data context, reducing reliance on external variables.</p>
<p>Sadly, we all know about the <a
  href="https://martinfowler.com/articles/nonDeterminism.html"rel="noopener noreferrer" target="_blank">flaky tests</a> that sometimes pass and sometimes fail, and how they can be a nightmare to deal with. Sometimes your CI tests jobs fails, blocks your deploy, and just rerunning the same job without changing anything makes it pass. The F.I.R.S.T principles advocate for the elimination of flaky tests, as they can erode confidence in the testing process and the codebase as a whole.</p>
<h2 id="self-validating">
Self-validating
</h2>
<p>Manual verification of test outcomes should be unnecessary. Tests should inherently indicate success or failure, facilitating quick identification of issues during the development process.</p>
<h2 id="thorough">
Thorough
</h2>
<p>Exhaustive testing is a fundamental aspect of the F.I.R.S.T principles. Developers are encouraged to cover all possible scenarios, including edge cases and potential failure points. Testing should extend beyond typical use cases and happy paths to encompass illegal arguments, security considerations, and the impact of large input values. Try to cover all the branches of your code and all possible scenarios.</p>
<h2 id="summarizing">
Summarizing
</h2>
<p>In conclusion, the F.I.R.S.T principles provide a structured approach to code testing, emphasizing speed, independence, repeatability, self-validation, and thoroughness. By incorporating these principles into the testing process, developers can enhance the readability and maintainability of their tests, and also the reliability and maintainability of their code, ultimately contributing to the delivery of high-quality software.</p>
]]></content:encoded></item><item><title>How to use docker engine without Docker Desktop on macOS with Colima</title><link>https://aalonso.dev/blog/2024/how-to-use-docker-engine-without-docker-desktop-macos-colima/</link><pubDate>Mon, 22 Jan 2024 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2024/how-to-use-docker-engine-without-docker-desktop-macos-colima/</guid><category>docker</category><category>macOS</category><category>tutorial</category><description>I'll show you how to use docker on macOS using Colima, a free and open source project that provides a native macOS backend for docker.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2024/how-to-use-docker-engine-without-docker-desktop-macos-colima/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>In August 2021, Docker announced that they would change their licensing model and that <a
  href="https://www.docker.com/blog/updating-product-subscriptions/"rel="noopener noreferrer" target="_blank">Docker Desktop would no longer be free for commercial use</a>. This made a lot of companies and developers look for alternatives to run docker containers on their Mac machines. That&rsquo;s because, if you are like me and a lot of other professional developers, your are probably coding for a company with a MacBook or another macOS machine, and you only use docker for development purposes, like say, have a local database instance for development. Probably, you don&rsquo;t even use the Docker Desktop GUI nor any of its &ldquo;fancy&rdquo; features, and you only interact with the docker engine from the command line. Ah, those good old fashioned <code>docker</code> and <code>docker-compose</code> commands.</p>
<p>Good news are, docker engine is licensed under Apache license, so you can use it for free, even for commercial purposes. There is not need to migrate to another container solution.</p>
<p>So okay, let&rsquo;s get rid of Docker Desktop and continue using docker from the CLI then. At the end of the day, Docker Desktop is just a GUI for the docker engine, and you don&rsquo;t need it, right? Well, not so fast.</p>
<p>The thing is, Docker containers are a linux native technology. You cannot have docker containers, or any other kind of containers, outside of Linux. So, in order to run docker on macOS (or even Windows), you need to have a Linux virtual machine running on your Mac. And that&rsquo;s exactly what Docker Desktop does. And that&rsquo;s why it&rsquo;s so heavy on your machine. It configures and run a full Linux virtual machine for you, out of the box, to use docker on it.</p>
<p>Hopefully, there are other alternatives to run docker on macOS without needed Docker Desktop. We just need a way to run and integrate a Linux VM with docker in the Mac, and one way is to use <a
  href="https://github.com/abiosoft/colima"rel="noopener noreferrer" target="_blank">Colima</a>, a container runtime for macOS that provides a backend for docker using qemu. It&rsquo;s build on top of <a
  href="https://github.com/lima-vm/lima"rel="noopener noreferrer" target="_blank">Lima</a>, that is like <a
  href="https://aalonso.dev/blog/tags/wsl/">WSL2 in Windows</a>, but for macOS: a lightweight virtual machine that runs a Linux distribution.</p>
<p>In this article, I&rsquo;ll show you how to use docker on macOS using Colima. It&rsquo;s very easy to install and use, and it&rsquo;s free and open source. Let&rsquo;s get started.</p>
<h2 id="step-by-step-guide-to-run-docker-engine-on-macos-with-colima">
Step by step guide to run docker engine on macOS with Colima
</h2>
<p>First what is first, get rid of Docker Desktop! You can uninstall it like any other app in macOS, just drag it to the trash bin. But I recommend you to first clean the installation from Docker Desktop itself, as we already saw it does a lot of things under the hook for you (set ups the linux VM, links docker, etc). It&rsquo;s better to ensure that everything is clean before installing Colima. To do so, open Docker Desktop and go to the Troubleshoot menu (the one with the bug in the upper right corner), and click on the &ldquo;Uninstall&rdquo; button. This will remove all the data and configuration files that Docker Desktop created on your machine.</p>
<p><div class="image-wrapper relative">
  <img
    src="https://aalonso.dev/images/articles/2024/how-to-use-docker-engine-without-docker-desktop-macos-colima/uninstall-docker-desktop.png"
    alt="Uninstall Docker Desktop"width="2024"height="1664"
    loading="lazy"
    class="article-image cursor-pointer transition-transform hover:scale-[1.02] mb-0!"
    data-full-src="https://aalonso.dev/images/articles/2024/how-to-use-docker-engine-without-docker-desktop-macos-colima/uninstall-docker-desktop.png"
    data-alt="Uninstall Docker Desktop"
  />
  <p class="text-center text-sm text-gray-500 dark:text-gray-400">Click on image to enlarge</p>
</div>
</p>
<p>Then, move the Docker Desktop app to the trash bin if required (the Desktop app will probably ask you to do so).</p>
<p>Next, let&rsquo;s move to the installation of Colima. You can install it using <a
  href="https://brew.sh/"rel="noopener noreferrer" target="_blank">Homebrew</a>, the package manager for macOS. Just run the following command in your terminal. Note that installing docker engine is necessary, as you deleted it when you uninstalled Docker Desktop.</p>
<pre><code class="language-bash">brew install docker-credential-helper docker colima
</code></pre>
<p>Now, let&rsquo;s create the linux virtual machine that will run the docker engine.</p>
<pre><code class="language-bash">colima start
</code></pre>
<p>This will create a new VM with the default config (2 CPUs, 2GiB RAM, 60GiB disk), but you can customize it to your needs. You can check <code>colima start --help</code> for more information. For example, if you want to use 4 CPUs, 4GiB RAM and 100GiB disk, you can run the following command:</p>
<pre><code class="language-bash">colima start -c 4 -m 4 -d 100
</code></pre>
<p>On my M2 Pro MacBook Pro, I&rsquo;m using the following command to start the VM, fine-tuned for Apple Silicon machines:</p>
<pre><code class="language-bash">colima start --arch aarch64 --vm-type=vz --vz-rosetta --cpu 4 --memory 4 --disk 64
</code></pre>
<p>What this command specifically does is:</p>
<ul>
<li><code>--arch aarch64</code>: Specifies the CPU architecture as ARM64 (used for Apple Silicon Macs).</li>
<li><code>--vm-type=vz</code>: Uses Apple&rsquo;s Virtualization.framework (VZ) as the virtualization backend.</li>
<li><code>--vz-rosetta</code>: Enables Rosetta 2 translation for running x86_64 containers on ARM.</li>
<li><code>--cpu 4</code>: Use 4 CPUs.</li>
<li><code>--memory 4</code>: Use 4GiB of RAM.</li>
<li><code>--disk 64</code>: Use 64GiB of disk space.</li>
</ul>
<p>Anyway, after you start the VM one way or another, you can run <code>docker ps</code> and see that the docker engine is running as before.</p>
<h3 id="using-docker-compose">
Using docker-compose
</h3>
<p>If you use <code>docker-compose</code> or the <code>compose</code> command in docker, we need to install it separately and link it as a CLI addon for docker. To do so, run the following command:</p>
<pre><code class="language-bash">brew install docker-compose
mkdir -p ~/.docker/cli-plugins
ln -sfn /opt/homebrew/opt/docker-compose/bin/docker-compose ~/.docker/cli-plugins/docker-compose
</code></pre>
<h3 id="autostart-on-login">
Autostart on login
</h3>
<p>If you want to start the VM automatically when you login to your Mac, you can do so by running the following command:</p>
<pre><code class="language-bash">brew services start colima
</code></pre>
<h2 id="troubleshooting">
Troubleshooting
</h2>
<p>Colima works with docker out of the box, and this is because <a
  href="https://github.com/abiosoft/colima/blob/main/docs/FAQ.md#cannot-connect-to-the-docker-daemon-at-unixvarrundockersock-is-the-docker-daemon-running"rel="noopener noreferrer" target="_blank">it uses docker contexts</a> to correctly set the docker daemon for you. However, some docker applications doesn&rsquo;t honor the docker contexts, and instead, relies on a hardcoded path for the usual location of the docker daemon.</p>
<p>If after having the above steps you get the following error when running docker commands:</p>
<pre><code class="language-txt">Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?
</code></pre>
<p>You probably need to set the <code>DOCKER_HOST</code> environment variable to the correct value, like this:</p>
<pre><code class="language-bash">export DOCKER_HOST=&quot;unix://${HOME}/.colima/default/docker.sock&quot;
</code></pre>
<p>Or you can instead link the Colima socket to the default socket path, but note that this may break other Docker servers</p>
<pre><code class="language-bash">sudo ln -sf $HOME/.colima/default/docker.sock /var/run/docker.sock
</code></pre>
<p>If you have other problems, take a look at their <a
  href="https://github.com/abiosoft/colima/tree/main/docs"rel="noopener noreferrer" target="_blank">docs in their repo</a>. I hope not! I didn&rsquo;t have any problem at all.</p>
]]></content:encoded></item><item><title>Software design patterns: the Builder pattern in Go</title><link>https://aalonso.dev/blog/2023/software-design-patterns-the-builder-pattern-go/</link><pubDate>Sat, 23 Dec 2023 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2023/software-design-patterns-the-builder-pattern-go/</guid><category>golang</category><category>patterns</category><category>programming</category><description>Builder is a creational software design pattern, which allows constructing complex objects step by step. I'll show you how to implement it in Go.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2023/software-design-patterns-the-builder-pattern-go/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>The builder pattern is a technique where a developer uses a &ldquo;builder&rdquo; to construct an object. The end result could
be anything - a string, a struct instance, or even a closure - but it is built using a builder. More often than not a
builder is used because the object being created is complex and needs to be constructed in multiple steps, so
the builder helps isolate each step and prevent bugs.</p>
<p>Imagine a complex object that requires laborious, step-by-step initialization of many fields and nested objects.
Such initialization code is usually buried inside a monstrous constructor with lots of parameters. Or even worse: scattered all over the client code.
Here is where the builder pattern comes to the rescue.</p>
<p>To illustrate the idea, I&rsquo;ll provide an easy example, for what the Builder pattern would be an overkill, but it&rsquo;ll help us understand this.
Obviously, as the title of this article says, I&rsquo;ll be using Go, but note that this is an universal software design pattern, so it can be applied to any language that supports some kind of Object Oriented Programming.</p>
<h2 id="naive-example">
Naive example
</h2>
<p>Imagine you have an <code>Car</code> struct:</p>
<pre><code class="language-go">type Car struct {
  Model string
  Type string
  Traction string
}
</code></pre>
<p>Under most circumstances a developer would create an instance of the <code>Car</code> struct in their code and assign
a value to each of these fields, but in some circumstances you might want to do more than that. For instance,
you might decide that when the <code>Type</code> field is set you can also assign the <code>Traction</code> field based on
the car&rsquo;s type. This could be achieved using a builder.</p>
<pre><code class="language-go">type Builder struct {
  c Car
}

func (b *Builder) Build() Car {
  return b.c
}

func (b *Builder) Model(model string) *Builder {
  b.c.Model = model
  return b
}

func (b *Builder) Type(carType string) *Builder {
  if carType == &quot;SUV&quot; {
    b.c.Traction = &quot;4x4&quot;
  } else {
    b.c.Traction = &quot;front&quot;
  }
  b.c.Type = carType
  return b
}
</code></pre>
<p>Now when we are constructing a <code>Car</code> we can use the builder to ensure that the <code>Traction</code> gets
set when we set the <code>Type</code> field. We could even add validation in and verify that the <code>Type</code> is a valid one - though at
that point we would need to also return an error.</p>
<p>Below we can see how this builder might be used (<a
  href="https://go.dev/play/p/zqL80PWsXoo"rel="noopener noreferrer" target="_blank">Go Playground link</a>):</p>
<pre><code class="language-go">b := &amp;Builder{}
car := b.
  Model(&quot;Toyota Rav4&quot;).
  Type(&quot;SUV&quot;).
  Build()
fmt.Println(car)
// {Toyota Rav4 SUV 4x4}
</code></pre>
<p>The magic of the builder pattern is that we can chain the builder methods together, and each method returns
the builder itself. This allows us to call the next method on the builder, and so on. The last method called
is the <code>Build</code> method, which returns the final object.</p>
<p>As said, this is a really basic example so the builder pattern might feel like overkill, but much more complex scenarios
exist where the builder pattern is incredibly helpful. For instance, constructing SQL queries can become complex
and we might need to handle conditional queries. In this case a builder can simplify things a bit.</p>
<h2 id="real-world-examples">
Real-world™ examples
</h2>
<p>Indeed, most SQL query builder libs out there implement this pattern. One of such libraries is <a
  href="https://github.com/Masterminds/squirrel"rel="noopener noreferrer" target="_blank">squirrel</a>.
Let&rsquo;s take a look at a few examples from the squirrel <a
  href="https://github.com/Masterminds/squirrel/blob/2499a263a40b58404497123c408797c35d09d935/README.md"rel="noopener noreferrer" target="_blank">docs at the moment of writing this article</a>. First, we start with a basic example of how squirrel can be used to generate the SQL query string we might use with the standard
library&rsquo;s <code>database/sql</code> package.</p>
<pre><code class="language-go">users := sq.Select(&quot;*&quot;).From(&quot;users&quot;).Join(&quot;emails USING (email_id)&quot;)
sql, args, err := users.ToSql()
// sql == &quot;SELECT * FROM users JOIN emails USING (email_id)&quot;

active := users.Where(sq.Eq{&quot;deleted_at&quot;: nil})
sql, args, err := active.ToSql()
// sql == &quot;SELECT * FROM users JOIN emails USING (email_id) WHERE deleted_at IS NULL&quot;
</code></pre>
<p>Now, let&rsquo;s move to a more complex scenario. Let&rsquo;s say we want to filter by name is a certain variable <code>q</code> has content, which is as simple as adding an if statement and adding
the conditional query</p>
<pre><code class="language-go">if len(q) &gt; 0 {
  users = users.Where(&quot;name LIKE ?&quot;, fmt.Sprint(&quot;%&quot;, q, &quot;%&quot;))
}
</code></pre>
<p>Notice how we assign the result of the <code>users.Where</code> function call to the <code>users</code> variable, allowing us to retain this
new build step. Now whenever we call <code>users.ToSql()</code> (that acts like the <code>Build()</code> function in the Car example) it will have the <code>Where</code> clause here only if the if statement
was true.</p>
<p>Builders can even be found in Go&rsquo;s standard library too. See <a
  href="https://pkg.go.dev/text/template"rel="noopener noreferrer" target="_blank"><code>text/template</code></a> and <a
  href="https://pkg.go.dev/html/template"rel="noopener noreferrer" target="_blank"><code>html/template</code></a> packages.
Both of the template packages have a <code>Template</code> type that uses the builder pattern:</p>
<pre><code class="language-go">tmpl, err := template.New(&quot;titleTest&quot;).
  Funcs(funcMap).
  Delims(&quot;[[&quot;, &quot;]]&quot;).
  Parse(templateText)
</code></pre>
<p>Both <a
  href="https://pkg.go.dev/html/template#Template.Funcs"rel="noopener noreferrer" target="_blank"><code>Funcs</code></a> and <a
  href="https://pkg.go.dev/html/template#Template.Delims"rel="noopener noreferrer" target="_blank"><code>Delims</code></a> return a <a
  href="https://pkg.go.dev/html/template#Template"rel="noopener noreferrer" target="_blank"><code>*Template</code></a>, and <a
  href="https://pkg.go.dev/html/template#Template.Parse"rel="noopener noreferrer" target="_blank"><code>Parse</code></a> returns both a *Template and an error kinda like our Role
example in the employee builder would have if we validated the role.</p>
<h2 id="conclusion">
Conclusion
</h2>
<p>To sum up - the builder pattern usually involves chaining functions on a single type as we &ldquo;build&rdquo; the end
result. It is used to build complex objects, and can be found in quite a few libraries.</p>
<p>If you want to go deeper into the Builder pattern and others, I invite you to take a look at the <a
  href="https://refactoring.guru/design-patterns/builder"rel="noopener noreferrer" target="_blank">refactoring.guru page</a>. It&rsquo;s a great resource to learn about software design patterns. Also, if you want to read more from me, you can check other <a
  href="https://aalonso.dev/blog/tags/patterns/">patterns articles in my blog</a>.</p>
]]></content:encoded></item><item><title>All you need to know to integrate a SQL database in Go</title><link>https://aalonso.dev/blog/2023/all-you-need-to-know-to-integrate-a-sql-database-in-golang/</link><pubDate>Sun, 26 Nov 2023 00:00:00 +0000</pubDate><guid isPermaLink="true">https://aalonso.dev/blog/2023/all-you-need-to-know-to-integrate-a-sql-database-in-golang/</guid><category>golang</category><category>programming</category><category>snippets</category><category>tutorial</category><description>In this article, I'll show you everything you need to know about the net/http package in Go, to start quick prototyping your next API.</description><content:encoded><![CDATA[<blockquote><p>Even when I do my best to ensure content is correctly rendered on RSS readers, some elements like custom info cards/notes and code style blocks can be shown without proper style or syntax highlighting.</p><p>You are free to continue reading as you prefer, but I recommend you to read <a href="https://aalonso.dev/blog/2023/all-you-need-to-know-to-integrate-a-sql-database-in-golang/?utm_source=rss&utm_medium=feed" rel="noopener noreferrer">this article in the website</a> if possible. It's fast, has no ads, and minimal privacy-respecting analytics (browser, OS, country and device type) via <a href="https://umami.is/" rel="noopener noreferrer">Umami</a>.</p></blockquote><p>In my previous article about <a
  href="https://aalonso.dev/blog/2023/create-a-fully-functional-api-server-in-go-in-under-5-minutes/">creating an fully functional API in Go under 5 minutes</a>, I showed you how to create a web server using only the standard library. There, we learned that Go standard libs are powerful enough to meet our needs.</p>
<p>In this article, and in a similar way to the aforementioned one, I&rsquo;ll show you everything you need to know about the <code>database/sql</code> package in Go, to quickly start integrating a SQL database in your application.</p>
<p>Let&rsquo;s see some snippets! Let&rsquo;s start by how to connect to a database.</p>
<h2 id="connecting-to-a-database">
Connecting to a database
</h2>
<p>Connecting to the database can be done with the <code>sql.Open</code> function. This function receives two parameters: the driver name and the connection string (aka <em>data source name</em>, or DSN). The driver name is the name of the driver that you want to use to connect to the database. The DSN is a string that contains the information needed to connect to the database. The DSN format depends on the database that you are using, but an example for PostgreSQL would be <code>postgres://user:password@host:port/database</code>.</p>
<pre><code class="language-go">// Configure the connection to the database.
db, err := sql.Open(&quot;DRIVER_NAME&quot;, &quot;CONNECTION_STRING&quot;)
if err != nil {
	log.Fatal(err)
}
defer db.Close()
</code></pre>
<p>However, the <code>sql.Open</code> function doesn&rsquo;t actually open a connection to the database. It just creates it, by returning a new <code>*sql.DB</code> object. The connection to the database is only opened once yo tries to operate on it for the first time. Therefore, if you have an error with your connection configuration (let&rsquo;s say, you introduced a wrong password or a typo in the database name), you won&rsquo;t know until you try to insert or query some data. How to avoid that? Is a common good practice to use the <code>Ping</code> method of the returned <code>*sql.DB</code> object just after its creation to check if the connection is working.</p>
<pre><code class="language-go">// Configure the connection to the database.
db, err := sql.Open(&quot;DRIVER_NAME&quot;, &quot;CONNECTION_STRING&quot;)
if err != nil {
	log.Fatal(err)
}
defer db.Close()

// Use Ping() to actually open the connection and check for any errors.
if err = db.Ping(); err != nil {
	log.Fatal(err)
}
</code></pre>
<h2 id="executing-a-sql-statement">
Executing a SQL statement
</h2>
<p>The obtained <code>*sql.DB</code> object has a generic method to execute SQL statements: the <code>Exec</code> method. This method receives a query string and a list of parameters. The query string is the parametrized SQL query that you want to execute, and the parameters are the values that you want to insert into the query string.</p>
<p>Why like that? Why not just passing an string with the SQL query already build? That&rsquo;s because the <code>Exec</code> method already escape the parameters to avoid SQL injection attacks.</p>
<h3 id="inserting-data">
Inserting data
</h3>
<p>The <code>Exec</code> method can be used to insert data into the database. Let&rsquo;s see an example for MySQL:</p>
<pre><code class="language-go">result, err := db.Exec(&quot;INSERT INTO customers (name) VALUES (?)&quot;, &quot;Alice&quot;)

if err != nil {
	log.Fatal(err)
}

// Note: The LastInsertID() method is not supported by PostgreSQL.
// Check the next snipped for a PostgreSQL example.
id, err := result.LastInsertId()
if err != nil {
	log.Fatal(err)
}

affected, err := result.RowsAffected()
if err != nil {
	log.Fatal(err)
}
</code></pre>
<p>About PostgreSQL, it depends if we want to get the ID of the inserted row or not. If we don&rsquo;t want the ID, it&rsquo;s the same as MySQL, but with proper PostgreSQL syntax (parameters are <code>$1</code>, <code>$2</code>, instead of interrogations marks). Let&rsquo;s see it:</p>
<pre><code class="language-go">result, err := db.Exec(&quot;INSERT INTO customers (name) VALUES ($1)&quot;, &quot;Alice&quot;)

if err != nil {
	log.Fatal(err)
}

affected, err := result.RowsAffected()

if err != nil {
	log.Fatal(err)
}
</code></pre>
<p>If we want to get the ID of the inserted row, we need to use the <code>RETURNING</code> clause in the query string, and instead of using the <code>Exec</code> method, we need to use the <code>QueryRow</code> method. This method is used to fetch data from the database, and we will see it in detail in the next section about querying the database. For now, let&rsquo;s see how to use it to get the ID of the inserted row:</p>
<pre><code class="language-go">var id int
err := db.QueryRow(&quot;INSERT INTO customers (name) VALUES ($1) RETURNING id&quot;, &quot;Alice&quot;).Scan(&amp;id)

if err != nil {
	log.Fatal(err)
}
</code></pre>
<h3 id="updating-and-deleting-data">
Updating and deleting data
</h3>
<p>Obviously, we can use the <code>Exec</code> method to update and delete data too. We just have to take care to use the correct syntax for our database engine. Let&rsquo;s see some examples:</p>
<pre><code class="language-go">// UPDATE in a MySQL syntax.
result, err := db.Exec(&quot;UPDATE customers SET name = ? WHERE id = ?&quot;, &quot;Alice&quot;, 1)

if err != nil {
	log.Fatal(err)
}

affected, err := result.RowsAffected()

if err != nil {
	log.Fatal(err)
}
</code></pre>
<pre><code class="language-go">// DELETE in a PostgreSQL syntax.
result, err := db.Exec(&quot;DELETE FROM customers WHERE id = $1&quot;, 1)

if err != nil {
	log.Fatal(err)
}

affected, err := result.RowsAffected()

if err != nil {
	log.Fatal(err)
}
</code></pre>
<h2 id="querying-the-database">
Querying the database
</h2>
<p>The <code>*sql.DB</code> object also have methods for querying the database. The <code>Query</code> method is used to execute a query that returns multiple rows, and the <code>QueryRow</code> method is used to execute a query that returns a single row. Both methods receive a query string and a list of parameters, similar to the <code>Exec</code> method that we already saw, and return a <code>*sql.Rows</code> object and a <code>*sql.Row</code> object, respectively.</p>
<h3 id="querying-a-single-row">
Querying a single row
</h3>
<p>Let&rsquo;s see an example of how to use the <code>QueryRow</code> method to query a single row from the database. The <code>*sql.Row</code> object returned by the <code>QueryRow</code> method has a <code>Scan</code> method that is used to scan the values of the returned row into variables. It receives a list of pointers to the variables where the values will be stored.</p>
<pre><code class="language-go">var name, surname string
err := db.QueryRow(&quot;SELECT name, surname FROM customers WHERE id = ?&quot;, 1).Scan(&amp;name, &amp;surname)
// err := db.QueryRow(&quot;SELECT name, surname FROM customers WHERE id = $1&quot;, 1).Scan(&amp;name, &amp;surname)

if err == sql.ErrNoRows {
	// There is not a row with the given ID.
	log.Fatal(&quot;no rows returned&quot;)
} else if err != nil {
	log.Fatal(err)
}

fmt.Printf(&quot;Name: %s, Surname: %s\n&quot;, name, surname)
</code></pre>
<h3 id="querying-multiple-rows">
Querying multiple rows
</h3>
<p>The <code>Query</code> method is used to execute a query that returns multiple rows. The <code>*sql.Rows</code> object returned by the <code>Query</code> method has a <code>Next</code> method to iterate over the returned rows. This method returns <code>true</code> if there is a new row to be scanned, and <code>false</code> if there are no more rows to be scanned. The <code>*sql.Rows</code> object also has a <code>Scan</code> method as <code>*sql.Row</code> has, and it&rsquo;s executed over the current row.</p>
<pre><code class="language-go">rows, err := db.Query(&quot;SELECT name FROM customers LIMIT 10&quot;)

if err != nil {
	log.Fatal(err)
}

// Don't forget to close the *sql.Rows when you are done.
defer rows.Close()
names := []string{}

for rows.Next() {
	var name string
	err := rows.Scan(&amp;name)
	if err != nil {
		log.Fatal(err)
	}
	names = append(names, name)
}

if err = rows.Err(); err != nil {
	log.Fatal(err)
}

fmt.Printf(&quot;Names: %v\n&quot;, names)
</code></pre>
<h2 id="transactions">
Transactions
</h2>
<p>All statements in a transaction use the same database connection, provided by the <code>*sql.DB</code> object. This object has a <code>Begin</code> method to start a new transaction, that returns a <code>*sql.Tx</code> object. The <code>*sql.Tx</code> object has a <code>Commit</code> and a <code>Rollback</code> methods, used to commit and to rollback the transaction respectively. Let&rsquo;s see it.</p>
<pre><code class="language-go">// Start a new transaction.
tx, err := db.Begin()

if err != nil {
	log.Fatal(err)
}

// Execute some SQL statements inside the transaction.
_, err := tx.Exec(&quot;INSERT INTO customers ...&quot;)
if err != nil {
	// Rollback the transaction if any of the statements fails.
	tx.Rollback()
	log.Fatal(err)
}

// More SQL statements here
// ...

// Commit the transaction.
err = tx.Commit()
if err != nil {
	log.Fatal(err)
}
</code></pre>
<h2 id="wrapping-up">
Wrapping up
</h2>
<p>And that&rsquo;s it. As you saw, the <code>database/sql</code> package has everything you need to integrate a SQL database into your golang application. You can connect to the database, insert and query data, and even use transactions, just using the standard library.</p>
<p>With this new knowledge, you are ready to implement a CRUD application in Golang. And if you want to make it a webapp (like an REST API), remember as I said at the beginning of this article that <a
  href="https://aalonso.dev/blog/2023/create-a-fully-functional-api-server-in-go-in-under-5-minutes/">I wrote another one about the <code>net/http</code> package</a>. Happy coding!</p>
]]></content:encoded></item></channel></rss>