<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: jantolentino</title>
    <description>The latest articles on DEV Community by jantolentino (@jantolentino).</description>
    <link>https://dev.to/jantolentino</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F673079%2F5d960c01-3d00-4b50-baab-440899281f42.jpg</url>
      <title>DEV Community: jantolentino</title>
      <link>https://dev.to/jantolentino</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jantolentino"/>
    <language>en</language>
    <item>
      <title>When Redis goes down, does your app die?</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:11:46 +0000</pubDate>
      <link>https://dev.to/jantolentino/when-redis-goes-down-does-your-app-die-4kmf</link>
      <guid>https://dev.to/jantolentino/when-redis-goes-down-does-your-app-die-4kmf</guid>
      <description>&lt;h2&gt;
  
  
  The Problem: The App Dies
&lt;/h2&gt;

&lt;p&gt;There was an incident that caused our application to fail because our Redis cache went down. It was in the middle of moving to a larger compute instance to handle a higher load, and since we only had one replica at the time, the cache became completely unavailable.&lt;/p&gt;

&lt;p&gt;The issue is that Laravel, by default, assumes your cache is always there. If Redis dies, the standard  &lt;code&gt;Cache::remember&lt;/code&gt; throws an exception and crashes the request with a 500 Internal Server Error. While &lt;strong&gt;it's terrible idea in production environment&lt;/strong&gt; and should be addressed in different manner. I figured why not try to attempt resolving this within the software-level.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylrkch1j1m2nw5gsrwhs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fylrkch1j1m2nw5gsrwhs.png" alt="Redis exception when down" width="800" height="526"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If we want to prioritize functionality over speed, we need to implement &lt;strong&gt;graceful degradation&lt;/strong&gt;. This is a software design principle where, if a service like Redis fails, the system falls back to a slower but reliable method—like querying the database directly—instead of failing catastrophically. The goal is for the system to continue operating, even if it's with reduced performance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resilient Cache-Aside
&lt;/h2&gt;

&lt;p&gt;The most straightforward way to handle this is wrapping your cache logic in a &lt;code&gt;try-catch&lt;/code&gt; block. It’s a quick fix that looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;ResilientCacheAside&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;rememberSafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Closure&lt;/span&gt; &lt;span class="nv"&gt;$callback&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$ttl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;\Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cache read failed: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$callback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$ttl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nc"&gt;\Log&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Cache write failed: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While this saves the app from crashing, it introduces a new problem: &lt;strong&gt;timeouts&lt;/strong&gt;. The application still has to wait for the Redis connection to time out before it triggers the exception. This adds latency to every single request while the cache is down, significantly degrading the user experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Circuit Breaker Pattern
&lt;/h2&gt;

&lt;p&gt;A much nicer approach I liked is the &lt;strong&gt;Circuit Breaker pattern&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It tracks failures over time. Once a threshold is reached, the &lt;em&gt;circuit opens&lt;/em&gt;, and the app skips the cache entirely for a set period, heading straight to the database. This eliminates the timeout delay for users. After a &lt;em&gt;cooling off&lt;/em&gt; period, the circuit enters a &lt;em&gt;half-open&lt;/em&gt; state to see if Redis is back up. If it is, the circuit closes and normal operations resume.&lt;/p&gt;

&lt;p&gt;Instead of writing all this logic from scratch, I found a solid PHP library called &lt;a href="https://github.com/ackintosh/ganesha" rel="noopener noreferrer"&gt;Ganesha&lt;/a&gt; that handles it beautifully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;p&gt;First, install the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;composer require ackintosh/ganesha
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, I created a &lt;code&gt;CircuitBreakerService&lt;/code&gt;. One key detail here is using the &lt;strong&gt;APCu adapter&lt;/strong&gt; to store the circuit state. It would introduce a circular dependency if you store your &lt;em&gt;Is Redis down?&lt;/em&gt; logic inside the Redis itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CircuitBreakerService&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$breakers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;

    &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Ganesha&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Builder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;withRateStrategy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Apcu&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; 
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;timeWindow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Monitor failures over the last 10 seconds&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failureRateThreshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Open circuit if 10% of requests fail&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;minimumRequests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Don't trigger until at least 5 requests are made&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;intervalToHalfOpen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// Wait 15s before checking if Redis is back&lt;/span&gt;
                &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;breakers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;callable&lt;/span&gt; &lt;span class="nv"&gt;$fallback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;mixed&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nv"&gt;$breaker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getBreaker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$breaker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;isAvailable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
                &lt;span class="nv"&gt;$breaker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;success&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nv"&gt;$breaker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$serviceKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$fallback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$fallback&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Ganesha, the &lt;code&gt;$serviceKey&lt;/code&gt; is just a unique string used to track the failure rate of a specific resource. By using different names (e.g., &lt;code&gt;redis_cache&lt;/code&gt; vs &lt;code&gt;external_api&lt;/code&gt;), you can trip the breaker for one service without affecting others.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage in the Application
&lt;/h2&gt;

&lt;p&gt;Now, I can use this service inside my &lt;code&gt;AnalyticsService&lt;/code&gt; to make my dashboard resilient:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getLowQuantityProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;stdClass&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;circuitBreaker&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'redis_cache'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;remember&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"analytics.low_quantity.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLowQuantityProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;productService&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getLowQuantityProducts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$threshold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;p&gt;I tweaked the configuration a bit and reduced the values in the &lt;code&gt;getBreaker&lt;/code&gt; method for this demonstration.&lt;/p&gt;

&lt;p&gt;During my testing, the data was retrieved from the cache as expected while everything was healthy. Once I stopped the Redis container and refreshed the site, the app promptly switched to the database, confirming the fallback was working.&lt;/p&gt;

&lt;p&gt;After a short while, I started the Redis container again. The circuit &lt;strong&gt;closed&lt;/strong&gt;, and the app returned to normal operation, pulling data from the cache once more.&lt;/p&gt;

