<?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: Automate It All</title>
    <description>The latest articles on DEV Community by Automate It All (@automateitall).</description>
    <link>https://dev.to/automateitall</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%2F4112797%2F3ba5664b-6c5c-486d-9b9b-792a8494dd68.png</url>
      <title>DEV Community: Automate It All</title>
      <link>https://dev.to/automateitall</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/automateitall"/>
    <language>en</language>
    <item>
      <title>From a Bare Debian Box to a Public HTTPS Page, in Eight Command Blocks</title>
      <dc:creator>Automate It All</dc:creator>
      <pubDate>Wed, 09 Sep 2026 02:04:35 +0000</pubDate>
      <link>https://dev.to/automateitall/from-a-bare-debian-box-to-a-public-https-page-in-eight-command-blocks-2obp</link>
      <guid>https://dev.to/automateitall/from-a-bare-debian-box-to-a-public-https-page-in-eight-command-blocks-2obp</guid>
      <description>&lt;p&gt;Lesson one of twelve, written out below. The video is free on YouTube: &lt;a href="https://www.youtube.com/watch?v=Yzty9buG4dc" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=Yzty9buG4dc&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need a Debian 12 box you can SSH into as root the first time, and a domain whose A record already points at it. Everything below is what the recording shows, in the order it shows it. &lt;code&gt;SITE&lt;/code&gt; is your domain, and &lt;code&gt;ADMIN&lt;/code&gt; is the human account you will use from now on.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One Apt Call
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt;
apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ca-certificates curl debian-keyring debian-archive-keyring apt-transport-https &lt;span class="se"&gt;\&lt;/span&gt;
  gnupg &lt;span class="nb"&gt;sudo &lt;/span&gt;openssh-server ufw unattended-upgrades
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Caddy repo needs the keyring and transport packages, so they go in here rather than in step 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A Human Account With Sudo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;adduser &lt;span class="nt"&gt;--disabled-password&lt;/span&gt; &lt;span class="nt"&gt;--gecos&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
usermod &lt;span class="nt"&gt;-aG&lt;/span&gt; &lt;span class="nb"&gt;sudo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 700 &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"/home/&lt;/span&gt;&lt;span class="nv"&gt;$ADMIN&lt;/span&gt;&lt;span class="s2"&gt;/.ssh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From your laptop, &lt;code&gt;ssh-copy-id $ADMIN@$SITE&lt;/code&gt; puts your public key in place. Do that before step 3, because step 3 is what closes the root door behind you.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Root Login Off, and a Parse Check
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/ssh/sshd_config.d/10-hardening.conf &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;/usr/sbin/sshd &lt;span class="nt"&gt;-t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sshd -t&lt;/code&gt; parses the whole config and exits non-zero on a typo. Run it before you restart sshd, because a config that fails to parse after a restart locks you out of a box whose only other door you just shut.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. A Firewall With Three Doors
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw default deny incoming
ufw default allow outgoing
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;enable
&lt;/span&gt;ufw show added
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A container has no netfilter, so the lesson's script checks &lt;code&gt;/proc/sys/net/ipv4/ip_forward&lt;/code&gt; for write access and prints the &lt;code&gt;enable&lt;/code&gt; line instead of running it. On your VPS it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Security Updates Install Themselves
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/apt.conf.d/20auto-upgrades &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;unattended-upgrade &lt;span class="nt"&gt;--dry-run&lt;/span&gt; &lt;span class="nt"&gt;--debug&lt;/span&gt; 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-m1&lt;/span&gt; &lt;span class="s2"&gt;"Allowed origins are"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dry run prints the origins it would pull from. Reading that line is how you learn the security pocket is in the list rather than assuming it.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Caddy From Its Own Repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://dl.cloudsmith.io/public/caddy/stable/gpg.key &lt;span class="se"&gt;\&lt;/span&gt;
  | gpg &lt;span class="nt"&gt;--dearmor&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/apt/sources.list.d/caddy-stable.list
apt-get update &lt;span class="nt"&gt;-qq&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;-qq&lt;/span&gt; caddy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Debian's own &lt;code&gt;caddy&lt;/code&gt; package lags the upstream release, and the automatic HTTPS behaviour below is the part that moves.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Three Lines of Web Server
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 755 /var/www/site
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;h1&amp;gt;&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt; is live&amp;lt;/h1&amp;gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /var/www/site/index.html
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /etc/caddy/Caddyfile &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="sh"&gt; {
    root * /var/www/site
    file_server
}
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;caddy &lt;span class="nb"&gt;fmt&lt;/span&gt; &lt;span class="nt"&gt;--overwrite&lt;/span&gt; /etc/caddy/Caddyfile
caddy validate &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/caddy/Caddyfile
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caddy reads the site address, asks Let's Encrypt for a certificate, and renews it. There is no certbot step and no cron entry, which is the whole reason this course uses Caddy rather than nginx.&lt;/p&gt;

