<?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: Mino Randriamanivo</title>
    <description>The latest articles on DEV Community by Mino Randriamanivo (@mino_rand).</description>
    <link>https://dev.to/mino_rand</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%2F2699940%2Fbbf571db-cf87-4c51-b924-1f078b25bd42.jpg</url>
      <title>DEV Community: Mino Randriamanivo</title>
      <link>https://dev.to/mino_rand</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mino_rand"/>
    <language>en</language>
    <item>
      <title>Maintenance &amp; security Journey in 2026 - Post-mortem measures in GHOST compromised websites</title>
      <dc:creator>Mino Randriamanivo</dc:creator>
      <pubDate>Wed, 02 Sep 2026 02:52:17 +0000</pubDate>
      <link>https://dev.to/mino_rand/maintenance-security-journey-in-2026-post-mortem-measures-in-ghost-compromised-websites-43bp</link>
      <guid>https://dev.to/mino_rand/maintenance-security-journey-in-2026-post-mortem-measures-in-ghost-compromised-websites-43bp</guid>
      <description>&lt;p&gt;Hi,&lt;/p&gt;

&lt;p&gt;An attack was ran on multiple Ghost websites (&lt;a href="https://kahiether.com/" rel="noopener noreferrer"&gt;https://kahiether.com/&lt;/a&gt;, etc..) this summer, 3rd August 2026 exactly. The issue was simple: a vulnerability in a recent update on Ghost CMS allowed an attacker to access the Admin API and perform all admin authorized operations through its endpoints. But the attacker don't want to raise alert so he keep the website content as similar as possible. The problem with markdown block is that it is transformed into html and directly visible when editing.&lt;/p&gt;

&lt;p&gt;The change was straightforward: the attacker retrieved a random (not all) article in HTML, appended text such as:&lt;/p&gt;

&lt;p&gt;“ロングウィッグは毛先のまとまりや絡まりやすさを確認しておくと、撮影前の手直しが楽になります。色味の違いを確認する場面では、着せ恋 コスプレウィッグが撮影時の印象を比べる手がかりになります。保管時に湿気や圧迫を避けることで、次回も扱いやすい状態を保てます”&lt;/p&gt;

&lt;p&gt;This content included a hyperlink designed to redirect visitors and boost the SEO of a Japanese website (backlink): &lt;a href="https://www.tsukicos.com/" rel="noopener noreferrer"&gt;https://www.tsukicos.com/&lt;/a&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%2Foxutq0de55dryrw0f6ge.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%2Foxutq0de55dryrw0f6ge.png" alt=" " width="800" height="431"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Fortunately, the fallback solution was simple:&lt;/p&gt;

&lt;p&gt;1/ Restricted access to /ghost by IP address, blocking unauthorized access to the Admin API =&amp;gt; This let to a general rule that might be discussed later "/wp-admin", "/admin", "/api", etc... must all be restricted by IP on any site, some will be also require protection by a basic auth layer server side. &lt;br&gt;
2/ Removed the injected code&lt;br&gt;
3/ Built and ran a small tool to convert the compromised HTML back into Markdown for all markdown content : &lt;a href="https://gongju.kahiether.com/app/ghost-table-rescue" rel="noopener noreferrer"&gt;https://gongju.kahiether.com/app/ghost-table-rescue&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am open to any suggestions and tips about these vulnerabilities and the recent problem with themes with the new Ghost versions.&lt;/p&gt;

&lt;p&gt;Have a good day&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>infosec</category>
      <category>security</category>
    </item>
    <item>
      <title>AI Security Engineer Journey in 2026: Recovering, Migrating and monitoring Traefik v3, Portainer across the Kahiether Stack</title>
      <dc:creator>Mino Randriamanivo</dc:creator>
      <pubDate>Thu, 23 Jul 2026 22:37:30 +0000</pubDate>
      <link>https://dev.to/mino_rand/ai-security-engineer-journey-in-2026-recovering-migrating-and-monitoring-traefik-v3-portainer-3g17</link>
      <guid>https://dev.to/mino_rand/ai-security-engineer-journey-in-2026-recovering-migrating-and-monitoring-traefik-v3-portainer-3g17</guid>
      <description>&lt;p&gt;Hi👋 PS : this is a follow up of &lt;a class="mentioned-user" href="https://dev.to/portainerio"&gt;@portainerio&lt;/a&gt;  and the request for a more updated version of Traefik in 2022 here : &lt;a href="https://medium.com/@randriamanivo/devops-journey-in-2022-using-portainer-traefik-and-docker-compose-to-quickly-deploy-an-example-aa5a2c43645d" rel="noopener noreferrer"&gt;https://medium.com/@randriamanivo/devops-journey-in-2022-using-portainer-traefik-and-docker-compose-to-quickly-deploy-an-example-aa5a2c43645d&lt;/a&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%2F7uirmun0s6qib2e6aghn.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%2F7uirmun0s6qib2e6aghn.png" alt=" " width="800" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;金繕い (Kintsugi) is a Japanese art where broken pottery is repaired with lacquer mixed with gold instead making the fractures become part of the object's history. Like Kintsugi, resilience is not about pretending nothing happened and putting everything like they were originally. It is about using disruption to build a system that carries the lessons of the past into a more resilient future.&lt;/p&gt;

&lt;p&gt;It was somewhere between the Basque Land and Cantabria (shout out to Lina) that the servers fell down and an alert email came. I had been postponing the full migration for a long time, but why ? Traefik v1 to v3 alone meant rewriting the label syntax across every stack, meaning having hundreds of line to rewrite and test them.&lt;/p&gt;

&lt;p&gt;This time the timing was not chosen, an attack triggered the migration and then here we are looking upon the main parts including analyzing the problem, defining solutions, applying the corrections, and monitoring the result. The mains section is to describe the Portainer and Traefik migration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding Out How Bad It Was
&lt;/h2&gt;




&lt;p&gt;Before making any change, first step is to run diagnostic pass on the server: uptime, disk, memory, running containers, listening ports.&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;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== DATE / UPTIME ==="&lt;/span&gt;
&lt;span class="nb"&gt;date
uptime
echo&lt;/span&gt; &lt;span class="s2"&gt;"=== DISQUE ==="&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== MEMOIRE ==="&lt;/span&gt;
free &lt;span class="nt"&gt;-h&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== CHARGE ==="&lt;/span&gt;
top &lt;span class="nt"&gt;-b&lt;/span&gt; &lt;span class="nt"&gt;-n1&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-25&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== DOCKER SERVICE ==="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== CONTAINERD SERVICE ==="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status containerd &lt;span class="nt"&gt;--no-pager&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== PORTS EN ECOUTE ==="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;ss &lt;span class="nt"&gt;-lntup&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;':(22|80|443|8000|8080|9000|9443)\b'&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
echo&lt;/span&gt; &lt;span class="s2"&gt;"=== CONTENEURS ==="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Ports}}\t{{.Image}}'&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"=== CONTENEURS EN ECHEC ==="&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps &lt;span class="nt"&gt;-a&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;exited &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--format&lt;/span&gt; &lt;span class="s1"&gt;'table {{.Names}}\t{{.Status}}\t{{.Image}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result: 0 containers but untouched volumes and networks. The main vulnerabilities that needed patch was the docker sock exposition. The second was the access to infrastructure administration especially Portainer and Traefik. The Ci/Cd was partly done with Github Actions and DockerHub which were already secure. &lt;/p&gt;

&lt;h2&gt;
  
  
  Rebuilding the Base System
&lt;/h2&gt;

&lt;p&gt;Before touching Traefik, Portainer, or security settings, the OS and Docker Engine needed a refresh : some operations were done to add a little layer of security there : Ban, IP whitelisting, bust and rate limiter.  The next step is to update Docker from the official repository.&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;apt-get update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  docker-ce &lt;span class="se"&gt;\&lt;/span&gt;
  docker-ce-cli &lt;span class="se"&gt;\&lt;/span&gt;
  containerd.io &lt;span class="se"&gt;\&lt;/span&gt;
  docker-buildx-plugin &lt;span class="se"&gt;\&lt;/span&gt;
  docker-compose-plugin
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Preparing the Traefik V3
&lt;/h2&gt;

&lt;p&gt;Security work is not a one-time task. What follows reduces exposure, it does not remove risk permanently. Set correct permissions on the ACME storage file used by Let's Encrypt, since a world-readable acme.json is a documented weak point in Traefik setups.&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; /opt/traefik-v3
&lt;span class="nb"&gt;sudo touch&lt;/span&gt; /opt/traefik-v3/acme.json
&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;600 /opt/traefik-v3/acme.json
&lt;span class="nb"&gt;sudo chown &lt;/span&gt;admin:admin /opt/traefik-v3/acme.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the upcoming step is to review exposed ports and dashboards against what actually needed to be public. This part, along with monitoring and resource tuning, took most of the time. Upgrading Traefik and Portainer themselves took about one hour combined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Traefik Upgrade
&lt;/h2&gt;

&lt;p&gt;Traefik v1 to v3 required a rewrite of the label syntax for every stack, since the static configuration, dynamic configuration, and middleware format changed between major versions.&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%2Ff1rz6ojqyman32fr71fh.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%2Ff1rz6ojqyman32fr71fh.png" alt=" " width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Router and entrypoint configuration for Traefik v3.7.6, with sensitive values replaced by placeholders:&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;docker-socket-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&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;mino189/ghcr.io--tecnativa--docker-socket-proxy:v0.4.2&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker-socket-proxy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Deny all Docker API write operations&lt;/span&gt;
      &lt;span class="na"&gt;POST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
      &lt;span class="c1"&gt;# Docker API sections required by Traefik discovery&lt;/span&gt;
      &lt;span class="na"&gt;PING&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;VERSION&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;EVENTS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;CONTAINERS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;
      &lt;span class="na"&gt;NETWORKS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-api&lt;/span&gt;
&lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&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;mino189/traefik:v3.7.6&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;traefik-v3&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# Logging&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--log.level=INFO"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--accesslog=true"&lt;/span&gt;
      &lt;span class="c1"&gt;# Dashboard&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api.dashboard=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api.insecure=false"&lt;/span&gt;
      &lt;span class="c1"&gt;# Docker provider&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.endpoint=tcp://docker-socket-proxy:2375"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.network=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.watch=true"&lt;/span&gt;
      &lt;span class="c1"&gt;# HTTP entrypoint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="c1"&gt;# HTTPS entrypoint&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.http3=true"&lt;/span&gt;
      &lt;span class="c1"&gt;# Redirect HTTP to HTTPS&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.to=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.scheme=https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.http.redirections.entrypoint.permanent=true"&lt;/span&gt;
      &lt;span class="c1"&gt;# Let's Encrypt&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.email=&amp;lt;TO_CHANGE_EMAIL&amp;gt;"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.storage=/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443/udp"&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="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/opt/traefik-v3/acme.json:/acme.json"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;web&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker-api&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.docker.network=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.rule=Host(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;`&amp;lt;TO_CHANGE_DOMAIN&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.tls.certresolver=letsencrypt"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.service=api@internal"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik-dashboard.middlewares=dashboard-ipallowlist,dashboard-ratelimit,dashboard-security"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-ipallowlist.ipallowlist.sourcerange=xxxxxxxx/xx"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-ratelimit.ratelimit.average=10"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-ratelimit.ratelimit.burst=20"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-ratelimit.ratelimit.period=1s"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-security.headers.contenttypenosniff=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-security.headers.framedeny=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-security.headers.referrerpolicy=no-referrer"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-security.headers.stsseconds=31536000"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.dashboard-security.headers.stsincludesubdomains=true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two changes matter compared to v1. HTTP/3 moved out of experimental status and became fully production ready in Traefik v3. And starting with v3.2, Traefik added a Fast Proxy engine, built on a zero-allocation pipeline, delivering a 50% performance improvement over the standard engine. the stack run v3.7.6, past that release, so this applies here. Official write-up: &lt;a href="https://traefik.io/blog/traefik-proxy-v3-2-a-munster-release" rel="noopener noreferrer"&gt;https://traefik.io/blog/traefik-proxy-v3-2-a-munster-release&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Portainer Upgrade
&lt;/h2&gt;

&lt;p&gt;Portainer was upgraded in the same row as Traefik, there is always a compatibility to check between Docker, Traefik and Portainer. The Portainer version is " Portainer Community Edition 2.39.4 LTS".&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%2Fgr1y49nt1ze2sajxqvl0.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%2Fgr1y49nt1ze2sajxqvl0.png" alt=" " width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Label Rewrite
&lt;/h2&gt;

&lt;p&gt;Every Traefik label across all 70 stacks needed to be rewritten for v3 syntax. The task itself is a configuration migration: the frontend rule, rate limit block, and custom headers on each service all needed translating into the new router, middleware, and service structure.&lt;/p&gt;

&lt;p&gt;Here is one actual example, before and after, for a single service, with the CSP domain list anonymised:&lt;/p&gt;

&lt;p&gt;Before (Traefik v1 syntax):&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;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.frontend.rule=Host:tools.hongkoala.com"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.docker.network=web"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.hongkoala-tools-node.server.port=3000"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.frontend.rateLimit.rateSet.rate=10"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.frontend.rateLimit.rateSet.burst=5"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.frontend.headers.customResponseHeaders=Content-Security-Policy:frame-ancestors&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-1&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-1&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-2&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-2&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-3&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-3&amp;gt;.tld"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After (Traefik v3 syntax):&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;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.docker.network=web"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.rule=Host(&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;`tools.hongkoala.com&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;`)"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.entrypoints=websecure"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.tls=true"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.tls.certresolver=letsencrypt"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.service=hongkoala-tools"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.hongkoala-tools.middlewares=hongkoala-tools-ratelimit,hongkoala-tools-headers"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.hongkoala-tools.loadbalancer.server.port=3000"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.hongkoala-tools-ratelimit.ratelimit.average=100"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.hongkoala-tools-ratelimit.ratelimit.period=1s"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.hongkoala-tools-ratelimit.ratelimit.burst=50"&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.hongkoala-tools-headers.headers.customResponseHeaders.Content-Security-Policy=frame-ancestors&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-1&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-1&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-2&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-2&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://&amp;lt;domain-3&amp;gt;.tld&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;https://*.&amp;lt;domain-3&amp;gt;.tld"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three structural changes are visible here: the frontend rule becomes a router rule using the &lt;code&gt;Host()\&lt;/code&gt; function syntax, rate limiting moves from a &lt;code&gt;frontend.rateLimit\&lt;/code&gt; block into a named, reusable middleware, and the CSP header moves from &lt;code&gt;customResponseHeaders\&lt;/code&gt; as a flat string into a middleware-scoped &lt;code&gt;customResponseHeaders.Content-Security-Policy\&lt;/code&gt; key.&lt;/p&gt;

&lt;p&gt;Doing this by hand across 70 stacks would have meant opening each docker-compose file individually, checking every label against the v3 documentation, and testing one at a time, since a malformed Traefik label usually fails silently instead of throwing an error. This is where AI accelerated the work rather than doing it: I fed each v1 block into a model along with the target v3 syntax as context, and it produced the translation shown above directly. That turned the rewrite into a review task across all 70 stacks instead of a manual, file-by-file rewrite, which is the direct reason the Traefik and Portainer upgrades took about one hour combined.&lt;/p&gt;

&lt;p&gt;ATTENTION : I have some websites with basic auth, It is strictly recommended to not feed AI LLM with code containing credential, and if it's already send, no panic, then we just need to change the credentials.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring Setup
&lt;/h2&gt;

&lt;p&gt;For a simple server like this a simple service monitoring is enough : A Google Apps Script checks every public site on a schedule, logs response time and status to a spreadsheet, and sends an alert email if a site is down or slow. IDs and email addresses below are replaced with placeholders.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;monitorWebsites&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// --- CONFIGURATION START (Values Kept) ---&lt;/span&gt;

  &lt;span class="c1"&gt;// 1. List of websites to monitor, separated by commas.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SITES_TO_MONITOR_STRING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://kahiether.com/,https://hery.kahiether.com/,https://nali.kahiether.com/,https://jiao.kahiether.com/,https://kizuna.kahiether.com/,https://shiro.kahiether.com/,https://mypa.kahiether.com/,https://resizer.kahiether.com/,https://mandika.kahiether.com/,https://mamaki.kahiether.com/,https://mpaka.kahiether.com/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. ID of the target Google Spreadsheet.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SPREADSHEET_ID&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;TO_CHANGE_SPREADSHEET_ID&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Name of the sheet (tab) within the spreadsheet where data will be stored.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SHEET_NAME&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xxxxxxxxxxxx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 4. Comma-separated list of email addresses for receiving alerts.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALERT_EMAILS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;TO_CHANGE_EMAIL_1&amp;gt;,&amp;lt;TO_CHANGE_EMAIL_2&amp;gt;&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// 5. Alert threshold for response time in milliseconds.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ALERT_THRESHOLD_MS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Example: 1 seconds&lt;/span&gt;

  &lt;span class="c1"&gt;// --- CONFIGURATION END ---&lt;/span&gt;
  &lt;span class="p"&gt;...........&lt;/span&gt;
  &lt;span class="c1"&gt;// --- Section Sites DOWN/Erreur ---&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;downAlerts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;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="nx"&gt;body&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`*** Les sites suivants sont HORS SERVICE ou ont signalé un code d'erreur (non-200) : ***\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
    downAlerts.forEach(alert =&amp;gt; {
      body += &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;- &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.siteUrl} | Erreur: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.detail} (Statut: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.value})\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
    });
    body += '\n';
  }

  // --- Section Problèmes de Performance ---
  if (perfAlerts.length &amp;gt; 0) {
    body += &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;*** Les sites suivants ont des problèmes de PERFORMANCE (Latence &amp;gt; &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{threshold} ms) : ***\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
    body += &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;(Le seuil d'alerte est fixé à &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{threshold} ms)\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
    perfAlerts.forEach(alert =&amp;gt; {
      body += &lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;- &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.siteUrl} | Temps de réponse: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.value} ms. (Vérification: &lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{alert.timestamp.toLocaleTimeString()})\n&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
    });
    body += '\n';
  }
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script checks every site in &lt;code&gt;SITES_TO_MONITOR_STRING\&lt;/code&gt;, logs each result to the &lt;code&gt;Monitoring_Log\&lt;/code&gt; sheet, and triggers two categories of alert: sites returning a non-200 status, and sites responding slower than &lt;code&gt;ALERT_THRESHOLD_MS\&lt;/code&gt; (1000 ms in this config).&lt;/p&gt;