&lt;p&gt;::prose-video{src="&lt;a href="https://youtu.be/dZ4TmU6k3Eg%22" rel="noopener noreferrer"&gt;https://youtu.be/dZ4TmU6k3Eg"&lt;/a&gt;}&lt;br&gt;
::&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;In testing, the transition is seamless. When Redis is up, we get the speed of the cache. When I simulate a Redis failure, the circuit trips, and the app continues to serve data from the DB without hanging on the usual timeouts.&lt;/p&gt;

&lt;p&gt;One big limitation I realized: if you store your Sessions in Redis, this approach doesn't automatically fix those. The site would still crash if Redis goes down. Sessions are a bit more complex because Laravel's session driver is loaded very early in the request lifecycle, long before the service classes are initialized.&lt;/p&gt;

&lt;p&gt;To handle that, we’d likely need to write a custom session driver that can fall back to the database or another store on the fly. I still need to explore that further, so that’s definitely a topic for another article.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/when-redis-goes-down-does-your-app-die" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>redis</category>
      <category>laravel</category>
      <category>circuitbreaker</category>
      <category>ganesha</category>
    </item>
    <item>
      <title>Using fstab instead of autofs for a samba share</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:11:12 +0000</pubDate>
      <link>https://dev.to/jantolentino/using-fstab-instead-of-autofs-for-a-samba-share-18a0</link>
      <guid>https://dev.to/jantolentino/using-fstab-instead-of-autofs-for-a-samba-share-18a0</guid>
      <description>&lt;p&gt;A few weeks ago, I borrowed a Raspberry Pi 4B from a friend. Since it wasn't going to be used for a while, I decided to set up a network storage for our home network. It seemed like the most practical use for the Pi.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problemo
&lt;/h2&gt;

&lt;p&gt;Everything was working great until today. After a quick look at the service status, here's what I was dealing with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;Sep 14 03:06:13 homeserver systemd[1]: Starting smb.service - Samba SMB Daemon...
Sep 14 03:06:14 homeserver systemd[1]: Started smb.service - Samba SMB Daemon.
Sep 14 03:06:26 homeserver smbd[2421]: &lt;span class="o"&gt;[&lt;/span&gt;2025/09/14 03:06:26.137220,  0] ../../source3/smbd/smb2_service.c:746&lt;span class="o"&gt;(&lt;/span&gt;make_connection_snum&lt;span class="o"&gt;)&lt;/span&gt;
Sep 14 03:06:26 homeserver smbd[2421]:   make_connection_snum: canonicalize_connect_path failed &lt;span class="k"&gt;for &lt;/span&gt;service SharedDrive, path /mnt/jantolentino_Drive/jantolentino_Drive
Sep 14 10:21:14 homeserver smbd[3609]: &lt;span class="o"&gt;[&lt;/span&gt;2025/09/14 10:21:14.823284,  0] ../../source3/smbd/smb2_service.c:746&lt;span class="o"&gt;(&lt;/span&gt;make_connection_snum&lt;span class="o"&gt;)&lt;/span&gt;
Sep 14 10:21:14 homeserver smbd[3609]:   make_connection_snum: canonicalize_connect_path failed &lt;span class="k"&gt;for &lt;/span&gt;service SharedDrive, path /mnt/jantolentino_Drive/jantolentino_Drive
Sep 14 10:22:21 homeserver smbd[3740]: &lt;span class="o"&gt;[&lt;/span&gt;2025/09/14 10:22:21.111827,  0] ../../source3/smbd/smb2_service.c:746&lt;span class="o"&gt;(&lt;/span&gt;make_connection_snum&lt;span class="o"&gt;)&lt;/span&gt;
Sep 14 10:22:21 homeserver smbd[3740]:   make_connection_snum: canonicalize_connect_path failed &lt;span class="k"&gt;for &lt;/span&gt;service SharedDrive, path /mnt/jantolentino_Drive/jantolentino_Drive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key error is &lt;code&gt;canonicalize_connect_path failed&lt;/code&gt;, which pointed to an issue with Samba accessing the drive's path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial setup
&lt;/h2&gt;

&lt;p&gt;The Raspberry Pi 4 has Fedora Server 38 running on it. I installed Samba, configured the firewall, and assigned a static IP address to the device on my network.&lt;/p&gt;

&lt;p&gt;I did, however, run into a couple of snags setting up the external storage. My drive is formatted with BTRFS and encrypted with a passphrase. I discovered two headaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;External drives often unmount automatically after a period of inactivity to save power and extend their lifespan.&lt;/li&gt;
&lt;li&gt;When my encrypted drive unmounts, it needs the passphrase to be unlocked and mounted again.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Having to manually enter my password every time the drive went idle would defeat the purpose of an always-on network share. Plus, I'm not always on a computer which I can easily access the server's terminal.&lt;/p&gt;

&lt;p&gt;After some reading online, I found a two-part solution: &lt;code&gt;autofs&lt;/code&gt; and &lt;code&gt;crypttab&lt;/code&gt;. The &lt;code&gt;autofs&lt;/code&gt; service automatically remounts a drive when it's accessed, which works perfectly with Samba. To handle the password issue, &lt;code&gt;crypttab&lt;/code&gt; securely "caches" the passphrase and uses it to unlock the drive when needed.&lt;/p&gt;

&lt;p&gt;This setup gave me an awesome network storage solution that worked flawlessly for a few weeks. Until today.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where It Went Wrong
&lt;/h2&gt;

&lt;p&gt;After some digging, it looked like a &lt;strong&gt;race condition&lt;/strong&gt;. The Samba service was trying to access the share &lt;em&gt;before&lt;/em&gt; &lt;code&gt;autofs&lt;/code&gt; had finished unlocking and mounting the encrypted drive. This timing issue was the root of the problem. Because Samba couldn't find the path, it failed.&lt;/p&gt;

&lt;p&gt;I needed a different approach that wouldn't have this race condition.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: fstab
&lt;/h2&gt;

&lt;p&gt;As it turns out, a much simpler solution is already built into Fedora (oops): &lt;code&gt;fstab&lt;/code&gt;.  It's powerful enough to handle everything I needed by ensuring the drive is mounted at boot time, well before Samba starts.&lt;/p&gt;

&lt;p&gt;First, I had to undo my &lt;code&gt;autofs&lt;/code&gt; configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Remove autofs and its config&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf remove autofs
&lt;span class="nb"&gt;sudo rm&lt;/span&gt; /etc/auto.jantolentino_Drive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I created a permanent mount point for the drive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; /mnt/jantolentino_Drive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Configuring &lt;code&gt;fstab&lt;/code&gt; was surprisingly easy. I just added the following line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# /etc/fstab&lt;/span&gt;
/dev/mapper/jantolentino_Drive /mnt/jantolentino_Drive btrfs defaults,nofail 0 2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;nofail&lt;/code&gt; option is important here; it prevents the system from hanging during boot if the drive isn't connected.&lt;/p&gt;

&lt;p&gt;Finally, I updated my Samba configuration to use the new static mount point.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[jantolentino_share]&lt;/span&gt;
  &lt;span class="py"&gt;comment&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;SharedDrive&lt;/span&gt;
  &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="err"&gt;/mnt/jantolentino_Drive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, a quick reboot.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;We now have a working setup again. This approach feels much more stable, and hopefully, it works for the long run&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jantolentino@homeserver:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status smb.service
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;sudo&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; password &lt;span class="k"&gt;for &lt;/span&gt;jantolentino:
● smb.service - Samba SMB Daemon
     Loaded: loaded &lt;span class="o"&gt;(&lt;/span&gt;/usr/lib/systemd/system/smb.service&lt;span class="p"&gt;;&lt;/span&gt; enabled&lt;span class="p"&gt;;&lt;/span&gt; preset: disabled&lt;span class="o"&gt;)&lt;/span&gt;
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
     Active: active &lt;span class="o"&gt;(&lt;/span&gt;running&lt;span class="o"&gt;)&lt;/span&gt; since Mon 2025-09-15 00:00:43 EDT&lt;span class="p"&gt;;&lt;/span&gt; 2 months 21 days ago
 Invocation: 346b968c905f4c2ebd09a4e14162382e
       Docs: man:smbd&lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;
             man:samba&lt;span class="o"&gt;(&lt;/span&gt;7&lt;span class="o"&gt;)&lt;/span&gt;
             man:smb.conf&lt;span class="o"&gt;(&lt;/span&gt;5&lt;span class="o"&gt;)&lt;/span&gt;
   Main PID: 1453 &lt;span class="o"&gt;(&lt;/span&gt;smbd&lt;span class="o"&gt;)&lt;/span&gt;
     Status: &lt;span class="s2"&gt;"smbd: ready to serve connections..."&lt;/span&gt;
      Tasks: 3 &lt;span class="o"&gt;(&lt;/span&gt;limit: 4124&lt;span class="o"&gt;)&lt;/span&gt;
     Memory: 2.5G &lt;span class="o"&gt;(&lt;/span&gt;peak: 2.8G&lt;span class="o"&gt;)&lt;/span&gt;
        CPU: 4min 59.375s
     CGroup: /system.slice/smb.service
             ├─1453 /usr/bin/smbd &lt;span class="nt"&gt;--foreground&lt;/span&gt; &lt;span class="nt"&gt;--no-process-group&lt;/span&gt;
             ├─1511 /usr/bin/smbd &lt;span class="nt"&gt;--foreground&lt;/span&gt; &lt;span class="nt"&gt;--no-process-group&lt;/span&gt;
             └─1512 /usr/bin/smbd &lt;span class="nt"&gt;--foreground&lt;/span&gt; &lt;span class="nt"&gt;--no-process-group&lt;/span&gt;

Sep 15 00:00:42 homeserver systemd[1]: Starting smb.service - Samba SMB Daemon...
Sep 15 00:00:43 homeserver systemd[1]: Started smb.service - Samba SMB Daemon.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;There are probably other ways to approach this, but I'm happy that things are in a better state and my files are secure. I also scheduled some BTRFS maintenance jobs like scrubbing to keep the drive healthy. I'll probably look more into my setup. But for now, everything's well.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/using-fstab-instead-of-autofs-for-a-stable-samba-share" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>fstab</category>
      <category>autofs</category>
      <category>samba</category>
      <category>raspberrypi</category>
    </item>
    <item>
      <title>Uptime Kuma on Fedora with Podman Quadlet</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:11:11 +0000</pubDate>
      <link>https://dev.to/jantolentino/uptime-kuma-on-fedora-with-podman-quadlet-92d</link>
      <guid>https://dev.to/jantolentino/uptime-kuma-on-fedora-with-podman-quadlet-92d</guid>
      <description>&lt;p&gt;Over the weekend, I decided to set up Uptime Kuma on my mini-server. I needed a quick way to monitor some important work services, and Uptime Kuma’s Slack notifications make it easy to get alerts directly in our channel whenever something goes down.&lt;/p&gt;

&lt;p&gt;While I was at it, I figured it was the perfect time to learn &lt;strong&gt;Podman Quadlet&lt;/strong&gt;. I have recently learned as a neat way to manage containers using declarative files (similar to Kubernetes YAML) that automatically generate systemd services so your apps start at boot and stay running.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1xqhwyltipr1da5kau.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4f1xqhwyltipr1da5kau.png" alt="Uptime Kuma monitoring screenshot" width="800" height="489"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Podman
&lt;/h2&gt;

&lt;p&gt;First, make sure Podman is ready to go. On my Fedora machine, that’s just a quick:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;dnf update &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;dnf &lt;span class="nb"&gt;install &lt;/span&gt;podman &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Project Organization
&lt;/h2&gt;

&lt;p&gt;I like to keep my tools organized under an &lt;code&gt;infrastructure&lt;/code&gt; directory.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/infrastructure/uptime-kuma/data
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/infrastructure/uptime-kuma
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We’re creating a &lt;code&gt;data&lt;/code&gt; directory here to mount as a volume so our Uptime Kuma configuration persists even if the container restarts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handling SELinux Permissions
&lt;/h2&gt;

&lt;p&gt;Since Fedora is strict about security, you need to tell SELinux that the container is allowed to write to your local data folder.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;chcon&lt;/span&gt; &lt;span class="nt"&gt;-t&lt;/span&gt; container_file_t &lt;span class="nt"&gt;-R&lt;/span&gt; ~/infrastructure/uptime-kuma/data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using &lt;code&gt;:Z&lt;/code&gt; in a YAML &lt;code&gt;hostPath&lt;/code&gt; isn't standard K8s syntax (unlike what we commonly find in Docker Compose files), so this manual &lt;code&gt;chcon&lt;/code&gt; approach is a reliable way to ensure the container has write access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the Kubernetes YAML
&lt;/h2&gt;

&lt;p&gt;Next, create a file named &lt;code&gt;uptime-kuma.yaml&lt;/code&gt;. This defines the pod and the container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Pod&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uptime-kuma-pod&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;containers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;uptime-kuma&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;lscr.io/linuxserver/uptime-kuma:latest&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;containerPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3001&lt;/span&gt;
      &lt;span class="na"&gt;hostPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3001&lt;/span&gt; &lt;span class="c1"&gt;## You can change this port&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;TZ&lt;/span&gt;
      &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Asia/Manila"&lt;/span&gt;
    &lt;span class="na"&gt;volumeMounts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;mountPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/app/data&lt;/span&gt;
      &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kuma-data&lt;/span&gt;
  &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kuma-data&lt;/span&gt;
    &lt;span class="na"&gt;hostPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/home/jantolentino/infrastructure/uptime-kuma/data&lt;/span&gt; &lt;span class="c1"&gt;## Change this path&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Directory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Moving to Quadlet (Systemd Integration)
&lt;/h2&gt;

&lt;p&gt;Instead of just running &lt;code&gt;podman kube play&lt;/code&gt;, we’ll use a &lt;strong&gt;Quadlet &lt;code&gt;.kube&lt;/code&gt; file&lt;/strong&gt;. This tells systemd to manage the Kubernetes YAML we just wrote.&lt;/p&gt;

&lt;p&gt;First, create the Quadlet directory if it doesn't exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.config/containers/systemd
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, create a file named &lt;code&gt;uptime-kuma.kube&lt;/code&gt; inside that folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Unit]&lt;/span&gt;
&lt;span class="py"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;Uptime&lt;/span&gt; &lt;span class="err"&gt;Kuma&lt;/span&gt; &lt;span class="err"&gt;Service&lt;/span&gt;
&lt;span class="py"&gt;Wants&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;network-online.target&lt;/span&gt;
&lt;span class="py"&gt;After&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;network-online.target&lt;/span&gt;