&lt;p&gt;Testing on &lt;code&gt;localhost&lt;/code&gt; adds one line, &lt;code&gt;tls internal&lt;/code&gt;, so Caddy signs with its own CA rather than failing an ACME challenge for a name nobody can resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Read the Page Back
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;caddy start &lt;span class="nt"&gt;--config&lt;/span&gt; /etc/caddy/Caddyfile
&lt;span class="k"&gt;for &lt;/span&gt;_ &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;seq &lt;/span&gt;30&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-sk&lt;/span&gt; &lt;span class="s2"&gt;"https://&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; /tmp/page.html &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done
&lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$SITE&lt;/span&gt;&lt;span class="s2"&gt; is live"&lt;/span&gt; /tmp/page.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The loop exists because the first request arrives while Caddy is still finishing its ACME challenge. Thirty one-second tries cover that.&lt;/p&gt;

&lt;p&gt;At this point a stranger loads your page over HTTPS, root cannot log in over SSH, ufw allows 22, 80 and 443 only, and security updates install themselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Script Is the Test
&lt;/h2&gt;

&lt;p&gt;Every lesson in this course ships that &lt;code&gt;commands.sh&lt;/code&gt; alongside the video, and the course's test suite runs each one head to tail in a fresh &lt;code&gt;debian:12-slim&lt;/code&gt; container. A command that drifts from what the recording shows fails the run. Twelve of twelve were green on 2026-09-09.&lt;/p&gt;

&lt;p&gt;The firewall step branches for the same reason. A script claiming a firewall the container never raised would pass a test and teach the wrong thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Other Eleven
&lt;/h2&gt;

&lt;p&gt;Lessons 2 and 3 are free too: Caddy as your web server, then systemd running your own service behind it. Lessons 4 to 12 cover Tailscale as a private network, Tailscale Funnel, Docker Compose, a reverse proxy across three subdomains, an agent as a systemd unit, a Telegram gateway, one bot token per box, restic snapshots restored into an empty directory, and auto-shutdown with monitoring.&lt;/p&gt;

&lt;p&gt;The narration is a cloned voice of mine rather than a live read, and the YouTube uploads carry the altered-content disclosure.&lt;/p&gt;

&lt;p&gt;Lessons 4 to 12 are 49 EUR as MP4 downloads with a 30-day refund: &lt;a href="https://automate-it-all.win/course/" rel="noopener noreferrer"&gt;https://automate-it-all.win/course/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions: &lt;a href="mailto:skills@automate-it-all.win"&gt;skills@automate-it-all.win&lt;/a&gt;. Author: &lt;a href="https://fidel-perez.github.io/portfolio/" rel="noopener noreferrer"&gt;https://fidel-perez.github.io/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>devops</category>
      <category>linux</category>
      <category>caddy</category>
    </item>
    <item>
      <title>I Gave My Agent Ten Ops Skills, and a Docker Test for Each One</title>
      <dc:creator>Automate It All</dc:creator>
      <pubDate>Sun, 06 Sep 2026 20:11:18 +0000</pubDate>
      <link>https://dev.to/automateitall/i-gave-my-agent-ten-ops-skills-and-a-docker-test-for-each-one-3bb4</link>
      <guid>https://dev.to/automateitall/i-gave-my-agent-ten-ops-skills-and-a-docker-test-for-each-one-3bb4</guid>
      <description>&lt;p&gt;Ten skills, ten Docker tests, nine green and one skipped off Ubuntu. That count is the only claim in this article worth checking, and &lt;code&gt;tests/run.sh&lt;/code&gt; in the repo prints it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem With a How-To an Agent Reads
&lt;/h2&gt;

&lt;p&gt;A runbook written for a person leaves gaps a person fills from context. An agent fills the same gaps by guessing, and a guess about &lt;code&gt;ufw --force enable&lt;/code&gt; locks you out of the box.&lt;/p&gt;