&lt;h2&gt;
  
  
  Image Storage
&lt;/h2&gt;

&lt;p&gt;Once the main part of the migration was done, another problem appeared. Some docker hub images were moved between 2021-2022 and today into legacy by their provider, so it was necessary to find the location of the image and make a copy. Two separate things happen here(not be confused).&lt;/p&gt;

&lt;p&gt;Public or third-party base images (nginx, postgres, node, etc.) are mirrored into my Docker Hub account through a GitHub Actions workflow, rather than pulled directly from the public namespace at deploy time.&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mirror Docker Images&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mirror&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&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;Checkout repository&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;Login to Docker Hub&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_PASSWORD }}&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;Mirror images&lt;/span&gt;
        &lt;span class="na"&gt;shell&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bash&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DOCKERHUB_USERNAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DOCKER_USERNAME }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;set -Eeuo pipefail&lt;/span&gt;

          &lt;span class="s"&gt;images_file="${GITHUB_WORKSPACE}/images.txt"&lt;/span&gt;

          &lt;span class="s"&gt;if [[ ! -f "${images_file}" ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::images.txt not found"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;if [[ -z "${DOCKERHUB_USERNAME}" ]]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "::error::DOCKER_USERNAME is empty"&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;

          &lt;span class="s"&gt;docker buildx version&lt;/span&gt;

          &lt;span class="s"&gt;mirrored=0&lt;/span&gt;
          &lt;span class="s"&gt;failed=0&lt;/span&gt;

          &lt;span class="s"&gt;#&lt;/span&gt;
          &lt;span class="s"&gt;# Retry a command when a registry returns HTTP 429.&lt;/span&gt;
          &lt;span class="s"&gt;#&lt;/span&gt;
          &lt;span class="s"&gt;retry_on_rate_limit() {&lt;/span&gt;
            &lt;span class="s"&gt;local max_attempts=5&lt;/span&gt;
            &lt;span class="s"&gt;local attempt=1&lt;/span&gt;
            &lt;span class="s"&gt;local delay=15&lt;/span&gt;
            &lt;span class="s"&gt;local output_file&lt;/span&gt;
            &lt;span class="s"&gt;local status&lt;/span&gt;

            &lt;span class="s"&gt;output_file="$(mktemp)"&lt;/span&gt;

            &lt;span class="s"&gt;while true; do&lt;/span&gt;
              &lt;span class="s"&gt;if "$@" &amp;gt; &amp;gt;(tee "${output_file}") 2&amp;gt; &amp;gt;(tee -a "${output_file}" &amp;gt;&amp;amp;2); then&lt;/span&gt;
                &lt;span class="s"&gt;rm -f "${output_file}"&lt;/span&gt;
                &lt;span class="s"&gt;return 0&lt;/span&gt;
              &lt;span class="s"&gt;else&lt;/span&gt;
                &lt;span class="s"&gt;status=$?&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;

              &lt;span class="s"&gt;if ! grep -qiE '429|Too Many Requests' "${output_file}"; then&lt;/span&gt;
                &lt;span class="s"&gt;rm -f "${output_file}"&lt;/span&gt;
                &lt;span class="s"&gt;return "${status}"&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;

              &lt;span class="s"&gt;if (( attempt &amp;gt;= max_attempts )); then&lt;/span&gt;
                &lt;span class="s"&gt;echo "::error::Command failed after ${max_attempts} attempts: $*" &amp;gt;&amp;amp;2&lt;/span&gt;
                &lt;span class="s"&gt;rm -f "${output_file}"&lt;/span&gt;
                &lt;span class="s"&gt;return "${status}"&lt;/span&gt;
              &lt;span class="s"&gt;fi&lt;/span&gt;

              &lt;span class="s"&gt;echo "::warning::Registry rate limit encountered. Retrying in ${delay} seconds..." &amp;gt;&amp;amp;2&lt;/span&gt;

              &lt;span class="s"&gt;sleep "${delay}"&lt;/span&gt;

              &lt;span class="s"&gt;attempt=$((attempt + 1))&lt;/span&gt;
              &lt;span class="s"&gt;delay=$((delay * 2))&lt;/span&gt;

              &lt;span class="s"&gt;: &amp;gt; "${output_file}"&lt;/span&gt;
            &lt;span class="s"&gt;done&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;

          &lt;span class="s"&gt;#&lt;/span&gt;
          &lt;span class="s"&gt;# Copy a multi-platform image to Docker Hub.&lt;/span&gt;
          &lt;span class="s"&gt;#&lt;/span&gt;
          &lt;span class="s"&gt;copy_image() {&lt;/span&gt;
            &lt;span class="s"&gt;local source="$1"&lt;/span&gt;
            &lt;span class="s"&gt;local destination="$2"&lt;/span&gt;

            &lt;span class="s"&gt;docker buildx imagetools create \&lt;/span&gt;
              &lt;span class="s"&gt;--tag "${destination}" \&lt;/span&gt;
              &lt;span class="s"&gt;"${source}"&lt;/span&gt;
          &lt;span class="s"&gt;}&lt;/span&gt;

          &lt;span class="s"&gt;while IFS= read -r image || [[ -n "${image}" ]]; do&lt;/span&gt;
            &lt;span class="s"&gt;image="${image%$'\r'}"&lt;/span&gt;

            &lt;span class="s"&gt;# Trim leading and trailing whitespace.&lt;/span&gt;
            &lt;span class="s"&gt;image="${image#"${image%%[![:space:]]*}"}"&lt;/span&gt;
            &lt;span class="s"&gt;image="${image%"${image##*[![:space:]]}"}"&lt;/span&gt;

            &lt;span class="s"&gt;# Skip blank lines and comments.&lt;/span&gt;
            &lt;span class="s"&gt;[[ -z "${image}" || "${image}" == \#* ]] &amp;amp;&amp;amp; continue&lt;/span&gt;

            &lt;span class="s"&gt;first_component="${image%%/*}"&lt;/span&gt;

            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;# Resolve the source registry.&lt;/span&gt;
            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;if [[ "${image}" != */* ]]; then&lt;/span&gt;
              &lt;span class="s"&gt;registry="docker.io"&lt;/span&gt;
              &lt;span class="s"&gt;source="docker.io/library/${image}"&lt;/span&gt;
              &lt;span class="s"&gt;repository_reference="library/${image}"&lt;/span&gt;

            &lt;span class="s"&gt;elif [[ "${first_component}" == *.* ||&lt;/span&gt;
                    &lt;span class="s"&gt;"${first_component}" == *:* ||&lt;/span&gt;
                    &lt;span class="s"&gt;"${first_component}" == "localhost" ]]; then&lt;/span&gt;
              &lt;span class="s"&gt;registry="${first_component}"&lt;/span&gt;
              &lt;span class="s"&gt;source="${image}"&lt;/span&gt;
              &lt;span class="s"&gt;repository_reference="${image#*/}"&lt;/span&gt;

            &lt;span class="s"&gt;else&lt;/span&gt;
              &lt;span class="s"&gt;registry="docker.io"&lt;/span&gt;
              &lt;span class="s"&gt;source="docker.io/${image}"&lt;/span&gt;
              &lt;span class="s"&gt;repository_reference="${image}"&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;

            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;# Extract the repository and destination tag.&lt;/span&gt;
            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;tag="latest"&lt;/span&gt;

            &lt;span class="s"&gt;if [[ "${repository_reference}" == *@* ]]; then&lt;/span&gt;
              &lt;span class="s"&gt;repository="${repository_reference%%@*}"&lt;/span&gt;
              &lt;span class="s"&gt;digest="${repository_reference#*@}"&lt;/span&gt;
              &lt;span class="s"&gt;digest_value="${digest#sha256:}"&lt;/span&gt;
              &lt;span class="s"&gt;tag="digest-${digest_value:0:12}"&lt;/span&gt;

            &lt;span class="s"&gt;elif [[ "${repository_reference##*/}" == *:* ]]; then&lt;/span&gt;
              &lt;span class="s"&gt;repository="${repository_reference%:*}"&lt;/span&gt;
              &lt;span class="s"&gt;tag="${repository_reference##*:}"&lt;/span&gt;

            &lt;span class="s"&gt;else&lt;/span&gt;
              &lt;span class="s"&gt;repository="${repository_reference}"&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;

            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;# Encode the registry and full repository path into one valid&lt;/span&gt;
            &lt;span class="s"&gt;# Docker Hub repository name.&lt;/span&gt;
            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;destination_repository="${registry}--${repository}"&lt;/span&gt;
            &lt;span class="s"&gt;destination_repository="${destination_repository//\//--}"&lt;/span&gt;
            &lt;span class="s"&gt;destination_repository="${destination_repository,,}"&lt;/span&gt;

            &lt;span class="s"&gt;destination="docker.io/${DOCKERHUB_USERNAME}/${destination_repository}:${tag}"&lt;/span&gt;

            &lt;span class="s"&gt;echo&lt;/span&gt;
            &lt;span class="s"&gt;echo "Source      : ${source}"&lt;/span&gt;
            &lt;span class="s"&gt;echo "Destination : ${destination}"&lt;/span&gt;
            &lt;span class="s"&gt;echo "Mirroring image..."&lt;/span&gt;

            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;# Create or update the destination manifest.&lt;/span&gt;
            &lt;span class="s"&gt;# Existing blobs are reused by the registry.&lt;/span&gt;
            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;if retry_on_rate_limit \&lt;/span&gt;
              &lt;span class="s"&gt;copy_image "${source}" "${destination}"; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "Mirrored: ${destination}"&lt;/span&gt;
              &lt;span class="s"&gt;mirrored=$((mirrored + 1))&lt;/span&gt;
            &lt;span class="s"&gt;else&lt;/span&gt;
              &lt;span class="s"&gt;echo "::error::Failed to mirror ${source}"&lt;/span&gt;
              &lt;span class="s"&gt;failed=$((failed + 1))&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;

            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;# Reduce request bursts when processing consecutive images.&lt;/span&gt;
            &lt;span class="s"&gt;#&lt;/span&gt;
            &lt;span class="s"&gt;sleep 10&lt;/span&gt;

          &lt;span class="s"&gt;done &amp;lt; "${images_file}"&lt;/span&gt;

          &lt;span class="s"&gt;echo&lt;/span&gt;
          &lt;span class="s"&gt;echo "Mirrored: ${mirrored}"&lt;/span&gt;
          &lt;span class="s"&gt;echo "Failed:   ${failed}"&lt;/span&gt;

          &lt;span class="s"&gt;if (( failed &amp;gt; 0 )); then&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow is manually triggered, reads image names from &lt;code&gt;images.txt\&lt;/code&gt;, and uses &lt;code&gt;docker buildx imagetools create\&lt;/code&gt; to copy each manifest from the public namespace into the Docker Hub account under the same name, confirming each copy with &lt;code&gt;docker buildx imagetools inspect\&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The self made Kahiether applications are different: each one has its own Dockerfile and gets built and pushed from its own repository, not mirrored. &lt;/p&gt;

&lt;p&gt;These apps follow the same design constraints across the board, described here: &lt;a href="https://kahiether.com/serving-a-clear-single-purpose-kahiether-microservice/" rel="noopener noreferrer"&gt;https://kahiether.com/serving-a-clear-single-purpose-kahiether-microservice/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Concretely, each Kahiether service is kept intentionally small. A service is typically built from three main files, main.js, styles.js, and index.html, with no folders, and each file kept under  around 1,000 lines including spacing. On dependencies, the backend relies as possible on only two dependencies, Express and Node.js, while the front end uses zero dependencies and no bundler like Webpack or Vite. Resource footprint follows the same logic: each microservice is expected to run under 0.2 vCPU and under 125 MB of RAM. Every service is also offline-first by default, shipping with a service worker so files are stored locally and the service keeps working without an internet connection once installed.&lt;/p&gt;

&lt;p&gt;This was the good new, while all the applications :  PosgreSQL, mariaDB, Ghost, Wordpress, Wikijs, portainer, traefik, netstat, neko, MongoDB, n8n, node-red, openWebUi, Kizuna, ChangTan LLM, etc... fell down, all the Kahiether services (&lt;a href="https://hery.kahiether.com/" rel="noopener noreferrer"&gt;https://hery.kahiether.com/&lt;/a&gt;, &lt;a href="https://nali.kahiether.com/" rel="noopener noreferrer"&gt;https://nali.kahiether.com/&lt;/a&gt;, ..) were still functional on all devices. It was the perfect test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Isolation During the Outage, and an Earlier Lesson
&lt;/h2&gt;

&lt;p&gt;Not only the Kahiether app survived but all the data are stored client side, so they can come and burn the server anytime, all user can still use their app (This is a strategy that Firebase already applies for years ago, websites like Netflix and other also have very interesting offline capabilities like watching local movies). A direct result of an offline-first architecture decision made years earlier.&lt;/p&gt;

&lt;p&gt;Apps built under this approach are designed to work without depending on the server being reachable at that moment. During the outage, &lt;code&gt;hery.kahiether.com\&lt;/code&gt; stayed usable specifically because it follows this pattern: get the app once, keep it working offline, update only when a full update can complete safely. &lt;/p&gt;

&lt;p&gt;NOTE : That strategy is documented here, with the reasoning behind it going back to unreliable network conditions in Madagascar: &lt;a href="https://kahiether.com/get-once-always-work-update-network/" rel="noopener noreferrer"&gt;https://kahiether.com/get-once-always-work-update-network/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This was not the first time a security incident had shaped how the apps are built. Before this, a WordPress instance where we ran Digigasy on a shared hosting was compromised through a vulnerable plugin in an another WordPress in the same hosting, This brough the first step of the architecture Docker.&lt;/p&gt;

&lt;p&gt;Different attack, same underlying pattern: shared environments and outdated dependencies concentrate risk in places we do not always control.&lt;/p&gt;

&lt;p&gt;The offline-first, isolated approach behind Kahiether was built with that kind of risk in mind from the start, you can attack and destroy the server, the data and the application is not there it's client side.There are still a lot of improvements that can be done on this server, lets just remember that its not a data critical server, we don't store visitor data, only and mainly content.&lt;/p&gt;

&lt;p&gt;But the question that always came along was, AI was used to check the yaml, then to rewrite very quicky some labels with only replacing the domain name app by app. But that AI was online, so if it was down or inaccessible my the redeployment would have taken much more time. AI reduced the label rewrite across 70 stacks from a manual, documentation-checking task to a review task, as shown in the before/after example above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap up : Where AI LLM is Taking Us ?
&lt;/h2&gt;

&lt;p&gt;The bigger question is what this means for long term future. In my opinion,  the real bottleneck AI LLM Models faces today is not generation speed, or accuracy. It is : &lt;br&gt;
1/authentic data, since a model cannot reliably improve by training on its own generated output. We will always need people to provide this data.&lt;br&gt;
2/Offline capabilities : In remote area with no internet access and only local network with the present devices. &lt;/p&gt;

&lt;p&gt;Machines, Factories, Robots automated physical work. But the current LLM AI is automating some cognitive work, especially those who can be repeated in the same condition easily (like replace the domain  ".rule=Host(&lt;code&gt;hongkoala.com\&lt;/code&gt;)" by another and rewrite the full label.)&lt;br&gt;
Whether this changes our market dynamic depends on adaptation, the way it did when cars replaced horses for transport, and when electricity replaced  oil lamps.&lt;/p&gt;

&lt;p&gt;The tools change, the need for people who understand what the tools are doing, and who can supply the real context a model does not have on its own, does not disappear.&lt;/p&gt;

&lt;p&gt;AI did not run this migration. It removed the repetitive part of it, so the work went to understanding security risks, monitoring, and resource tuning instead of rewriting hundreds (or I think thousands) of labels from traefik v1 to v3.&lt;br&gt;
We might see soon the discussion about AI LLM powered autonomous devices&lt;/p&gt;

&lt;h2&gt;
  
  
  That's all.
&lt;/h2&gt;

&lt;p&gt;Happy to share the struggles and these experiments. A long journey about learning, building, innovating in the most challenging conditions 🚀 &lt;/p&gt;

&lt;p&gt;Read more: &lt;a href="https://medium.com/@randriamanivo/intrapreneur-innovator-journey-2026-building-innovative-products-and-accelerating-it-with-1de3a9d52bc1" rel="noopener noreferrer"&gt;https://medium.com/@randriamanivo/intrapreneur-innovator-journey-2026-building-innovative-products-and-accelerating-it-with-1de3a9d52bc1&lt;/a&gt;&lt;/p&gt;

</description>
      <category>kahiether</category>
      <category>portainer</category>
      <category>traefik</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