&lt;span class="nn"&gt;[Kube]&lt;/span&gt;
&lt;span class="c"&gt;## Path to your Kubernetes YAML&lt;/span&gt;
&lt;span class="py"&gt;Yaml&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;/home/jantolentino/&lt;/span&gt;&lt;span class="mf"&gt;inf&lt;/span&gt;&lt;span class="err"&gt;rastructure/uptime-kuma/uptime-kuma.yaml&lt;/span&gt;

&lt;span class="nn"&gt;[Install]&lt;/span&gt;
&lt;span class="py"&gt;WantedBy&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;default.target&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Activating the Service
&lt;/h2&gt;

&lt;p&gt;The awesome part is that when you reload the systemd daemon, Podman’s Quadlet generator sees your &lt;code&gt;.kube&lt;/code&gt; file and creates a real systemd service automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;## Refresh systemd to find the new Quadlet&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; daemon-reload

&lt;span class="c"&gt;## Start and enable the service&lt;/span&gt;
systemctl &lt;span class="nt"&gt;--user&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--now&lt;/span&gt; uptime-kuma.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Enable "Linger"
&lt;/h2&gt;

&lt;p&gt;By default, "user" services stop when you log out. Since this is a server, we want it to run 24/7 even without an active SSH session. Use the &lt;code&gt;loginctl&lt;/code&gt; command to enable "lingering":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;loginctl enable-linger jantolentino
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your Uptime Kuma instance will start automatically as soon as the server boots up. No more manual starts needed!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/uptime-kuma-on-fedora-with-podman-quadlets" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>uptimekuma</category>
      <category>podman</category>
      <category>quadlet</category>
      <category>fedora</category>
    </item>
    <item>
      <title>Testing scheduled jobs using faketime</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:10:36 +0000</pubDate>
      <link>https://dev.to/jantolentino/testing-scheduled-jobs-using-faketime-2b4l</link>
      <guid>https://dev.to/jantolentino/testing-scheduled-jobs-using-faketime-2b4l</guid>
      <description>&lt;p&gt;Recently, I was tasked with testing some scheduled exports to investigate a defect. Unit tests should have caught it, but something else was going on. I had to simulate a real execution in a container by faking its internal clock.&lt;/p&gt;

&lt;p&gt;This is where I utilized &lt;code&gt;faketime&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;faketime&lt;/code&gt; is a tool that manipulates system time for a specific program without changing the system's actual time. It works by intercepting the system calls a program makes to get the current date and time, allowing you to run programs as if it were a different date and time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check for the faketime package
&lt;/h2&gt;

&lt;p&gt;First, you need to make sure the &lt;code&gt;faketime&lt;/code&gt; package is available in your container image, as it's not commonly included. To confirm that the library file exists, you can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;container_name&lt;span class="o"&gt;}&lt;/span&gt; find / &lt;span class="nt"&gt;-name&lt;/span&gt; libfaketime.so.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If that command finds the library file (e.g., at &lt;code&gt;/usr/lib/faketime/libfaketime.so.1&lt;/code&gt;), you're good to go. Otherwise, you'll have to install it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;

&lt;p&gt;So how do you use it? It's actually pretty simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;podman &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;container_name&lt;span class="o"&gt;}&lt;/span&gt; sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"LD_PRELOAD=/usr/lib/faketime/libfaketime.so.1 FAKETIME_NO_CACHE=1 FAKETIME=&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-u&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'30 days ago'&lt;/span&gt; &lt;span class="s1"&gt;'+%Y-%m-%d 00:01:00'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; php artisan schedule:work"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's being passed here are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;LD_PRELOAD&lt;/code&gt;: Tells the shell to load the &lt;code&gt;faketime&lt;/code&gt; library before running the command.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAKETIME_NO_CACHE&lt;/code&gt;: Prevents the time from being cached, which is important for testing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;FAKETIME&lt;/code&gt;: Sets the exact time you want to simulate. In this case, I'm using &lt;code&gt;date&lt;/code&gt; to get a formatted string for "30 days ago at 1:00 AM UTC."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Since I was testing an export, I had to make sure it worked on a daily, monthly, and yearly basis. Using this command, I could just change the &lt;code&gt;date&lt;/code&gt; string to simulate any scenario. This was a really interesting and effective way to test.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/testing-scheduled-jobs-with-faketime" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>faketime</category>
      <category>laraveltesting</category>
      <category>jobtesting</category>
      <category>scheduledjobs</category>
    </item>
    <item>
      <title>Suspend, Background, Disown</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:10:34 +0000</pubDate>
      <link>https://dev.to/jantolentino/suspend-background-disown-3hj1</link>
      <guid>https://dev.to/jantolentino/suspend-background-disown-3hj1</guid>
      <description>&lt;p&gt;I'm currently working on a hobby project, using crawlers to download media files for a dataset. Some of these are large files that also need preprocessing. Instead of building a whole system to manage this, I'm just creating simple scripts to handle them one at a time.&lt;/p&gt;