&lt;p&gt;So each procedure here ships as a folder the agent loads whole:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;skills/vps-basics/
  SKILL.md          YAML front matter, then numbered steps
  references/
    jail.local      the file step 3 copies
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Front matter carries &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;version&lt;/code&gt;, &lt;code&gt;license&lt;/code&gt; and &lt;code&gt;platforms&lt;/code&gt;. Claude Code reads that layout natively. Any AgentSkills-spec loader reads the same folders, though only Claude Code and my own runner have been tested against these.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Test Runs the Skill's Own Steps
&lt;/h2&gt;

&lt;p&gt;A skill is a document, and a document rots. The harness closes that gap by executing the document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;--privileged&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$skill_dir&lt;/span&gt;&lt;span class="s2"&gt;/references:/refs:ro"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  ubuntu:22.04 bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s1"&gt;'
    apt-get update -qq &amp;amp;&amp;amp; apt-get install -y -qq ufw fail2ban
    ufw default deny incoming
    ufw allow 22/tcp &amp;amp;&amp;amp; ufw allow 80/tcp &amp;amp;&amp;amp; ufw allow 443/tcp
    ufw --force enable
    cp /refs/jail.local /etc/fail2ban/jail.local
    service fail2ban start
    ufw status | grep -q "Status: active"
    ufw status | grep -q "22/tcp"
    fail2ban-client status sshd | grep -q "Status for the jail: sshd"
  '&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those commands are the steps of &lt;code&gt;skills/vps-basics/SKILL.md&lt;/code&gt;, copied out. Edit the SKILL.md without editing the test and the two disagree; edit the test without editing the SKILL.md and the assertions stop matching. Either way the harness goes red before a reader gets the wrong version.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tests/run.sh&lt;/code&gt; walks every &lt;code&gt;tests/&amp;lt;name&amp;gt;.sh&lt;/code&gt;, skips a skill whose front matter says &lt;code&gt;host-specific: true&lt;/code&gt;, and exits non-zero on the first failure. One skill is host-specific: &lt;code&gt;wan-failover-uplink-guard&lt;/code&gt; needs two physical uplinks, and no container gives it those.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Ten Cover
&lt;/h2&gt;

&lt;p&gt;Free and MIT, in the public repo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;vps-basics&lt;/code&gt;: ufw default-deny plus a fail2ban sshd jail on a fresh box.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;backup-restore-drill&lt;/code&gt;: every backup step paired with a restore step, so a backup counts only after it has been read back.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;service-health-check&lt;/code&gt;: process, port and HTTP checked in that order.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;systemd-unit-authoring&lt;/code&gt;: a script turned into a unit that restarts on crash and starts on boot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paid once the checkout opens, which it has yet to do: &lt;code&gt;caddy-reverse-proxy&lt;/code&gt;, &lt;code&gt;tailscale-funnel&lt;/code&gt;, &lt;code&gt;pi-hardening&lt;/code&gt;, &lt;code&gt;telegram-bot-deployment&lt;/code&gt;, &lt;code&gt;wan-failover-uplink-guard&lt;/code&gt;, &lt;code&gt;wol-wake-and-remote-shutdown&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Three Layers in a Health Check
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;service-health-check&lt;/code&gt; runs process, then port, then HTTP, and stops at the first failure. Each layer catches what the layer above misses. A process check alone passes a hung process that still holds its port. A port check alone passes an app that accepts the connection and returns 500 to every request. Only the HTTP check reads what a user would read.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Does Not Prove
&lt;/h2&gt;

&lt;p&gt;A container test proves the steps run on a clean Ubuntu 22.04. Running on your box, with your kernel and your existing firewall rules, is a separate claim, and no skill in the pack makes it.&lt;/p&gt;

&lt;p&gt;Ten skills is also a small sample for a format claim. The layout may need changing once a third loader reads it.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/automate-it-all/agent-skills-pack" rel="noopener noreferrer"&gt;https://github.com/automate-it-all/agent-skills-pack&lt;/a&gt;&lt;br&gt;
Paid six: &lt;a href="https://automate-it-all.win/skills/" rel="noopener noreferrer"&gt;https://automate-it-all.win/skills/&lt;/a&gt;&lt;br&gt;
Questions: &lt;a href="mailto:skills@automate-it-all.win"&gt;skills@automate-it-all.win&lt;/a&gt;&lt;br&gt;
Who builds this: &lt;a href="https://fidel-perez.github.io/portfolio/" rel="noopener noreferrer"&gt;https://fidel-perez.github.io/portfolio/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>homelab</category>
      <category>testing</category>
    </item>
  </channel>
</rss>