&lt;p&gt;Given the volume of data I'm working with, I realized the scripts were going to take a long time to run on my mini-server. So, I needed to run them as background processes.&lt;/p&gt;

&lt;p&gt;In the past, I've always relied on &lt;code&gt;nohup&lt;/code&gt; or &lt;code&gt;tmux&lt;/code&gt; for this kind of thing. But today, I remembered a neat trick I recently learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suspend the Process
&lt;/h2&gt;

&lt;p&gt;It turns out you can suspend a process that's already running. While your script or command is active, just press &lt;code&gt;CTRL + Z&lt;/code&gt;. You'll get a message like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[1]+  Stopped  (your command)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[1]&lt;/code&gt; is the job number of the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resume in the Background
&lt;/h2&gt;

&lt;p&gt;After you suspend the process, you can type &lt;code&gt;bg&lt;/code&gt; and hit enter. This command will resume the job, but it will run in the background now.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[1]+ (your command) &amp;amp;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It will confirm that it's running in the background, usually with a message ending in that notable ampersand (&lt;code&gt;&amp;amp;&lt;/code&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Disown the Process
&lt;/h2&gt;

&lt;p&gt;This is the most important part of the trick. The process is still attached to your current shell session, so you need to detach (or disown) it. The main reason I always stuck with &lt;code&gt;nohup&lt;/code&gt; in the past was to prevent the process from stopping when I logged out.&lt;/p&gt;

&lt;p&gt;To disown it, you simply enter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;disown&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; %1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%1&lt;/code&gt; refers to the job number. Now the process is fully independent of your session.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's wrong with nohup?
&lt;/h2&gt;

&lt;p&gt;Nothing, really. The main difference is you use &lt;code&gt;nohup&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; you start a process. The trick above lets you do it &lt;em&gt;after&lt;/em&gt; a process is already running. That's what makes it so neat!&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Of course, &lt;code&gt;tmux&lt;/code&gt; detached sessions still rule, but for simple things like this, I find this is a handy trick. It's especially useful when I'm on a server that doesn't have &lt;code&gt;tmux&lt;/code&gt; installed and I don't have permission to add it. Meh.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/suspend-background-disown" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terminal</category>
      <category>trick</category>
      <category>tips</category>
      <category>tmux</category>
    </item>
    <item>
      <title>No measurement, no optimization</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:10:01 +0000</pubDate>
      <link>https://dev.to/jantolentino/no-measurement-no-optimization-32m</link>
      <guid>https://dev.to/jantolentino/no-measurement-no-optimization-32m</guid>
      <description>&lt;h1&gt;
  
  
  No measurement, no optimization.
&lt;/h1&gt;

&lt;p&gt;There's a common saying in programming: "Don't optimize what you haven't measured yet." You can only truly appreciate this advice when you experience it yourself. And I definitely did.&lt;/p&gt;

&lt;p&gt;I'm always looking forward to projects where I can optimize the code after the first release. It's really satisfying to refine code and make it cleaner and more efficient. But I've learned the hard way that optimizing without measuring the performance changes can be a disaster, especially with my limited experience. Most of our performance issues stem from database queries, not from our program’s architecture.&lt;/p&gt;

&lt;p&gt;After our initial release, there were two queries that I had to write separately because of how complex they were. I planned to optimize them after the release, and I thought combining them would be more efficient. I tested it, and everything looked fine—the output was the same and there were no issues.&lt;/p&gt;

&lt;p&gt;Feeling good about my work, I submitted a merge request late that day and waited for feedback. The next day, I saw that no one had reviewed it yet. My curiosity took over, and I decided to measure the performance myself, hoping to validate my work. It didn't.&lt;/p&gt;

&lt;p&gt;I opened Postman and sent 100 requests to calculate the average response time. It wasn't the best approach, but it would get the job done. The new implementation had a higher overall response time. While some requests were faster than the old version, there were also significant spikes. Ultimately, it wasn’t better at all. I had actually made it worse.&lt;/p&gt;

&lt;p&gt;I closed my merge request and noted that the code was flawed and could cause performance issues. After digging into it, I realized I had overlooked how crucial it was to understand the structure of the data and its data types, which had an unforeseen impact on performance.&lt;/p&gt;

&lt;p&gt;Even so, it was a valuable experience that I genuinely enjoyed. I learned more about Laravel performance tools, discovered how useful Postman is for this kind of work, and gained experience researching potential performance regressions.&lt;/p&gt;

&lt;p&gt;Ultimately, optimizing based on instinct is just the beginning. You still need to back it up with accurate measurements. As they say, you can only call it optimized if you have the benchmarks to prove it!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/no-measurement-no-optimization" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>optimization</category>
      <category>performance</category>
      <category>programming</category>
      <category>laravel</category>
    </item>
    <item>
      <title>New Portfolio in the Works</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:09:59 +0000</pubDate>
      <link>https://dev.to/jantolentino/new-portfolio-in-the-works-3ka9</link>
      <guid>https://dev.to/jantolentino/new-portfolio-in-the-works-3ka9</guid>
      <description>&lt;h1&gt;
  
  
  New Portfolio in the Works.
&lt;/h1&gt;

&lt;p&gt;I’ve always wanted a cool, chibi-style profile picture for my portfolio, but I've never been good at drawing. I even tried paying for art commissions, but nothing really worked out. So, my portfolio always ended with text for the logo.&lt;/p&gt;

&lt;p&gt;Then, OpenAI released a new Ghibli-style image generator, and it could turn any portrait photo into Ghibli-style art! I got so excited and tried it right away.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bza0rhl4ckhel5bdstt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bza0rhl4ckhel5bdstt.png" alt="Ghibi-style generated via OpenAI" width="613" height="919"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Ghibi-style generated via OpenAI&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And it's awesome! I got inspired to immediately start designing a portfolio landing page. I checked my Pinterest boards, looking at old pins and searching for new ideas. I found this amazing portfolio design on Figma and was instantly captivated.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5bvqiyxrfxvmpt170uc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fq5bvqiyxrfxvmpt170uc.png" alt="Design inspiration from Figma community by 三秋十李Sergio." width="799" height="620"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Design inspiration from Figma community by 三秋十李Sergio.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I really liked the vibe of it. I’ve never been good at putting into words what I like about this, but I think it’s the modern and techy style that’s capturing me.&lt;/p&gt;

&lt;p&gt;Right away, I knew this was the style I wanted to try next. The original design was a bit too detailed and cold for me, and I wanted something a little warmer, so that's what I went for.&lt;/p&gt;

&lt;p&gt;Even though I’m more intuitive when designing in Figma, I still use Inkscape for my casual projects. It gives me more flexibility with gradients and tools that make it easy to create design elements. After sitting for hours, nudging shapes pixel by pixel, I finally made my not-so-final draft. Honestly, I'm really happy with how it turned out! There's still a lot to fix and improve, and I'll definitely work on the legibility, but I'm already pretty happy with the look. It's far from done, but it's really coming along.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlzg4q0q994ytuesx02b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwlzg4q0q994ytuesx02b.png" alt="Initial draft design" width="800" height="736"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The text are tight and I was exploring the design for the next section. I didn't commit to this though&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foisyh9dc5r0y61liyg7f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foisyh9dc5r0y61liyg7f.png" alt="Not-so-final design in tablet screen" width="799" height="506"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatv7jwsilgnjapwjas0m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fatv7jwsilgnjapwjas0m.png" alt="Not-so-final design in desktop screen" width="799" height="506"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Finally, I was satisfied with this. Some elements are too thin but it's getting there.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've even been trying to make it work on different screen sizes, which has turned out to be a big challenge. I'll probably have to keep exploring the layout next time. Here's a little peek at how the mobile version is looking. I've also been playing with animations, like making the background fade in slowly, and it turned out pretty cool!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw1y66nb6huat5mj7zbce.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fw1y66nb6huat5mj7zbce.png" alt="Mobile-version in development of the new design" width="719" height="1584"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Mobile-version in development of the new design&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I don't plan on replacing my current site with this, but it’s nice to see a new design coming together. I'll host it on a separate site once I have the time. More than anything, I’m just happy and excited about this sudden inspiration and the joy of building this design as an experiment.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/new-portfolio-in-the-works" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>portfoliodesign</category>
      <category>webdesign</category>
      <category>figma</category>
      <category>inkscape</category>
    </item>
    <item>
      <title>Installing and setting up Postgres in Ubuntu</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:09:25 +0000</pubDate>
      <link>https://dev.to/jantolentino/installing-and-setting-up-postgres-in-ubuntu-4n4k</link>
      <guid>https://dev.to/jantolentino/installing-and-setting-up-postgres-in-ubuntu-4n4k</guid>
      <description>&lt;p&gt;At work, we handle a lot of short-term projects, typically lasting 3-8 weeks. These projects don't require a complex deployment setup. Instead, we deploy each project on a single VPS instance, with all the stacks bundled together on the same system.&lt;/p&gt;

&lt;p&gt;Sure, it's a single point of failure and not the ideal setup, it however matches our clients’ budget and allows us to deliver quickly. Luckily, we've never faced a catastrophic failure with this approach (yet).&lt;/p&gt;

&lt;p&gt;In these types of projects, which come and go, each one is deployed on its own VPS. Which also means I set up a DB instance on each VPS. Honestly, this routine task could be scripted, but I actually enjoy doing it manually.&lt;/p&gt;

&lt;p&gt;Overall, the routine involves installing the packages, ensuring they're running and will restart on reboot or failure, setting up the users, creating a database, and granting user privileges to the database. Depending on the circumstances, additional configuration might be needed if necessary or requested. However, this routine is what I consider the minimum or baseline.&lt;/p&gt;

&lt;p&gt;It's worth noting that I'm mostly deploying on Ubuntu systems, so some commands are specific to this distro.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing the Postgres
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL package is available in the main repository, so it can be installed with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install &lt;/span&gt;postgres postgresql-contrib
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once installed, PostgreSQL is available on your system, but it isn't running yet!&lt;/p&gt;

&lt;h2&gt;
  
  
  PostgreSQL service up and running
&lt;/h2&gt;

&lt;p&gt;The PostgreSQL process is managed by systemd. After installation, a systemd service unit file is provided. You just need to start the service and enable it to start on reboot.&lt;/p&gt;

&lt;p&gt;To start the PostgreSQL service, simply enter the command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start postgres.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can verify that it is running using the status command in systemctl.&lt;/p&gt;

&lt;p&gt;To ensure that the PostgreSQL service automatically starts on reboot, use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;postgres.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that you have a PostgreSQL instance up and running, the next step is to set up users to connect to it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a new user
&lt;/h2&gt;

&lt;p&gt;Out of the box, after installing the PostgreSQL package, a postgres user is created with administrative privileges by default.&lt;br&gt;
To create a new DB user, you need to switch to the postgres user. You can do this with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;su postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can confirm if the switch was successful by checking your username with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;whoami&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To create a new DB user, run the createuser command. It will prompt you for the role name and ask if the new role should be a superuser. Enter y to confirm if you want the new role to have superuser privileges.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;createuser –interative
  Enter name of role to add: &lt;span class="o"&gt;{{&lt;/span&gt;username&lt;span class="o"&gt;}}&lt;/span&gt;
  Shall the new role be a superuser? &lt;span class="o"&gt;(&lt;/span&gt;y/n&lt;span class="o"&gt;)&lt;/span&gt; y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Creating a new database
&lt;/h2&gt;

&lt;p&gt;Creating a new database is very easy. Simply use the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;createdb &lt;span class="o"&gt;{{&lt;/span&gt;database_name&lt;span class="o"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that's done, you have created a new database. However, the new user doesn't have privileges to access or modify the database yet. To grant these permissions, you can use an SQL query to assign the appropriate privileges to your user.&lt;/p&gt;

&lt;p&gt;Enter psql to start a session where you can run SQL queries:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;psql
GRANT CONNECT ON DATABASE &lt;span class="o"&gt;{{&lt;/span&gt;database_name&lt;span class="o"&gt;}}&lt;/span&gt; T O &lt;span class="o"&gt;{{&lt;/span&gt;username&lt;span class="o"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
GRANT pg_read_all_data TO &lt;span class="o"&gt;{{&lt;/span&gt;username&lt;span class="o"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
GRANT pg_write_all_data TO &lt;span class="o"&gt;{{&lt;/span&gt;username&lt;span class="o"&gt;}}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This grants the user permission to connect to the database and provides read and write access to all the data in the database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Authenticating user
&lt;/h2&gt;

&lt;p&gt;Currently, the user you just created has been granted access to connect to the database. However, you still need to log in as that user.&lt;/p&gt;

&lt;p&gt;There are several types of authentication you can use to connect, depending on your use case. For this routine, I use password-based authentication to connect to the PostgreSQL instance.&lt;/p&gt;

&lt;p&gt;What I typically do is set or change the password for the newly created user. As the postgres user, which has the highest authority, you can do this with the following SQL command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ALTER USER &lt;span class="o"&gt;{{&lt;/span&gt;username&lt;span class="o"&gt;}}&lt;/span&gt; WITH PASSWORD &lt;span class="s1"&gt;'{{new_password}}'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once that's done, you can exit the SQL shell and then exit the postgres user shell. This requires two exit commands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="se"&gt;\q&lt;/span&gt; &lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Others
&lt;/h2&gt;

&lt;p&gt;There are easier ways to set up Postgres, but I find using Docker images more efficient. It's quick and simple. For large-scale server deployments, Ansible scripts are often a better option.&lt;/p&gt;




&lt;h3&gt;
  
  
  Reference Notes
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/how-to-install-postgresql-on-ubuntu-20-04-quickstart" rel="noopener noreferrer"&gt;Installing PostgreSQL on Ubuntu 20.04&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://stackoverflow.com/questions/22483555/postgresql-give-all-permissions-to-a-user-on-a-postgresql-database" rel="noopener noreferrer"&gt;Granting permissions to a user/role for a database&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/installing-and-setting-up-postgresql-in-ubuntu" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>postgres</category>
      <category>ubuntu</category>
      <category>database</category>
    </item>
    <item>
      <title>Hello, world!</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:09:23 +0000</pubDate>
      <link>https://dev.to/jantolentino/hello-world-892</link>
      <guid>https://dev.to/jantolentino/hello-world-892</guid>
      <description>&lt;p&gt;Finally! My personal blog pings the world of the internet with a hearty "Hello, world!"&lt;/p&gt;

&lt;p&gt;It honestly took me a while to buckle down and do it, but after a bunch of pins on Pinterest and checking out other people's sites, I finally crafted a design I'm really happy with.&lt;/p&gt;

&lt;p&gt;I’ve always wanted a place where I could write about my interests and share my experiences with unusual technologies I find online. I want to also improve my writing and communication skills. So, putting up a personal blog felt like a great way to improve.&lt;/p&gt;

&lt;p&gt;I had a lot to figure out before I built the site. I explored different frontend frameworks, researched which cloud hosting providers were cheap and reliable, and planned out how to get a seamless deployment.&lt;/p&gt;

&lt;p&gt;In the end, I went with &lt;a href="https://nuxtjs.com" rel="noopener noreferrer"&gt;NuxtJS&lt;/a&gt; and &lt;a href="https://vuejs.org/" rel="noopener noreferrer"&gt;VueJS&lt;/a&gt; for the frontend. Working with VueJS is fantastic, and I'm really enjoying the experience. I stored the project in &lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; and used its CI/CD to automatically build a Docker image and deploy it to a VPS I bought from &lt;a href="https://www.digitalocean.com" rel="noopener noreferrer"&gt;Digital Ocean&lt;/a&gt;. For security and diagnostics, I also added &lt;a href="https://www.cloudflare.com" rel="noopener noreferrer"&gt;Cloudflare&lt;/a&gt; to protect and optimize the server. I think my site is pretty well-configured at this point.&lt;/p&gt;

&lt;p&gt;There are still a lot of features I need to add, like view components for code blocks, lists, and cards. But now that I’ve taken the hardest first step, I'm confident the rest will be a lot smoother.&lt;/p&gt;

&lt;p&gt;There's so much to learn and so much to enjoy. I plan to write about every moment I spend building my hobby projects. This personal site is just the beginning.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/hello-world" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blog</category>
      <category>nuxt</category>
      <category>vue</category>
      <category>digitalocean</category>
    </item>
    <item>
      <title>Getting the Function Keys Back on a Realme Book i5</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:08:50 +0000</pubDate>
      <link>https://dev.to/jantolentino/getting-the-function-keys-back-on-a-realme-book-i5-387p</link>
      <guid>https://dev.to/jantolentino/getting-the-function-keys-back-on-a-realme-book-i5-387p</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.pexels.com%2Fphotos%2F4065748%2Fpexels-photo-4065748.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fimages.pexels.com%2Fphotos%2F4065748%2Fpexels-photo-4065748.jpeg" alt="Close-up of a laptop keyboard" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few years ago, I bought a &lt;a href="https://www.realme.com/ph/realme-book" rel="noopener noreferrer"&gt;Realme Book i5&lt;/a&gt; for work. I bought it because it was slim, the display was nice, and it was a great lightweight machine to work on away from my desk.&lt;/p&gt;

&lt;p&gt;There was one problem I didn't expect. I use it mostly to write code, which means I constantly use the F1–F12 keys. On this laptop, the top row defaults to media keys, and I can't trigger the real F-keys without holding the &lt;code&gt;Fn&lt;/code&gt; key. Unlike laptops from Lenovo that let you flip an &lt;code&gt;Fn&lt;/code&gt; lock, this laptop has no such option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working around this friction
&lt;/h2&gt;

&lt;p&gt;My first thought was to remap the keys with &lt;code&gt;evremap&lt;/code&gt;. But this wasn't as easy as I thought. When I checked for raw input with &lt;code&gt;evtest&lt;/code&gt;, the F1 to F12 keys didn't send any events at all unless the &lt;code&gt;Fn&lt;/code&gt; key was held down.&lt;/p&gt;

&lt;p&gt;This was disappointing, so I opted to remap a few Fn keys that I use frequently—such as F12 to trigger "find references" in my IDE. Often, I'd just resort to working with an external keyboard when necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Digging into the firmware
&lt;/h2&gt;

&lt;p&gt;Nonetheless, I was convinced that a toggle existed somewhere, because inside the UEFI setup screen, the F-keys behave like normal F-keys by default. My theory was that once the kernel loads during boot, something flips them to media mode. That led me to suspect the firmware.&lt;/p&gt;

&lt;p&gt;At that point, I browsed for the BIOS file online, decompiled it, and read through the disassembly manually, looking for the flag that set the top-row mode. Given that I had no idea what to look for in the first place, I never found it, even after searching for a bunch of strings I suspected might be related. Eventually, I shelved the project and moved on.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffyzm135fi291wb73s9h7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffyzm135fi291wb73s9h7.png" alt="UEFI variable list from RU.EFI with the Setup variable highlighted" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking it back up with Claude Code
&lt;/h2&gt;

&lt;p&gt;I came back to it recently with Claude Code and my old archived dump. Hoping it could help me find what I was looking for, I pointed it at the extracted files, let it review everything I had, and had it work through the ACPI tables.&lt;/p&gt;

&lt;p&gt;After a few hours of going back and forth, it finally landed on something that actually made sense.&lt;/p&gt;

&lt;p&gt;Over the course of this, I learned that flashing the BIOS was not an option since the firmware is signed and protected. But I didn't actually need to touch the firmware. Instead, Linux can load custom ACPI tables from the &lt;code&gt;initramfs&lt;/code&gt; at boot and apply the override I needed. That was good enough for me.&lt;/p&gt;

&lt;p&gt;From Claude Code's investigation, we gathered that the EC exposes three fields that control the &lt;code&gt;Fn&lt;/code&gt; key behavior: &lt;code&gt;FNSP&lt;/code&gt;, &lt;code&gt;FNRV&lt;/code&gt;, and &lt;code&gt;MFNM&lt;/code&gt;. Setting them to &lt;code&gt;FNSP = 0&lt;/code&gt;, &lt;code&gt;FNRV = 1&lt;/code&gt;, and &lt;code&gt;MFNM = 0&lt;/code&gt; forces the standard F-key mode. It took quite a while to find the right combo to finally get this behavior.&lt;/p&gt;

&lt;p&gt;Now, the plan was to write those values at boot through a supplemental SSDT. To apply this, we create a dummy device in the system bus (&lt;code&gt;\_SB&lt;/code&gt;) with its own &lt;code&gt;_INI&lt;/code&gt; method. During the initialization phase, this custom device will run its method and flip the EC fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;First, note that the kernel needs a &lt;code&gt;CONFIG_ACPI_TABLE_UPGRADE&lt;/code&gt; configuration enabled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Write the SSDT
&lt;/h3&gt;

&lt;p&gt;Create a new file and name it &lt;code&gt;fnfix.dsl&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="n"&gt;DefinitionBlock&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"SSDT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"REALME"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"FNFIX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0x00000001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Reference the existing Embedded Controller and its fields&lt;/span&gt;
    &lt;span class="n"&gt;External&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DeviceObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;External&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FNSP&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FieldUnitObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;External&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FNRV&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FieldUnitObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;External&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MFNM&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FieldUnitObj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;Scope&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Device&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;FNFX&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_HID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"REAL0001"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Custom Hardware ID&lt;/span&gt;

            &lt;span class="c1"&gt;// Runs once during ACPI initialization&lt;/span&gt;
            &lt;span class="n"&gt;Method&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_INI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NotSerialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FNSP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
                &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FNRV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;_SB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PC00&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LPCB&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;H_EC&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MFNM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Compile
&lt;/h3&gt;

&lt;p&gt;Compile using &lt;code&gt;iasl&lt;/code&gt;, which produces &lt;code&gt;fnfix.aml&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iasl fnfix.dsl

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now package the AML the way the kernel expects it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; kernel/firmware/acpi
&lt;span class="nb"&gt;cp &lt;/span&gt;fnfix.aml kernel/firmware/acpi/
find kernel | cpio &lt;span class="nt"&gt;-H&lt;/span&gt; newc &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; acpi_override.img
&lt;span class="nb"&gt;sudo cp &lt;/span&gt;acpi_override.img /boot/

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Load it before the main initramfs
&lt;/h3&gt;

&lt;p&gt;We need to make sure that the ACPI image is the first &lt;code&gt;initrd&lt;/code&gt; the bootloader loads, ahead of the normal initramfs.&lt;/p&gt;

&lt;p&gt;Depending on the bootloader, we simply append it before the main image. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;title   Linux
linux   /vmlinuz-linux
initrd  /acpi_override.img
initrd  /initramfs-linux.img
options root=UUID=xxxx-xxxx-xxxx rw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we can reboot.&lt;/p&gt;

&lt;p&gt;The kernel loads the overlay, the dummy device's &lt;code&gt;_INI&lt;/code&gt; method runs during initialization, and the top row sends real F1-F12 events without needing to hold the &lt;code&gt;Fn&lt;/code&gt; key!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/getting-the-function-keys-back-on-a-realme-book-i5" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>realmebooki5</category>
      <category>functionkeys</category>
      <category>fnlock</category>
      <category>f1f12</category>
    </item>
    <item>
      <title>Getting into the world of Ansible</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:08:48 +0000</pubDate>
      <link>https://dev.to/jantolentino/getting-into-the-world-of-ansible-4n91</link>
      <guid>https://dev.to/jantolentino/getting-into-the-world-of-ansible-4n91</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjantolentino.dev%2Fblogs%2Fgetting-into-the-world-of-ansible-01.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fjantolentino.dev%2Fblogs%2Fgetting-into-the-world-of-ansible-01.png" alt="Getting into the world of Ansible" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;h1&gt;
  
  
  Getting into the world of Ansible
&lt;/h1&gt;

&lt;p&gt;Ever since I started exploring DevOps, I’ve always loved the idea of automating server setups with a single script. I have a strong interest for scaling web services to handle high traffic while ensuring stable uptime.&lt;/p&gt;

&lt;p&gt;For a long time, the closest tool I've been aware of was Ansible. However, it always had a steep learning curve for me, and I didn't have much free time to invest in it. Plus, paying for extra servers just to tinker with felt a bit expensive, and I wasn't interested enough to mess with local virtualization.&lt;/p&gt;

&lt;p&gt;Lately, thanks to AI tools, the barrier to entry for Ansible feels much lower. Getting explanations and answers to specific problems is so much easier now.&lt;/p&gt;

&lt;p&gt;So, with my humble homelab setup, I decided to explore migrating my current apps and manage them with Ansible instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting Started and Early Retrospective
&lt;/h2&gt;

&lt;p&gt;Not knowing where to begin, I asked Gemini to outline the basic topics I needed to understand. Grasping the core concepts of tasks, playbooks, vaults, variables, and inventories was straightforward. The overwhelming parts were modules, roles, and how to organize the files.&lt;/p&gt;

&lt;p&gt;I understood the fundamental purpose of roles and how they manage complexity and reduce redundancy by packaging tasks into reusable components. However, it conflicted with my personal preference. Roles exponentially increased the complexity of my folder structure. As someone who likes to keep things simple and flat—much like C projects usually do—it was tricky for me to accept. So, I decided to skip roles for now and stick to a near flat structure.&lt;/p&gt;

&lt;p&gt;Modules, on the other hand, are vast. I felt like I had to memorize dozens of them to get started, which felt restricting. Thankfully, most of what I needed fell under the &lt;code&gt;ansible.builtin&lt;/code&gt; namespace, so I didn’t have to worry too much about third-party modules just yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Migrating Uptime Kuma for Testing
&lt;/h2&gt;

&lt;p&gt;I have a mini-server at home and a VPS hosted in Hetzner. I call them my home node and cloud node, respectively.&lt;/p&gt;

&lt;p&gt;I previously moved my Uptime Kuma instance to the &lt;code&gt;cloud_nodes&lt;/code&gt; to ensure it actually stays up. It wouldn't be very useful if it crashed during a power or internet outage at home. Since Uptime Kuma is the simplest app in my stack, I decided to use it as the test subject for my Ansible migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;First, you need a &lt;strong&gt;master node&lt;/strong&gt;. This is the workstation that reaches out and accesses your servers. In my case, I used my primary PC.&lt;/p&gt;

&lt;p&gt;You also need to make sure you can SSH into those servers. Ideally, set up your SSH private keys to skip the hassle of typing or maintaining passwords. My servers and my workstation are all connected via Tailscale, which securely bridges the connections and makes routing incredibly simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting Up the Project
&lt;/h2&gt;

&lt;p&gt;To start an Ansible project, you basically need four things: a configuration file (&lt;code&gt;ansible.cfg&lt;/code&gt;), a list of your servers (&lt;code&gt;inventory.yml&lt;/code&gt;), a main playbook to run tasks (&lt;code&gt;site.yml&lt;/code&gt;), and the task files themselves.&lt;/p&gt;

&lt;p&gt;First, I defined &lt;code&gt;inventory.yml&lt;/code&gt;, which lists my actual servers. In its simplest form, it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;all&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;children&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;home_nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;p1_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;tailscale IP&amp;gt;&lt;/span&gt;
          &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jantolentino&lt;/span&gt;
    &lt;span class="na"&gt;cloud_nodes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;p2_server&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ansible_host&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;tailscale IP&amp;gt;&lt;/span&gt;
          &lt;span class="na"&gt;ansible_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;jantolentino&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, I created the &lt;code&gt;ansible.cfg&lt;/code&gt; configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[defaults]&lt;/span&gt;
&lt;span class="py"&gt;inventory&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;inventory.yml&lt;/span&gt;
&lt;span class="py"&gt;host_key_checking&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;False&lt;/span&gt;
&lt;span class="py"&gt;stdout_callback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;default&lt;/span&gt;
&lt;span class="py"&gt;result_format&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;yaml&lt;/span&gt;
&lt;span class="py"&gt;roles_path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;roles&lt;/span&gt;
&lt;span class="py"&gt;gathering&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;smart&lt;/span&gt;
&lt;span class="py"&gt;fact_caching&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;jsonfile&lt;/span&gt;
&lt;span class="py"&gt;fact_caching_connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;/tmp/ansible_facts&lt;/span&gt;
&lt;span class="py"&gt;fact_caching_timeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;86400&lt;/span&gt;

&lt;span class="nn"&gt;[ssh_connection]&lt;/span&gt;
&lt;span class="py"&gt;pipelining&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I enabled &lt;code&gt;pipelining&lt;/code&gt; to speed things up. It ensures that Ansible doesn't create a brand new SSH connection for every single task, which drastically lowers execution time. The other configurations mostly help with troubleshooting or caching.&lt;/p&gt;

&lt;p&gt;With the foundation set, it was time to write the actual instructions. I started by creating my main entry point, &lt;code&gt;site.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build home infrastructure&lt;/span&gt;
  &lt;span class="na"&gt;import_playbook&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;playbooks/home_infra.yml&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before migrating Uptime Kuma, I wanted to make sure my workstation could actually command the servers. So, I created a simple "ping" task under a &lt;code&gt;tasks&lt;/code&gt; directory named &lt;code&gt;ping.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ping host to verify connectivity&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.ping&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, I tied it all together by creating a playbook specifically for the &lt;code&gt;home_nodes&lt;/code&gt; to define what should execute on those servers in &lt;code&gt;playbooks/home_nodes.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Manage home infrastructure apps&lt;/span&gt;
  &lt;span class="na"&gt;hosts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;home_nodes&lt;/span&gt;
  &lt;span class="na"&gt;tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ping server&lt;/span&gt;
      &lt;span class="na"&gt;ansible.builtin.include_tasks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;../tasks/ping.yml&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Testing the Basics
&lt;/h2&gt;

&lt;p&gt;To run the tasks, the command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.yml site.yml

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This runs the ping task against all servers defined in the inventory. You can limit it to a specific group or node by adding the &lt;code&gt;--limit&lt;/code&gt; flag, like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ansible-playbook &lt;span class="nt"&gt;-i&lt;/span&gt; inventory.yml site.yml &lt;span class="nt"&gt;--limit&lt;/span&gt; home_nodes

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhytjuqd4cuic0wwa1m0v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fhytjuqd4cuic0wwa1m0v.png" alt="Running the ansible-playbook command to ping" width="800" height="478"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Running the ansible-playbook command to ping the server&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  The Migration Plan
&lt;/h2&gt;

&lt;p&gt;Now for the actual migration. I wanted to add my Uptime Kuma instance to this setup.&lt;/p&gt;

&lt;p&gt;Thankfully, I had previously deployed my apps using &lt;strong&gt;Podman Quadlets&lt;/strong&gt;. This declarative approach made my Ansible migration plan much easier to execute. Note that Uptime Kuma was already running on the Hetzner server. My goal was simply to define its state in Ansible so I could manage it moving forward, not to destroy and recreate the instance.&lt;/p&gt;

&lt;p&gt;Ansible should ensure the instance is configured correctly without causing data loss unless explicitly intended.&lt;/p&gt;

&lt;p&gt;My task outline looked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Copy the Quadlet files to the server.&lt;/li&gt;
&lt;li&gt;Create the necessary application directories.&lt;/li&gt;
&lt;li&gt;Run a dry-run to ensure the Quadlet syntax is valid.&lt;/li&gt;
&lt;li&gt;Reload systemd to generate the service file.&lt;/li&gt;
&lt;li&gt;Start or restart the application service.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is what my &lt;code&gt;quadlet_setup.yml&lt;/code&gt; roughly looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# Assertions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure app_name is provided&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app_name is defined&lt;/span&gt;
    &lt;span class="na"&gt;fail_msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Error:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;You&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;specify&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'app_name'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;when&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;including&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;this&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;task&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;file."&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Enable user lingering&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;loginctl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;enable-linger&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;creates&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/lib/systemd/linger/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setup&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Copy quadlet configuration files&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;../quadlets/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;dest&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/.config/containers/systemd/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;basename&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0644"&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quadlet_files&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;default([])&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quadlet_templates&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dry-run Quadlet files to check syntax&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;cmd&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/usr/libexec/podman/quadlet -dryrun -user&lt;/span&gt;
  &lt;span class="na"&gt;register&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;quadlet_check&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure the application directory exists&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;directory&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0755"&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_directories&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;default([])&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;setup&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Reload systemd (user) and start service&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.service"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;started&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;become_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ansible_become_flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Restart service if configuration changed&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.service"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;restarted&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;become_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ansible_become_flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i"&lt;/span&gt;
  &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;(quadlet_templates.changed | default(false)) or (app_config_changed | default(false))&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Getting this right made me appreciate two specific Ansible features: &lt;code&gt;tags&lt;/code&gt; and &lt;code&gt;become&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Because I used &lt;code&gt;tags&lt;/code&gt;, I can skip certain tasks. For example, I don’t need to enable user lingering every single time I run the playbook. I can just add &lt;code&gt;--skip-tags setup&lt;/code&gt; to my command to save time. And whenever a task needed root privileges, &lt;code&gt;become&lt;/code&gt; handled it perfectly. I just pass the &lt;code&gt;-K&lt;/code&gt; flag when running the playbook so it prompts for my sudo password.&lt;/p&gt;

&lt;p&gt;With all of that logic in place, I finally ran it. Naturally, there were a few issues to fix along the way, mostly typos in my container or network files. Running &lt;code&gt;/usr/libexec/podman/quadlet -dryrun -user&lt;/code&gt; was the key for catching those errors early.&lt;/p&gt;

&lt;p&gt;Ansible ad-hoc commands helped me check the system state, though sometimes just SSHing directly into the server was faster for debugging.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cpr9xt05zn45kzw3b6j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5cpr9xt05zn45kzw3b6j.png" alt="Running the ansible-playbook command with an error" width="800" height="663"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Running the ansible-playbook command with an error&lt;/em&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Teardown: Decommissioning the App
&lt;/h2&gt;

&lt;p&gt;I was also curious about how to cleanly destroy the instance if I ever needed to decommission it. I created a &lt;code&gt;tasks/quadlet_remove.yml&lt;/code&gt; file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="c1"&gt;# Assertions&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ensure app_name is defined&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.assert&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;that&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app_name is defined and app_name | length &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;quadlet_files is defined and quadlet_files | length &amp;gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;fail_msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CRITICAL&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ERROR:&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'app_name'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;and&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'quadlet_files'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;be&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;provided."&lt;/span&gt;
    &lt;span class="na"&gt;success_msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Check&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;passed.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Proceeding&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;with&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;removal&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}."&lt;/span&gt;

&lt;span class="c1"&gt;# Teardown&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stop and disable the systemd (user) service&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}.service"&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stopped&lt;/span&gt;
    &lt;span class="na"&gt;enabled&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;become_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ansible_become_flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i"&lt;/span&gt;
  &lt;span class="na"&gt;failed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Forcefully purge remaining active or broken container states&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;podman&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;rm&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-f&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_name&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;changed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;failed_when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Cleanup Quadlet definition files from systemd directory&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/home/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}/.config/containers/systemd/{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;basename&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;absent&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;quadlet_files&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Force systemd (user) daemon to reload and flush generated services&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.systemd_service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;daemon_reload&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;user&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;become_user&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ansible_user&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;vars&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;ansible_become_flags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-i"&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Permanently remove application storage directories from host&lt;/span&gt;
  &lt;span class="na"&gt;ansible.builtin.file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;item&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
    &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;absent&lt;/span&gt;
  &lt;span class="na"&gt;loop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;app_directories&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;default([])&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;}}"&lt;/span&gt;
  &lt;span class="na"&gt;become&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;destroy-data&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To remove the app and clean up, I simply swap out &lt;code&gt;quadlet_setup.yml&lt;/code&gt; for &lt;code&gt;quadlet_remove.yml&lt;/code&gt; in my playbook.&lt;/p&gt;

&lt;h2&gt;
  
  
  Looking Forward
&lt;/h2&gt;

&lt;p&gt;There are still a few things left to explore. I want to figure out how to autonomously back up application data and find a straightforward way to roll back deployments if something breaks. I plan to dedicate more time to that soon.&lt;/p&gt;

&lt;p&gt;Also, as the project grows, I’ll likely have to bite the bullet and dive into roles and Ansible's Jinja2 template engine to keep things manageable.&lt;/p&gt;

&lt;p&gt;But for now, I successfully brought Uptime Kuma under Ansible's control. Next up is migrating the rest of my apps. I currently manage my environment variables using Doppler, so getting Doppler tokens to securely pull variables into these servers via Ansible will be my next big hurdle.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Cheers!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/getting-into-the-world-of-ansible" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ansible</category>
      <category>devops</category>
      <category>automation</category>
      <category>servermanagement</category>
    </item>
    <item>
      <title>I got my Associate Cloud Engineer Certification!</title>
      <dc:creator>jantolentino</dc:creator>
      <pubDate>Sun, 27 Sep 2026 07:08:17 +0000</pubDate>
      <link>https://dev.to/jantolentino/i-got-my-associate-cloud-engineer-certification-4l5n</link>
      <guid>https://dev.to/jantolentino/i-got-my-associate-cloud-engineer-certification-4l5n</guid>
      <description>&lt;p&gt;I finally did it—I passed my &lt;strong&gt;Associate Cloud Engineer Certification&lt;/strong&gt; exam! The journey had a lot of ups and downs, and ironically, they weren't even related to the exam itself. Honestly, the biggest challenge was finding enough time to review and do the study sessions.&lt;/p&gt;

&lt;p&gt;Back in March, I saw an email about a "Get Certified" program and was immediately interested. I really wanted to learn more about opportunities in the cloud, so I applied with my work email, which turned out to be a good move.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshqso0p023g5phn366ay.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fshqso0p023g5phn366ay.png" alt="Early promotional email I received from GCP" width="800" height="645"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Early promotional email I received from GCP&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few days later, I got a confirmation email that I was in. The program included some really helpful review sessions. They gave me the confidence I needed to tackle the exam. As the sessions went on, it got a bit trickier to keep up. We had a lot of mixed discussions about the basics, principles, and different Google Cloud products. We often focused on which software was better for specific use cases. GCP has many solutions for a single problem, but each one has its own nuances in terms of pricing, reliability, maintenance, and others. These discussions were honestly super insightful.&lt;/p&gt;

&lt;p&gt;We also covered topics like setting up virtual machines, routers, and load balancing. We learned how to store different types of data and how to follow security best practices. I was already familiar with most of these processes, but not with how to navigate Google Cloud's specific software. I didn't have much experience with that before.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fryhkw2mkqs4cnlyo0gus.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fryhkw2mkqs4cnlyo0gus.png" alt="Diagrams of URL Map &amp;amp; Forwarding Rules" width="800" height="971"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;I had to rely on diagrams to fully grasp the concepts (URL Map &amp;amp; Forwarding Rules)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Looking back, the hands-on labs were what helped me the most. They were awesome! They gave me a deeper understanding of how to actually manage and configure things in the cloud. They even covered the basics, like how to navigate the cloud's webpage, which is so simple yet so helpful when you don't know where to find things. We were often told the exam wasn't very technical; it was more essential to gain better understanding the available products, principles, best practices, especially with real-world examples. Even so, the program gave me the necessary experience and skills to deliver scalable solutions and setting up things in the cloud.&lt;/p&gt;

&lt;p&gt;Almost three months went by, and with my no-cost voucher about to expire at the end of May, I finally took the exam. Today, after what felt like an endless two-day wait, I got my results! All those months of studying and learning paid off. Now, it's on to the next adventure: Cloud Architect! Wish me luck!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3v8hyk7lv9z31taf1nt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fv3v8hyk7lv9z31taf1nt.png" alt="My official certification from Credly" width="800" height="613"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;My official certification from Credly&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://jantolentino.dev/blog/associate-cloud-engineer" rel="noopener noreferrer"&gt;jantolentino.dev&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gcp</category>
      <category>googlecloud</category>
      <category>associatecloudengineer</category>
      <category>certification</category>
    </item>
  </channel>
</rss>
