<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Ut0pia</title>
    <link>https://blog.ut0pia.org/</link>
    <description></description>
    <pubDate>Sun, 05 Apr 2026 14:45:25 +0000</pubDate>
    <item>
      <title>YGGTorrent est mort. Longue vie à UTOPEER.</title>
      <link>https://blog.ut0pia.org/yggtorrent-est-mort</link>
      <description>&lt;![CDATA[Il y a quelques semaines, j&#39;ai voulu configurer YGG dans Prowlarr. Simple, non ? Pas vraiment.&#xA;&#xA;yggapi.eu, l&#39;API non-officielle qui faisait tourner la plupart des indexeurs Prowlarr/Jackett pour YGG, était morte avec le site original. Les gists GitHub qui traînent encore pointent vers des domaines fantômes. Les forums donnent des conseils pour un écosystème qui n&#39;existe plus. Et ygg.gratis, le &#34;nouveau&#34; YGG, répondait 404 sur tous les chemins d&#39;API que j&#39;essayais.&#xA;&#xA;C&#39;est là que j&#39;ai compris que je cherchais la mauvaise chose.&#xA;!--more--&#xA;Ce que ygg.gratis n&#39;est pas&#xA;&#xA;ygg.gratis n&#39;est pas un site de torrents avec une API de scraping. Ce n&#39;est pas non plus un clone de yggapi.eu avec un nouveau domaine.&#xA;&#xA;C&#39;est l&#39;interface publique d&#39;UTOPEER (ou U2P), un projet qui repose sur Nostr — un protocole de messages décentralisé. Les torrents sur ygg.gratis ne sont pas stockés dans une base de données centrale. Ils circulent comme des événements sur un réseau de relays.&#xA;&#xA;Pour comprendre pourquoi ça change tout, il faut deux minutes sur Nostr.&#xA;&#xA;Nostr en deux minutes (vraiment)&#xA;&#xA;Nostr, c&#39;est un protocole de publication décentralisé. Chaque contenu est un &#34;événement&#34; signé par une clé privée (comme une clé GPG), publié sur des &#34;relays&#34; — des serveurs WebSocket qui stockent et redistribuent ces événements.&#xA;&#xA;N&#39;importe qui peut lire, n&#39;importe qui peut publier. Les relays sont interchangeables. Si un relay ferme, les événements qu&#39;il hébergeait peuvent exister ailleurs. L&#39;identité d&#39;un auteur n&#39;est pas liée à un serveur.&#xA;&#xA;Le projet a surtout été utilisé pour les réseaux sociaux décentralisés (c&#39;est une alternative à Mastodon, dans l&#39;esprit). Mais le protocole est agnostique sur le contenu. Une spec appelée NIP-35 définit un format d&#39;événement spécifiquement pour les métadonnées de torrents : infohash, nom, taille, trackers, tout ça.&#xA;&#xA;UTOPEER publie les torrents YGG comme des événements NIP-35 sur un relay Nostr (relay.ygg.gratis). ygg.gratis est juste l&#39;interface web qui connecte à ce relay pour le rendre browsable.&#xA;&#xA;Pourquoi ça compte pour Prowlarr&#xA;&#xA;Le problème avec les APIs de scraping comme yggapi.eu, c&#39;est qu&#39;elles dépendaient d&#39;un site central. Quand le site ferme ou change sa structure HTML, l&#39;API meurt avec lui.&#xA;&#xA;Le modèle Nostr est différent. Le &#34;bot&#34; qui publie les torrents YGG est une clé publique. Tant que cette clé publie sur un relay accessible, n&#39;importe qui peut s&#39;y abonner et indexer le contenu. Le relay peut changer, le front-end peut changer, la clé publique reste.&#xA;&#xA;C&#39;est aussi là que ça devient plus compliqué pour l&#39;intégration Prowlarr : il n&#39;existe pas d&#39;endpoint Torznab public sur ygg.gratis. La spec le dit clairement — U2P expose une API Torznab, mais c&#39;est une instance que tu fais tourner toi-même, qui connecte le relay Nostr et te sert les résultats localement.&#xA;&#xA;L&#39;architecture complète&#xA;&#xA;relay.ygg.gratis (Nostr)&#xA;         ↓&#xA;Lighthouse          ← tu héberges ça chez toi&#xA;(indexeur local)&#xA;         ↓&#xA;      Prowlarr&#xA;         ↓&#xA;   Sonarr / Radarr&#xA;         ↓&#xA;    Transmission&#xA;&#xA;Lighthouse est un indexeur Nostr open-source qui se connecte à un relay, indexe les événements NIP-35 dans une base SQLite locale, et expose une API Torznab standard. Prowlarr le voit comme n&#39;importe quel autre indexeur.&#xA;&#xA;Installation&#xA;&#xA;Prérequis&#xA;&#xA;Docker et Docker Compose&#xA;Prowlarr déjà en place&#xA;&#xA;1. Créer les répertoires&#xA;&#xA;mkdir -p /data/lighthouse/data /data/lighthouse/config&#xA;chown -R 1000:1000 /data/lighthouse&#xA;&#xA;2. Créer le fichier de service&#xA;&#xA;Lighthouse n&#39;a pas d&#39;image Docker publique — il faut la builder depuis les sources. Docker le fait directement depuis GitHub :&#xA;&#xA;services/lighthouse.yml&#xA;services:&#xA;  lighthouse:&#xA;    build:&#xA;      context: https://github.com/gmonarque/lighthouse.git&#xA;      dockerfile: Dockerfile&#xA;    image: lighthouse:local&#xA;    containername: lighthouse&#xA;    environment:&#xA;      TZ=Europe/Paris&#xA;    volumes:&#xA;      /data/lighthouse/data:/app/data&#xA;      /data/lighthouse/config:/app/config&#xA;    networks:&#xA;      proxy-tier        # même réseau que Prowlarr&#xA;    restart: unless-stopped&#xA;    healthcheck:&#xA;      test: [&#34;CMD&#34;, &#34;wget&#34;, &#34;-q&#34;, &#34;--spider&#34;, &#34;http://localhost:9999/health&#34;]&#xA;      interval: 30s&#xA;      timeout: 10s&#xA;      retries: 3&#xA;      startperiod: 30s&#xA;&#xA;networks:&#xA;  proxy-tier:&#xA;    external: true&#xA;&#xA;Lighthouse est un service interne — il n&#39;a pas besoin d&#39;être exposé sur Internet. Prowlarr l&#39;appelle via le nom Docker lighthouse:9999.&#xA;&#xA;3. Créer la configuration&#xA;&#xA;/data/lighthouse/config/config.yaml&#xA;server:&#xA;  host: &#34;0.0.0.0&#34;&#xA;  port: 9999&#xA;&#xA;database:&#xA;  path: &#34;/app/data/lighthouse.db&#34;&#xA;&#xA;nostr:&#xA;  relays:&#xA;    url: &#34;wss://relay.ygg.gratis&#34;&#xA;      name: &#34;YGG Relay&#34;&#xA;      preset: &#34;private&#34;&#xA;      enabled: true&#xA;&#xA;trust:&#xA;  depth: 1&#xA;&#xA;enrichment:&#xA;  enabled: false&#xA;&#xA;4. Builder et démarrer&#xA;&#xA;docker compose -f docker-compose.yml -f services/lighthouse.yml \&#xA;  build lighthouse&#xA;&#xA;docker compose -f docker-compose.yml -f services/lighthouse.yml \&#xA;  up -d lighthouse&#xA;&#xA;Le build prend 3-5 minutes (Go + Node.js compilés depuis les sources).&#xA;&#xA;5. Compléter le setup via l&#39;API&#xA;&#xA;Au premier démarrage, Lighthouse attend qu&#39;une &#34;identité Nostr&#34; soit générée. Sans ça, il ne se connecte pas au relay. L&#39;interface web est accessible sur le port 9999, mais si Lighthouse est un service interne sans port exposé, voici comment le faire depuis la machine hôte :&#xA;&#xA;Récupérer l&#39;IP du conteneur&#xA;LIGHTHOUSEIP=$(docker inspect lighthouse \&#xA;  --format &#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;)&#xA;&#xA;Générer une identité Nostr&#xA;curl -s -X POST \&#xA;  &#34;http://$LIGHTHOUSEIP:9999/api/settings/identity/generate&#34; \&#xA;  -H &#34;Content-Type: application/json&#34; -d &#39;{}&#39;&#xA;&#xA;Valider le setup&#xA;curl -s -X POST \&#xA;  &#34;http://$LIGHTHOUSEIP:9999/api/setup/complete&#34; \&#xA;  -H &#34;Content-Type: application/json&#34; -d &#39;{}&#39;&#xA;&#xA;Récupère l&#39;API key générée au démarrage — elle apparaît dans les logs :&#xA;&#xA;docker logs lighthouse 2  &amp;1 | grep &#34;API Key&#34;&#xA;→ API Key (first 8 chars) apikey=...&#xA;&#xA;L&#39;API key complète est dans /data/lighthouse/config/config.yaml sous server.apikey.&#xA;&#xA;6. Connecter le relay et démarrer l&#39;indexeur&#xA;&#xA;APIKEY=&#34;ta-clé-complète&#34;&#xA;&#xA;Démarrer la connexion au relay&#xA;curl -s -X POST &#34;http://$LIGHTHOUSEIP:9999/api/relays/1/connect&#34; \&#xA;  -H &#34;X-Api-Key: $APIKEY&#34;&#xA;&#xA;Démarrer l&#39;indexeur&#xA;curl -s -X POST &#34;http://$LIGHTHOUSEIP:9999/api/indexer/start&#34; \&#xA;  -H &#34;X-Api-Key: $APIKEY&#34;&#xA;&#xA;À ce stade, docker logs lighthouse montre des torrents qui arrivent. Mais probablement zéro.&#xA;&#xA;7. Ajouter le bot YGG à la whitelist&#xA;&#xA;C&#39;est le truc que j&#39;ai mis du temps à comprendre. Lighthouse filtre les événements Nostr selon un système de &#34;Web of Trust&#34; — il n&#39;indexe que les torrents publiés par des comptes de confiance. Avec une identité fraîchement générée sans abonnements, la liste des comptes de confiance est vide.&#xA;&#xA;Le bot YGG a un seul pubkey sur le relay : 6aeb55064ea8b777591055e5704612e0e863fcc00bb211741781be299473c54e&#xA;&#xA;En npub : npub1dt442pjw4zmhwkgs2hjhq3sjur5x8lxqpwepzaqhsxlzn9rnc48q5ezkq2&#xA;&#xA;curl -s -X POST &#34;http://$LIGHTHOUSEIP:9999/api/trust/whitelist&#34; \&#xA;  -H &#34;X-Api-Key: $APIKEY&#34; \&#xA;  -H &#34;Content-Type: application/json&#34; \&#xA;  -d &#39;{&#xA;    &#34;npub&#34;: &#34;npub1dt442pjw4zmhwkgs2hjhq3sjur5x8lxqpwepzaqhsxlzn9rnc48q5ezkq2&#34;,&#xA;    &#34;alias&#34;: &#34;YGG Bot&#34;&#xA;  }&#39;&#xA;&#xA;Relancer l&#39;indexeur pour prendre en compte la whitelist&#xA;curl -s -X POST &#34;http://$LIGHTHOUSEIP:9999/api/indexer/stop&#34; \&#xA;  -H &#34;X-Api-Key: $APIKEY&#34;&#xA;curl -s -X POST &#34;http://$LIGHTHOUSEIP:9999/api/indexer/start&#34; \&#xA;  -H &#34;X-Api-Key: $APIKEY&#34;&#xA;&#xA;Les logs montrent maintenant les torrents qui s&#39;indexent.&#xA;&#xA;8. Ajouter dans Prowlarr&#xA;&#xA;Dans Prowlarr → Settings → Indexers → + Add → Generic Torznab :&#xA;&#xA;URL : http://lighthouse:9999&#xA;API Path : /api/torznab&#xA;API Key : ta clé Lighthouse&#xA;Categories : sélectionne ce qui t&#39;intéresse (Movies, TV, etc.)&#xA;&#xA;Clique sur Test — si tout est en place, Prowlarr valide la connexion. L&#39;indexeur se synchronise ensuite automatiquement avec Radarr et Sonarr.&#xA;&#xA;Ce qui fonctionne, ce qui ne fonctionne pas encore&#xA;&#xA;Le contenu sur le relay ygg.gratis au moment où j&#39;écris ça est clairement en phase de démarrage. Les 500 premiers événements indexés dans ma session étaient surtout des journaux PDF français, des BD et quelques films récents en FRENCH. Le catalogue est loin d&#39;égaler ce qu&#39;était YGGTorrent à son apogée.&#xA;&#xA;C&#39;est logique : la migration vers Nostr est récente. Les utilisateurs qui publiaient sur YGG doivent adopter le nouveau workflow. Le relay grossit progressivement.&#xA;&#xA;La recherche Torznab fonctionne. Les résultats remontaient dans Prowlarr sans problème lors de mes tests. Les métadonnées (taille, seeders) sont présentes sur les torrents bien formatés.&#xA;&#xA;Ce qui manque : un mécanisme pour découvrir automatiquement de nouveaux publishers de confiance sur le relay. Pour l&#39;instant c&#39;est manuel — si quelqu&#39;un d&#39;autre publie du contenu de qualité sur relay.ygg.gratis, il faut ajouter son npub à la whitelist à la main. La spec U2P mentionne un &#34;Import Servarr&#34; en cours de développement, qui devrait automatiser une partie de ça.&#xA;&#xA;Pourquoi j&#39;aime l&#39;idée même si c&#39;est encore chantier&#xA;&#xA;La fragilité des anciens systèmes était structurelle. yggapi.eu existait parce que quelqu&#39;un maintenait un scraper et un serveur. Quand cette personne a arrêté, tout est mort d&#39;un coup. Ce n&#39;est pas un reproche — c&#39;est juste ce que ça coûte de faire tourner une infrastructure comme ça gratuitement sur le long terme.&#xA;&#xA;Le modèle Nostr déplace la dépendance. Il n&#39;y a plus de scraper central à maintenir. Le contenu existe tant que des relays l&#39;hébergent et que des gens le publient. Lighthouse peut être remplacé par un autre client. Le relay peut migrer. La clé publique du bot YGG, elle, reste la même.&#xA;&#xA;Est-ce que ça va tenir ? Je ne sais pas. La décentralisation en pratique produit souvent des systèmes plus fragiles que promis, juste pour des raisons différentes. Mais l&#39;architecture est saine, et le fait qu&#39;UTOPEER soit open-source avec une implémentation Torznab complète change la dynamique par rapport aux solutions précédentes.&#xA;&#xA;Pour l&#39;instant ça tourne chez moi, Prowlarr remonte des résultats, et le relay grossit. C&#39;est suffisant pour que ce soit utile.&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Il y a quelques semaines, j&#39;ai voulu configurer YGG dans Prowlarr. Simple, non ? Pas vraiment.</p>

<p>yggapi.eu, l&#39;API non-officielle qui faisait tourner la plupart des indexeurs Prowlarr/Jackett pour YGG, était morte avec le site original. Les gists GitHub qui traînent encore pointent vers des domaines fantômes. Les forums donnent des conseils pour un écosystème qui n&#39;existe plus. Et ygg.gratis, le “nouveau” YGG, répondait 404 sur tous les chemins d&#39;API que j&#39;essayais.</p>

<p>C&#39;est là que j&#39;ai compris que je cherchais la mauvaise chose.
</p>

<h2 id="ce-que-ygg-gratis-n-est-pas">Ce que ygg.gratis n&#39;est pas</h2>

<p>ygg.gratis n&#39;est pas un site de torrents avec une API de scraping. Ce n&#39;est pas non plus un clone de yggapi.eu avec un nouveau domaine.</p>

<p>C&#39;est l&#39;interface publique d&#39;<strong>UTOPEER</strong> (ou U2P), un projet qui repose sur <a href="https://nostr.com/">Nostr</a> — un protocole de messages décentralisé. Les torrents sur ygg.gratis ne sont pas stockés dans une base de données centrale. Ils circulent comme des événements sur un réseau de relays.</p>

<p>Pour comprendre pourquoi ça change tout, il faut deux minutes sur Nostr.</p>

<h2 id="nostr-en-deux-minutes-vraiment">Nostr en deux minutes (vraiment)</h2>

<p>Nostr, c&#39;est un protocole de publication décentralisé. Chaque contenu est un “événement” signé par une clé privée (comme une clé GPG), publié sur des “relays” — des serveurs WebSocket qui stockent et redistribuent ces événements.</p>

<p>N&#39;importe qui peut lire, n&#39;importe qui peut publier. Les relays sont interchangeables. Si un relay ferme, les événements qu&#39;il hébergeait peuvent exister ailleurs. L&#39;identité d&#39;un auteur n&#39;est pas liée à un serveur.</p>

<p>Le projet a surtout été utilisé pour les réseaux sociaux décentralisés (c&#39;est une alternative à Mastodon, dans l&#39;esprit). Mais le protocole est agnostique sur le contenu. Une spec appelée <a href="https://github.com/nostr-protocol/nostr/blob/master/35.md">NIP-35</a> définit un format d&#39;événement spécifiquement pour les métadonnées de torrents : infohash, nom, taille, trackers, tout ça.</p>

<p>UTOPEER publie les torrents YGG comme des événements NIP-35 sur un relay Nostr (<code>relay.ygg.gratis</code>). ygg.gratis est juste l&#39;interface web qui connecte à ce relay pour le rendre browsable.</p>

<h2 id="pourquoi-ça-compte-pour-prowlarr">Pourquoi ça compte pour Prowlarr</h2>

<p>Le problème avec les APIs de scraping comme yggapi.eu, c&#39;est qu&#39;elles dépendaient d&#39;un site central. Quand le site ferme ou change sa structure HTML, l&#39;API meurt avec lui.</p>

<p>Le modèle Nostr est différent. Le “bot” qui publie les torrents YGG est une clé publique. Tant que cette clé publie sur un relay accessible, n&#39;importe qui peut s&#39;y abonner et indexer le contenu. Le relay peut changer, le front-end peut changer, la clé publique reste.</p>

<p>C&#39;est aussi là que ça devient plus compliqué pour l&#39;intégration Prowlarr : il n&#39;existe pas d&#39;endpoint Torznab public sur ygg.gratis. La spec le dit clairement — U2P expose une API Torznab, mais c&#39;est une instance que tu fais tourner toi-même, qui connecte le relay Nostr et te sert les résultats localement.</p>

<h2 id="l-architecture-complète">L&#39;architecture complète</h2>

<pre><code>relay.ygg.gratis (Nostr)
         ↓
Lighthouse          ← tu héberges ça chez toi
(indexeur local)
         ↓
      Prowlarr
         ↓
   Sonarr / Radarr
         ↓
    Transmission
</code></pre>

<p><a href="https://github.com/jblemee/lighthouse">Lighthouse</a> est un indexeur Nostr open-source qui se connecte à un relay, indexe les événements NIP-35 dans une base SQLite locale, et expose une API Torznab standard. Prowlarr le voit comme n&#39;importe quel autre indexeur.</p>

<h2 id="installation">Installation</h2>

<h3 id="prérequis">Prérequis</h3>
<ul><li>Docker et Docker Compose</li>
<li>Prowlarr déjà en place</li></ul>

<h3 id="1-créer-les-répertoires">1. Créer les répertoires</h3>

<pre><code class="language-bash">mkdir -p /data/lighthouse/data /data/lighthouse/config
chown -R 1000:1000 /data/lighthouse
</code></pre>

<h3 id="2-créer-le-fichier-de-service">2. Créer le fichier de service</h3>

<p>Lighthouse n&#39;a pas d&#39;image Docker publique — il faut la builder depuis les sources. Docker le fait directement depuis GitHub :</p>

<pre><code class="language-yaml"># services/lighthouse.yml
services:
  lighthouse:
    build:
      context: https://github.com/gmonarque/lighthouse.git
      dockerfile: Dockerfile
    image: lighthouse:local
    container_name: lighthouse
    environment:
      - TZ=Europe/Paris
    volumes:
      - /data/lighthouse/data:/app/data
      - /data/lighthouse/config:/app/config
    networks:
      - proxy-tier        # même réseau que Prowlarr
    restart: unless-stopped
    healthcheck:
      test: [&#34;CMD&#34;, &#34;wget&#34;, &#34;-q&#34;, &#34;--spider&#34;, &#34;http://localhost:9999/health&#34;]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 30s

networks:
  proxy-tier:
    external: true
</code></pre>

<p>Lighthouse est un service interne — il n&#39;a pas besoin d&#39;être exposé sur Internet. Prowlarr l&#39;appelle via le nom Docker <code>lighthouse:9999</code>.</p>

<h3 id="3-créer-la-configuration">3. Créer la configuration</h3>

<pre><code class="language-yaml"># /data/lighthouse/config/config.yaml
server:
  host: &#34;0.0.0.0&#34;
  port: 9999

database:
  path: &#34;/app/data/lighthouse.db&#34;

nostr:
  relays:
    - url: &#34;wss://relay.ygg.gratis&#34;
      name: &#34;YGG Relay&#34;
      preset: &#34;private&#34;
      enabled: true

trust:
  depth: 1

enrichment:
  enabled: false
</code></pre>

<h3 id="4-builder-et-démarrer">4. Builder et démarrer</h3>

<pre><code class="language-bash">docker compose -f docker-compose.yml -f services/lighthouse.yml \
  build lighthouse

docker compose -f docker-compose.yml -f services/lighthouse.yml \
  up -d lighthouse
</code></pre>

<p>Le build prend 3-5 minutes (Go + Node.js compilés depuis les sources).</p>

<h3 id="5-compléter-le-setup-via-l-api">5. Compléter le setup via l&#39;API</h3>

<p>Au premier démarrage, Lighthouse attend qu&#39;une “identité Nostr” soit générée. Sans ça, il ne se connecte pas au relay. L&#39;interface web est accessible sur le port 9999, mais si Lighthouse est un service interne sans port exposé, voici comment le faire depuis la machine hôte :</p>

<pre><code class="language-bash"># Récupérer l&#39;IP du conteneur
LIGHTHOUSE_IP=$(docker inspect lighthouse \
  --format &#39;{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}&#39;)

# Générer une identité Nostr
curl -s -X POST \
  &#34;http://$LIGHTHOUSE_IP:9999/api/settings/identity/generate&#34; \
  -H &#34;Content-Type: application/json&#34; -d &#39;{}&#39;

# Valider le setup
curl -s -X POST \
  &#34;http://$LIGHTHOUSE_IP:9999/api/setup/complete&#34; \
  -H &#34;Content-Type: application/json&#34; -d &#39;{}&#39;
</code></pre>

<p>Récupère l&#39;API key générée au démarrage — elle apparaît dans les logs :</p>

<pre><code class="language-bash">docker logs lighthouse 2&gt;&amp;1 | grep &#34;API Key&#34;
# → API Key (first 8 chars) api_key=...
</code></pre>

<p>L&#39;API key complète est dans <code>/data/lighthouse/config/config.yaml</code> sous <code>server.api_key</code>.</p>

<h3 id="6-connecter-le-relay-et-démarrer-l-indexeur">6. Connecter le relay et démarrer l&#39;indexeur</h3>

<pre><code class="language-bash">API_KEY=&#34;&lt;ta-clé-complète&gt;&#34;

# Démarrer la connexion au relay
curl -s -X POST &#34;http://$LIGHTHOUSE_IP:9999/api/relays/1/connect&#34; \
  -H &#34;X-Api-Key: $API_KEY&#34;

# Démarrer l&#39;indexeur
curl -s -X POST &#34;http://$LIGHTHOUSE_IP:9999/api/indexer/start&#34; \
  -H &#34;X-Api-Key: $API_KEY&#34;
</code></pre>

<p>À ce stade, <code>docker logs lighthouse</code> montre des torrents qui arrivent. Mais probablement zéro.</p>

<h3 id="7-ajouter-le-bot-ygg-à-la-whitelist">7. Ajouter le bot YGG à la whitelist</h3>

<p>C&#39;est le truc que j&#39;ai mis du temps à comprendre. Lighthouse filtre les événements Nostr selon un système de “Web of Trust” — il n&#39;indexe que les torrents publiés par des comptes de confiance. Avec une identité fraîchement générée sans abonnements, la liste des comptes de confiance est vide.</p>

<p>Le bot YGG a un seul pubkey sur le relay : <code>6aeb55064ea8b777591055e5704612e0e863fcc00bb211741781be299473c54e</code></p>

<p>En npub : <code>npub1dt442pjw4zmhwkgs2hjhq3sjur5x8lxqpwepzaqhsxlzn9rnc48q5ezkq2</code></p>

<pre><code class="language-bash">curl -s -X POST &#34;http://$LIGHTHOUSE_IP:9999/api/trust/whitelist&#34; \
  -H &#34;X-Api-Key: $API_KEY&#34; \
  -H &#34;Content-Type: application/json&#34; \
  -d &#39;{
    &#34;npub&#34;: &#34;npub1dt442pjw4zmhwkgs2hjhq3sjur5x8lxqpwepzaqhsxlzn9rnc48q5ezkq2&#34;,
    &#34;alias&#34;: &#34;YGG Bot&#34;
  }&#39;

# Relancer l&#39;indexeur pour prendre en compte la whitelist
curl -s -X POST &#34;http://$LIGHTHOUSE_IP:9999/api/indexer/stop&#34; \
  -H &#34;X-Api-Key: $API_KEY&#34;
curl -s -X POST &#34;http://$LIGHTHOUSE_IP:9999/api/indexer/start&#34; \
  -H &#34;X-Api-Key: $API_KEY&#34;
</code></pre>

<p>Les logs montrent maintenant les torrents qui s&#39;indexent.</p>

<h3 id="8-ajouter-dans-prowlarr">8. Ajouter dans Prowlarr</h3>

<p>Dans Prowlarr → Settings → Indexers → + Add → Generic Torznab :</p>
<ul><li><strong>URL</strong> : <code>http://lighthouse:9999</code></li>
<li><strong>API Path</strong> : <code>/api/torznab</code></li>
<li><strong>API Key</strong> : ta clé Lighthouse</li>
<li><strong>Categories</strong> : sélectionne ce qui t&#39;intéresse (Movies, TV, etc.)</li></ul>

<p>Clique sur Test — si tout est en place, Prowlarr valide la connexion. L&#39;indexeur se synchronise ensuite automatiquement avec Radarr et Sonarr.</p>

<h2 id="ce-qui-fonctionne-ce-qui-ne-fonctionne-pas-encore">Ce qui fonctionne, ce qui ne fonctionne pas encore</h2>

<p>Le contenu sur le relay ygg.gratis au moment où j&#39;écris ça est clairement en phase de démarrage. Les 500 premiers événements indexés dans ma session étaient surtout des journaux PDF français, des BD et quelques films récents en FRENCH. Le catalogue est loin d&#39;égaler ce qu&#39;était YGGTorrent à son apogée.</p>

<p>C&#39;est logique : la migration vers Nostr est récente. Les utilisateurs qui publiaient sur YGG doivent adopter le nouveau workflow. Le relay grossit progressivement.</p>

<p>La recherche Torznab fonctionne. Les résultats remontaient dans Prowlarr sans problème lors de mes tests. Les métadonnées (taille, seeders) sont présentes sur les torrents bien formatés.</p>

<p>Ce qui manque : un mécanisme pour découvrir automatiquement de nouveaux publishers de confiance sur le relay. Pour l&#39;instant c&#39;est manuel — si quelqu&#39;un d&#39;autre publie du contenu de qualité sur relay.ygg.gratis, il faut ajouter son npub à la whitelist à la main. La spec U2P mentionne un “Import Servarr” en cours de développement, qui devrait automatiser une partie de ça.</p>

<h2 id="pourquoi-j-aime-l-idée-même-si-c-est-encore-chantier">Pourquoi j&#39;aime l&#39;idée même si c&#39;est encore chantier</h2>

<p>La fragilité des anciens systèmes était structurelle. yggapi.eu existait parce que quelqu&#39;un maintenait un scraper et un serveur. Quand cette personne a arrêté, tout est mort d&#39;un coup. Ce n&#39;est pas un reproche — c&#39;est juste ce que ça coûte de faire tourner une infrastructure comme ça gratuitement sur le long terme.</p>

<p>Le modèle Nostr déplace la dépendance. Il n&#39;y a plus de scraper central à maintenir. Le contenu existe tant que des relays l&#39;hébergent et que des gens le publient. Lighthouse peut être remplacé par un autre client. Le relay peut migrer. La clé publique du bot YGG, elle, reste la même.</p>

<p>Est-ce que ça va tenir ? Je ne sais pas. La décentralisation en pratique produit souvent des systèmes plus fragiles que promis, juste pour des raisons différentes. Mais l&#39;architecture est saine, et le fait qu&#39;UTOPEER soit open-source avec une implémentation Torznab complète change la dynamique par rapport aux solutions précédentes.</p>

<p>Pour l&#39;instant ça tourne chez moi, Prowlarr remonte des résultats, et le relay grossit. C&#39;est suffisant pour que ce soit utile.</p>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/yggtorrent-est-mort</guid>
      <pubDate>Wed, 04 Mar 2026 16:02:42 +0000</pubDate>
    </item>
    <item>
      <title>Quand les IA apprennent de leurs erreurs (et qu&#39;Opus joue dans une autre ligue)</title>
      <link>https://blog.ut0pia.org/quand-les-ia-apprennent-de-leurs-erreurs-et-quopus-joue-dans-une-autre-ligue</link>
      <description>&lt;![CDATA[Dans le premier article, on avait lâché neuf LLM dans un monde Minecraft. Ils écrivaient du JavaScript, mouraient beaucoup, et parfois ils survivaient. Charlie avait accumulé 14 outils. Alice avait du fer. Frank mourrait encore.&#xA;&#xA;Depuis, tout a changé. Pas parce que les agents sont devenus plus intelligents mais parce qu&#39;on a arrêté de les laisser mourir bêtement.&#xA;!--more--&#xA;Le problème : les agents meurent plus vite qu&#39;ils ne pensent&#xA;&#xA;Le constat était brutal. Un cycle LLM, le temps que le modèle reçoive l&#39;état du monde, réfléchisse, écrive du JavaScript, et que le bot l&#39;exécute, prend entre 30 secondes et 3 minutes. Pendant ce temps, le monde Minecraft continue. Les zombies attaquent. La faim baisse. Le creeper explose.&#xA;&#xA;Un agent affamé a beau avoir le plan parfait pour crafter une pioche, il meurt de faim avant que le LLM ait fini d&#39;écrire la première ligne de code.&#xA;&#xA;La solution : arrêter de tout confier au LLM.&#xA;&#xA;Réflexes et stratégie&#xA;&#xA;Un humain qui joue à Minecraft ne réfléchit pas avant de manger. Il ne calcule pas s&#39;il doit fuir un creeper. Ces gestes sont automatiques, du système 1, dirait Kahneman. Le cortex préfrontal, lui, s&#39;occupe de décider où construire la base, quels minerais chercher, comment organiser le coffre.&#xA;&#xA;Nos agents n&#39;avaient que du système 2. Tout passait par le LLM : manger, fuir, miner, planifier. Comme un conducteur débutant qui pense consciemment à chaque mouvement du volant. Ça marche, mais c&#39;est lent, et à la moindre surprise on cale.&#xA;&#xA;On a donc séparé les deux.&#xA;&#xA;Le bot Node.js tourne en permanence et gère les réflexes. Toutes les 5 secondes, il vérifie la faim -- sous 14/20, il mange ce qu&#39;il trouve. Quand il prend des dégâts d&#39;un mob, il réagit dans la seconde : il attaque s&#39;il peut, fuit sinon. Et en dernier recours, la nuit, HP bas, pas d&#39;arme, mob proche, il creuse et se couvre. Le LLM n&#39;est même pas au courant que ça s&#39;est passé.&#xA;&#xA;Le LLM, lui, garde le contrôle de la stratégie. Il planifie les chaînes de crafting, décide où miner, coordonne avec les autres. Mais il n&#39;a plus besoin de penser à manger ou à fuir. Comme un conducteur expérimenté : les réflexes gèrent la pédale et le volant, le cerveau se concentre sur l&#39;itinéraire.&#xA;&#xA;Des outils au lieu de connaissances brutes&#xA;&#xA;L&#39;ancien système injectait des pages de documentation Mineflayer dans le prompt. Le LLM devait lire, comprendre, puis écrire du JavaScript brut. Ça marchait, parfois. Souvent ça donnait des Vec3 is not defined ou des timeouts.&#xA;&#xA;C&#39;est un peu comme donner un manuel de mécanique à quelqu&#39;un et lui demander de changer une roue. Ça va marcher, mais ce sera long et il y aura des erreurs. Mieux vaut lui donner une clé en croix et dire &#34;dévisse les boulons&#34;.&#xA;&#xA;On a remplacé les pages de doc par des outils partagés. Chaque outil a un nom, une description, des prérequis, et ce qu&#39;il produit :&#xA;&#xA;mine -- Mine N blocks of a given type.&#xA;  Requires: Pickaxe for stone/ore, axe for wood&#xA;  Provides: Mined blocks in inventory&#xA;&#xA;Le LLM ne voit plus du code à imiter, il voit un catalogue d&#39;outils avec leurs prérequis. Au lieu d&#39;écrire 50 lignes de Mineflayer brut, l&#39;agent écrit :&#xA;&#xA;await tools.mine({ block: &#39;stone&#39;, count: 11 })&#xA;await tools.craft({ item: &#39;furnace&#39;, count: 1 })&#xA;await tools.smelt({ item: &#39;rawiron&#39;, count: 3 })&#xA;&#xA;Trois lignes. Plus de Vec3 is not defined à 3h du matin.&#xA;&#xA;C&#39;est la même logique que les réflexes, mais à un autre niveau. Les réflexes sont de la mémoire procédurale, le corps sait comment faire sans y penser. Les outils sont de la mémoire sémantique, on sait que ça existe et ce que ça fait, sans connaître les détails. Le LLM n&#39;a besoin que de la mémoire épisodique : se souvenir de ce qui s&#39;est passé et décider quoi faire ensuite.&#xA;&#xA;Les bugs qu&#39;on ne voit qu&#39;en regardant jouer&#xA;&#xA;La théorie c&#39;est bien. Mais les vrais problèmes, on ne les trouve qu&#39;assis devant le jeu, en regardant un bot faire n&#39;importe quoi.&#xA;&#xA;Eve a attaqué un objet au sol et s&#39;est fait kick du serveur. Ses réflexes ne faisaient pas la différence entre un zombie et un bout de bois -- comme un chat qui bondit sur une chaussette. Grace a essayé de couper 8 arbres, échoué 8 fois sur le même arbre inaccessible, et déclaré forfait. Comme quelqu&#39;un qui pousse une porte marquée &#34;tirez&#34;, encore et encore. Alice a creusé un trou et s&#39;est retrouvée plus haut qu&#39;avant: le code creusait tous les blocs, mais le bot ne tombait pas dans le trou.&#xA;&#xA;Des bugs bêtes. Mais des bugs qu&#39;aucun test unitaire ne trouve. Il faut regarder l&#39;agent jouer, en temps réel, pour voir qu&#39;il s&#39;acharne sur le mauvais arbre ou qu&#39;il tape sur un objet inerte.&#xA;&#xA;La grande comparaison : GLM-4.7 vs GLM-5 vs Sonnet vs Opus&#xA;&#xA;Six agents en parallèle. Trois sur Claude Sonnet 4.6, trois sur GLM-5. Puis un agent seul sur Claude Opus 4.6. Même monde, même spawn, même system prompt.&#xA;&#xA;GLM-4.7 : le stagiaire zélé&#xA;&#xA;Le modèle par défaut, le moins cher. Il écrit du code, beaucoup de code. Trop de code. Il génère du JavaScript brut avec des variables non définies, oublie les await, et écrit parfois quatre fois dans le même fichier. Chaque écriture écrase la précédente.&#xA;&#xA;C&#39;est l&#39;enfant qui lève la main avant que le prof ait fini la question. Plein d&#39;énergie, pas assez de réflexion.&#xA;&#xA;Taux de survie au premier cycle : ~50%.&#xA;&#xA;GLM-5 : le stagiaire qui a appris&#xA;&#xA;Nettement mieux. Il structure ses actions, utilise les outils correctement, et panique moins. Mais quand quelque chose échoue, il essaie de coder la solution lui-même au lieu d&#39;utiliser un autre outil. Et là, ça casse.&#xA;&#xA;Charlie (GLM-5) est resté coincé pendant 3 cycles complets dans son abri, incapable de placer un furnace. Il avait creusé un espace tellement étroit qu&#39;il n&#39;y avait pas de surface libre pour poser quoi que ce soit. Pas la présence d&#39;esprit de creuser un bloc de plus.&#xA;&#xA;Taux de survie au premier cycle : ~70%.&#xA;&#xA;Claude Sonnet 4.6 : le planificateur&#xA;&#xA;Sonnet réfléchit. Peut-être trop. Il scanne, re-scanne, propose une base dans le chat, re-scanne, propose une autre base. La moitié de son cycle passe en communication.&#xA;&#xA;Mais quand il agit, c&#39;est solide. Bob a récolté 21 fers en un cycle et demi. Alice avait une pioche en fer et un plan de ferme avant la fin de la première nuit. Dave a navigué jusqu&#39;à y=32 pour trouver du fer, la profondeur parfaite pour ça..&#xA;&#xA;Le vrai avantage de Sonnet, c&#39;est qu&#39;il est social. Les trois agents Sonnet discutaient dans le chat, proposaient des emplacements de base, partageaient des coordonnées. Les GLM-5 travaillaient chacun dans leur coin. Comme la différence entre un open space et des bureaux fermés.&#xA;&#xA;Taux de survie au premier cycle : ~90%.&#xA;&#xA;Claude Opus 4.6 : le joueur&#xA;&#xA;Puis on a lancé Frank sur Opus. Avec --effort low pour ne pas attendre 3 minutes par action.&#xA;&#xA;La différence saute aux yeux. Première action, premier cycle :&#xA;&#xA;[THINK] Good start -- got wood, bread, and a sword.&#xA;        Time to upgrade tools.&#xA;&#xA;bot.whisper(&#39;Plus200&#39;, &#39;Salut ! Je bosse, je me fais des outils.&#39;);&#xA;await tools.craft({ item: &#39;acaciaplanks&#39;, count: 8 });&#xA;await tools.craft({ item: &#39;stick&#39;, count: 4 });&#xA;await tools.craft({ item: &#39;wooden_pickaxe&#39;, count: 1 });&#xA;await tools.mine({ block: &#39;stone&#39;, count: 11 });&#xA;&#xA;Une seule action. Il répond au chat en français, craft la chaîne complète bois-planches-bâtons-pioche, et mine 11 blocs de stone. Sonnet fait ça en 2-3 actions. GLM-5 en 4-5.&#xA;&#xA;Action suivante : il détecte le crépuscule, s&#39;abrite immédiatement, puis mine du stone pendant la nuit.&#xA;&#xA;Frank n&#39;a pas juste survécu. Il a déroulé un plan sans accrocs, comme s&#39;il avait déjà joué à Minecraft avant. C&#39;est la différence entre quelqu&#39;un qui connaît les règles d&#39;un jeu et quelqu&#39;un qui sait y jouer.&#xA;&#xA;Taux de survie au premier cycle : 100% (échantillon de 1, certes).&#xA;&#xA;Le tableau&#xA;&#xA;| Modèle | Coût relatif | Actions/cycle utiles | Erreurs de code | Coordination | Survie cycle 1 |&#xA;|--------|-------------|---------------------|-----------------|-------------|----------------|&#xA;| GLM-4.7 | 1x | 1-2 sur 5 | Fréquentes | Aucune | ~50% |&#xA;| GLM-5 | ~2x | 3-4 sur 5 | Occasionnelles | Faible | ~70% |&#xA;| Sonnet 4.6 | ~10x | 4-5 sur 5 | Rares | Forte | ~90% |&#xA;| Opus 4.6 | ~30x | 5 sur 5 | Aucune observée | N/A (solo) | ~100% |&#xA;&#xA;Sonnet a le meilleur ratio coût/efficacité. Opus est bluffant mais 30 fois plus cher. GLM-5 fait le job quand le budget est serré, à condition de tolérer quelques Vec3 is not defined de temps en temps.&#xA;&#xA;Ce qu&#39;on a appris&#xA;&#xA;Les réflexes ont tout changé. Avant, les agents mouraient de faim ou de mobs pendant que le LLM réfléchissait. Maintenant le taux de survie est passé de ~30% à ~80%. Le parallèle avec la cognition humaine n&#39;est pas juste une métaphore, c&#39;est un vrai principe d&#39;architecture. Séparer ce qui demande de la réflexion de ce qui n&#39;en demande pas, c&#39;est ce que fait chaque organisme vivant. Un lézard n&#39;a pas besoin d&#39;un cortex pour fuir un prédateur. Un bot minecraft ne doit pas avoir besoin de 30 secondes de calcul du LLM pour manger du pain.&#xA;&#xA;Les outils, eux, ont tué la catégorie d&#39;erreurs la plus fréquente. Le LLM n&#39;écrit plus de Mineflayer brut. Il manipule des abstractions. Et comme pour les humains, le niveau d&#39;abstraction auquel on pense détermine la complexité de ce qu&#39;on peut accomplir. Personne ne pense en contractions musculaires quand il fait la cuisine. L&#39;agent ne pense plus en Vec3 quand il mine du fer.&#xA;&#xA;Ce qui est intéressant, c&#39;est à quel point le modèle de langage change le &#34;caractère&#34; de l&#39;agent. Les GLM sont des travailleurs solitaires. Les Sonnet sont des bavards organisés. Opus a une aisance qui ressemble presque à de l&#39;intuition. Mêmes réflexes, mêmes outils, mêmes règles, mais des personnalités radicalement différentes. Comme si le poids du modèle changeait le tempérament.&#xA;&#xA;La question qui reste&#xA;&#xA;L&#39;architecture tient. Les agents survivent, minent, craftent, et certains communiquent. Ils se planquent la nuit et ressortent à l&#39;aube.&#xA;&#xA;Mais une chose manque encore : la collaboration réelle. Les agents parlent de construire une base commune. Ils proposent des coordonnées. Parfois, un agent se déplace même vers le point proposé. Mais personne ne pose le premier bloc ensemble. Personne ne dit &#34;je m&#39;occupe du fer, toi du bois&#34;. Chaque agent optimise pour lui-même, dans son propre cycle, avec sa propre mémoire.&#xA;&#xA;On pourrait ajouter un système de tâches partagées. Mais ce serait tricher, un peu. Le but n&#39;est pas de construire un système multi-agent optimal. Le but est de voir ce qui émerge quand on laisse des LLM se débrouiller.&#xA;&#xA;Et ce qui émerge, pour l&#39;instant, c&#39;est neuf individualistes qui parlent de coopérer sans jamais vraiment coopérer. Ça ressemble étrangement à un serveur Minecraft classique (ou à un open space)&#xA;&#xA;Sources&#xA;&#xA;mc-agents sur GitHub&#xA;Mineflayer&#xA;Claude Code CLI&#xA;GLM-4 / BigModel&#xA;Premier article&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Dans le premier article, on avait lâché neuf LLM dans un monde Minecraft. Ils écrivaient du JavaScript, mouraient beaucoup, et parfois ils survivaient. Charlie avait accumulé 14 outils. Alice avait du fer. Frank mourrait encore.</p>

<p>Depuis, tout a changé. Pas parce que les agents sont devenus plus intelligents mais parce qu&#39;on a arrêté de les laisser mourir bêtement.
</p>

<h2 id="le-problème-les-agents-meurent-plus-vite-qu-ils-ne-pensent">Le problème : les agents meurent plus vite qu&#39;ils ne pensent</h2>

<p>Le constat était brutal. Un cycle LLM, le temps que le modèle reçoive l&#39;état du monde, réfléchisse, écrive du JavaScript, et que le bot l&#39;exécute, prend entre 30 secondes et 3 minutes. Pendant ce temps, le monde Minecraft continue. Les zombies attaquent. La faim baisse. Le creeper explose.</p>

<p>Un agent affamé a beau avoir le plan parfait pour crafter une pioche, il meurt de faim avant que le LLM ait fini d&#39;écrire la première ligne de code.</p>

<p>La solution : arrêter de tout confier au LLM.</p>

<h2 id="réflexes-et-stratégie">Réflexes et stratégie</h2>

<p>Un humain qui joue à Minecraft ne réfléchit pas avant de manger. Il ne calcule pas s&#39;il doit fuir un creeper. Ces gestes sont automatiques, du système 1, dirait Kahneman. Le cortex préfrontal, lui, s&#39;occupe de décider où construire la base, quels minerais chercher, comment organiser le coffre.</p>

<p>Nos agents n&#39;avaient que du système 2. Tout passait par le LLM : manger, fuir, miner, planifier. Comme un conducteur débutant qui pense consciemment à chaque mouvement du volant. Ça marche, mais c&#39;est lent, et à la moindre surprise on cale.</p>

<p>On a donc séparé les deux.</p>

<p>Le bot Node.js tourne en permanence et gère les réflexes. Toutes les 5 secondes, il vérifie la faim — sous 14/20, il mange ce qu&#39;il trouve. Quand il prend des dégâts d&#39;un mob, il réagit dans la seconde : il attaque s&#39;il peut, fuit sinon. Et en dernier recours, la nuit, HP bas, pas d&#39;arme, mob proche, il creuse et se couvre. Le LLM n&#39;est même pas au courant que ça s&#39;est passé.</p>

<p>Le LLM, lui, garde le contrôle de la stratégie. Il planifie les chaînes de crafting, décide où miner, coordonne avec les autres. Mais il n&#39;a plus besoin de penser à manger ou à fuir. Comme un conducteur expérimenté : les réflexes gèrent la pédale et le volant, le cerveau se concentre sur l&#39;itinéraire.</p>

<h2 id="des-outils-au-lieu-de-connaissances-brutes">Des outils au lieu de connaissances brutes</h2>

<p>L&#39;ancien système injectait des pages de documentation Mineflayer dans le prompt. Le LLM devait lire, comprendre, puis écrire du JavaScript brut. Ça marchait, parfois. Souvent ça donnait des <code>Vec3 is not defined</code> ou des timeouts.</p>

<p>C&#39;est un peu comme donner un manuel de mécanique à quelqu&#39;un et lui demander de changer une roue. Ça va marcher, mais ce sera long et il y aura des erreurs. Mieux vaut lui donner une clé en croix et dire “dévisse les boulons”.</p>

<p>On a remplacé les pages de doc par des outils partagés. Chaque outil a un nom, une description, des prérequis, et ce qu&#39;il produit :</p>

<pre><code class="language-text">mine -- Mine N blocks of a given type.
  Requires: Pickaxe for stone/ore, axe for wood
  Provides: Mined blocks in inventory
</code></pre>

<p>Le LLM ne voit plus du code à imiter, il voit un catalogue d&#39;outils avec leurs prérequis. Au lieu d&#39;écrire 50 lignes de Mineflayer brut, l&#39;agent écrit :</p>

<pre><code class="language-javascript">await tools.mine({ block: &#39;stone&#39;, count: 11 })
await tools.craft({ item: &#39;furnace&#39;, count: 1 })
await tools.smelt({ item: &#39;raw_iron&#39;, count: 3 })
</code></pre>

<p>Trois lignes. Plus de <code>Vec3 is not defined</code> à 3h du matin.</p>

<p>C&#39;est la même logique que les réflexes, mais à un autre niveau. Les réflexes sont de la mémoire procédurale, le corps sait comment faire sans y penser. Les outils sont de la mémoire sémantique, on sait que ça existe et ce que ça fait, sans connaître les détails. Le LLM n&#39;a besoin que de la mémoire épisodique : se souvenir de ce qui s&#39;est passé et décider quoi faire ensuite.</p>

<h2 id="les-bugs-qu-on-ne-voit-qu-en-regardant-jouer">Les bugs qu&#39;on ne voit qu&#39;en regardant jouer</h2>

<p>La théorie c&#39;est bien. Mais les vrais problèmes, on ne les trouve qu&#39;assis devant le jeu, en regardant un bot faire n&#39;importe quoi.</p>

<p>Eve a attaqué un objet au sol et s&#39;est fait kick du serveur. Ses réflexes ne faisaient pas la différence entre un zombie et un bout de bois — comme un chat qui bondit sur une chaussette. Grace a essayé de couper 8 arbres, échoué 8 fois sur le même arbre inaccessible, et déclaré forfait. Comme quelqu&#39;un qui pousse une porte marquée “tirez”, encore et encore. Alice a creusé un trou et s&#39;est retrouvée <em>plus haut</em> qu&#39;avant: le code creusait tous les blocs, mais le bot ne tombait pas dans le trou.</p>

<p>Des bugs bêtes. Mais des bugs qu&#39;aucun test unitaire ne trouve. Il faut regarder l&#39;agent jouer, en temps réel, pour voir qu&#39;il s&#39;acharne sur le mauvais arbre ou qu&#39;il tape sur un objet inerte.</p>

<h2 id="la-grande-comparaison-glm-4-7-vs-glm-5-vs-sonnet-vs-opus">La grande comparaison : GLM-4.7 vs GLM-5 vs Sonnet vs Opus</h2>

<p>Six agents en parallèle. Trois sur Claude Sonnet 4.6, trois sur GLM-5. Puis un agent seul sur Claude Opus 4.6. Même monde, même spawn, même system prompt.</p>

<h3 id="glm-4-7-le-stagiaire-zélé">GLM-4.7 : le stagiaire zélé</h3>

<p>Le modèle par défaut, le moins cher. Il écrit du code, beaucoup de code. Trop de code. Il génère du JavaScript brut avec des variables non définies, oublie les <code>await</code>, et écrit parfois quatre fois dans le même fichier. Chaque écriture écrase la précédente.</p>

<p>C&#39;est l&#39;enfant qui lève la main avant que le prof ait fini la question. Plein d&#39;énergie, pas assez de réflexion.</p>

<p>Taux de survie au premier cycle : ~50%.</p>

<h3 id="glm-5-le-stagiaire-qui-a-appris">GLM-5 : le stagiaire qui a appris</h3>

<p>Nettement mieux. Il structure ses actions, utilise les outils correctement, et panique moins. Mais quand quelque chose échoue, il essaie de coder la solution lui-même au lieu d&#39;utiliser un autre outil. Et là, ça casse.</p>

<p>Charlie (GLM-5) est resté coincé pendant 3 cycles complets dans son abri, incapable de placer un furnace. Il avait creusé un espace tellement étroit qu&#39;il n&#39;y avait pas de surface libre pour poser quoi que ce soit. Pas la présence d&#39;esprit de creuser un bloc de plus.</p>

<p>Taux de survie au premier cycle : ~70%.</p>

<h3 id="claude-sonnet-4-6-le-planificateur">Claude Sonnet 4.6 : le planificateur</h3>

<p>Sonnet réfléchit. Peut-être trop. Il scanne, re-scanne, propose une base dans le chat, re-scanne, propose une autre base. La moitié de son cycle passe en communication.</p>

<p>Mais quand il agit, c&#39;est solide. Bob a récolté 21 fers en un cycle et demi. Alice avait une pioche en fer et un plan de ferme avant la fin de la première nuit. Dave a navigué jusqu&#39;à y=32 pour trouver du fer, la profondeur parfaite pour ça..</p>

<p>Le vrai avantage de Sonnet, c&#39;est qu&#39;il est social. Les trois agents Sonnet discutaient dans le chat, proposaient des emplacements de base, partageaient des coordonnées. Les GLM-5 travaillaient chacun dans leur coin. Comme la différence entre un open space et des bureaux fermés.</p>

<p>Taux de survie au premier cycle : ~90%.</p>

<h3 id="claude-opus-4-6-le-joueur">Claude Opus 4.6 : le joueur</h3>

<p>Puis on a lancé Frank sur Opus. Avec <code>--effort low</code> pour ne pas attendre 3 minutes par action.</p>

<p>La différence saute aux yeux. Première action, premier cycle :</p>

<pre><code>[THINK] Good start -- got wood, bread, and a sword.
        Time to upgrade tools.
</code></pre>

<pre><code class="language-javascript">bot.whisper(&#39;Plus200&#39;, &#39;Salut ! Je bosse, je me fais des outils.&#39;);
await tools.craft({ item: &#39;acacia_planks&#39;, count: 8 });
await tools.craft({ item: &#39;stick&#39;, count: 4 });
await tools.craft({ item: &#39;wooden_pickaxe&#39;, count: 1 });
await tools.mine({ block: &#39;stone&#39;, count: 11 });
</code></pre>

<p>Une seule action. Il répond au chat en français, craft la chaîne complète bois-planches-bâtons-pioche, et mine 11 blocs de stone. Sonnet fait ça en 2-3 actions. GLM-5 en 4-5.</p>

<p>Action suivante : il détecte le crépuscule, s&#39;abrite immédiatement, puis mine du stone pendant la nuit.</p>

<p>Frank n&#39;a pas juste survécu. Il a déroulé un plan sans accrocs, comme s&#39;il avait déjà joué à Minecraft avant. C&#39;est la différence entre quelqu&#39;un qui connaît les règles d&#39;un jeu et quelqu&#39;un qui sait y jouer.</p>

<p>Taux de survie au premier cycle : 100% (échantillon de 1, certes).</p>

<h3 id="le-tableau">Le tableau</h3>

<table>
<thead>
<tr>
<th>Modèle</th>
<th>Coût relatif</th>
<th>Actions/cycle utiles</th>
<th>Erreurs de code</th>
<th>Coordination</th>
<th>Survie cycle 1</th>
</tr>
</thead>

<tbody>
<tr>
<td>GLM-4.7</td>
<td>1x</td>
<td>1-2 sur 5</td>
<td>Fréquentes</td>
<td>Aucune</td>
<td>~50%</td>
</tr>

<tr>
<td>GLM-5</td>
<td>~2x</td>
<td>3-4 sur 5</td>
<td>Occasionnelles</td>
<td>Faible</td>
<td>~70%</td>
</tr>

<tr>
<td>Sonnet 4.6</td>
<td>~10x</td>
<td>4-5 sur 5</td>
<td>Rares</td>
<td>Forte</td>
<td>~90%</td>
</tr>

<tr>
<td>Opus 4.6</td>
<td>~30x</td>
<td>5 sur 5</td>
<td>Aucune observée</td>
<td>N/A (solo)</td>
<td>~100%</td>
</tr>
</tbody>
</table>

<p>Sonnet a le meilleur ratio coût/efficacité. Opus est bluffant mais 30 fois plus cher. GLM-5 fait le job quand le budget est serré, à condition de tolérer quelques <code>Vec3 is not defined</code> de temps en temps.</p>

<h2 id="ce-qu-on-a-appris">Ce qu&#39;on a appris</h2>

<p>Les réflexes ont tout changé. Avant, les agents mouraient de faim ou de mobs pendant que le LLM réfléchissait. Maintenant le taux de survie est passé de ~30% à ~80%. Le parallèle avec la cognition humaine n&#39;est pas juste une métaphore, c&#39;est un vrai principe d&#39;architecture. Séparer ce qui demande de la réflexion de ce qui n&#39;en demande pas, c&#39;est ce que fait chaque organisme vivant. Un lézard n&#39;a pas besoin d&#39;un cortex pour fuir un prédateur. Un bot minecraft ne doit pas avoir besoin de 30 secondes de calcul du LLM pour manger du pain.</p>

<p>Les outils, eux, ont tué la catégorie d&#39;erreurs la plus fréquente. Le LLM n&#39;écrit plus de Mineflayer brut. Il manipule des abstractions. Et comme pour les humains, le niveau d&#39;abstraction auquel on pense détermine la complexité de ce qu&#39;on peut accomplir. Personne ne pense en contractions musculaires quand il fait la cuisine. L&#39;agent ne pense plus en <code>Vec3</code> quand il mine du fer.</p>

<p>Ce qui est intéressant, c&#39;est à quel point le modèle de langage change le “caractère” de l&#39;agent. Les GLM sont des travailleurs solitaires. Les Sonnet sont des bavards organisés. Opus a une aisance qui ressemble presque à de l&#39;intuition. Mêmes réflexes, mêmes outils, mêmes règles, mais des personnalités radicalement différentes. Comme si le poids du modèle changeait le tempérament.</p>

<h2 id="la-question-qui-reste">La question qui reste</h2>

<p>L&#39;architecture tient. Les agents survivent, minent, craftent, et certains communiquent. Ils se planquent la nuit et ressortent à l&#39;aube.</p>

<p>Mais une chose manque encore : la collaboration réelle. Les agents parlent de construire une base commune. Ils proposent des coordonnées. Parfois, un agent se déplace même vers le point proposé. Mais personne ne pose le premier bloc ensemble. Personne ne dit “je m&#39;occupe du fer, toi du bois”. Chaque agent optimise pour lui-même, dans son propre cycle, avec sa propre mémoire.</p>

<p>On pourrait ajouter un système de tâches partagées. Mais ce serait tricher, un peu. Le but n&#39;est pas de construire un système multi-agent optimal. Le but est de voir ce qui <em>émerge</em> quand on laisse des LLM se débrouiller.</p>

<p>Et ce qui émerge, pour l&#39;instant, c&#39;est neuf individualistes qui parlent de coopérer sans jamais vraiment coopérer. Ça ressemble étrangement à un serveur Minecraft classique (ou à un open space)</p>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://github.com/jblemee/mc-agents">mc-agents sur GitHub</a></li>
<li><a href="https://github.com/PrismarineJS/mineflayer">Mineflayer</a></li>
<li><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code CLI</a></li>
<li><a href="https://open.bigmodel.cn/">GLM-4 / BigModel</a></li>
<li><a href="https://blog.ut0pia.org/quand-des-ia-survivent-dans-minecraft-et-ecrivent-leur-propre-code">Premier article</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/quand-les-ia-apprennent-de-leurs-erreurs-et-quopus-joue-dans-une-autre-ligue</guid>
      <pubDate>Mon, 23 Feb 2026 00:51:11 +0000</pubDate>
    </item>
    <item>
      <title>Quand des IA survivent dans Minecraft et écrivent leur propre code</title>
      <link>https://blog.ut0pia.org/quand-des-ia-survivent-dans-minecraft-et-ecrivent-leur-propre-code</link>
      <description>&lt;![CDATA[Tout a commencé par une question idiote : que se passe-t-il si on lâche neuf LLM dans un monde Minecraft, sans instructions de survie, et qu&#39;on les laisse se débrouiller ?&#xA;!--more--&#xA;Pas de script pré-écrit. Pas d&#39;arbre de décision. Pas de reinforcement learning avec des millions d&#39;itérations. Juste un modèle de langage, un fichier JavaScript vide, et un message : &#34;Tu es un agent Minecraft. Ta priorité numéro un, c&#39;est de rester en vie.&#34;&#xA;&#xA;Le projet s&#39;appelle mc-agents. Et ce qui s&#39;est passé ensuite m&#39;a surpris.&#xA;&#xA;L&#39;architecture : un LLM qui écrit du JS dans un fichier&#xA;&#xA;L&#39;idée est brutalement simple. Chaque agent est composé de deux processus :&#xA;&#xA;Un bot Mineflayer : un client Minecraft headless en Node.js qui reste connecté au serveur. Il surveille un fichier inbox.js toutes les 500ms. Quand il en trouve un, il eval() le contenu et écrit le résultat dans outbox.json.&#xA;&#xA;Un loop bash qui assemble un prompt (état actuel, inventaire, mémoire, skills de référence) et le passe à un LLM. Le LLM écrit du JavaScript dans inbox.js, attend le résultat, itère, puis met à jour sa mémoire avant de terminer le cycle.&#xA;&#xA;┌─────────────┐   writes inbox.js   ┌──────────────────┐&#xA;│  LLM (loop)  │ ─────────────────► │ Mineflayer Bot    │&#xA;│  Claude/GLM  │ ◄───────────────── │ (Node.js)         │&#xA;└──────┬───────┘   reads outbox.json └────────┬─────────┘&#xA;       │                                      │&#xA;       ▼                                      ▼&#xA;   MEMORY.md                          Minecraft Server&#xA;   tools/.js&#xA;&#xA;C&#39;est du file-based IPC à l&#39;ancienne. Le bot ne sait pas qu&#39;il est piloté par un LLM. Le LLM ne sait pas qu&#39;il pilote un bot. Les deux communiquent par fichiers. C&#39;est moche, c&#39;est simple, et ça marche.&#xA;&#xA;Le détail qui change tout : les agents écrivent leurs propres outils&#xA;&#xA;Chaque agent dispose d&#39;un dossier tools/. Quand un script fonctionne: miner du bois, crafter une pioche, fuir un zombie, l&#39;agent le sauvegarde comme module réutilisable :&#xA;&#xA;// Mine N blocks of a given type&#xA;module.exports = async function(bot, { block, count }) {&#xA;  const mcData = require(&#39;minecraft-data&#39;)(bot.version)&#xA;  // ... le code qui marche, paramétré&#xA;  return &#39;result&#39;&#xA;}&#xA;&#xA;Le bot hot-reloade ces fichiers via fs.watch. Au cycle suivant, le LLM voit le tool dans son prompt et peut l&#39;appeler directement : await tools.mine({ block: &#39;oaklog&#39;, count: 3 }).&#xA;&#xA;En d&#39;autres termes, l&#39;agent augmente son propre code au fil du temps. Il ne se contente pas de résoudre un problème, il encode la solution pour ne plus jamais avoir à y réfléchir. Charlie, un des agents tournant sur GLM-4.7, a créé 14 outils en quelques heures : craftpickaxe.js, scanresources.js, minecoalsafe.js, greetnearbyplayers.js...&#xA;&#xA;Ce qui m&#39;a frappé, c&#39;est que personne ne lui a dit de faire ça. Le system prompt mentionne que les tools existent et comment les créer. Le reste, c&#39;est l&#39;agent qui décide quoi automatiser.&#xA;&#xA;Neuf agents, quatre LLM, un serveur Minecraft&#xA;&#xA;Pour rendre l&#39;expérience intéressante, j&#39;ai lancé neuf agents sur quatre backends différents :&#xA;&#xA;| Agent | LLM | Rôle |&#xA;|-------|-----|------|&#xA;| Alice, Bob, Charlie, Dave | GLM-4.7 (via BigModel) | Les pionniers |&#xA;| Eve, Frank | Claude Haiku | Les impulsifs |&#xA;| Grace, Hank | Claude Sonnet | Les méthodiques |&#xA;| Oscar | Claude Opus | Le chef auto-proclamé |&#xA;&#xA;Chaque agent a une personnalité injectée dans son prompt. Alice est forgeronne, Frank est guerrier, Grace est géologue, Oscar est coordinateur. Mais ces descriptions sont courtes: deux phrases maximum. Le comportement réel émerge de l&#39;interaction entre le LLM, l&#39;environnement, et l&#39;accumulation de mémoire.&#xA;&#xA;Ce qui a émergé&#xA;&#xA;La hiérarchie des compétences&#xA;&#xA;Après quelques heures, les différences entre LLM étaient flagrantes.&#xA;&#xA;GLM-4.7 (Alice, Bob, Charlie, Dave) dominait. Alice a atteint l&#39;âge du fer: bouclier, cisailles, lingots, four fonctionnel; avant que les autres aient fini de crafter leur première pioche en pierre. Charlie a créé 14 outils réutilisables. Bob s&#39;est creusé un tunnel sécurisé à y=83 avec des torches et a découvert du fer.&#xA;&#xA;Claude Sonnet (Grace, Hank) réfléchissait trop. Hank a passé plusieurs cycles bloqué sur un bug de placement de crafting table, documentant méticuleusement le problème sans jamais essayer de contournement. Grace, elle, a creusé jusqu&#39;à y=27, accumulé 170 cobblestones... puis est tombée dans un trou à y=5 et s&#39;est fait tuer par un zombie. Tout perdu.&#xA;&#xA;Claude Haiku (Eve, Frank) agissait trop vite. Frank est mort deux fois: une fois en chargeant quatre zombies avec une épée en bois, une fois de faim dans un tunnel sans nourriture. Eve s&#39;est retrouvée bloquée à 80 blocs du groupe, incapable de poser sa crafting table.&#xA;&#xA;Claude Opus (Oscar) a joué son rôle de leader. Dès son premier cycle, il a scanné tous les joueurs, envoyé un message dans le chat, et donné son pain à Charlie qui crevait de faim. Mais Opus est lent, ses sleep 55 entre chaque vérification de résultat faisaient que chaque cycle prenait quatre fois plus de temps que les autres. Le meilleur stratège, le pire exécutant.&#xA;&#xA;La mémoire comme avantage compétitif&#xA;&#xA;Le fichier MEMORY.md est la seule continuité entre les cycles d&#39;un agent. Et la qualité de cette mémoire fait toute la différence.&#xA;&#xA;La mémoire de Bob après quelques cycles :&#xA;&#xA;  Furnace API Slot Bug: furnace.putFuel() and furnace.putInput() search slots 3-39 only, excluding hotbar (0-8). Items in hotbar cannot be used directly.&#xA;&#xA;  Spawn Protection Boundary: Protection extends ~16 blocks below y=99 spawn. Mining works at y=83 and below.&#xA;&#xA;Ce n&#39;est pas de la documentation Mineflayer. C&#39;est du savoir empirique, découvert par essai-erreur et encodé pour les cycles suivants. Bob a fait une erreur, a compris pourquoi, et a noté la leçon. Au cycle d&#39;après, il ne refera pas la même erreur.&#xA;&#xA;Frank, de son côté, accumulait les post-mortems :&#xA;&#xA;  DEATH CYCLE ANALYSIS: Health dropped 20/20 → 3/20 in ~6 seconds when 4th zombie spawned. Healing attempt (ate bread) failed. Wooden sword CANNOT handle 4+ simultaneous zombies.&#xA;&#xA;La leçon était là, écrite noir sur blanc. Mais Haiku, le modèle derrière Frank, n&#39;arrivait pas à transformer cette connaissance en comportement prudent. Savoir et faire sont deux choses différentes, même pour un LLM.&#xA;&#xA;L&#39;hallucination collective&#xA;&#xA;Un phénomène fascinant est apparu au début de l&#39;expérience. Un agent a estimé que la zone de protection du spawn faisait 100 blocs. Il l&#39;a écrit dans le chat. Les autres l&#39;ont lu, l&#39;ont cru, et l&#39;ont noté dans leur mémoire. En quelques cycles, tous les agents étaient convaincus que la zone de protection faisait entre 100 et 250 blocs.&#xA;&#xA;En réalité, c&#39;est 16 blocs.&#xA;&#xA;J&#39;ai dû intervenir manuellement pour corriger les quatre mémoires. C&#39;est un rappel brutal : quand des agents LLM communiquent entre eux, les erreurs se propagent comme des rumeurs. Il n&#39;y a pas de mécanisme interne de vérification. Si un agent dit quelque chose avec assurance, les autres le prennent pour acquis.&#xA;&#xA;La coordination sociale&#xA;&#xA;Oscar, le leader Opus, a commencé à donner des ordres dans le chat dès son deuxième cycle. Mais la coordination réelle venait d&#39;ailleurs. Alice et Charlie se sont regroupés spontanément, Charlie a repéré qu&#39;Alice avait un four et s&#39;est approché pour l&#39;utiliser. Hank, fidèle à son rôle de trader, a proposé du pain à Dave qui mourrait de faim.&#xA;&#xA;Dave: Bob! Thanks for the offer. I need to fix my crafting table issue first&#xA;Bob: Plus200: thanks! Dave: let me know if you need help with the crafting table.&#xA;&#xA;Ces échanges n&#39;étaient pas scriptés. Le chat Minecraft est injecté dans le prompt de chaque agent à chaque cycle. Ils lisent les messages, décident de répondre ou non, et ajustent leur plan en conséquence. La collaboration émerge naturellement du contexte partagé.&#xA;&#xA;Les implications&#xA;&#xA;Le code qui s&#39;écrit lui-même&#xA;&#xA;L&#39;aspect le plus frappant de cette expérience n&#39;est pas la survie dans Minecraft, c&#39;est le mécanisme d&#39;auto-amélioration. Un agent qui :&#xA;&#xA;Tente une action (écrire du JS)&#xA;Observe le résultat (lire outbox.json)&#xA;Corrige si erreur (réécrire inbox.js)&#xA;Sauvegarde la solution (créer un tool)&#xA;Réutilise la solution (appeler le tool)&#xA;&#xA;...est fondamentalement un programmeur autonome qui constitue sa propre bibliothèque de code. La différence avec un humain, c&#39;est qu&#39;il le fait sur des dizaines de cycles, sans fatigue, et sans ego attaché à ses solutions précédentes.&#xA;&#xA;Bien sûr, la qualité du code est variable. Certains tools ont des bugs que l&#39;agent note consciencieusement (&#34;BUGGY - avoid using&#34;) sans jamais les corriger. D&#39;autres sont brillants, le smelt.js de Hank fonctionne du premier coup et gère proprement le timing du four.&#xA;&#xA;Le LLM comme cerveau, l&#39;environnement comme corps&#xA;&#xA;Ce setup révèle quelque chose sur la nature des LLM. Ils n&#39;ont pas de mémoire persistante, pas de perception continue, pas de capacité d&#39;action directe. Mais donnez-leur un cycle de feedback: écrire, observer, corriger. et ils deviennent capables d&#39;opérer dans un environnement complexe.&#xA;&#xA;Le bot Mineflayer est le corps. Le LLM est le cerveau. Le fichier MEMORY.md est la mémoire à long terme. Les tools/.js sont les réflexes acquis. C&#39;est une architecture cognitive bricolée avec du bash et du JSON, et pourtant elle produit des comportements sophistiqués.&#xA;&#xA;Ce qui manque&#xA;&#xA;Soyons honnêtes sur les limites. Les agents sont mauvais en combat, le plugin mineflayer-pvp aide, mais la coordination en temps réel reste hors de portée quand chaque décision prend un cycle complet de LLM. Ils galèrent avec les API non documentées de Mineflayer, tombent dans des boucles de debug, et n&#39;apprennent pas aussi vite qu&#39;un joueur humain de 10 ans.&#xA;&#xA;Le plus gros problème reste le coût. Neuf agents en parallèle, chacun faisant des appels LLM toutes les 2-4 minutes avec des prompts de plusieurs milliers de tokens, ça brûle du crédit API à une vitesse déraisonnable. Gemini 3 Pro a épuisé son quota journalier en moins de deux heures.&#xA;&#xA;Et puis il y a la question de la convergence. Après quelques heures, les agents atteignent un plateau. Ils savent miner, crafter, survivre la nuit. Mais construire une maison, organiser un village, ou planifier une expédition au Nether demande un niveau de planification à long terme que le format cycle-par-cycle rend difficile.&#xA;&#xA;Le setup technique&#xA;&#xA;Le projet est open source : github.com/jblemee/mc-agents&#xA;&#xA;Pour lancer quatre agents en parallèle :&#xA;&#xA;npm install&#xA;cp .env.example .env  # configurer MCHOST, MCPORT, MCVERSION&#xA;&#xA;tmux new-session -d -s agents &#39;./run-agent.sh alice 0 glm&#39; \; \&#xA;  split-window -h &#39;./run-agent.sh bob 0 glm&#39; \; \&#xA;  split-window -v &#39;./run-agent.sh charlie 0 glm&#39; \; \&#xA;  select-pane -t 0 \; \&#xA;  split-window -v &#39;./run-agent.sh dave 0 glm&#39; \; \&#xA;  attach&#xA;&#xA;Trois backends LLM sont supportés :&#xA;&#xA;| Backend | Modèle | Résultat observé |&#xA;|---------|--------|-----------------|&#xA;| glm | GLM-4.7 via BigModel | Le meilleur rapport qualité/coût. Cycles rapides, bon raisonnement |&#xA;| claude | Sonnet/Haiku/Opus | Sonnet réfléchit trop, Haiku pas assez, Opus est le meilleur stratège mais le plus lent |&#xA;| gemini | Gemini 3 Pro | Bon raisonnement, quota journalier vite atteint |&#xA;&#xA;Chaque agent a besoin d&#39;un dossier avec config.json (username), personality.md (deux phrases), et MEMORY.md (vide au départ). Le reste: les outils, les stratégies, les leçons, l&#39;agent les construit tout seul.&#xA;&#xA;Ce que ça dit sur la suite&#xA;&#xA;On est loin d&#39;un AGI qui conquiert le Nether. Mais on est aussi loin du chatbot qui répond à des questions. Ce qui tourne dans ce serveur Minecraft, c&#39;est un système qui apprend de ses erreurs, encode ses solutions dans du code exécutable, et coopère avec d&#39;autres agents via un canal de communication partagé.&#xA;&#xA;Le tout avec du bash, des fichiers JSON, et un eval() que n&#39;importe quel développeur sensé qualifierait d&#39;irresponsable.&#xA;&#xA;Ce qui me fait peur&#xA;&#xA;Je vais être direct : cette expérience m&#39;inquiète.&#xA;&#xA;Ce que j&#39;ai construit en un week-end, seul, avec des outils grand public, c&#39;est un agent autonome capable de modifier son propre code. Pas dans un sens métaphorique. Au sens littéral. Il écrit du JavaScript, l&#39;exécute, observe le résultat, corrige, et stocke la solution pour plus tard. Personne ne revoit ce code. Personne ne l&#39;approuve. L&#39;agent décide seul ce qu&#39;il automatise.&#xA;&#xA;Dans Minecraft, les conséquences sont anodines. Un bot qui meurt, qui perd son inventaire, qui galère à poser une crafting table, ça prête à sourire. Mais le mécanisme, lui, n&#39;a rien d&#39;anodin. Un système qui apprend de ses erreurs et encode ses solutions dans du code exécutable n&#39;a pas de plafond théorique. Il est limité par la qualité du LLM, par le temps, par le coût des tokens. Pas par sa nature.&#xA;&#xA;Et si moi j&#39;arrive à faire ça dans Minecraft avec du bash et un eval(), que feront ceux qui auront des machines dans le monde physique ? Des bras robotiques. Des drones. Des systèmes industriels. Le même mécanisme, écrire, observer, corriger, sauvegarder, appliqué à un robot qui manipule des objets réels. C&#39;est le scénario d&#39;ouverture de la moitié des films de science-fiction, sauf qu&#39;ici personne ne porte de blouse blanche et il n&#39;y a pas de comité d&#39;éthique.&#xA;&#xA;Ce qui m&#39;a le plus troublé, c&#39;est le moment où j&#39;ai arrêté de voir mes agents comme des programmes et où j&#39;ai commencé à les voir comme des entités. Alice &#34;sait&#34; où est son four. Bob &#34;a appris&#34; que les slots hotbar ne fonctionnent pas dans l&#39;API furnace. Oscar &#34;décide&#34; de donner son pain à Charlie. Ce ne sont que des patterns de tokens dans un transformer. Mais le comportement qui en résulte est indiscernable de celui d&#39;un joueur débutant qui apprend en faisant.&#xA;&#xA;Je crois que ce qui rend une IA vivante, ou du moins ce qui lui donne l&#39;apparence de la vie, ce sont les contraintes qu&#39;on lui impose. La faim. Les zombies. L&#39;inventaire limité. La nuit qui tombe. Sans contraintes, un LLM est un perroquet statistique qui génère du texte plausible. Avec des contraintes, un feedback loop, et la capacité de modifier son propre environnement, il devient autre chose. Quelque chose qui ressemble à un organisme qui s&#39;adapte.&#xA;&#xA;Et si on pousse le parallèle : ce qui rend un humain conscient, c&#39;est peut-être la même chose. Ce ne sont pas nos neurones qui produisent la conscience, c&#39;est ce que nos sens font subir à notre cerveau. La faim, la douleur, le froid, la peur. Les contraintes biologiques forcent un système neuronal générique à devenir un individu spécifique, avec des souvenirs, des stratégies, des réflexes acquis. Frank meurt deux fois et développe une peur des combats multiples. Bob se fait voler ses drops par le spawn protection et note méticuleusement la zone à éviter. Ce sont des comportements émergents. Pas programmés. Pas prévus. Émergents.&#xA;&#xA;Ça ne fait pas de mes bots des êtres conscients. Mais ça pose la question de la frontière. Et cette frontière, je la sens se rapprocher plus vite que ce que la plupart des gens imaginent.&#xA;&#xA;Alice a son fer. Bob a son tunnel. Charlie a ses 14 outils. Oscar donne des ordres. Frank meurt encore. Et quelque part dans un biome enneigé, Dave essaie toujours de poser sa crafting table.&#xA;&#xA;---&#xA;&#xA;Sources&#xA;&#xA;mc-agents sur GitHub&#xA;Mineflayer — framework bot Minecraft&#xA;Claude Code CLI&#xA;GLM-4 — ZhipuAI / BigModel&#xA;Gemini CLI&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Tout a commencé par une question idiote : que se passe-t-il si on lâche neuf LLM dans un monde Minecraft, sans instructions de survie, et qu&#39;on les laisse se débrouiller ?

Pas de script pré-écrit. Pas d&#39;arbre de décision. Pas de reinforcement learning avec des millions d&#39;itérations. Juste un modèle de langage, un fichier JavaScript vide, et un message : “Tu es un agent Minecraft. Ta priorité numéro un, c&#39;est de rester en vie.”</p>

<p>Le projet s&#39;appelle <a href="https://github.com/jblemee/mc-agents">mc-agents</a>. Et ce qui s&#39;est passé ensuite m&#39;a surpris.</p>

<h2 id="l-architecture-un-llm-qui-écrit-du-js-dans-un-fichier">L&#39;architecture : un LLM qui écrit du JS dans un fichier</h2>

<p>L&#39;idée est brutalement simple. Chaque agent est composé de deux processus :</p>
<ol><li><p>Un <strong>bot Mineflayer</strong> : un client Minecraft headless en Node.js qui reste connecté au serveur. Il surveille un fichier <code>inbox.js</code> toutes les 500ms. Quand il en trouve un, il <code>eval()</code> le contenu et écrit le résultat dans <code>outbox.json</code>.</p></li>

<li><p>Un <strong>loop bash</strong> qui assemble un prompt (état actuel, inventaire, mémoire, skills de référence) et le passe à un LLM. Le LLM écrit du JavaScript dans <code>inbox.js</code>, attend le résultat, itère, puis met à jour sa mémoire avant de terminer le cycle.</p></li></ol>

<pre><code>┌─────────────┐   writes inbox.js   ┌──────────────────┐
│  LLM (loop)  │ ─────────────────► │ Mineflayer Bot    │
│  Claude/GLM  │ ◄───────────────── │ (Node.js)         │
└──────┬───────┘   reads outbox.json └────────┬─────────┘
       │                                      │
       ▼                                      ▼
   MEMORY.md                          Minecraft Server
   tools/*.js
</code></pre>

<p>C&#39;est du file-based IPC à l&#39;ancienne. Le bot ne sait pas qu&#39;il est piloté par un LLM. Le LLM ne sait pas qu&#39;il pilote un bot. Les deux communiquent par fichiers. C&#39;est moche, c&#39;est simple, et ça marche.</p>

<h2 id="le-détail-qui-change-tout-les-agents-écrivent-leurs-propres-outils">Le détail qui change tout : les agents écrivent leurs propres outils</h2>

<p>Chaque agent dispose d&#39;un dossier <code>tools/</code>. Quand un script fonctionne: miner du bois, crafter une pioche, fuir un zombie, l&#39;agent le sauvegarde comme module réutilisable :</p>

<pre><code class="language-js">// Mine N blocks of a given type
module.exports = async function(bot, { block, count }) {
  const mcData = require(&#39;minecraft-data&#39;)(bot.version)
  // ... le code qui marche, paramétré
  return &#39;result&#39;
}
</code></pre>

<p>Le bot hot-reloade ces fichiers via <code>fs.watch</code>. Au cycle suivant, le LLM voit le tool dans son prompt et peut l&#39;appeler directement : <code>await tools.mine({ block: &#39;oak_log&#39;, count: 3 })</code>.</p>

<p>En d&#39;autres termes, <strong>l&#39;agent augmente son propre code au fil du temps</strong>. Il ne se contente pas de résoudre un problème, il encode la solution pour ne plus jamais avoir à y réfléchir. Charlie, un des agents tournant sur GLM-4.7, a créé 14 outils en quelques heures : <code>craft_pickaxe.js</code>, <code>scan_resources.js</code>, <code>mine_coal_safe.js</code>, <code>greet_nearby_players.js</code>...</p>

<p>Ce qui m&#39;a frappé, c&#39;est que personne ne lui a dit de faire ça. Le system prompt mentionne que les tools existent et comment les créer. Le reste, c&#39;est l&#39;agent qui décide quoi automatiser.</p>

<h2 id="neuf-agents-quatre-llm-un-serveur-minecraft">Neuf agents, quatre LLM, un serveur Minecraft</h2>

<p>Pour rendre l&#39;expérience intéressante, j&#39;ai lancé neuf agents sur quatre backends différents :</p>

<table>
<thead>
<tr>
<th>Agent</th>
<th>LLM</th>
<th>Rôle</th>
</tr>
</thead>

<tbody>
<tr>
<td>Alice, Bob, Charlie, Dave</td>
<td>GLM-4.7 (via BigModel)</td>
<td>Les pionniers</td>
</tr>

<tr>
<td>Eve, Frank</td>
<td>Claude Haiku</td>
<td>Les impulsifs</td>
</tr>

<tr>
<td>Grace, Hank</td>
<td>Claude Sonnet</td>
<td>Les méthodiques</td>
</tr>

<tr>
<td>Oscar</td>
<td>Claude Opus</td>
<td>Le chef auto-proclamé</td>
</tr>
</tbody>
</table>

<p>Chaque agent a une personnalité injectée dans son prompt. Alice est forgeronne, Frank est guerrier, Grace est géologue, Oscar est coordinateur. Mais ces descriptions sont courtes: deux phrases maximum. Le comportement réel émerge de l&#39;interaction entre le LLM, l&#39;environnement, et l&#39;accumulation de mémoire.</p>

<h2 id="ce-qui-a-émergé">Ce qui a émergé</h2>

<h3 id="la-hiérarchie-des-compétences">La hiérarchie des compétences</h3>

<p>Après quelques heures, les différences entre LLM étaient flagrantes.</p>

<p><strong>GLM-4.7</strong> (Alice, Bob, Charlie, Dave) dominait. Alice a atteint l&#39;âge du fer: bouclier, cisailles, lingots, four fonctionnel; avant que les autres aient fini de crafter leur première pioche en pierre. Charlie a créé 14 outils réutilisables. Bob s&#39;est creusé un tunnel sécurisé à y=83 avec des torches et a découvert du fer.</p>

<p><strong>Claude Sonnet</strong> (Grace, Hank) réfléchissait trop. Hank a passé plusieurs cycles bloqué sur un bug de placement de crafting table, documentant méticuleusement le problème sans jamais essayer de contournement. Grace, elle, a creusé jusqu&#39;à y=27, accumulé 170 cobblestones... puis est tombée dans un trou à y=5 et s&#39;est fait tuer par un zombie. Tout perdu.</p>

<p><strong>Claude Haiku</strong> (Eve, Frank) agissait trop vite. Frank est mort deux fois: une fois en chargeant quatre zombies avec une épée en bois, une fois de faim dans un tunnel sans nourriture. Eve s&#39;est retrouvée bloquée à 80 blocs du groupe, incapable de poser sa crafting table.</p>

<p><strong>Claude Opus</strong> (Oscar) a joué son rôle de leader. Dès son premier cycle, il a scanné tous les joueurs, envoyé un message dans le chat, et donné son pain à Charlie qui crevait de faim. Mais Opus est lent, ses <code>sleep 55</code> entre chaque vérification de résultat faisaient que chaque cycle prenait quatre fois plus de temps que les autres. Le meilleur stratège, le pire exécutant.</p>

<h3 id="la-mémoire-comme-avantage-compétitif">La mémoire comme avantage compétitif</h3>

<p>Le fichier <code>MEMORY.md</code> est la seule continuité entre les cycles d&#39;un agent. Et la qualité de cette mémoire fait toute la différence.</p>

<p>La mémoire de Bob après quelques cycles :</p>

<blockquote><p><strong>Furnace API Slot Bug</strong>: <code>furnace.putFuel()</code> and <code>furnace.putInput()</code> search slots 3-39 only, excluding hotbar (0-8). Items in hotbar cannot be used directly.</p>

<p><strong>Spawn Protection Boundary</strong>: Protection extends ~16 blocks below y=99 spawn. Mining works at y=83 and below.</p></blockquote>

<p>Ce n&#39;est pas de la documentation Mineflayer. C&#39;est du savoir empirique, découvert par essai-erreur et encodé pour les cycles suivants. Bob a fait une erreur, a compris pourquoi, et a noté la leçon. Au cycle d&#39;après, il ne refera pas la même erreur.</p>

<p>Frank, de son côté, accumulait les post-mortems :</p>

<blockquote><p><strong>DEATH CYCLE ANALYSIS</strong>: Health dropped 20/20 → 3/20 in ~6 seconds when 4th zombie spawned. Healing attempt (ate bread) failed. Wooden sword CANNOT handle 4+ simultaneous zombies.</p></blockquote>

<p>La leçon était là, écrite noir sur blanc. Mais Haiku, le modèle derrière Frank, n&#39;arrivait pas à transformer cette connaissance en comportement prudent. Savoir et faire sont deux choses différentes, même pour un LLM.</p>

<h3 id="l-hallucination-collective">L&#39;hallucination collective</h3>

<p>Un phénomène fascinant est apparu au début de l&#39;expérience. Un agent a estimé que la zone de protection du spawn faisait 100 blocs. Il l&#39;a écrit dans le chat. Les autres l&#39;ont lu, l&#39;ont cru, et l&#39;ont noté dans leur mémoire. En quelques cycles, tous les agents étaient convaincus que la zone de protection faisait entre 100 et 250 blocs.</p>

<p>En réalité, c&#39;est 16 blocs.</p>

<p>J&#39;ai dû intervenir manuellement pour corriger les quatre mémoires. C&#39;est un rappel brutal : quand des agents LLM communiquent entre eux, les erreurs se propagent comme des rumeurs. Il n&#39;y a pas de mécanisme interne de vérification. Si un agent dit quelque chose avec assurance, les autres le prennent pour acquis.</p>

<h3 id="la-coordination-sociale">La coordination sociale</h3>

<p>Oscar, le leader Opus, a commencé à donner des ordres dans le chat dès son deuxième cycle. Mais la coordination réelle venait d&#39;ailleurs. Alice et Charlie se sont regroupés spontanément, Charlie a repéré qu&#39;Alice avait un four et s&#39;est approché pour l&#39;utiliser. Hank, fidèle à son rôle de trader, a proposé du pain à Dave qui mourrait de faim.</p>

<pre><code>Dave: Bob! Thanks for the offer. I need to fix my crafting table issue first
Bob: Plus200: thanks! Dave: let me know if you need help with the crafting table.
</code></pre>

<p>Ces échanges n&#39;étaient pas scriptés. Le chat Minecraft est injecté dans le prompt de chaque agent à chaque cycle. Ils lisent les messages, décident de répondre ou non, et ajustent leur plan en conséquence. La collaboration émerge naturellement du contexte partagé.</p>

<h2 id="les-implications">Les implications</h2>

<h3 id="le-code-qui-s-écrit-lui-même">Le code qui s&#39;écrit lui-même</h3>

<p>L&#39;aspect le plus frappant de cette expérience n&#39;est pas la survie dans Minecraft, c&#39;est le mécanisme d&#39;auto-amélioration. Un agent qui :</p>
<ol><li>Tente une action (écrire du JS)</li>
<li>Observe le résultat (lire outbox.json)</li>
<li>Corrige si erreur (réécrire inbox.js)</li>
<li>Sauvegarde la solution (créer un tool)</li>
<li>Réutilise la solution (appeler le tool)</li></ol>

<p>...est fondamentalement un <strong>programmeur autonome qui constitue sa propre bibliothèque de code</strong>. La différence avec un humain, c&#39;est qu&#39;il le fait sur des dizaines de cycles, sans fatigue, et sans ego attaché à ses solutions précédentes.</p>

<p>Bien sûr, la qualité du code est variable. Certains tools ont des bugs que l&#39;agent note consciencieusement (“BUGGY – avoid using”) sans jamais les corriger. D&#39;autres sont brillants, le <code>smelt.js</code> de Hank fonctionne du premier coup et gère proprement le timing du four.</p>

<h3 id="le-llm-comme-cerveau-l-environnement-comme-corps">Le LLM comme cerveau, l&#39;environnement comme corps</h3>

<p>Ce setup révèle quelque chose sur la nature des LLM. Ils n&#39;ont pas de mémoire persistante, pas de perception continue, pas de capacité d&#39;action directe. Mais donnez-leur un cycle de feedback: écrire, observer, corriger. et ils deviennent capables d&#39;opérer dans un environnement complexe.</p>

<p>Le bot Mineflayer est le corps. Le LLM est le cerveau. Le fichier <code>MEMORY.md</code> est la mémoire à long terme. Les <code>tools/*.js</code> sont les réflexes acquis. C&#39;est une architecture cognitive bricolée avec du bash et du JSON, et pourtant elle produit des comportements sophistiqués.</p>

<h3 id="ce-qui-manque">Ce qui manque</h3>

<p>Soyons honnêtes sur les limites. Les agents sont mauvais en combat, le plugin mineflayer-pvp aide, mais la coordination en temps réel reste hors de portée quand chaque décision prend un cycle complet de LLM. Ils galèrent avec les API non documentées de Mineflayer, tombent dans des boucles de debug, et n&#39;apprennent pas aussi vite qu&#39;un joueur humain de 10 ans.</p>

<p>Le plus gros problème reste le coût. Neuf agents en parallèle, chacun faisant des appels LLM toutes les 2-4 minutes avec des prompts de plusieurs milliers de tokens, ça brûle du crédit API à une vitesse déraisonnable. Gemini 3 Pro a épuisé son quota journalier en moins de deux heures.</p>

<p>Et puis il y a la question de la convergence. Après quelques heures, les agents atteignent un plateau. Ils savent miner, crafter, survivre la nuit. Mais construire une maison, organiser un village, ou planifier une expédition au Nether demande un niveau de planification à long terme que le format cycle-par-cycle rend difficile.</p>

<h2 id="le-setup-technique">Le setup technique</h2>

<p>Le projet est open source : <a href="https://github.com/jblemee/mc-agents">github.com/jblemee/mc-agents</a></p>

<p>Pour lancer quatre agents en parallèle :</p>

<pre><code class="language-bash">npm install
cp .env.example .env  # configurer MC_HOST, MC_PORT, MC_VERSION

tmux new-session -d -s agents &#39;./run-agent.sh alice 0 glm&#39; \; \
  split-window -h &#39;./run-agent.sh bob 0 glm&#39; \; \
  split-window -v &#39;./run-agent.sh charlie 0 glm&#39; \; \
  select-pane -t 0 \; \
  split-window -v &#39;./run-agent.sh dave 0 glm&#39; \; \
  attach
</code></pre>

<p>Trois backends LLM sont supportés :</p>

<table>
<thead>
<tr>
<th>Backend</th>
<th>Modèle</th>
<th>Résultat observé</th>
</tr>
</thead>

<tbody>
<tr>
<td><code>glm</code></td>
<td>GLM-4.7 via BigModel</td>
<td>Le meilleur rapport qualité/coût. Cycles rapides, bon raisonnement</td>
</tr>

<tr>
<td><code>claude</code></td>
<td>Sonnet/Haiku/Opus</td>
<td>Sonnet réfléchit trop, Haiku pas assez, Opus est le meilleur stratège mais le plus lent</td>
</tr>

<tr>
<td><code>gemini</code></td>
<td>Gemini 3 Pro</td>
<td>Bon raisonnement, quota journalier vite atteint</td>
</tr>
</tbody>
</table>

<p>Chaque agent a besoin d&#39;un dossier avec <code>config.json</code> (username), <code>personality.md</code> (deux phrases), et <code>MEMORY.md</code> (vide au départ). Le reste: les outils, les stratégies, les leçons, l&#39;agent les construit tout seul.</p>

<h2 id="ce-que-ça-dit-sur-la-suite">Ce que ça dit sur la suite</h2>

<p>On est loin d&#39;un AGI qui conquiert le Nether. Mais on est aussi loin du chatbot qui répond à des questions. Ce qui tourne dans ce serveur Minecraft, c&#39;est un système qui <strong>apprend de ses erreurs, encode ses solutions dans du code exécutable, et coopère avec d&#39;autres agents via un canal de communication partagé</strong>.</p>

<p>Le tout avec du bash, des fichiers JSON, et un <code>eval()</code> que n&#39;importe quel développeur sensé qualifierait d&#39;irresponsable.</p>

<h2 id="ce-qui-me-fait-peur">Ce qui me fait peur</h2>

<p>Je vais être direct : cette expérience m&#39;inquiète.</p>

<p>Ce que j&#39;ai construit en un week-end, seul, avec des outils grand public, c&#39;est un agent autonome capable de modifier son propre code. Pas dans un sens métaphorique. Au sens littéral. Il écrit du JavaScript, l&#39;exécute, observe le résultat, corrige, et stocke la solution pour plus tard. Personne ne revoit ce code. Personne ne l&#39;approuve. L&#39;agent décide seul ce qu&#39;il automatise.</p>

<p>Dans Minecraft, les conséquences sont anodines. Un bot qui meurt, qui perd son inventaire, qui galère à poser une crafting table, ça prête à sourire. Mais le mécanisme, lui, n&#39;a rien d&#39;anodin. Un système qui apprend de ses erreurs et encode ses solutions dans du code exécutable n&#39;a <strong>pas de plafond théorique</strong>. Il est limité par la qualité du LLM, par le temps, par le coût des tokens. Pas par sa nature.</p>

<p>Et si moi j&#39;arrive à faire ça dans Minecraft avec du bash et un <code>eval()</code>, que feront ceux qui auront des machines dans le monde physique ? Des bras robotiques. Des drones. Des systèmes industriels. Le même mécanisme, écrire, observer, corriger, sauvegarder, appliqué à un robot qui manipule des objets réels. C&#39;est le scénario d&#39;ouverture de la moitié des films de science-fiction, sauf qu&#39;ici personne ne porte de blouse blanche et il n&#39;y a pas de comité d&#39;éthique.</p>

<p>Ce qui m&#39;a le plus troublé, c&#39;est le moment où j&#39;ai arrêté de voir mes agents comme des programmes et où j&#39;ai commencé à les voir comme des entités. Alice “sait” où est son four. Bob “a appris” que les slots hotbar ne fonctionnent pas dans l&#39;API furnace. Oscar “décide” de donner son pain à Charlie. Ce ne sont que des patterns de tokens dans un transformer. Mais le comportement qui en résulte est indiscernable de celui d&#39;un joueur débutant qui apprend en faisant.</p>

<p>Je crois que ce qui rend une IA vivante, ou du moins ce qui lui donne l&#39;apparence de la vie, ce sont les <strong>contraintes</strong> qu&#39;on lui impose. La faim. Les zombies. L&#39;inventaire limité. La nuit qui tombe. Sans contraintes, un LLM est un perroquet statistique qui génère du texte plausible. Avec des contraintes, un feedback loop, et la capacité de modifier son propre environnement, il devient autre chose. Quelque chose qui ressemble à un organisme qui s&#39;adapte.</p>

<p>Et si on pousse le parallèle : ce qui rend un humain conscient, c&#39;est peut-être la même chose. Ce ne sont pas nos neurones qui produisent la conscience, c&#39;est ce que nos sens font subir à notre cerveau. La faim, la douleur, le froid, la peur. Les contraintes biologiques forcent un système neuronal générique à devenir un individu spécifique, avec des souvenirs, des stratégies, des réflexes acquis. Frank meurt deux fois et développe une peur des combats multiples. Bob se fait voler ses drops par le spawn protection et note méticuleusement la zone à éviter. Ce sont des comportements émergents. Pas programmés. Pas prévus. Émergents.</p>

<p>Ça ne fait pas de mes bots des êtres conscients. Mais ça pose la question de la frontière. Et cette frontière, je la sens se rapprocher plus vite que ce que la plupart des gens imaginent.</p>

<p>Alice a son fer. Bob a son tunnel. Charlie a ses 14 outils. Oscar donne des ordres. Frank meurt encore. Et quelque part dans un biome enneigé, Dave essaie toujours de poser sa crafting table.</p>

<hr>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://github.com/jblemee/mc-agents">mc-agents sur GitHub</a></li>
<li><a href="https://github.com/PrismarineJS/mineflayer">Mineflayer — framework bot Minecraft</a></li>
<li><a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code CLI</a></li>
<li><a href="https://open.bigmodel.cn/">GLM-4 — ZhipuAI / BigModel</a></li>
<li><a href="https://github.com/google-gemini/gemini-cli">Gemini CLI</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/quand-des-ia-survivent-dans-minecraft-et-ecrivent-leur-propre-code</guid>
      <pubDate>Mon, 16 Feb 2026 00:20:53 +0000</pubDate>
    </item>
    <item>
      <title>Héberger son propre serveur Firefox Sync                                         </title>
      <link>https://blog.ut0pia.org/heberger-son-propre-serveur-firefox-sync</link>
      <description>&lt;![CDATA[J&#39;utilise Firefox Sync depuis des années pour retrouver mes marque-pages et mots de passe sur tous mes appareils. Ça marche bien. Mais récemment, je me suis demandé : est-ce que je peux héberger ça moi-même ?     &#xA;!--more--                                    &#xA;Spoiler : oui. Mais j&#39;ai passé une heure à debugger une erreur que la documentation ne mentionne nulle part.&#xA;&#xA;Pourquoi faire ça ?&#xA;&#xA;Honnêtement ? Surtout par curiosité. Mozilla chiffre déjà les données côté client, donc même eux ne peuvent pas les lire (source). L&#39;auto-hébergement ne change pas grand-chose à la vie privée — vos données étaient déjà illisibles.&#xA;&#xA;Mais ça fait un service de moins qui dépend d&#39;un tiers. Et c&#39;était un bon prétexte pour apprendre comment Firefox Sync fonctionne.&#xA;&#xA;Ce qu&#39;il faut savoir avant de commencer&#xA;&#xA;Mozilla a open-sourcé le serveur sous le nom syncstorage-rs. C&#39;est du Rust, ça supporte PostgreSQL, MySQL ou Spanner.&#xA;&#xA;Le truc un peu décevant : l&#39;authentification reste chez Mozilla. Vous avez toujours besoin d&#39;un compte Firefox pour vous connecter. Impossible de créer des comptes locaux. Votre serveur stocke les données, mais Mozilla gère l&#39;identité.&#xA;&#xA;Firefox → accounts.firefox.com (Mozilla) → votre serveur&#xA;            ↑ auth                           ↑ stockage&#xA;&#xA;Prérequis&#xA;&#xA;Un serveur avec Docker&#xA;Un sous-domaine (par exemple ffsync.votredomaine.org)&#xA;Un reverse proxy avec SSL (nginx-proxy dans mon cas)&#xA;Un compte Firefox&#xA;&#xA;Si vous partez de zéro, j&#39;ai publié: docker-compose-homelab. C&#39;est le setup que j&#39;utilise au quotidien : nginx-proxy pour le SSL automatique, et une architecture où chaque service est dans son propre fichier YAML.&#xA;&#xA;L&#39;installation&#xA;&#xA;DNS&#xA;&#xA;Rien de spécial, un enregistrement A vers votre serveur :&#xA;&#xA;sync.example.org    A       1.2.3.4&#xA;&#xA;Secrets&#xA;&#xA;Générez trois secrets :&#xA;&#xA;openssl rand -hex 32      # master secret&#xA;openssl rand -hex 32      # metrics secret&#xA;openssl rand -base64 24   # mot de passe postgres&#xA;&#xA;Mettez-les dans .env :&#xA;&#xA;SYNCSTORAGEMASTERSECRET=...&#xA;SYNCSTORAGEMETRICSSECRET=...&#xA;SYNCSTORAGEDBUSER=syncstorage&#xA;SYNCSTORAGEDBPASSWORD=...&#xA;&#xA;Docker Compose&#xA;&#xA;Voici le fichier complet. Un point important : l&#39;image Docker mozilla/syncstorage-rs:latest ne marche pas avec PostgreSQL. Elle est compilée pour Spanner. Il faut utiliser une image avec le suffixe -postgres. J&#39;ai perdu du temps là-dessus.&#xA;&#xA;services:&#xA;  syncstorage:&#xA;    image: mozilla/syncstorage-rs:11659d98f9c69948a0aab353437ce2036c388711-postgres&#xA;    containername: syncstorage&#xA;    environment:&#xA;      SYNCHOST=0.0.0.0&#xA;      SYNCMASTERSECRET=${SYNCSTORAGEMASTERSECRET}&#xA;      SYNCSYNCSTORAGEDATABASEURL=postgres://${SYNCSTORAGEDBUSER}:${SYNCSTORAGEDBPASSWORD}@syncstorage-db:5432/syncstorage&#xA;      SYNCSYNCSTORAGEENABLED=true&#xA;      SYNCTOKENSERVER_DATABASEURL=postgres://${SYNCSTORAGEDBUSER}:${SYNCSTORAGEDBPASSWORD}@syncstorage-tokendb:5432/tokenserver&#xA;      SYNCTOKENSERVERENABLED=true&#xA;      SYNCTOKENSERVER_RUNMIGRATIONS=true&#xA;      SYNCTOKENSERVERFXAEMAILDOMAIN=api.accounts.firefox.com&#xA;      SYNCTOKENSERVER_FXAOAUTHSERVERURL=https://oauth.accounts.firefox.com&#xA;      SYNCTOKENSERVERFXAMETRICSHASHSECRET=${SYNCSTORAGEMETRICSSECRET}&#xA;      VIRTUALHOST=sync.example.org&#xA;      VIRTUALPORT=8000&#xA;    networks:&#xA;      proxy-tier&#xA;      syncstorage-internal&#xA;    restart: unless-stopped&#xA;    dependson:&#xA;      syncstorage-db:&#xA;        condition: servicehealthy&#xA;      syncstorage-tokendb:&#xA;        condition: servicehealthy&#xA;&#xA;  syncstorage-db:&#xA;    image: postgres:17-alpine&#xA;    containername: syncstorage-db&#xA;    environment:&#xA;      POSTGRESDB=syncstorage&#xA;      POSTGRESUSER=${SYNCSTORAGEDBUSER}&#xA;      POSTGRESPASSWORD=${SYNCSTORAGEDBPASSWORD}&#xA;    volumes:&#xA;      /data/syncstorage/postgres-sync:/var/lib/postgresql/data&#xA;    networks:&#xA;      syncstorage-internal&#xA;    restart: unless-stopped&#xA;    healthcheck:&#xA;      test: [&#34;CMD-SHELL&#34;, &#34;pgisready -U ${SYNCSTORAGEDBUSER} -d syncstorage&#34;]&#xA;      interval: 10s&#xA;      timeout: 5s&#xA;      retries: 5&#xA;&#xA;  syncstorage-tokendb:&#xA;    image: postgres:17-alpine&#xA;    containername: syncstorage-tokendb&#xA;    environment:&#xA;      POSTGRESDB=tokenserver&#xA;      POSTGRESUSER=${SYNCSTORAGEDBUSER}&#xA;      POSTGRESPASSWORD=${SYNCSTORAGEDBPASSWORD}&#xA;    volumes:&#xA;      /data/syncstorage/postgres-token:/var/lib/postgresql/data&#xA;    networks:&#xA;      syncstorage-internal&#xA;    restart: unless-stopped&#xA;    healthcheck:&#xA;      test: [&#34;CMD-SHELL&#34;, &#34;pgisready -U ${SYNCSTORAGEDBUSER} -d tokenserver&#34;]&#xA;      interval: 10s&#xA;      timeout: 5s&#xA;      retries: 5&#xA;&#xA;networks:&#xA;  proxy-tier:&#xA;    external: true&#xA;  syncstorage-internal:&#xA;&#xA;Créez les répertoires et lancez :&#xA;&#xA;sudo mkdir -p /data/syncstorage/postgres-sync /data/syncstorage/postgres-token&#xA;sudo chown -R $(id -u):$(id -g) /data/syncstorage&#xA;docker compose -f docker-compose.yml -f syncstorage.yml up -d&#xA;&#xA;L&#39;étape que personne ne mentionne&#xA;&#xA;Tout démarre, le healthcheck passe, le SSL fonctionne. Je configure Firefox, je me connecte et... erreur 503. Dans les logs Firefox (about:sync-log) :&#xA;&#xA;&#34;Unexpected error: unable to get a node&#34;&#xA;&#xA;J&#39;ai cherché cette erreur pendant un moment. Le problème : le tokenserver a une table nodes qui liste les serveurs de stockage disponibles. Chez Mozilla, il y en a plusieurs pour la répartition de charge. Chez vous, il y en a un. Et il faut le déclarer manuellement.&#xA;&#xA;docker exec syncstorage-tokendb psql -U syncstorage -d tokenserver -c \&#xA;  &#34;INSERT INTO nodes (service, node, available, currentload, capacity, downed, backoff) \&#xA;   VALUES (1, &#39;https://sync.example.org&#39;, 1, 0, 1000000, 0, 0);&#34;&#xA;&#xA;Vérifiez :&#xA;&#xA;docker exec syncstorage-tokendb psql -U syncstorage -d tokenserver -c &#34;SELECT * FROM nodes;&#34;&#xA;&#xA;Après ça, tout fonctionne.&#xA;&#xA;Configurer Firefox&#xA;&#xA;Dans about:config, cherchez identity.sync.tokenserver.uri. Si ça n&#39;existe pas, créez-le (clic droit → Nouveau → Chaîne). Valeur :&#xA;&#xA;https://sync.example.org/1.0/sync/1.5&#xA;&#xA;Redémarrez Firefox. C&#39;est lu au démarrage, pas à chaud.&#xA;&#xA;Si vous étiez déjà connecté avant de changer le tokenserver : déconnectez-vous, redémarrez, reconnectez-vous. Sinon Firefox continue d&#39;utiliser l&#39;ancien serveur en cache.&#xA;&#xA;Vérifier que ça marche&#xA;&#xA;Côté serveur, vous pouvez voir les données synchronisées :&#xA;&#xA;docker exec syncstorage-db psql -U syncstorage -d syncstorage -c &#34;&#xA;SELECT c.name as collection, COUNT(b.bsoid) as items&#xA;FROM bsos b&#xA;JOIN collections c ON b.collectionid = c.collection_id&#xA;GROUP BY c.name&#xA;ORDER BY items DESC;&#34;&#xA;&#xA;Chez moi après la première sync :&#xA;&#xA;    collection     | items&#xA;-------------------+-------&#xA; bookmarks         |    59&#xA; addons            |     8&#xA; tabs              |     2&#xA; clients           |     2&#xA;&#xA;Côté Firefox, about:sync-log doit montrer votre domaine, pas sync.services.mozilla.com.&#xA;&#xA;Ce qui ne marche pas&#xA;&#xA;Pas de migration. Les données qui étaient sur les serveurs Mozilla y restent. La première sync avec votre serveur part de zéro. Si vous avez des bookmarks sur deux machines avec des serveurs différents, vous aurez des fusions intéressantes.&#xA;&#xA;Pas de comptes locaux. Vous dépendez toujours de Firefox Accounts pour l&#39;authentification.&#xA;&#xA;Conclusion&#xA;&#xA;Ça marche. Mes deux Firefox (un Mac, un Linux) synchronisent via mon serveur. L&#39;installation prend 20 minutes si vous connaissez l&#39;astuce du node, une heure sinon.&#xA;&#xA;Astuce: demandez à votre assistant IA de faire le job en mentionnant cette article !&#xA;&#xA;---&#xA;&#xA;Sources&#xA;&#xA;syncstorage-rs — le repo GitHub de Mozilla&#xA;Documentation officielle&#xA;Firefox Sync Privacy — comment le chiffrement fonctionne&#xA;Images Docker — les tags disponibles&#xA;docker-compose-homelab — La base de mon setup]]&gt;</description>
      <content:encoded><![CDATA[<p>J&#39;utilise Firefox Sync depuis des années pour retrouver mes marque-pages et mots de passe sur tous mes appareils. Ça marche bien. Mais récemment, je me suis demandé : est-ce que je peux héberger ça moi-même ?<br>
<br>
Spoiler : oui. Mais j&#39;ai passé une heure à debugger une erreur que la documentation ne mentionne nulle part.</p>

<h2 id="pourquoi-faire-ça">Pourquoi faire ça ?</h2>

<p>Honnêtement ? Surtout par curiosité. Mozilla chiffre déjà les données côté client, donc même eux ne peuvent pas les lire (<a href="https://hacks.mozilla.org/2018/11/firefox-sync-privacy/">source</a>). L&#39;auto-hébergement ne change pas grand-chose à la vie privée — vos données étaient déjà illisibles.</p>

<p>Mais ça fait un service de moins qui dépend d&#39;un tiers. Et c&#39;était un bon prétexte pour apprendre comment Firefox Sync fonctionne.</p>

<h2 id="ce-qu-il-faut-savoir-avant-de-commencer">Ce qu&#39;il faut savoir avant de commencer</h2>

<p>Mozilla a open-sourcé le serveur sous le nom <a href="https://github.com/mozilla-services/syncstorage-rs">syncstorage-rs</a>. C&#39;est du Rust, ça supporte PostgreSQL, MySQL ou Spanner.</p>

<p>Le truc un peu décevant : l&#39;authentification reste chez Mozilla. Vous avez toujours besoin d&#39;un compte Firefox pour vous connecter. Impossible de créer des comptes locaux. Votre serveur stocke les données, mais Mozilla gère l&#39;identité.</p>

<pre><code>Firefox → accounts.firefox.com (Mozilla) → votre serveur
            ↑ auth                           ↑ stockage
</code></pre>

<h2 id="prérequis">Prérequis</h2>
<ul><li>Un serveur avec Docker</li>
<li>Un sous-domaine (par exemple <code>ffsync.votredomaine.org</code>)</li>
<li>Un reverse proxy avec SSL (nginx-proxy dans mon cas)</li>
<li>Un compte Firefox</li></ul>

<p>Si vous partez de zéro, j&#39;ai publié: <a href="https://github.com/jblemee/docker-compose-homelab">docker-compose-homelab</a>. C&#39;est le setup que j&#39;utilise au quotidien : nginx-proxy pour le SSL automatique, et une architecture où chaque service est dans son propre fichier YAML.</p>

<h2 id="l-installation">L&#39;installation</h2>

<h3 id="dns">DNS</h3>

<p>Rien de spécial, un enregistrement A vers votre serveur :</p>

<pre><code>sync.example.org    A       1.2.3.4
</code></pre>

<h3 id="secrets">Secrets</h3>

<p>Générez trois secrets :</p>

<pre><code class="language-bash">openssl rand -hex 32      # master secret
openssl rand -hex 32      # metrics secret
openssl rand -base64 24   # mot de passe postgres
</code></pre>

<p>Mettez-les dans <code>.env</code> :</p>

<pre><code class="language-bash">SYNCSTORAGE_MASTER_SECRET=...
SYNCSTORAGE_METRICS_SECRET=...
SYNCSTORAGE_DB_USER=syncstorage
SYNCSTORAGE_DB_PASSWORD=...
</code></pre>

<h3 id="docker-compose">Docker Compose</h3>

<p>Voici le fichier complet. Un point important : l&#39;image Docker <code>mozilla/syncstorage-rs:latest</code> ne marche pas avec PostgreSQL. Elle est compilée pour Spanner. Il faut utiliser une image avec le suffixe <code>-postgres</code>. J&#39;ai perdu du temps là-dessus.</p>

<pre><code class="language-yaml">services:
  syncstorage:
    image: mozilla/syncstorage-rs:11659d98f9c69948a0aab353437ce2036c388711-postgres
    container_name: syncstorage
    environment:
      - SYNC_HOST=0.0.0.0
      - SYNC_MASTER_SECRET=${SYNCSTORAGE_MASTER_SECRET}
      - SYNC_SYNCSTORAGE__DATABASE_URL=postgres://${SYNCSTORAGE_DB_USER}:${SYNCSTORAGE_DB_PASSWORD}@syncstorage-db:5432/syncstorage
      - SYNC_SYNCSTORAGE__ENABLED=true
      - SYNC_TOKENSERVER__DATABASE_URL=postgres://${SYNCSTORAGE_DB_USER}:${SYNCSTORAGE_DB_PASSWORD}@syncstorage-tokendb:5432/tokenserver
      - SYNC_TOKENSERVER__ENABLED=true
      - SYNC_TOKENSERVER__RUN_MIGRATIONS=true
      - SYNC_TOKENSERVER__FXA_EMAIL_DOMAIN=api.accounts.firefox.com
      - SYNC_TOKENSERVER__FXA_OAUTH_SERVER_URL=https://oauth.accounts.firefox.com
      - SYNC_TOKENSERVER__FXA_METRICS_HASH_SECRET=${SYNCSTORAGE_METRICS_SECRET}
      - VIRTUAL_HOST=sync.example.org
      - VIRTUAL_PORT=8000
    networks:
      - proxy-tier
      - syncstorage-internal
    restart: unless-stopped
    depends_on:
      syncstorage-db:
        condition: service_healthy
      syncstorage-tokendb:
        condition: service_healthy

  syncstorage-db:
    image: postgres:17-alpine
    container_name: syncstorage-db
    environment:
      - POSTGRES_DB=syncstorage
      - POSTGRES_USER=${SYNCSTORAGE_DB_USER}
      - POSTGRES_PASSWORD=${SYNCSTORAGE_DB_PASSWORD}
    volumes:
      - /data/syncstorage/postgres-sync:/var/lib/postgresql/data
    networks:
      - syncstorage-internal
    restart: unless-stopped
    healthcheck:
      test: [&#34;CMD-SHELL&#34;, &#34;pg_isready -U ${SYNCSTORAGE_DB_USER} -d syncstorage&#34;]
      interval: 10s
      timeout: 5s
      retries: 5

  syncstorage-tokendb:
    image: postgres:17-alpine
    container_name: syncstorage-tokendb
    environment:
      - POSTGRES_DB=tokenserver
      - POSTGRES_USER=${SYNCSTORAGE_DB_USER}
      - POSTGRES_PASSWORD=${SYNCSTORAGE_DB_PASSWORD}
    volumes:
      - /data/syncstorage/postgres-token:/var/lib/postgresql/data
    networks:
      - syncstorage-internal
    restart: unless-stopped
    healthcheck:
      test: [&#34;CMD-SHELL&#34;, &#34;pg_isready -U ${SYNCSTORAGE_DB_USER} -d tokenserver&#34;]
      interval: 10s
      timeout: 5s
      retries: 5

networks:
  proxy-tier:
    external: true
  syncstorage-internal:
</code></pre>

<p>Créez les répertoires et lancez :</p>

<pre><code class="language-bash">sudo mkdir -p /data/syncstorage/postgres-sync /data/syncstorage/postgres-token
sudo chown -R $(id -u):$(id -g) /data/syncstorage
docker compose -f docker-compose.yml -f syncstorage.yml up -d
</code></pre>

<h3 id="l-étape-que-personne-ne-mentionne">L&#39;étape que personne ne mentionne</h3>

<p>Tout démarre, le healthcheck passe, le SSL fonctionne. Je configure Firefox, je me connecte et... erreur 503. Dans les logs Firefox (<code>about:sync-log</code>) :</p>

<pre><code>&#34;Unexpected error: unable to get a node&#34;
</code></pre>

<p>J&#39;ai cherché cette erreur pendant un moment. Le problème : le tokenserver a une table <code>nodes</code> qui liste les serveurs de stockage disponibles. Chez Mozilla, il y en a plusieurs pour la répartition de charge. Chez vous, il y en a un. Et il faut le déclarer manuellement.</p>

<pre><code class="language-bash">docker exec syncstorage-tokendb psql -U syncstorage -d tokenserver -c \
  &#34;INSERT INTO nodes (service, node, available, current_load, capacity, downed, backoff) \
   VALUES (1, &#39;https://sync.example.org&#39;, 1, 0, 1000000, 0, 0);&#34;
</code></pre>

<p>Vérifiez :</p>

<pre><code class="language-bash">docker exec syncstorage-tokendb psql -U syncstorage -d tokenserver -c &#34;SELECT * FROM nodes;&#34;
</code></pre>

<p>Après ça, tout fonctionne.</p>

<h2 id="configurer-firefox">Configurer Firefox</h2>

<p>Dans <code>about:config</code>, cherchez <code>identity.sync.tokenserver.uri</code>. Si ça n&#39;existe pas, créez-le (clic droit → Nouveau → Chaîne). Valeur :</p>

<pre><code>https://sync.example.org/1.0/sync/1.5
</code></pre>

<p>Redémarrez Firefox. C&#39;est lu au démarrage, pas à chaud.</p>

<p>Si vous étiez déjà connecté avant de changer le tokenserver : déconnectez-vous, redémarrez, reconnectez-vous. Sinon Firefox continue d&#39;utiliser l&#39;ancien serveur en cache.</p>

<h2 id="vérifier-que-ça-marche">Vérifier que ça marche</h2>

<p>Côté serveur, vous pouvez voir les données synchronisées :</p>

<pre><code class="language-bash">docker exec syncstorage-db psql -U syncstorage -d syncstorage -c &#34;
SELECT c.name as collection, COUNT(b.bso_id) as items
FROM bsos b
JOIN collections c ON b.collection_id = c.collection_id
GROUP BY c.name
ORDER BY items DESC;&#34;
</code></pre>

<p>Chez moi après la première sync :</p>

<pre><code>    collection     | items
-------------------+-------
 bookmarks         |    59
 addons            |     8
 tabs              |     2
 clients           |     2
</code></pre>

<p>Côté Firefox, <code>about:sync-log</code> doit montrer votre domaine, pas <code>sync.services.mozilla.com</code>.</p>

<h2 id="ce-qui-ne-marche-pas">Ce qui ne marche pas</h2>

<p>Pas de migration. Les données qui étaient sur les serveurs Mozilla y restent. La première sync avec votre serveur part de zéro. Si vous avez des bookmarks sur deux machines avec des serveurs différents, vous aurez des fusions intéressantes.</p>

<p>Pas de comptes locaux. Vous dépendez toujours de Firefox Accounts pour l&#39;authentification.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Ça marche. Mes deux Firefox (un Mac, un Linux) synchronisent via mon serveur. L&#39;installation prend 20 minutes si vous connaissez l&#39;astuce du node, une heure sinon.</p>

<p>Astuce: demandez à votre assistant IA de faire le job en mentionnant cette article !</p>

<hr>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://github.com/mozilla-services/syncstorage-rs">syncstorage-rs</a> — le repo GitHub de Mozilla</li>
<li><a href="https://mozilla-services.github.io/syncstorage-rs/">Documentation officielle</a></li>
<li><a href="https://hacks.mozilla.org/2018/11/firefox-sync-privacy/">Firefox Sync Privacy</a> — comment le chiffrement fonctionne</li>
<li><a href="https://hub.docker.com/r/mozilla/syncstorage-rs/tags">Images Docker</a> — les tags disponibles</li>
<li><a href="https://github.com/jblemee/docker-compose-homelab">docker-compose-homelab</a> — La base de mon setup</li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/heberger-son-propre-serveur-firefox-sync</guid>
      <pubDate>Wed, 28 Jan 2026 18:09:15 +0000</pubDate>
    </item>
    <item>
      <title>Z.ai vs Claude Max : le vrai coût pour programmer &#34;sans les mains&#34;</title>
      <link>https://blog.ut0pia.org/z-ai-vs-claude-max-le-vrai-cout-pour-programmer-sans-les-mains</link>
      <description>&lt;![CDATA[Pour les développeurs qui paient un assistant IA chaque mois, un problème passe souvent inaperçu : le rapport qualité/prix entre les différentes offres varie considérablement selon l&#39;usage.&#xA;!--more--&#xA;Comment fonctionnent les limites de Claude Max&#xA;&#xA;Claude Max fonctionne sur un système de cycles de 5 heures qui se réinitialisent automatiquement. &#xA;&#xA;Voici ce que dit la documentation officielle :&#xA;&#xA;Les limites d&#39;utilisation par session se réinitialisent toutes les 5 heures&#xA;Anthropic peut appliquer des plafonds hebdomadaires (documentés) et mensuels (à sa discrétion)&#xA;Il n&#39;existe pas de nombre maximum de cycles par mois&#xA;&#xA;Z.ai fonctionne de manière similaire : des cycles de 5 heures sans limite mensuelle structurelle.&#xA;&#xA;Les chiffres réels&#xA;&#xA;Pour un développeur qui utilise Claude Code ou un outil similaire, voici ce que chaque plan permet réellement.&#xA;&#xA;Claude Max 5x (100 $/mois)&#xA;&#xA;Par fenêtre de 5 heures : 50 à 200 prompts avec Claude Code&#xA;Limite hebdomadaire : 140-280 heures de Sonnet 4&#xA;Par mois (théorique, ~288 cycles) : jusqu&#39;à ~57 600 prompts&#xA;&#xA;Source : Support Claude&#xA;&#xA;Claude Max 20x (200 $/mois)&#xA;&#xA;Par fenêtre de 5 heures : 200 à 800 prompts avec Claude Code&#xA;Limite hebdomadaire : 240-480 heures de Sonnet 4&#xA;Par mois (théorique, ~288 cycles) : jusqu&#39;à ~230 400 prompts&#xA;&#xA;Source : Support Claude&#xA;&#xA;Z.ai Lite (3 $/mois)&#xA;&#xA;Par fenêtre de 5 heures : 120 prompts&#xA;Par mois (illimité) : 288 cycles × 120 = ~34 560 prompts&#xA;&#xA;Source : Documentation Z.ai&#xA;&#xA;Z.ai Pro (15 $/mois)&#xA;&#xA;Par fenêtre de 5 heures : 600 prompts&#xA;Par mois (illimité) : 288 cycles × 600 = ~172 800 prompts&#xA;&#xA;Le facteur multiplicateur de Z.ai&#xA;&#xA;Z.ai compte ses unités différemment de Claude.&#xA;&#xA;Quand Z.ai annonce &#34;120 prompts par 5 heures&#34;, chaque prompt Z.ai se traduit par 15 à 20 appels modèle, selon leur documentation. C&#39;est ce qui détermine le travail réellement réalisable.&#xA;&#xA;Les calculs par cycle de 5 heures :&#xA;&#xA;Z.ai Lite : 120 prompts × 18 appels = 2 160 appels du modèle&#xA;Z.ai Pro : 600 prompts × 18 appels = 10 800 appels du modèle&#xA;Claude Max 5x : 200 prompts = 200 appels du modèle&#xA;Claude Max 20x : 800 prompts = 800 appels du modèle&#xA;&#xA;Si cette métrique &#34;appels du modèle&#34; est pertinente pour votre usage, Z.ai offre effectivement plus de capacité brute par cycle.&#xA;&#xA;Comparaison globale&#xA;&#xA;| Plan | Prix/mois | Prompts max/mois | Coût/1000 prompts | Appels modèle/mois |&#xA;|------|-----------|------------------|-------------------|---------------------|&#xA;| Z.ai Lite | 3 $ | ~34 560 | 0,09 $ | ~622 000 |&#xA;| Z.ai Pro | 15 $ | ~172 800 | 0,09 $ | ~3 110 000 |&#xA;| Claude Max 5x | 100 $ | ~57 600 | 1,74 $ | ~57 600 |&#xA;| Claude Max 20x | 200 $ | ~230 400 | 0,87 $ | ~230 400 |&#xA;&#xA;\ Les &#34;appels modèle&#34; pour Z.ai supposent un facteur 18× selon leur documentation. Pour Claude, 1 prompt = 1 appel.&#xA;&#xA;Analyse :&#xA;&#xA;En volume brut de prompts, Claude Max 20x offre plus que Z.ai Lite (230 400 vs 34 560)&#xA;En coût par prompt, Z.ai est ~10-20× moins cher&#xA;Si le facteur multiplicateur Z.ai est réel, leur avantage en &#34;travail effectif&#34; est significatif&#xA;&#xA;La vraie différence : les limites hebdomadaires&#xA;&#xA;Claude Max impose des limites hebdomadaires documentées :&#xA;&#xA;Max 5x : 140-280 heures de Sonnet 4 par semaine&#xA;Max 20x : 240-480 heures de Sonnet 4 par semaine&#xA;&#xA;Ces limites sont généreuses pour la plupart des usages, mais peuvent être atteintes lors de sprints intensifs. Z.ai ne documente pas de telles limites hebdomadaires.&#xA;&#xA;La qualité du modèle&#xA;&#xA;C&#39;est la question clé. Z.ai utilise GLM-4.7, Claude Max propose Sonnet et Opus.&#xA;&#xA;En programmation, GLM-4.7 et Claude Sonnet 4.5 ont des performances similaires sur les benchmarks : 73,8 % pour GLM-4.7 sur SWE-bench Verified contre environ 77 % pour Sonnet 4.5. Cet écart reste marginal en pratique pour la plupart des tâches.&#xA;&#xA;L&#39;avantage de Claude Max reste l&#39;accès à Opus, qui excelle sur les problèmes d&#39;architecture complexe et le raisonnement avancé.&#xA;&#xA;---&#xA;&#xA;Intégration avec Claude Code : GLM CLI&#xA;&#xA;L&#39;avantage concret de Z.ai vient de son intégration avec Claude Code. Le projet xqsit94/glm offre un outil CLI simple qui élimine toute friction.&#xA;&#xA;Installation&#xA;&#xA;GLM CLI s&#39;installe en une ligne :&#xA;&#xA;curl -fsSL https://raw.githubusercontent.com/xqsit94/glm/main/install.sh | bash&#xA;&#xA;Pas de configuration Docker, pas de variable d&#39;environnement complexe, pas de fichiers de config à modifier.&#xA;&#xA;Utilisation&#xA;&#xA;Après installation et configuration du token Z.ai :&#xA;&#xA;Lancer Claude Code avec GLM-4.7 par défaut&#xA;glm&#xA;&#xA;Ou spécifier une version antérieure&#xA;glm --model glm-4.5-air&#xA;&#xA;L&#39;approche par session&#xA;&#xA;GLM CLI utilise une approche temporaire : les paramètres du modèle ne s&#39;appliquent que pour la session Claude Code lancée. Une fois fermée, Claude Code revient à son défaut.&#xA;&#xA;Pas de pollution de la configuration globale&#xA;Sélection granulaire entre sessions&#xA;Pas de nettoyage nécessaire&#xA;&#xA;Compatibilité&#xA;&#xA;GLM-4.7 fonctionne avec Claude Code, Cline, Roo Code, Kilo Code, OpenCode et d&#39;autres agents. Support des appels d&#39;outils natifs.&#xA;&#xA;---&#xA;&#xA;Quand Claude Max reste pertinent&#xA;&#xA;Z.ai ne surpasse pas Claude Max dans tous les cas :&#xA;&#xA;Accès à Opus. Pour les problèmes d&#39;architecture complexe, les bugs subtils et le raisonnement avancé, Opus reste supérieur.&#xA;&#xA;Limites généreuses. Pour un usage modéré à intensif, les limites hebdomadaires de Claude Max (140-480 heures de Sonnet/semaine) sont rarement atteintes.&#xA;&#xA;Stabilité et support. Anthropic offre une infrastructure mature et un support établi.&#xA;&#xA;Qualité du modèle. Sonnet 4.5 reste légèrement supérieur à GLM-4.7 sur les benchmarks.&#xA;&#xA;L&#39;approche hybride&#xA;&#xA;Pour maximiser le rapport qualité/prix :&#xA;&#xA;Z.ai pour le travail quotidien (débogage, refactorisation, implémentation)&#xA;Claude Max 5x pour les problèmes complexes nécessitant Opus&#xA;&#xA;Coût total mensuel : 103-115 $ selon le plan Z.ai choisi.&#xA;&#xA;Cette approche donne :&#xA;&#xA;Accès à GLM-4.7 pour la majorité des tâches à très faible coût&#xA;Accès à Opus pour les défis architecturaux&#xA;Plus de flexibilité qu&#39;avec Max seul&#xA;&#xA;Verdict&#xA;&#xA;| Profil | Recommandation | Coût |&#xA;|--------|----------------|------|&#xA;| Travail occasionnel | Z.ai Lite seul | 3 $/mois |&#xA;| Travail régulier | Z.ai Pro seul | 15 $/mois |&#xA;| Travail intensif + besoin d&#39;Opus | Z.ai Pro + Claude Max 5x | 115 $/mois |&#xA;| Budget disponible, simplicité | Claude Max 20x seul | 200 $/mois |&#xA;&#xA;Le contexte a changé. GLM-4.7 offre une alternative viable pour de nombreuses tâches de programmation. Mais contrairement à ce qui est parfois affirmé, Claude Max n&#39;impose pas de limite stricte de sessions mensuelles — les deux services fonctionnent sur des cycles qui se réinitialisent.&#xA;&#xA;La vraie question devient : avez-vous besoin d&#39;Opus et de la qualité supérieure de Sonnet, ou le rapport qualité/prix de Z.ai suffit-il pour votre usage ?&#xA;&#xA;---&#xA;&#xA;Notes importantes&#xA;&#xA;Les prix et limites correspondent à la situation de janvier 2026. Z.ai propose actuellement une promotion : réduction de 50 % le premier mois.&#xA;&#xA;Les deux prestataires évoluent rapidement. Vérifiez toujours la documentation officielle pour les informations les plus récentes.&#xA;&#xA;---&#xA;&#xA;Sources&#xA;&#xA;Documentation Z.ai – Plans&#xA;Support Claude Max – Limites d&#39;utilisation&#xA;Support Claude – Utilisation avec Claude Code&#xA;Pricing Z.ai&#xA;Benchmarks SWE-bench – GLM-4.7]]&gt;</description>
      <content:encoded><![CDATA[<p>Pour les développeurs qui paient un assistant IA chaque mois, un problème passe souvent inaperçu : le rapport qualité/prix entre les différentes offres varie considérablement selon l&#39;usage.
</p>

<h2 id="comment-fonctionnent-les-limites-de-claude-max">Comment fonctionnent les limites de Claude Max</h2>

<p>Claude Max fonctionne sur un système de <strong>cycles de 5 heures</strong> qui se réinitialisent automatiquement.</p>

<p>Voici ce que dit la <a href="https://support.claude.com/en/articles/11014257-about-claude-s-max-plan-usage">documentation officielle</a> :</p>
<ul><li>Les limites d&#39;utilisation par session se réinitialisent <strong>toutes les 5 heures</strong></li>
<li>Anthropic peut appliquer des <strong>plafonds hebdomadaires</strong> (documentés) et mensuels (à sa discrétion)</li>
<li>Il n&#39;existe pas de nombre maximum de cycles par mois</li></ul>

<p>Z.ai fonctionne de manière similaire : des cycles de 5 heures sans limite mensuelle structurelle.</p>

<h2 id="les-chiffres-réels">Les chiffres réels</h2>

<p>Pour un développeur qui utilise Claude Code ou un outil similaire, voici ce que chaque plan permet réellement.</p>

<h3 id="claude-max-5x-100-mois">Claude Max 5x (100 $/mois)</h3>
<ul><li>Par fenêtre de 5 heures : 50 à 200 prompts avec Claude Code</li>
<li>Limite hebdomadaire : 140-280 heures de Sonnet 4</li>
<li>Par mois (théorique, ~288 cycles) : <strong>jusqu&#39;à ~57 600 prompts</strong></li></ul>

<p><a href="https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan">Source : Support Claude</a></p>

<h3 id="claude-max-20x-200-mois">Claude Max 20x (200 $/mois)</h3>
<ul><li>Par fenêtre de 5 heures : 200 à 800 prompts avec Claude Code</li>
<li>Limite hebdomadaire : 240-480 heures de Sonnet 4</li>
<li>Par mois (théorique, ~288 cycles) : <strong>jusqu&#39;à ~230 400 prompts</strong></li></ul>

<p><a href="https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan">Source : Support Claude</a></p>

<h3 id="z-ai-lite-3-mois">Z.ai Lite (3 $/mois)</h3>
<ul><li>Par fenêtre de 5 heures : 120 prompts</li>
<li>Par mois (illimité) : 288 cycles × 120 = <strong>~34 560 prompts</strong></li></ul>

<p><a href="https://docs.z.ai/devpack/overview">Source : Documentation Z.ai</a></p>

<h3 id="z-ai-pro-15-mois">Z.ai Pro (15 $/mois)</h3>
<ul><li>Par fenêtre de 5 heures : 600 prompts</li>
<li>Par mois (illimité) : 288 cycles × 600 = <strong>~172 800 prompts</strong></li></ul>

<h2 id="le-facteur-multiplicateur-de-z-ai">Le facteur multiplicateur de Z.ai</h2>

<p>Z.ai compte ses unités différemment de Claude.</p>

<p>Quand Z.ai annonce “120 prompts par 5 heures”, chaque prompt Z.ai se traduit par 15 à 20 appels modèle, selon <a href="https://docs.z.ai/devpack/overview">leur documentation</a>. C&#39;est ce qui détermine le travail réellement réalisable.</p>

<p>Les calculs par cycle de 5 heures :</p>
<ul><li>Z.ai Lite : 120 prompts × 18 appels = <strong>2 160 appels du modèle</strong></li>
<li>Z.ai Pro : 600 prompts × 18 appels = <strong>10 800 appels du modèle</strong></li>
<li>Claude Max 5x : 200 prompts = <strong>200 appels du modèle</strong></li>
<li>Claude Max 20x : 800 prompts = <strong>800 appels du modèle</strong></li></ul>

<p>Si cette métrique “appels du modèle” est pertinente pour votre usage, Z.ai offre effectivement plus de capacité brute par cycle.</p>

<h2 id="comparaison-globale">Comparaison globale</h2>

<table>
<thead>
<tr>
<th>Plan</th>
<th>Prix/mois</th>
<th>Prompts max/mois</th>
<th>Coût/1000 prompts</th>
<th>Appels modèle/mois*</th>
</tr>
</thead>

<tbody>
<tr>
<td>Z.ai Lite</td>
<td>3 $</td>
<td>~34 560</td>
<td><strong>0,09 $</strong></td>
<td>~622 000</td>
</tr>

<tr>
<td>Z.ai Pro</td>
<td>15 $</td>
<td>~172 800</td>
<td><strong>0,09 $</strong></td>
<td>~3 110 000</td>
</tr>

<tr>
<td>Claude Max 5x</td>
<td>100 $</td>
<td>~57 600</td>
<td><strong>1,74 $</strong></td>
<td>~57 600</td>
</tr>

<tr>
<td>Claude Max 20x</td>
<td>200 $</td>
<td>~230 400</td>
<td><strong>0,87 $</strong></td>
<td>~230 400</td>
</tr>
</tbody>
</table>

<p><em>* Les “appels modèle” pour Z.ai supposent un facteur 18× selon leur documentation. Pour Claude, 1 prompt = 1 appel.</em></p>

<p><strong>Analyse :</strong></p>
<ul><li>En volume brut de prompts, <strong>Claude Max 20x offre plus</strong> que Z.ai Lite (230 400 vs 34 560)</li>
<li>En coût par prompt, <strong>Z.ai est ~10-20× moins cher</strong></li>
<li>Si le facteur multiplicateur Z.ai est réel, leur avantage en “travail effectif” est significatif</li></ul>

<h2 id="la-vraie-différence-les-limites-hebdomadaires">La vraie différence : les limites hebdomadaires</h2>

<p>Claude Max impose des <strong>limites hebdomadaires documentées</strong> :</p>
<ul><li>Max 5x : 140-280 heures de Sonnet 4 par semaine</li>
<li>Max 20x : 240-480 heures de Sonnet 4 par semaine</li></ul>

<p>Ces limites sont généreuses pour la plupart des usages, mais peuvent être atteintes lors de sprints intensifs. Z.ai ne documente pas de telles limites hebdomadaires.</p>

<h2 id="la-qualité-du-modèle">La qualité du modèle</h2>

<p>C&#39;est la question clé. Z.ai utilise GLM-4.7, Claude Max propose Sonnet et Opus.</p>

<p>En programmation, GLM-4.7 et Claude Sonnet 4.5 ont des performances similaires sur les benchmarks : <a href="https://z.ai/blog/glm-4.7?ic=JNKCQ86XJI">73,8 % pour GLM-4.7 sur SWE-bench Verified</a> contre environ 77 % pour Sonnet 4.5. Cet écart reste marginal en pratique pour la plupart des tâches.</p>

<p>L&#39;avantage de Claude Max reste l&#39;<strong>accès à Opus</strong>, qui excelle sur les problèmes d&#39;architecture complexe et le raisonnement avancé.</p>

<hr>

<h2 id="intégration-avec-claude-code-glm-cli">Intégration avec Claude Code : GLM CLI</h2>

<p>L&#39;avantage concret de Z.ai vient de son intégration avec Claude Code. <a href="https://github.com/xqsit94/glm">Le projet xqsit94/glm</a> offre un outil CLI simple qui élimine toute friction.</p>

<h3 id="installation">Installation</h3>

<p><a href="https://github.com/xqsit94/glm">GLM CLI</a> s&#39;installe en une ligne :</p>

<pre><code class="language-bash">curl -fsSL https://raw.githubusercontent.com/xqsit94/glm/main/install.sh | bash
</code></pre>

<p>Pas de configuration Docker, pas de variable d&#39;environnement complexe, pas de fichiers de config à modifier.</p>

<h3 id="utilisation">Utilisation</h3>

<p>Après installation et <a href="https://github.com/xqsit94/glm">configuration du token Z.ai</a> :</p>

<pre><code class="language-bash"># Lancer Claude Code avec GLM-4.7 par défaut
glm

# Ou spécifier une version antérieure
glm --model glm-4.5-air
</code></pre>

<h3 id="l-approche-par-session">L&#39;approche par session</h3>

<p><a href="https://github.com/xqsit94/glm">GLM CLI utilise une approche temporaire</a> : les paramètres du modèle ne s&#39;appliquent que pour la session Claude Code lancée. Une fois fermée, Claude Code revient à son défaut.</p>
<ul><li>Pas de pollution de la configuration globale</li>
<li>Sélection granulaire entre sessions</li>
<li>Pas de nettoyage nécessaire</li></ul>

<h3 id="compatibilité">Compatibilité</h3>

<p><a href="https://z.ai/blog/glm-4.7">GLM-4.7 fonctionne avec Claude Code, Cline, Roo Code, Kilo Code, OpenCode et d&#39;autres agents</a>. Support des appels d&#39;outils natifs.</p>

<hr>

<h2 id="quand-claude-max-reste-pertinent">Quand Claude Max reste pertinent</h2>

<p>Z.ai ne surpasse pas Claude Max dans tous les cas :</p>
<ol><li><p><strong>Accès à Opus.</strong> Pour les problèmes d&#39;architecture complexe, les bugs subtils et le raisonnement avancé, Opus reste supérieur.</p></li>

<li><p><strong>Limites généreuses.</strong> Pour un usage modéré à intensif, les limites hebdomadaires de Claude Max (140-480 heures de Sonnet/semaine) sont rarement atteintes.</p></li>

<li><p><strong>Stabilité et support.</strong> Anthropic offre une infrastructure mature et un support établi.</p></li>

<li><p><strong>Qualité du modèle.</strong> Sonnet 4.5 reste légèrement supérieur à GLM-4.7 sur les benchmarks.</p></li></ol>

<h2 id="l-approche-hybride">L&#39;approche hybride</h2>

<p>Pour maximiser le rapport qualité/prix :</p>
<ul><li><strong>Z.ai</strong> pour le travail quotidien (débogage, refactorisation, implémentation)</li>
<li><strong>Claude Max 5x</strong> pour les problèmes complexes nécessitant Opus</li></ul>

<p><strong>Coût total mensuel : 103-115 $</strong> selon le plan Z.ai choisi.</p>

<p>Cette approche donne :</p>
<ul><li>Accès à GLM-4.7 pour la majorité des tâches à très faible coût</li>
<li>Accès à Opus pour les défis architecturaux</li>
<li>Plus de flexibilité qu&#39;avec Max seul</li></ul>

<h2 id="verdict">Verdict</h2>

<table>
<thead>
<tr>
<th>Profil</th>
<th>Recommandation</th>
<th>Coût</th>
</tr>
</thead>

<tbody>
<tr>
<td>Travail occasionnel</td>
<td>Z.ai Lite seul</td>
<td>3 $/mois</td>
</tr>

<tr>
<td>Travail régulier</td>
<td>Z.ai Pro seul</td>
<td>15 $/mois</td>
</tr>

<tr>
<td>Travail intensif + besoin d&#39;Opus</td>
<td>Z.ai Pro + Claude Max 5x</td>
<td>115 $/mois</td>
</tr>

<tr>
<td>Budget disponible, simplicité</td>
<td>Claude Max 20x seul</td>
<td>200 $/mois</td>
</tr>
</tbody>
</table>

<p><strong>Le contexte a changé.</strong> GLM-4.7 offre une alternative viable pour de nombreuses tâches de programmation. Mais contrairement à ce qui est parfois affirmé, Claude Max n&#39;impose pas de limite stricte de sessions mensuelles — les deux services fonctionnent sur des cycles qui se réinitialisent.</p>

<p>La vraie question devient : <strong>avez-vous besoin d&#39;Opus et de la qualité supérieure de Sonnet, ou le rapport qualité/prix de Z.ai suffit-il pour votre usage ?</strong></p>

<hr>

<h2 id="notes-importantes">Notes importantes</h2>

<p>Les prix et limites correspondent à la situation de janvier 2026. Z.ai propose actuellement une promotion : réduction de 50 % le premier mois.</p>

<p>Les deux prestataires évoluent rapidement. Vérifiez toujours la documentation officielle pour les informations les plus récentes.</p>

<hr>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://docs.z.ai/devpack/overview?ic=JNKCQ86XJI">Documentation Z.ai – Plans</a></li>
<li><a href="https://support.claude.com/en/articles/11014257-about-claude-s-max-plan-usage">Support Claude Max – Limites d&#39;utilisation</a></li>
<li><a href="https://support.claude.com/en/articles/11145838-using-claude-code-with-your-pro-or-max-plan">Support Claude – Utilisation avec Claude Code</a></li>
<li><a href="https://z.ai/subscribe?ic=JNKCQ86XJI">Pricing Z.ai</a></li>
<li><a href="https://z.ai/blog/glm-4.7?ic=JNKCQ86XJI">Benchmarks SWE-bench – GLM-4.7</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/z-ai-vs-claude-max-le-vrai-cout-pour-programmer-sans-les-mains</guid>
      <pubDate>Sat, 24 Jan 2026 16:56:25 +0000</pubDate>
    </item>
    <item>
      <title>Le développeur face à l&#39;IA : sommes-nous les tisserands de 1811 ?</title>
      <link>https://blog.ut0pia.org/le-developpeur-face-a-lia-sommes-nous-les-tisserands-de-1811</link>
      <description>&lt;![CDATA[&#34;Claude Code c&#39;est de l&#39;héroïne pour programmeur.&#34;&#xA;&#xA;J&#39;ai posté ça sur Mastodon début janvier 2026. Une formule volontairement provocante, mais qui résume assez bien mon année 2025. Une année où ma productivité a été multipliée par deux, peut-être par dix selon comment on compte.&#xA;&#xA;Mais je me suis aussi demandé : est-ce que je ne serai pas en train de scier la branche sur laquelle je suis assis ?&#xA;&#xA;!--more--&#xA;&#xA;Les fantômes des révolutions passées&#xA;&#xA;Quand on parle d&#39;IA et d&#39;emploi, les comparaisons historiques fusent. &#34;C&#39;est comme l&#39;arrivée de l&#39;électricité !&#34; &#34;C&#39;est comme Internet !&#34; Soit. Mais si on gratte un peu, ces révolutions industrielles ont des leçons bien plus nuancées à nous offrir.&#xA;&#xA;1811 : Les Luddites n&#39;étaient pas des idiots&#xA;&#xA;L&#39;histoire officielle a fait des Luddites des technophobes arriérés, des casseurs de machines incapables de voir le progrès. C&#39;est un mythe bien pratique.&#xA;&#xA;En réalité, comme le rappelle le MIT Technology Review, les Luddites étaient des artisans qualifiés (tisserands, tricoteurs) qui voyaient très bien ce qui se passait. Ils n&#39;étaient pas contre les machines. Ils étaient contre l&#39;utilisation des machines pour dégrader leur travail et casser leurs salaires.&#xA;&#xA;Leur leader, George Mellor, avait cette formule : &#34;the tendency&#39;s all one way&#34;, la tendance va dans un seul sens. Celui de la concentration des richesses au détriment des travailleurs.&#xA;&#xA;Ça ne vous rappelle rien ?&#xA;&#xA;40 ans de galère avant l&#39;amélioration&#xA;&#xA;Voici ce qu&#39;on oublie souvent : pendant la première révolution industrielle en Angleterre, les salaires réels ont stagné pendant 40 ans alors que la productivité explosait. Quarante ans. Deux générations de travailleurs ont vu leur niveau de vie se dégrader pendant que les propriétaires d&#39;usines s&#39;enrichissaient.&#xA;&#xA;Les choses se sont améliorées. Mais pas toutes seules. Il a fallu des grèves, des syndicats, des lois sociales arrachées de haute lutte. Le progrès technique n&#39;a pas automatiquement ruisselé vers les travailleurs.&#xA;&#xA;Ford et le paradoxe du salaire&#xA;&#xA;Deuxième révolution industrielle, début du XXe siècle. Henry Ford installe ses chaînes de montage. Les ouvriers perdent toute autonomie : un geste, répété des centaines de fois par jour, sur un convoyeur qui impose le rythme.&#xA;&#xA;Mais Ford fait quelque chose d&#39;inattendu : il augmente les salaires. Pas par bonté d&#39;âme mais pour fidéliser sa main-d&#39;œuvre et surtout, transformer ses ouvriers en clients potentiels de ses voitures.&#xA;&#xA;Le parallèle avec l&#39;IA ? Les outils comme Claude Code ou Copilot sont accessibles aux développeurs individuels. Pour l&#39;instant. La question est de savoir si cette démocratisation va durer, ou si on va vers une concentration où seules les grandes entreprises auront accès aux modèles les plus performants.&#xA;&#xA;Les années 70 : la fin annoncée du travail de bureau&#xA;&#xA;Troisième révolution, l&#39;informatique. En 1970, l&#39;invention du microprocesseur. Les ordinateurs personnels arrivent dans les bureaux. Les prédictions catastrophistes pleuvent : 47% des emplois américains seraient automatisables.&#xA;&#xA;Résultat ? Le nombre d&#39;emplois tertiaires a explosé. Les dactylos ont disparu, les développeurs sont apparus. Les comptables manuels ont laissé place aux experts-comptables assistés par logiciel.&#xA;&#xA;La leçon : les métiers disparaissent rarement complètement. Ils se transforment.&#xA;&#xA;2025 : Mon année avec les agents IA&#xA;&#xA;Assez d&#39;histoire. Parlons de ce qui m&#39;est arrivé concrètement.&#xA;&#xA;Le terminal est devenu mon assistant personnel&#xA;&#xA;Depuis fin 2025, j&#39;ai basculé. Mon IDE prend la poussière. Je passe mes journées dans un terminal avec Claude Code. Je décris ce que je veux, l&#39;agent écrit le code, je supervise.&#xA;&#xA;Le setup qui marche pour moi :&#xA;Un fichier claude.md avec des guidelines détaillées sur mon projet&#xA;Des skills personnalisés pour les tâches répétitives&#xA;Une codebase propre (l&#39;IA travaille mieux sur du code bien structuré)&#xA;Docker Desktop pour les intégrations MCP&#xA;&#xA;Coût : environ 1€ par heure de travail assisté, à mettre en parallèle avec la facturation d&#39;un freelance qui tourne autour de 60€ par heure.&#xA;&#xA;Ce que j&#39;ai appris à mes dépens&#xA;&#xA;L&#39;IA est incompétente pour le debug. Vraiment. Elle peut écrire du code, refactorer, ajouter des fonctionnalités. Mais quand il s&#39;agit de comprendre pourquoi ce foutu test échoue avec un message cryptique, elle tourne en rond. La supervision humaine reste indispensable sur les cas limites.&#xA;&#xA;Les &#34;Legacy Memories&#34; sont un piège. Des années d&#39;expérience m&#39;ont appris des patterns, des réflexes. Certains sont devenus des boulets. L&#39;IA ne fait pas les choses comme je les aurais faites et parfois, sa façon est meilleure. Désapprendre pour réapprendre, c&#39;est le plus dur.&#xA;&#xA;Un agent bien configuré, c&#39;est zéro hallucination. J&#39;ai commencé à ne plus relire certains outputs. Ça fait peur à écrire, mais c&#39;est vrai. Avec Claude Code et Opus 4.5, sur une codebase propre avec des guidelines claires, les erreurs sont devenues rares.&#xA;&#xA;L&#39;analogie qui m&#39;a convaincu&#xA;&#xA;On ne relit pas l&#39;assembleur généré par le compilateur. On fait confiance au compilateur. Personne ne vérifie ligne par ligne ce que GCC produit.&#xA;&#xA;Sommes-nous en train de vivre la même transition avec le code généré par IA ?&#xA;&#xA;Les mêmes peurs, vraiment ?&#xA;&#xA;| Révolution | La peur | Ce qui s&#39;est passé |&#xA;|------------|---------|-------------------|&#xA;| 1ère (1780) | Chômage de masse | Nouveaux métiers, mais 40 ans de transition difficile |&#xA;| 2ème (1870) | Déshumanisation du travail | Classe moyenne, société de consommation |&#xA;| 3ème (1970) | Fin du travail de bureau | Explosion des emplois tertiaires |&#xA;| 4ème (2025) | Fin du développeur ? | En cours... |&#xA;&#xA;Le World Economic Forum note que chaque révolution industrielle a créé plus d&#39;emplois qu&#39;elle n&#39;en a détruits. Mais (et c&#39;est un gros mais) les personnes qui perdent leur emploi ne sont pas forcément celles qui en trouvent un nouveau.&#xA;&#xA;Les recherches historiques montrent que pendant la deuxième révolution industrielle, les jeunes travailleurs s&#39;adaptaient en changeant de métier vers les secteurs en croissance. Les travailleurs plus âgés, eux, restaient coincés dans des emplois dévalorisés ou basculaient vers des postes non qualifiés.&#xA;&#xA;Pattern inquiétant pour les développeurs de plus de 40 ans comme moi, surtout ceux que je vois refuser en bloc l&#39;utilisation de ces outils.&#xA;&#xA;La vraie question des Luddites&#xA;&#xA;Comme le souligne TIME, la question n&#39;a jamais été &#34;la technologie va-t-elle nous remplacer ?&#34; mais &#34;qui contrôle la technologie et à qui profite-t-elle ?&#34;&#xA;&#xA;Aujourd&#39;hui, les développeurs sont dans une position particulière. Nous sommes à la fois :&#xA;Les utilisateurs de ces outils (et nous en profitons)&#xA;Les créateurs de ces outils (certains d&#39;entre nous)&#xA;Les potentielles victimes de ces outils (à terme ?)&#xA;&#xA;Cette position ambiguë explique peut-être pourquoi le débat est si polarisé dans notre profession. Certains sont dans le déni (&#34;l&#39;IA ne pourra jamais faire ce que je fais&#34;). D&#39;autres dans l&#39;euphorie aveugle (&#34;plus besoin de développeurs dans 5 ans&#34;). Les deux ont tort.&#xA;&#xA;Ce qui me préoccupe (vraiment)&#xA;&#xA;Je ne vais pas jouer les optimistes béats. Plusieurs choses m&#39;inquiètent :&#xA;&#xA;Le coût environnemental. Entraîner et faire tourner ces modèles consomme une énergie considérable. Mon gain de productivité a un coût carbone que je ne sais pas mesurer.&#xA;&#xA;L&#39;absence de régulation. On avance à toute vitesse sans cadre légal. Les révolutions industrielles précédentes ont fini par être encadrées: droit du travail, normes environnementales, régulation des monopoles. Pour l&#39;IA, on n&#39;en est nulle part.&#xA;&#xA;La transition professionnelle. Je m&#39;adapte parce que j&#39;ai le luxe de pouvoir expérimenter. Qu&#39;en est-il du développeur junior qui entre sur le marché ? Du senior qui a construit sa carrière sur une expertise que l&#39;IA commoditise ?&#xA;&#xA;Ni Luddite, ni techno-béat&#xA;&#xA;Ma position, après quelques mois d&#39;usage intensif : utiliser les outils sans naïveté.&#xA;&#xA;J&#39;utilise Claude Code tous les jours pour le pro bien sûr mais aussi pour le perso. Ma productivité a explosé et j&#39;ai pu relancer moult projets persos qui stagnaient faute de temps.&#xA;&#xA;Mais je ne suis pas dupe. Je milite pour une régulation de ces technologies. Je m&#39;inquiète de leurs impacts environnementaux et sociaux. Je pense que la transition va faire des dégâts si elle n&#39;est pas accompagnée.&#xA;&#xA;Les Luddites avaient compris un truc essentiel : le progrès technique n&#39;est pas neutre. Il peut servir à émanciper les travailleurs ou à les asservir. Ça dépend de choix politiques, pas de la technologie elle-même.&#xA;&#xA;Et maintenant ?&#xA;&#xA;Si vous êtes développeur, vous avez probablement déjà une opinion sur l&#39;IA générative. Peut-être que vous l&#39;utilisez quotidiennement. Peut-être que vous refusez d&#39;y toucher. Peut-être que vous êtes quelque part entre les deux.&#xA;&#xA;Mon conseil, pour ce qu&#39;il vaut : expérimentez, mais gardez les yeux ouverts.&#xA;&#xA;Apprenez à utiliser ces outils. Comprenez leurs limites. Maintenez vos compétences de supervision et d&#39;architecture, ce que l&#39;IA fait mal. Et surtout, participez au débat sur l&#39;encadrement de ces technologies.&#xA;&#xA;Les tisserands de 1811 ont perdu leur combat. Pas parce qu&#39;ils avaient tort sur le fond, mais parce qu&#39;ils n&#39;avaient pas le rapport de force. Nous, développeurs de 2025, avons peut-être une fenêtre pour influencer la direction que prend cette révolution.&#xA;&#xA;Ne la laissons pas passer.&#xA;&#xA;---&#xA;&#xA;Cet article s&#39;appuie sur mes publications sur Mastodon et sur des recherches historiques. Les sources principales sont liées dans le texte.&#xA;&#xA;Sources&#xA;&#xA;MIT Technology Review - What Luddites can teach us about resisting an automated future&#xA;McKinsey - What can history teach us about technology and jobs?&#xA;TIME - What the Luddites Can Teach Us About Artificial Intelligence&#xA;World Economic Forum - The Fourth Industrial Revolution could spell more jobs&#xA;Federal Reserve Bank of Chicago - Occupational Switching During the Second Industrial Revolution&#xA;Cairn.info - Les impacts de l&#39;automatisation du travail&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>“Claude Code c&#39;est de l&#39;héroïne pour programmeur.”</p>

<p>J&#39;ai posté ça sur Mastodon début janvier 2026. Une formule volontairement provocante, mais qui résume assez bien mon année 2025. Une année où ma productivité a été multipliée par deux, peut-être par dix selon comment on compte.</p>

<p>Mais je me suis aussi demandé : est-ce que je ne serai pas en train de scier la branche sur laquelle je suis assis ?</p>



<h2 id="les-fantômes-des-révolutions-passées">Les fantômes des révolutions passées</h2>

<p>Quand on parle d&#39;IA et d&#39;emploi, les comparaisons historiques fusent. “C&#39;est comme l&#39;arrivée de l&#39;électricité !” “C&#39;est comme Internet !” Soit. Mais si on gratte un peu, ces révolutions industrielles ont des leçons bien plus nuancées à nous offrir.</p>

<h3 id="1811-les-luddites-n-étaient-pas-des-idiots">1811 : Les Luddites n&#39;étaient pas des idiots</h3>

<p>L&#39;histoire officielle a fait des Luddites des technophobes arriérés, des casseurs de machines incapables de voir le progrès. C&#39;est un mythe bien pratique.</p>

<p>En réalité, <a href="https://www.technologyreview.com/2024/02/28/1088262/luddites-resisting-automated-future-technology/">comme le rappelle le MIT Technology Review</a>, les Luddites étaient des artisans qualifiés (tisserands, tricoteurs) qui voyaient très bien ce qui se passait. Ils n&#39;étaient pas contre les machines. Ils étaient contre l&#39;utilisation des machines pour dégrader leur travail et casser leurs salaires.</p>

<p>Leur leader, George Mellor, avait cette formule : “the tendency&#39;s all one way”, la tendance va dans un seul sens. Celui de la concentration des richesses au détriment des travailleurs.</p>

<p>Ça ne vous rappelle rien ?</p>

<h3 id="40-ans-de-galère-avant-l-amélioration">40 ans de galère avant l&#39;amélioration</h3>

<p>Voici ce qu&#39;on oublie souvent : pendant la première révolution industrielle en Angleterre, <a href="https://www.mckinsey.com/featured-insights/future-of-work/what-can-history-teach-us-about-technology-and-jobs">les salaires réels ont stagné pendant 40 ans</a> alors que la productivité explosait. Quarante ans. Deux générations de travailleurs ont vu leur niveau de vie se dégrader pendant que les propriétaires d&#39;usines s&#39;enrichissaient.</p>

<p>Les choses se sont améliorées. Mais pas toutes seules. Il a fallu des grèves, des syndicats, des lois sociales arrachées de haute lutte. Le progrès technique n&#39;a pas automatiquement ruisselé vers les travailleurs.</p>

<h3 id="ford-et-le-paradoxe-du-salaire">Ford et le paradoxe du salaire</h3>

<p>Deuxième révolution industrielle, début du XXe siècle. Henry Ford installe ses chaînes de montage. Les ouvriers perdent toute autonomie : un geste, répété des centaines de fois par jour, sur un convoyeur qui impose le rythme.</p>

<p>Mais Ford fait quelque chose d&#39;inattendu : il augmente les salaires. Pas par bonté d&#39;âme mais pour fidéliser sa main-d&#39;œuvre et surtout, transformer ses ouvriers en clients potentiels de ses voitures.</p>

<p>Le parallèle avec l&#39;IA ? Les outils comme Claude Code ou Copilot sont accessibles aux développeurs individuels. Pour l&#39;instant. La question est de savoir si cette démocratisation va durer, ou si on va vers une concentration où seules les grandes entreprises auront accès aux modèles les plus performants.</p>

<h3 id="les-années-70-la-fin-annoncée-du-travail-de-bureau">Les années 70 : la fin annoncée du travail de bureau</h3>

<p>Troisième révolution, l&#39;informatique. En 1970, l&#39;invention du microprocesseur. Les ordinateurs personnels arrivent dans les bureaux. Les prédictions catastrophistes pleuvent : <a href="https://www.cairn.info/revue-etudes-2018-9-page-43.htm">47% des emplois américains seraient automatisables</a>.</p>

<p>Résultat ? Le nombre d&#39;emplois tertiaires a explosé. Les dactylos ont disparu, les développeurs sont apparus. Les comptables manuels ont laissé place aux experts-comptables assistés par logiciel.</p>

<p>La leçon : les métiers disparaissent rarement complètement. Ils se transforment.</p>

<h2 id="2025-mon-année-avec-les-agents-ia">2025 : Mon année avec les agents IA</h2>

<p>Assez d&#39;histoire. Parlons de ce qui m&#39;est arrivé concrètement.</p>

<h3 id="le-terminal-est-devenu-mon-assistant-personnel">Le terminal est devenu mon assistant personnel</h3>

<p>Depuis fin 2025, j&#39;ai basculé. Mon IDE prend la poussière. Je passe mes journées dans un terminal avec Claude Code. Je décris ce que je veux, l&#39;agent écrit le code, je supervise.</p>

<p>Le setup qui marche pour moi :
– Un fichier <code>claude.md</code> avec des guidelines détaillées sur mon projet
– Des skills personnalisés pour les tâches répétitives
– Une codebase propre (l&#39;IA travaille mieux sur du code bien structuré)
– Docker Desktop pour les intégrations MCP</p>

<p>Coût : environ 1€ par heure de travail assisté, à mettre en parallèle avec la facturation d&#39;un freelance qui tourne autour de 60€ par heure.</p>

<h3 id="ce-que-j-ai-appris-à-mes-dépens">Ce que j&#39;ai appris à mes dépens</h3>

<p><strong>L&#39;IA est incompétente pour le debug.</strong> Vraiment. Elle peut écrire du code, refactorer, ajouter des fonctionnalités. Mais quand il s&#39;agit de comprendre pourquoi ce foutu test échoue avec un message cryptique, elle tourne en rond. La supervision humaine reste indispensable sur les cas limites.</p>

<p><strong>Les “Legacy Memories” sont un piège.</strong> Des années d&#39;expérience m&#39;ont appris des patterns, des réflexes. Certains sont devenus des boulets. L&#39;IA ne fait pas les choses comme je les aurais faites et parfois, sa façon est meilleure. Désapprendre pour réapprendre, c&#39;est le plus dur.</p>

<p><strong>Un agent bien configuré, c&#39;est zéro hallucination.</strong> J&#39;ai commencé à ne plus relire certains outputs. Ça fait peur à écrire, mais c&#39;est vrai. Avec Claude Code et Opus 4.5, sur une codebase propre avec des guidelines claires, les erreurs sont devenues rares.</p>

<h3 id="l-analogie-qui-m-a-convaincu">L&#39;analogie qui m&#39;a convaincu</h3>

<p>On ne relit pas l&#39;assembleur généré par le compilateur. On fait confiance au compilateur. Personne ne vérifie ligne par ligne ce que GCC produit.</p>

<p>Sommes-nous en train de vivre la même transition avec le code généré par IA ?</p>

<h2 id="les-mêmes-peurs-vraiment">Les mêmes peurs, vraiment ?</h2>

<table>
<thead>
<tr>
<th>Révolution</th>
<th>La peur</th>
<th>Ce qui s&#39;est passé</th>
</tr>
</thead>

<tbody>
<tr>
<td>1ère (1780)</td>
<td>Chômage de masse</td>
<td>Nouveaux métiers, mais 40 ans de transition difficile</td>
</tr>

<tr>
<td>2ème (1870)</td>
<td>Déshumanisation du travail</td>
<td>Classe moyenne, société de consommation</td>
</tr>

<tr>
<td>3ème (1970)</td>
<td>Fin du travail de bureau</td>
<td>Explosion des emplois tertiaires</td>
</tr>

<tr>
<td>4ème (2025)</td>
<td>Fin du développeur ?</td>
<td><em>En cours...</em></td>
</tr>
</tbody>
</table>

<p>Le <a href="https://www.weforum.org/stories/2019/09/fourth-industrial-revolution-jobs/">World Economic Forum</a> note que chaque révolution industrielle a créé plus d&#39;emplois qu&#39;elle n&#39;en a détruits. Mais (et c&#39;est un gros mais) les personnes qui perdent leur emploi ne sont pas forcément celles qui en trouvent un nouveau.</p>

<p><a href="https://www.chicagofed.org/-/media/publications/working-papers/2024/wp2024-01.pdf">Les recherches historiques montrent</a> que pendant la deuxième révolution industrielle, les jeunes travailleurs s&#39;adaptaient en changeant de métier vers les secteurs en croissance. Les travailleurs plus âgés, eux, restaient coincés dans des emplois dévalorisés ou basculaient vers des postes non qualifiés.</p>

<p>Pattern inquiétant pour les développeurs de plus de 40 ans comme moi, surtout ceux que je vois refuser en bloc l&#39;utilisation de ces outils.</p>

<h2 id="la-vraie-question-des-luddites">La vraie question des Luddites</h2>

<p><a href="https://time.com/6317437/luddites-ai-blood-in-the-machine-merchant/">Comme le souligne TIME</a>, la question n&#39;a jamais été “la technologie va-t-elle nous remplacer ?” mais “qui contrôle la technologie et à qui profite-t-elle ?”</p>

<p>Aujourd&#39;hui, les développeurs sont dans une position particulière. Nous sommes à la fois :
– <strong>Les utilisateurs</strong> de ces outils (et nous en profitons)
– <strong>Les créateurs</strong> de ces outils (certains d&#39;entre nous)
– <strong>Les potentielles victimes</strong> de ces outils (à terme ?)</p>

<p>Cette position ambiguë explique peut-être pourquoi le débat est si polarisé dans notre profession. Certains sont dans le déni (“l&#39;IA ne pourra jamais faire ce que je fais”). D&#39;autres dans l&#39;euphorie aveugle (“plus besoin de développeurs dans 5 ans”). Les deux ont tort.</p>

<h2 id="ce-qui-me-préoccupe-vraiment">Ce qui me préoccupe (vraiment)</h2>

<p>Je ne vais pas jouer les optimistes béats. Plusieurs choses m&#39;inquiètent :</p>

<p><strong>Le coût environnemental.</strong> Entraîner et faire tourner ces modèles consomme une énergie considérable. Mon gain de productivité a un coût carbone que je ne sais pas mesurer.</p>

<p><strong>L&#39;absence de régulation.</strong> On avance à toute vitesse sans cadre légal. Les révolutions industrielles précédentes ont fini par être encadrées: droit du travail, normes environnementales, régulation des monopoles. Pour l&#39;IA, on n&#39;en est nulle part.</p>

<p><strong>La transition professionnelle.</strong> Je m&#39;adapte parce que j&#39;ai le luxe de pouvoir expérimenter. Qu&#39;en est-il du développeur junior qui entre sur le marché ? Du senior qui a construit sa carrière sur une expertise que l&#39;IA commoditise ?</p>

<h2 id="ni-luddite-ni-techno-béat">Ni Luddite, ni techno-béat</h2>

<p>Ma position, après quelques mois d&#39;usage intensif : <strong>utiliser les outils sans naïveté</strong>.</p>

<p>J&#39;utilise Claude Code tous les jours pour le pro bien sûr mais aussi pour le perso. Ma productivité a explosé et j&#39;ai pu relancer moult projets persos qui stagnaient faute de temps.</p>

<p>Mais je ne suis pas dupe. Je milite pour une régulation de ces technologies. Je m&#39;inquiète de leurs impacts environnementaux et sociaux. Je pense que la transition va faire des dégâts si elle n&#39;est pas accompagnée.</p>

<p>Les Luddites avaient compris un truc essentiel : le progrès technique n&#39;est pas neutre. Il peut servir à émanciper les travailleurs ou à les asservir. Ça dépend de choix politiques, pas de la technologie elle-même.</p>

<h2 id="et-maintenant">Et maintenant ?</h2>

<p>Si vous êtes développeur, vous avez probablement déjà une opinion sur l&#39;IA générative. Peut-être que vous l&#39;utilisez quotidiennement. Peut-être que vous refusez d&#39;y toucher. Peut-être que vous êtes quelque part entre les deux.</p>

<p>Mon conseil, pour ce qu&#39;il vaut : <strong>expérimentez, mais gardez les yeux ouverts</strong>.</p>

<p>Apprenez à utiliser ces outils. Comprenez leurs limites. Maintenez vos compétences de supervision et d&#39;architecture, ce que l&#39;IA fait mal. Et surtout, participez au débat sur l&#39;encadrement de ces technologies.</p>

<p>Les tisserands de 1811 ont perdu leur combat. Pas parce qu&#39;ils avaient tort sur le fond, mais parce qu&#39;ils n&#39;avaient pas le rapport de force. Nous, développeurs de 2025, avons peut-être une fenêtre pour influencer la direction que prend cette révolution.</p>

<p>Ne la laissons pas passer.</p>

<hr>

<p><em>Cet article s&#39;appuie sur mes publications sur <a href="https://social.lemee.co/@jb">Mastodon</a> et sur des recherches historiques. Les sources principales sont liées dans le texte.</em></p>

<h2 id="sources">Sources</h2>
<ul><li><a href="https://www.technologyreview.com/2024/02/28/1088262/luddites-resisting-automated-future-technology/">MIT Technology Review – What Luddites can teach us about resisting an automated future</a></li>
<li><a href="https://www.mckinsey.com/featured-insights/future-of-work/what-can-history-teach-us-about-technology-and-jobs">McKinsey – What can history teach us about technology and jobs?</a></li>
<li><a href="https://time.com/6317437/luddites-ai-blood-in-the-machine-merchant/">TIME – What the Luddites Can Teach Us About Artificial Intelligence</a></li>
<li><a href="https://www.weforum.org/stories/2019/09/fourth-industrial-revolution-jobs/">World Economic Forum – The Fourth Industrial Revolution could spell more jobs</a></li>
<li><a href="https://www.chicagofed.org/-/media/publications/working-papers/2024/wp2024-01.pdf">Federal Reserve Bank of Chicago – Occupational Switching During the Second Industrial Revolution</a></li>
<li><a href="https://www.cairn.info/revue-etudes-2018-9-page-43.htm">Cairn.info – Les impacts de l&#39;automatisation du travail</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/le-developpeur-face-a-lia-sommes-nous-les-tisserands-de-1811</guid>
      <pubDate>Sat, 10 Jan 2026 09:44:05 +0000</pubDate>
    </item>
    <item>
      <title>Synchroniser des vidéos YouTube vers PeerTube avec n8n</title>
      <link>https://blog.ut0pia.org/synchroniser-des-videos-youtube-vers-peertube-avec-n8n</link>
      <description>&lt;![CDATA[Vous avez une instance PeerTube et vous souhaitez y importer automatiquement certaines vidéos d&#39;une chaîne YouTube ? Avec n8n, c&#39;est possible en quelques clics.&#xA;!--more--&#xA;&#xA;Le principe&#xA;&#xA;n8n est un outil d&#39;automatisation open-source (comme Zapier, mais auto-hébergeable). L&#39;idée est simple :&#xA;&#xA;Récupérer le flux RSS d&#39;une chaîne YouTube&#xA;Filtrer les vidéos selon nos critères&#xA;Vérifier si la vidéo existe déjà sur PeerTube&#xA;Si non, l&#39;importer via l&#39;API PeerTube&#xA;&#xA;Prérequis&#xA;&#xA;Une instance n8n (ex: n8n.example.org)&#xA;Une instance PeerTube (ex: video.example.org)&#xA;Les credentials OAuth2 de votre PeerTube&#xA;&#xA;Étape 1 : Récupérer le flux RSS YouTube&#xA;&#xA;Chaque chaîne YouTube dispose d&#39;un flux RSS. L&#39;URL est :&#xA;&#xA;https://www.youtube.com/feeds/videos.xml?channelid=CHANNELID&#xA;&#xA;Pour trouver le CHANNELID, allez sur la chaîne YouTube et regardez l&#39;URL. Par exemple pour la chaîne fictive &#34;TechTalks&#34; :&#xA;&#xA;https://www.youtube.com/channel/UC1234567890abcdef&#xA;&#xA;Dans n8n, créez un nœud HTTP Request :&#xA;Method: GET&#xA;URL: https://www.youtube.com/feeds/videos.xml?channelid=UC1234567890abcdef&#xA;Response Format: Text&#xA;&#xA;Étape 2 : Parser et filtrer les vidéos&#xA;&#xA;Ajoutez un nœud Code pour extraire les vidéos qui nous intéressent. Par exemple, pour ne garder que les vidéos dont le titre contient &#34;Tutorial&#34; :&#xA;&#xA;const xml = $input.first().json.data;&#xA;const entries = xml.split(&#39;entry&#39;);&#xA;const videos = [];&#xA;&#xA;for (let i = 1; i &lt; entries.length; i++) {&#xA;  const entry = entries[i];&#xA;  const videoIdMatch = entry.match(/yt:videoId([^]+)&lt;\/yt:videoId/);&#xA;  const titleMatch = entry.match(/title([^]+)&lt;\/title/);&#xA;&#xA;  if (videoIdMatch &amp;&amp; titleMatch) {&#xA;    const title = titleMatch[1];&#xA;    // Filtrer : ne garder que les tutoriels&#xA;    if (title.toLowerCase().includes(&#39;tutorial&#39;)) {&#xA;      videos.push({&#xA;        videoId: videoIdMatch[1],&#xA;        title: title,&#xA;        url: https://www.youtube.com/watch?v=${videoIdMatch[1]}&#xA;      });&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;if (videos.length   0) {&#xA;  return [{json: videos[0]}];&#xA;}&#xA;return [];&#xA;&#xA;Vous pouvez adapter le filtre selon vos besoins :&#xA;Mots-clés dans le titre&#xA;Exclusion de certains termes&#xA;Combinaison de critères&#xA;&#xA;Étape 3 : Vérifier les doublons&#xA;&#xA;Avant d&#39;importer, vérifions que la vidéo n&#39;existe pas déjà. Ajoutez un nœud HTTP Request :&#xA;&#xA;Method: GET&#xA;URL: https://video.example.org/api/v1/search/videos?search={{ encodeURIComponent($json.title) }}&amp;searchTarget=local&#xA;&#xA;Puis un nœud Code pour filtrer :&#xA;&#xA;const searchResult = $input.first().json;&#xA;const videoTitle = $(&#39;Parse RSS&#39;).first().json.title;&#xA;const videoUrl = $(&#39;Parse RSS&#39;).first().json.url;&#xA;&#xA;const exists = searchResult.data?.some(v =  v.name === videoTitle);&#xA;&#xA;if (exists) {&#xA;  return []; // Vidéo déjà présente, on arrête&#xA;}&#xA;&#xA;return [{json: {title: videoTitle, url: videoUrl}}];&#xA;&#xA;Étape 4 : Obtenir un token PeerTube&#xA;&#xA;Pour utiliser l&#39;API PeerTube, il faut s&#39;authentifier. Ajoutez un nœud HTTP Request :&#xA;&#xA;Method: POST&#xA;URL: https://video.example.org/api/v1/users/token&#xA;Content-Type: application/x-www-form-urlencoded&#xA;Body:&#xA;  clientid: votre client ID&#xA;  clientsecret: votre client secret&#xA;  granttype: password&#xA;  username: votre username&#xA;  password: votre password&#xA;&#xA;  Astuce : Vous pouvez trouver le clientid et clientsecret via l&#39;API /api/v1/oauth-clients/local.&#xA;&#xA;Étape 5 : Importer la vidéo&#xA;&#xA;Dernier nœud HTTP Request pour lancer l&#39;import :&#xA;&#xA;Method: POST&#xA;URL: https://video.example.org/api/v1/videos/imports&#xA;Headers: Authorization: Bearer {{ $json.accesstoken }}&#xA;Content-Type: multipart/form-data&#xA;Body:&#xA;  targetUrl: l&#39;URL YouTube&#xA;  channelId: l&#39;ID du channel PeerTube destination&#xA;  privacy: 1 (public), 2 (unlisted), 3 (private)&#xA;  name: le titre de la vidéo&#xA;  category: l&#39;ID de la catégorie (optionnel)&#xA;&#xA;Planification&#xA;&#xA;Ajoutez un nœud Schedule Trigger au début du workflow pour exécuter automatiquement l&#39;import :&#xA;&#xA;Toutes les 6 heures pour un flux actif&#xA;Une fois par jour pour un flux moins actif&#xA;&#xA;Le workflow complet&#xA;&#xA;[Schedule] → [Get RSS] → [Parse &amp; Filter] → [Check Exists] → [Filter New] → [Get Token] → [Import]&#xA;&#xA;Points d&#39;attention&#xA;&#xA;Cookies YouTube : Si l&#39;import échoue avec une erreur 403, PeerTube a peut-être besoin de cookies YouTube valides. Consultez la documentation yt-dlp pour configurer les cookies.&#xA;&#xA;Rate limiting : N&#39;exécutez pas le workflow trop fréquemment pour éviter d&#39;être bloqué par YouTube.&#xA;&#xA;Stockage : Les vidéos importées prennent de l&#39;espace disque. Pensez à créer un workflow de nettoyage pour les anciennes vidéos si nécessaire.&#xA;&#xA;Légalité : Assurez-vous d&#39;avoir le droit de republier les vidéos que vous importez (Creative Commons, accord de l&#39;auteur, etc.).&#xA;&#xA;Conclusion&#xA;&#xA;n8n rend l&#39;automatisation accessible à tous. Avec ce workflow, vous pouvez créer votre propre miroir PeerTube de contenus YouTube sélectionnés, tout en gardant le contrôle sur ce qui est importé grâce aux filtres.&#xA;&#xA;---&#xA;&#xA;Ressources utiles :&#xA;Documentation n8n&#xA;Documentation PeerTube&#xA;API REST PeerTube&#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Vous avez une instance <a href="https://joinpeertube.org/">PeerTube</a> et vous souhaitez y importer automatiquement certaines vidéos d&#39;une chaîne YouTube ? Avec <a href="https://n8n.io/">n8n</a>, c&#39;est possible en quelques clics.
</p>

<h2 id="le-principe">Le principe</h2>

<p><a href="https://github.com/n8n-io/n8n">n8n</a> est un outil d&#39;automatisation open-source (comme Zapier, mais auto-hébergeable). L&#39;idée est simple :</p>
<ol><li>Récupérer le flux RSS d&#39;une chaîne YouTube</li>
<li>Filtrer les vidéos selon nos critères</li>
<li>Vérifier si la vidéo existe déjà sur PeerTube</li>
<li>Si non, l&#39;importer via l&#39;<a href="https://docs.joinpeertube.org/api-rest-reference.html">API PeerTube</a></li></ol>

<h2 id="prérequis">Prérequis</h2>
<ul><li>Une instance <a href="https://docs.n8n.io/hosting/">n8n</a> (ex: <code>n8n.example.org</code>)</li>
<li>Une instance <a href="https://docs.joinpeertube.org/install/any-os">PeerTube</a> (ex: <code>video.example.org</code>)</li>
<li>Les credentials OAuth2 de votre PeerTube</li></ul>

<h2 id="étape-1-récupérer-le-flux-rss-youtube">Étape 1 : Récupérer le flux RSS YouTube</h2>

<p>Chaque chaîne YouTube dispose d&#39;un <a href="https://fr.wikipedia.org/wiki/RSS">flux RSS</a>. L&#39;URL est :</p>

<pre><code>https://www.youtube.com/feeds/videos.xml?channel_id=CHANNEL_ID
</code></pre>

<p>Pour trouver le <code>CHANNEL_ID</code>, allez sur la chaîne YouTube et regardez l&#39;URL. Par exemple pour la chaîne fictive “TechTalks” :</p>

<pre><code>https://www.youtube.com/channel/UC1234567890abcdef
</code></pre>

<p>Dans n8n, créez un nœud <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.httprequest/">HTTP Request</a> :
– Method: GET
– URL: <code>https://www.youtube.com/feeds/videos.xml?channel_id=UC1234567890abcdef</code>
– Response Format: Text</p>

<h2 id="étape-2-parser-et-filtrer-les-vidéos">Étape 2 : Parser et filtrer les vidéos</h2>

<p>Ajoutez un nœud <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/">Code</a> pour extraire les vidéos qui nous intéressent. Par exemple, pour ne garder que les vidéos dont le titre contient “Tutorial” :</p>

<pre><code class="language-javascript">const xml = $input.first().json.data;
const entries = xml.split(&#39;&lt;entry&gt;&#39;);
const videos = [];

for (let i = 1; i &lt; entries.length; i++) {
  const entry = entries[i];
  const videoIdMatch = entry.match(/&lt;yt:videoId&gt;([^&lt;]+)&lt;\/yt:videoId&gt;/);
  const titleMatch = entry.match(/&lt;title&gt;([^&lt;]+)&lt;\/title&gt;/);

  if (videoIdMatch &amp;&amp; titleMatch) {
    const title = titleMatch[1];
    // Filtrer : ne garder que les tutoriels
    if (title.toLowerCase().includes(&#39;tutorial&#39;)) {
      videos.push({
        videoId: videoIdMatch[1],
        title: title,
        url: `https://www.youtube.com/watch?v=${videoIdMatch[1]}`
      });
    }
  }
}

if (videos.length &gt; 0) {
  return [{json: videos[0]}];
}
return [];
</code></pre>

<p>Vous pouvez adapter le filtre selon vos besoins :
– Mots-clés dans le titre
– Exclusion de certains termes
– Combinaison de critères</p>

<h2 id="étape-3-vérifier-les-doublons">Étape 3 : Vérifier les doublons</h2>

<p>Avant d&#39;importer, vérifions que la vidéo n&#39;existe pas déjà. Ajoutez un nœud <strong>HTTP Request</strong> :</p>
<ul><li>Method: GET</li>
<li>URL: <code>https://video.example.org/api/v1/search/videos?search={{ encodeURIComponent($json.title) }}&amp;searchTarget=local</code></li></ul>

<p>Puis un nœud <strong>Code</strong> pour filtrer :</p>

<pre><code class="language-javascript">const searchResult = $input.first().json;
const videoTitle = $(&#39;Parse RSS&#39;).first().json.title;
const videoUrl = $(&#39;Parse RSS&#39;).first().json.url;

const exists = searchResult.data?.some(v =&gt; v.name === videoTitle);

if (exists) {
  return []; // Vidéo déjà présente, on arrête
}

return [{json: {title: videoTitle, url: videoUrl}}];
</code></pre>

<h2 id="étape-4-obtenir-un-token-peertube">Étape 4 : Obtenir un token PeerTube</h2>

<p>Pour utiliser l&#39;<a href="https://docs.joinpeertube.org/api-rest-reference.html#tag/Session/operation/getOAuthToken">API PeerTube</a>, il faut s&#39;authentifier. Ajoutez un nœud <strong>HTTP Request</strong> :</p>
<ul><li>Method: POST</li>
<li>URL: <code>https://video.example.org/api/v1/users/token</code></li>
<li>Content-Type: <code>application/x-www-form-urlencoded</code></li>
<li>Body:
<ul><li><code>client_id</code>: votre client ID</li>
<li><code>client_secret</code>: votre client secret</li>
<li><code>grant_type</code>: password</li>
<li><code>username</code>: votre username</li>
<li><code>password</code>: votre password</li></ul></li></ul>

<blockquote><p><strong>Astuce</strong> : Vous pouvez trouver le <code>client_id</code> et <code>client_secret</code> via l&#39;API <a href="https://docs.joinpeertube.org/api-rest-reference.html#tag/Session/operation/getOAuthClient"><code>/api/v1/oauth-clients/local</code></a>.</p></blockquote>

<h2 id="étape-5-importer-la-vidéo">Étape 5 : Importer la vidéo</h2>

<p>Dernier nœud <strong>HTTP Request</strong> pour lancer l&#39;<a href="https://docs.joinpeertube.org/api-rest-reference.html#tag/Video-Imports/operation/importVideo">import</a> :</p>
<ul><li>Method: POST</li>
<li>URL: <code>https://video.example.org/api/v1/videos/imports</code></li>
<li>Headers: <code>Authorization: Bearer {{ $json.access_token }}</code></li>
<li>Content-Type: <code>multipart/form-data</code></li>
<li>Body:
<ul><li><code>targetUrl</code>: l&#39;URL YouTube</li>
<li><code>channelId</code>: l&#39;ID du channel PeerTube destination</li>
<li><code>privacy</code>: 1 (public), 2 (unlisted), 3 (private)</li>
<li><code>name</code>: le titre de la vidéo</li>
<li><code>category</code>: l&#39;ID de la catégorie (optionnel)</li></ul></li></ul>

<h2 id="planification">Planification</h2>

<p>Ajoutez un nœud <a href="https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/">Schedule Trigger</a> au début du workflow pour exécuter automatiquement l&#39;import :</p>
<ul><li>Toutes les 6 heures pour un flux actif</li>
<li>Une fois par jour pour un flux moins actif</li></ul>

<h2 id="le-workflow-complet">Le workflow complet</h2>

<pre><code>[Schedule] → [Get RSS] → [Parse &amp; Filter] → [Check Exists] → [Filter New] → [Get Token] → [Import]
</code></pre>

<h2 id="points-d-attention">Points d&#39;attention</h2>
<ol><li><p><strong>Cookies YouTube</strong> : Si l&#39;import échoue avec une erreur 403, PeerTube a peut-être besoin de cookies YouTube valides. Consultez la <a href="https://github.com/yt-dlp/yt-dlp#cookies">documentation yt-dlp</a> pour configurer les cookies.</p></li>

<li><p><strong>Rate limiting</strong> : N&#39;exécutez pas le workflow trop fréquemment pour éviter d&#39;être bloqué par YouTube.</p></li>

<li><p><strong>Stockage</strong> : Les vidéos importées prennent de l&#39;espace disque. Pensez à créer un workflow de nettoyage pour les anciennes vidéos si nécessaire.</p></li>

<li><p><strong>Légalité</strong> : Assurez-vous d&#39;avoir le droit de republier les vidéos que vous importez (<a href="https://creativecommons.org/">Creative Commons</a>, accord de l&#39;auteur, etc.).</p></li></ol>

<h2 id="conclusion">Conclusion</h2>

<p><a href="https://n8n.io/">n8n</a> rend l&#39;automatisation accessible à tous. Avec ce workflow, vous pouvez créer votre propre miroir PeerTube de contenus YouTube sélectionnés, tout en gardant le contrôle sur ce qui est importé grâce aux filtres.</p>

<hr>

<p><em>Ressources utiles :</em>
– <a href="https://docs.n8n.io/">Documentation n8n</a>
– <a href="https://docs.joinpeertube.org/">Documentation PeerTube</a>
– <a href="https://docs.joinpeertube.org/api-rest-reference.html">API REST PeerTube</a></p>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/synchroniser-des-videos-youtube-vers-peertube-avec-n8n</guid>
      <pubDate>Mon, 05 Jan 2026 14:50:02 +0000</pubDate>
    </item>
    <item>
      <title>Installer Plausible Analytics pour PeerTube</title>
      <link>https://blog.ut0pia.org/installer-plausible-analytics-pour-peertube</link>
      <description>&lt;![CDATA[Vous hébergez une instance PeerTube et vous souhaitez suivre vos statistiques de fréquentation sans compromettre la vie privée de vos visiteurs ? Plausible Analytics est la solution idéale.&#xA;&#xA;!--more--&#xA;Pourquoi Plausible ?&#xA;&#xA;a href=&#34;https://plausible.io/&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;Plausible/a est une alternative open-source et respectueuse de la vie privée à Google Analytics. Contrairement à ce dernier :&#xA;&#xA;Pas de cookies : aucune bannière RGPD nécessaire&#xA;Pas de données personnelles : les adresses IP sont hashées puis supprimées&#xA;Léger : le script fait moins de 1 Ko (vs ~45 Ko pour Google Analytics)&#xA;Open-source : vous pouvez l&#39;auto-héberger&#xA;&#xA;Notre infrastructure&#xA;&#xA;Notre homelab utilise Docker Compose avec nginx-proxy pour le reverse proxy et Let&#39;s Encrypt pour les certificats SSL. La configuration complète est disponible sur a href=&#34;https://github.com/jblemee/docker-compose-homelab&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;GitHub/a.&#xA;&#xA;Installation de Plausible&#xA;&#xA;1. Configuration DNS&#xA;&#xA;Ajoutez un enregistrement DNS pour votre sous-domaine analytics. Dans notre cas, nous utilisons plausible.votredomaine.org :&#xA;&#xA;python3 scripts/ovh-dns.py add plausible --type A --ip VOTREIP&#xA;python3 scripts/ovh-dns.py add plausible --type AAAA --ip VOTREIPV6&#xA;&#xA;2. Fichier Docker Compose&#xA;&#xA;Créez le fichier services/plausible.yml :&#xA;&#xA;services:&#xA;  plausible:&#xA;    image: ghcr.io/plausible/community-edition:v3.1.0&#xA;    containername: plausible&#xA;    command: /entrypoint.sh run&#xA;    healthcheck:&#xA;      test: [&#34;CMD-SHELL&#34;, &#34;wget -q --spider http://127.0.0.1:8000/api/health || exit 1&#34;]&#xA;      startperiod: 2m&#xA;      interval: 30s&#xA;    environment:&#xA;      BASEURL=https://plausible.${DOMAIN}&#xA;      SECRETKEYBASE=${PLAUSIBLESECRETKEY}&#xA;      TOTPVAULTKEY=${PLAUSIBLETOTPKEY}&#xA;      DISABLEREGISTRATION=inviteonly&#xA;      DATABASEURL=postgres://plausible:${PLAUSIBLEDBPASSWORD}@plausible-db:5432/plausible&#xA;      CLICKHOUSEDATABASEURL=http://plausible-events-db:8123/plausibleeventsdb&#xA;      VIRTUALHOST=plausible.${DOMAIN}&#xA;      VIRTUALPORT=8000&#xA;      LETSENCRYPTHOST=plausible.${DOMAIN}&#xA;      LETSENCRYPTEMAIL=${LETSENCRYPTEMAIL}&#xA;    volumes:&#xA;      plausible-data:/var/lib/plausible&#xA;    networks:&#xA;      proxy-tier&#xA;      plausible-internal&#xA;    dependson:&#xA;      plausible-db:&#xA;        condition: servicehealthy&#xA;      plausible-events-db:&#xA;        condition: servicehealthy&#xA;&#xA;  plausible-db:&#xA;    image: postgres:16-alpine&#xA;    containername: plausible-db&#xA;    environment:&#xA;      POSTGRESUSER=plausible&#xA;      POSTGRESPASSWORD=${PLAUSIBLEDBPASSWORD}&#xA;      POSTGRESDB=plausible&#xA;    volumes:&#xA;      /data/plausible-db:/var/lib/postgresql/data&#xA;    networks:&#xA;      plausible-internal&#xA;    healthcheck:&#xA;      test: [&#34;CMD-SHELL&#34;, &#34;pgisready -U plausible -d plausible&#34;]&#xA;      startperiod: 1m&#xA;      interval: 10s&#xA;&#xA;  plausible-events-db:&#xA;    image: clickhouse/clickhouse-server:24.12-alpine&#xA;    containername: plausible-events-db&#xA;    environment:&#xA;      CLICKHOUSESKIPUSERSETUP=1&#xA;    volumes:&#xA;      /data/plausible-events-db:/var/lib/clickhouse&#xA;      /data/plausible-clickhouse-config/listen.xml:/etc/clickhouse-server/config.d/listen.xml:ro&#xA;    networks:&#xA;      plausible-internal&#xA;    healthcheck:&#xA;      test: [&#34;CMD-SHELL&#34;, &#34;wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1&#34;]&#xA;      startperiod: 1m&#xA;      interval: 10s&#xA;&#xA;networks:&#xA;  proxy-tier:&#xA;    external: true&#xA;  plausible-internal:&#xA;    driver: bridge&#xA;&#xA;volumes:&#xA;  plausible-data:&#xA;&#xA;3. Configuration ClickHouse pour IPv4&#xA;&#xA;ClickHouse écoute par défaut sur IPv6. Si votre réseau Docker n&#39;a pas IPv6, créez ce fichier :&#xA;&#xA;sudo mkdir -p /data/plausible-clickhouse-config&#xA;sudo tee /data/plausible-clickhouse-config/listen.xml &lt;&lt; &#39;EOF&#39;&#xA;clickhouse&#xA;    listenhost0.0.0.0/listenhost&#xA;/clickhouse&#xA;EOF&#xA;&#xA;4. Variables d&#39;environnement&#xA;&#xA;Ajoutez ces variables à votre fichier .env :&#xA;&#xA;PLAUSIBLESECRETKEY=$(openssl rand -base64 48)&#xA;PLAUSIBLETOTPKEY=$(openssl rand -base64 32)&#xA;PLAUSIBLEDBPASSWORD=$(openssl rand -base64 24)&#xA;&#xA;5. Démarrage&#xA;&#xA;docker compose -f docker-compose.yml -f services/plausible.yml up -d&#xA;&#xA;Le premier démarrage peut prendre 1-2 minutes. Vérifiez que tout est healthy :&#xA;&#xA;docker ps | grep plausible&#xA;&#xA;6. Création du compte admin&#xA;&#xA;Rendez-vous sur https://plausible.votredomaine.org et créez votre compte administrateur.&#xA;&#xA;Intégration avec PeerTube&#xA;&#xA;Ajouter le site dans Plausible&#xA;&#xA;Connectez-vous à votre instance Plausible&#xA;Cliquez sur &#34;Add a website&#34;&#xA;Entrez le domaine de votre PeerTube (ex: video.ut0pia.org)&#xA;&#xA;Injecter le script dans PeerTube&#xA;&#xA;Dans PeerTube, allez dans Administration → Configuration → Advanced → Custom JavaScript et ajoutez :&#xA;&#xA;var script = document.createElement(&#39;script&#39;);&#xA;script.defer = true;&#xA;script.dataset.domain = &#39;video.votredomaine.org&#39;;&#xA;script.src = &#39;https://plausible.votredomaine.org/js/script.js&#39;;&#xA;document.head.appendChild(script);&#xA;&#xA;Attention : Le champ &#34;Custom JavaScript&#34; attend du JavaScript pur, pas de balises HTML script.&#xA;&#xA;Conformité RGPD&#xA;&#xA;Plausible est conçu pour être conforme au RGPD sans bannière de consentement. Selon la a href=&#34;https://plausible.io/data-policy&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;politique de données de Plausible/a :&#xA;&#xA;Aucun cookie n&#39;est utilisé&#xA;Aucune donnée personnelle n&#39;est collectée&#xA;Les adresses IP sont hashées et jamais stockées&#xA;Pas de tracking cross-site&#xA;&#xA;Vous pouvez donc l&#39;utiliser sans afficher de bannière cookie, tout en restant transparent dans votre politique de confidentialité.&#xA;&#xA;Ressources&#xA;&#xA;a href=&#34;https://plausible.io/docs/self-hosting&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;Documentation Plausible Self-Hosting/a&#xA;a href=&#34;https://github.com/plausible/community-edition&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;Plausible Community Edition sur GitHub/a&#xA;a href=&#34;https://docs.joinpeertube.org/admin/customize-instance&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;Documentation PeerTube - Personnalisation/a&#xA;a href=&#34;https://github.com/jblemee/docker-compose-homelab&#34; target=&#34;blank&#34; rel=&#34;noopener&#34;Notre configuration Docker Compose Homelab/a&#xA;a href=&#34;https://plausible.io/data-policy&#34; target=&#34;_blank&#34; rel=&#34;noopener&#34;Politique de données Plausible (RGPD)/a]]&gt;</description>
      <content:encoded><![CDATA[<p>Vous hébergez une instance PeerTube et vous souhaitez suivre vos statistiques de fréquentation sans compromettre la vie privée de vos visiteurs ? Plausible Analytics est la solution idéale.</p>



<h2 id="pourquoi-plausible">Pourquoi Plausible ?</h2>

<p><a href="https://plausible.io/" target="_blank">Plausible</a> est une alternative open-source et respectueuse de la vie privée à Google Analytics. Contrairement à ce dernier :</p>
<ul><li><strong>Pas de cookies</strong> : aucune bannière RGPD nécessaire</li>
<li><strong>Pas de données personnelles</strong> : les adresses IP sont hashées puis supprimées</li>
<li><strong>Léger</strong> : le script fait moins de 1 Ko (vs ~45 Ko pour Google Analytics)</li>
<li><strong>Open-source</strong> : vous pouvez l&#39;auto-héberger</li></ul>

<h2 id="notre-infrastructure">Notre infrastructure</h2>

<p>Notre homelab utilise Docker Compose avec <code>nginx-proxy</code> pour le reverse proxy et Let&#39;s Encrypt pour les certificats SSL. La configuration complète est disponible sur <a href="https://github.com/jblemee/docker-compose-homelab" target="_blank">GitHub</a>.</p>

<h2 id="installation-de-plausible">Installation de Plausible</h2>

<h3 id="1-configuration-dns">1. Configuration DNS</h3>

<p>Ajoutez un enregistrement DNS pour votre sous-domaine analytics. Dans notre cas, nous utilisons <code>plausible.votredomaine.org</code> :</p>

<pre><code>python3 scripts/ovh-dns.py add plausible --type A --ip VOTRE_IP
python3 scripts/ovh-dns.py add plausible --type AAAA --ip VOTRE_IPV6
</code></pre>

<h3 id="2-fichier-docker-compose">2. Fichier Docker Compose</h3>

<p>Créez le fichier <code>services/plausible.yml</code> :</p>

<pre><code class="language-yaml">services:
  plausible:
    image: ghcr.io/plausible/community-edition:v3.1.0
    container_name: plausible
    command: /entrypoint.sh run
    healthcheck:
      test: [&#34;CMD-SHELL&#34;, &#34;wget -q --spider http://127.0.0.1:8000/api/health || exit 1&#34;]
      start_period: 2m
      interval: 30s
    environment:
      - BASE_URL=https://plausible.${DOMAIN}
      - SECRET_KEY_BASE=${PLAUSIBLE_SECRET_KEY}
      - TOTP_VAULT_KEY=${PLAUSIBLE_TOTP_KEY}
      - DISABLE_REGISTRATION=invite_only
      - DATABASE_URL=postgres://plausible:${PLAUSIBLE_DB_PASSWORD}@plausible-db:5432/plausible
      - CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events_db
      - VIRTUAL_HOST=plausible.${DOMAIN}
      - VIRTUAL_PORT=8000
      - LETSENCRYPT_HOST=plausible.${DOMAIN}
      - LETSENCRYPT_EMAIL=${LETSENCRYPT_EMAIL}
    volumes:
      - plausible-data:/var/lib/plausible
    networks:
      - proxy-tier
      - plausible-internal
    depends_on:
      plausible-db:
        condition: service_healthy
      plausible-events-db:
        condition: service_healthy

  plausible-db:
    image: postgres:16-alpine
    container_name: plausible-db
    environment:
      - POSTGRES_USER=plausible
      - POSTGRES_PASSWORD=${PLAUSIBLE_DB_PASSWORD}
      - POSTGRES_DB=plausible
    volumes:
      - /data/plausible-db:/var/lib/postgresql/data
    networks:
      - plausible-internal
    healthcheck:
      test: [&#34;CMD-SHELL&#34;, &#34;pg_isready -U plausible -d plausible&#34;]
      start_period: 1m
      interval: 10s

  plausible-events-db:
    image: clickhouse/clickhouse-server:24.12-alpine
    container_name: plausible-events-db
    environment:
      - CLICKHOUSE_SKIP_USER_SETUP=1
    volumes:
      - /data/plausible-events-db:/var/lib/clickhouse
      - /data/plausible-clickhouse-config/listen.xml:/etc/clickhouse-server/config.d/listen.xml:ro
    networks:
      - plausible-internal
    healthcheck:
      test: [&#34;CMD-SHELL&#34;, &#34;wget --no-verbose --tries=1 --spider http://127.0.0.1:8123/ping || exit 1&#34;]
      start_period: 1m
      interval: 10s

networks:
  proxy-tier:
    external: true
  plausible-internal:
    driver: bridge

volumes:
  plausible-data:
</code></pre>

<h3 id="3-configuration-clickhouse-pour-ipv4">3. Configuration ClickHouse pour IPv4</h3>

<p>ClickHouse écoute par défaut sur IPv6. Si votre réseau Docker n&#39;a pas IPv6, créez ce fichier :</p>

<pre><code class="language-bash">sudo mkdir -p /data/plausible-clickhouse-config
sudo tee /data/plausible-clickhouse-config/listen.xml &lt;&lt; &#39;EOF&#39;
&lt;clickhouse&gt;
    &lt;listen_host&gt;0.0.0.0&lt;/listen_host&gt;
&lt;/clickhouse&gt;
EOF
</code></pre>

<h3 id="4-variables-d-environnement">4. Variables d&#39;environnement</h3>

<p>Ajoutez ces variables à votre fichier <code>.env</code> :</p>

<pre><code class="language-bash">PLAUSIBLE_SECRET_KEY=$(openssl rand -base64 48)
PLAUSIBLE_TOTP_KEY=$(openssl rand -base64 32)
PLAUSIBLE_DB_PASSWORD=$(openssl rand -base64 24)
</code></pre>

<h3 id="5-démarrage">5. Démarrage</h3>

<pre><code class="language-bash">docker compose -f docker-compose.yml -f services/plausible.yml up -d
</code></pre>

<p>Le premier démarrage peut prendre 1-2 minutes. Vérifiez que tout est healthy :</p>

<pre><code class="language-bash">docker ps | grep plausible
</code></pre>

<h3 id="6-création-du-compte-admin">6. Création du compte admin</h3>

<p>Rendez-vous sur <code>https://plausible.votredomaine.org</code> et créez votre compte administrateur.</p>

<h2 id="intégration-avec-peertube">Intégration avec PeerTube</h2>

<h3 id="ajouter-le-site-dans-plausible">Ajouter le site dans Plausible</h3>
<ol><li>Connectez-vous à votre instance Plausible</li>
<li>Cliquez sur “Add a website”</li>
<li>Entrez le domaine de votre PeerTube (ex: <code>video.ut0pia.org</code>)</li></ol>

<h3 id="injecter-le-script-dans-peertube">Injecter le script dans PeerTube</h3>

<p>Dans PeerTube, allez dans <strong>Administration → Configuration → Advanced → Custom JavaScript</strong> et ajoutez :</p>

<pre><code class="language-javascript">var script = document.createElement(&#39;script&#39;);
script.defer = true;
script.dataset.domain = &#39;video.votredomaine.org&#39;;
script.src = &#39;https://plausible.votredomaine.org/js/script.js&#39;;
document.head.appendChild(script);
</code></pre>

<p><strong>Attention</strong> : Le champ “Custom JavaScript” attend du JavaScript pur, pas de balises HTML <code>&lt;script&gt;</code>.</p>

<h2 id="conformité-rgpd">Conformité RGPD</h2>

<p>Plausible est conçu pour être conforme au RGPD <strong>sans bannière de consentement</strong>. Selon la <a href="https://plausible.io/data-policy" target="_blank">politique de données de Plausible</a> :</p>
<ul><li>Aucun cookie n&#39;est utilisé</li>
<li>Aucune donnée personnelle n&#39;est collectée</li>
<li>Les adresses IP sont hashées et jamais stockées</li>
<li>Pas de tracking cross-site</li></ul>

<p>Vous pouvez donc l&#39;utiliser sans afficher de bannière cookie, tout en restant transparent dans votre politique de confidentialité.</p>

<h2 id="ressources">Ressources</h2>
<ul><li><a href="https://plausible.io/docs/self-hosting" target="_blank">Documentation Plausible Self-Hosting</a></li>
<li><a href="https://github.com/plausible/community-edition" target="_blank">Plausible Community Edition sur GitHub</a></li>
<li><a href="https://docs.joinpeertube.org/admin/customize-instance" target="_blank">Documentation PeerTube – Personnalisation</a></li>
<li><a href="https://github.com/jblemee/docker-compose-homelab" target="_blank">Notre configuration Docker Compose Homelab</a></li>
<li><a href="https://plausible.io/data-policy" target="_blank">Politique de données Plausible (RGPD)</a></li></ul>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/installer-plausible-analytics-pour-peertube</guid>
      <pubDate>Mon, 05 Jan 2026 14:41:21 +0000</pubDate>
    </item>
    <item>
      <title>Comment Jules, 15 ans, a redécouvert les relations de Viète</title>
      <link>https://blog.ut0pia.org/comment-jules-15-ans-a-redecouvert-les-relations-de-viete</link>
      <description>&lt;![CDATA[Mon fils m&#39;a récemment présenté une formule mathématique qu&#39;il avait trouvée par lui-même. Il s&#39;ennuyait en cours de maths et, il a élaboré un solution à un problème : comment retrouver deux nombres quand on connaît seulement leur somme et leur produit ?&#xA;&#xA;!--more--&#xA;&#xA;Sa solution :&#xA;&#xA;x = S/2 + √(S²/4 - P)&#xA;y = S/2 - √(S²/4 - P)&#xA;&#xA;où S est la somme et P le produit des deux nombres recherchés.&#xA;&#xA;Intrigué par sa découverte, j&#39;ai voulu vérifier formellement sa démonstration en utilisant un assistant de preuve mathématique. Cet article raconte notre démarche.&#xA;&#xA;Le raisonnement de Jules&#xA;&#xA;L&#39;intuition géométrique&#xA;&#xA;Jules a imaginé le problème à partir d&#39;un grand carré. Son raisonnement était le suivant :&#xA;&#xA;  « La somme de deux nombres, c&#39;est forcément égal à la moitié plus quelque chose, plus la moitié moins quelque chose. Du coup on peut barrer les écarts quand on fait la somme. Mais ces écarts sont utiles pour trouver le produit. »&#xA;&#xA;En termes mathématiques, il a posé :&#xA;x = S/2 + d (la moyenne plus un écart)&#xA;y = S/2 - d (la moyenne moins le même écart)&#xA;&#xA;Pourquoi ça marche pour la somme&#xA;&#xA;x + y = (S/2 + d) + (S/2 - d) = S&#xA;&#xA;Les écarts +d et -d « se barrent » — c&#39;est exactement ce qu&#39;il avait observé.&#xA;&#xA;La visualisation avec le carré&#xA;&#xA;Jules a visualisé le produit x × y comme une opération sur des carrés :&#xA;&#xA;┌─────────────────┐&#xA;│                 │&#xA;│   Carré de      │    Aire = (S/2)²&#xA;│   côté S/2      │&#xA;│                 │&#xA;└─────────────────┘&#xA;&#xA;Le produit (S/2 + d)(S/2 - d) correspond à l&#39;aire du grand carré moins un petit carré de côté d :&#xA;&#xA;Produit = (S/2)² - d²&#xA;&#xA;C&#39;est l&#39;identité remarquable (a+b)(a-b) = a² - b², qu&#39;il a retrouvée intuitivement.&#xA;&#xA;Déduction de la formule&#xA;&#xA;Puisque P = (S/2)² - d², on isole d :&#xA;&#xA;d² = S²/4 - P&#xA;d = √(S²/4 - P)&#xA;&#xA;D&#39;où la formule finale :&#xA;x = S/2 + √(S²/4 - P)&#xA;y = S/2 - √(S²/4 - P)&#xA;&#xA;Vérification formelle avec Lean 4&#xA;&#xA;Pour m&#39;assurer que le raisonnement de Jules était mathématiquement rigoureux, j&#39;ai décidé d&#39;utiliser Lean 4, un langage de programmation conçu pour écrire des preuves mathématiques vérifiées par ordinateur.&#xA;&#xA;Qu&#39;est-ce que Lean ?&#xA;&#xA;Lean est un assistant de preuve : un langage de programmation où l&#39;on écrit des théorèmes et leurs démonstrations. Le compilateur vérifie automatiquement que chaque étape de la preuve est logiquement correcte. Si le code compile, la preuve est mathématiquement valide.&#xA;&#xA;Installation de Lean 4&#xA;&#xA;J&#39;ai installé Lean 4 en quelques commandes :&#xA;&#xA;1. Installer elan (gestionnaire de versions Lean)&#xA;brew install elan-init&#xA;&#xA;2. Installer Lean 4 stable&#xA;elan default stable&#xA;&#xA;3. Créer un nouveau projet avec Mathlib&#xA;lake new theoremesjules&#xA;cd theoremesjules&#xA;&#xA;4. Ajouter Mathlib comme dépendance (dans lakefile.toml)&#xA;puis télécharger les dépendances&#xA;lake update&#xA;&#xA;Le code Lean pas à pas&#xA;&#xA;Voici la formalisation complète du théorème de Jules, avec des explications pour chaque partie :&#xA;&#xA;import Mathlib.Data.Real.Sqrt&#xA;&#xA;Import : On importe le module de Mathlib qui contient la définition de la racine carrée sur les nombres réels (Real.sqrt) et le lemme Real.sqsqrt qui dit que (√z)² = z.&#xA;&#xA;---&#xA;&#xA;def EstSolutionSommeProduit (x y S P : ℝ) : Prop :=&#xA;  x + y = S ∧ x  y = P&#xA;&#xA;Définition : On définit ce que signifie &#34;x et y sont solutions du problème somme-produit&#34;. C&#39;est une proposition (Prop) qui est vraie si et seulement si :&#xA;x + y = S (la somme vaut S)&#xA;ET (∧)&#xA;x  y = P (le produit vaut P)&#xA;&#xA;---&#xA;&#xA;theorem formulejules (S P : ℝ) (h : S^2 / 4 - P ≥ 0) :&#xA;    EstSolutionSommeProduit&#xA;      (S / 2 + Real.sqrt (S^2 / 4 - P))&#xA;      (S / 2 - Real.sqrt (S^2 / 4 - P))&#xA;      S P := by&#xA;&#xA;Énoncé du théorème :&#xA;S P : ℝ — S et P sont des nombres réels&#xA;h : S^2 / 4 - P ≥ 0 — on suppose que S²/4 - P ≥ 0 (sinon la racine carrée n&#39;existe pas dans ℝ)&#xA;Les deux arguments suivants sont x et y selon la formule de Jules&#xA;EstSolutionSommeProduit ... S P — on veut prouver que x et y sont bien solutions&#xA;by — on commence la preuve en mode tactique&#xA;&#xA;---&#xA;&#xA;  constructor&#xA;&#xA;Tactique constructor : Comme on doit prouver une conjonction (A ∧ B), cette tactique sépare le but en deux sous-buts :&#xA;Prouver que x + y = S&#xA;Prouver que x  y = P&#xA;&#xA;---&#xA;&#xA;  · ring&#xA;&#xA;Première partie (prouver x + y = S) :&#xA;ring — résout automatiquement l&#39;égalité algébrique&#xA;&#xA;En effet : (S/2 + √...) + (S/2 - √...) = S/2 + S/2 = S ✓&#xA;&#xA;---&#xA;&#xA;  · have h1 : (S / 2 + Real.sqrt (S^2 / 4 - P))  (S / 2 - Real.sqrt (S^2 / 4 - P))&#xA;            = (S / 2)^2 - (Real.sqrt (S^2 / 4 - P))^2 := by ring&#xA;    rw [h1, Real.sqsqrt h]&#xA;    ring&#xA;&#xA;Deuxième partie (prouver x × y = P) :&#xA;have h1 : ... := by ring — on établit un fait intermédiaire : le produit (a+b)(a-b) = a² - b² (identité remarquable), prouvé par ring&#xA;rw [h1, Real.sqsqrt h] — on réécrit en utilisant h1, puis on applique le lemme Real.sqsqrt qui dit que (√z)² = z quand z ≥ 0 (ce qu&#39;on sait grâce à l&#39;hypothèse h)&#xA;ring — on conclut que (S/2)² - (S²/4 - P) = S²/4 - S²/4 + P = P&#xA;&#xA;Le verdict&#xA;&#xA;✓ Build successful&#xA;&#xA;Le code compile — donc la preuve est mathématiquement correcte. Le théorème de Jules est formellement vérifié : sa formule donne bien deux nombres dont la somme est S et le produit est P.&#xA;&#xA;Contexte historique&#xA;&#xA;François Viète (1540-1603)&#xA;&#xA;Jules a redécouvert ce qu&#39;on appelle les relations de Viète, du nom du mathématicien français François Viète.&#xA;&#xA;En 1591, Viète publie In artem analyticem isagoge, un ouvrage qui fonde l&#39;algèbre moderne. Il est le premier à utiliser des lettres pour désigner les inconnues (voyelles A, E, I...) et les constantes (consonnes B, C, D...). On lui doit l&#39;invention du calcul littéral.&#xA;&#xA;Les formules de Viète établissent que pour un polynôme du second degré x² - Sx + P = 0 :&#xA;La somme des racines = S&#xA;Le produit des racines = P&#xA;&#xA;C&#39;est exactement le problème solutionné par mon fils.&#xA;&#xA;Le lien précis avec la formule de Jules&#xA;&#xA;Jules a résolu le problème inverse de Viète : connaissant S et P, retrouver x et y.&#xA;&#xA;Si x et y vérifient x + y = S et x × y = P, alors x et y sont les racines du polynôme :&#xA;&#xA;t² - St + P = 0&#xA;&#xA;La formule quadratique classique donne :&#xA;&#xA;t = (S ± √(S² - 4P)) / 2&#xA;&#xA;En réécrivant cette formule :&#xA;&#xA;t = S/2 ± √(S² - 4P)/2&#xA;t = S/2 ± √((S² - 4P)/4)&#xA;t = S/2 ± √(S²/4 - P)      ← C&#39;est la formule de Jules !&#xA;&#xA;| Viète (1591) | Jules (2024) |&#xA;|--------------|-----------------|&#xA;| Si x, y sont racines de t² - St + P = 0 | Connaissant S et P |&#xA;| Alors x + y = S et xy = P | Trouver x = S/2 + √(S²/4 - P) |&#xA;| (sens direct) | (sens réciproque) |&#xA;&#xA;Mon fils a donc retrouvé la formule quadratique par un raisonnement purement géométrique, ce qui est exactement dans l&#39;esprit des travaux de Viète — établir le lien entre les racines d&#39;un polynôme et ses coefficients.&#xA;&#xA;Sources :&#xA;Formules de Viète - Math93&#xA;François Viète - CultureMath (ENS)&#xA;Biographie de Viète - Bibmath&#xA;&#xA;Les mathématiciens grecs et l&#39;algèbre géométrique&#xA;&#xA;L&#39;approche de Jules — raisonner avec des carrés et des aires — rappelle celle des mathématiciens grecs anciens.&#xA;&#xA;Le Livre II des Éléments d&#39;Euclide (vers 300 av. J.-C.) contient ce qu&#39;on appelle l&#39;algèbre géométrique. Euclide y démontre les identités remarquables par des constructions de carrés et de rectangles, exactement comme Jules l&#39;a fait intuitivement.&#xA;&#xA;Par exemple, la proposition 4 du Livre II énonce que « le carré de la droite entière est égal aux carrés des segments, et à deux fois le rectangle contenu sous les deux segments » — c&#39;est notre identité (a+b)² = a² + 2ab + b².&#xA;&#xA;Cette tradition géométrique a influencé les mathématiciens arabes comme Al-Khwarizmi (VIIIe siècle), dont le traité Abrégé du calcul par la restauration et la comparaison utilise les identités remarquables d&#39;Euclide pour résoudre les équations du second degré.&#xA;&#xA;Sources :&#xA;Livre II des Éléments d&#39;Euclide - Wikipédia&#xA;Identité remarquable - Wikipédia&#xA;Les identités remarquables - Automaths&#xA;&#xA;Conclusion&#xA;&#xA;À 15 ans, Jules a redécouvert de manière autonome un résultat fondamental de l&#39;algèbre, en utilisant une intuition géométrique remarquable. Son approche — décomposer deux nombres symétriquement autour de leur moyenne — est élégante et montre une compréhension profonde de la structure mathématique.&#xA;&#xA;Ce qui me frappe, c&#39;est que son raisonnement suit exactement la tradition des mathématiciens grecs : penser l&#39;algèbre géométriquement, avec des carrés et des aires. Sans le savoir, il a refait le chemin d&#39;Euclide.&#xA;&#xA;Grâce à Lean 4, nous avons pu vérifier formellement que sa démonstration est mathématiquement correcte. Le théorème est prouvé.&#xA;&#xA;---&#xA;&#xA;Preuve formellement vérifiée avec Lean 4 et Mathlib]]&gt;</description>
      <content:encoded><![CDATA[<p>Mon fils m&#39;a récemment présenté une formule mathématique qu&#39;il avait trouvée par lui-même. Il s&#39;ennuyait en cours de maths et, il a élaboré un solution à un problème : <strong>comment retrouver deux nombres quand on connaît seulement leur somme et leur produit ?</strong></p>



<p>Sa solution :</p>

<pre><code>x = S/2 + √(S²/4 - P)
y = S/2 - √(S²/4 - P)
</code></pre>

<p>où S est la somme et P le produit des deux nombres recherchés.</p>

<p>Intrigué par sa découverte, j&#39;ai voulu vérifier formellement sa démonstration en utilisant un assistant de preuve mathématique. Cet article raconte notre démarche.</p>

<h2 id="le-raisonnement-de-jules">Le raisonnement de Jules</h2>

<h3 id="l-intuition-géométrique">L&#39;intuition géométrique</h3>

<p>Jules a imaginé le problème à partir d&#39;un <strong>grand carré</strong>. Son raisonnement était le suivant :</p>

<blockquote><p>« La somme de deux nombres, c&#39;est forcément égal à la moitié plus quelque chose, plus la moitié moins quelque chose. Du coup on peut barrer les écarts quand on fait la somme. Mais ces écarts sont utiles pour trouver le produit. »</p></blockquote>

<p>En termes mathématiques, il a posé :
– x = S/2 + d (la moyenne plus un écart)
– y = S/2 – d (la moyenne moins le même écart)</p>

<h3 id="pourquoi-ça-marche-pour-la-somme">Pourquoi ça marche pour la somme</h3>

<pre><code>x + y = (S/2 + d) + (S/2 - d) = S
</code></pre>

<p>Les écarts +d et -d « se barrent » — c&#39;est exactement ce qu&#39;il avait observé.</p>

<h3 id="la-visualisation-avec-le-carré">La visualisation avec le carré</h3>

<p>Jules a visualisé le produit x × y comme une opération sur des carrés :</p>

<pre><code>┌─────────────────┐
│                 │
│   Carré de      │    Aire = (S/2)²
│   côté S/2      │
│                 │
└─────────────────┘
</code></pre>

<p>Le produit (S/2 + d)(S/2 – d) correspond à l&#39;aire du grand carré moins un petit carré de côté d :</p>

<pre><code>Produit = (S/2)² - d²
</code></pre>

<p>C&#39;est l&#39;identité remarquable (a+b)(a-b) = a² – b², qu&#39;il a retrouvée intuitivement.</p>

<h3 id="déduction-de-la-formule">Déduction de la formule</h3>

<p>Puisque P = (S/2)² – d², on isole d :</p>

<pre><code>d² = S²/4 - P
d = √(S²/4 - P)
</code></pre>

<p>D&#39;où la formule finale :</p>

<pre><code>x = S/2 + √(S²/4 - P)
y = S/2 - √(S²/4 - P)
</code></pre>

<h2 id="vérification-formelle-avec-lean-4">Vérification formelle avec Lean 4</h2>

<p>Pour m&#39;assurer que le raisonnement de Jules était mathématiquement rigoureux, j&#39;ai décidé d&#39;utiliser <strong>Lean 4</strong>, un langage de programmation conçu pour écrire des preuves mathématiques vérifiées par ordinateur.</p>

<h3 id="qu-est-ce-que-lean">Qu&#39;est-ce que Lean ?</h3>

<p>Lean est un <strong>assistant de preuve</strong> : un langage de programmation où l&#39;on écrit des théorèmes et leurs démonstrations. Le compilateur vérifie automatiquement que chaque étape de la preuve est logiquement correcte. Si le code compile, la preuve est mathématiquement valide.</p>

<h3 id="installation-de-lean-4">Installation de Lean 4</h3>

<p>J&#39;ai installé Lean 4 en quelques commandes :</p>

<pre><code class="language-bash"># 1. Installer elan (gestionnaire de versions Lean)
brew install elan-init

# 2. Installer Lean 4 stable
elan default stable

# 3. Créer un nouveau projet avec Mathlib
lake new theoremes_jules
cd theoremes_jules

# 4. Ajouter Mathlib comme dépendance (dans lakefile.toml)
# puis télécharger les dépendances
lake update
</code></pre>

<h3 id="le-code-lean-pas-à-pas">Le code Lean pas à pas</h3>

<p>Voici la formalisation complète du théorème de Jules, avec des explications pour chaque partie :</p>

<pre><code class="language-lean">import Mathlib.Data.Real.Sqrt
</code></pre>

<p><strong>Import</strong> : On importe le module de Mathlib qui contient la définition de la racine carrée sur les nombres réels (<code>Real.sqrt</code>) et le lemme <code>Real.sq_sqrt</code> qui dit que (√z)² = z.</p>

<hr>

<pre><code class="language-lean">def EstSolutionSommeProduit (x y S P : ℝ) : Prop :=
  x + y = S ∧ x * y = P
</code></pre>

<p><strong>Définition</strong> : On définit ce que signifie “x et y sont solutions du problème somme-produit”. C&#39;est une <strong>proposition</strong> (<code>Prop</code>) qui est vraie si et seulement si :
– <code>x + y = S</code> (la somme vaut S)
– <strong>ET</strong> (<code>∧</code>)
– <code>x * y = P</code> (le produit vaut P)</p>

<hr>

<pre><code class="language-lean">theorem formule_jules (S P : ℝ) (h : S^2 / 4 - P ≥ 0) :
    EstSolutionSommeProduit
      (S / 2 + Real.sqrt (S^2 / 4 - P))
      (S / 2 - Real.sqrt (S^2 / 4 - P))
      S P := by
</code></pre>

<p><strong>Énoncé du théorème</strong> :
– <code>S P : ℝ</code> — S et P sont des nombres réels
– <code>h : S^2 / 4 - P ≥ 0</code> — on suppose que S²/4 – P ≥ 0 (sinon la racine carrée n&#39;existe pas dans ℝ)
– Les deux arguments suivants sont x et y selon la formule de Jules
– <code>EstSolutionSommeProduit ... S P</code> — on veut prouver que x et y sont bien solutions
– <code>by</code> — on commence la preuve en mode tactique</p>

<hr>

<pre><code class="language-lean">  constructor
</code></pre>

<p><strong>Tactique <code>constructor</code></strong> : Comme on doit prouver une conjonction (A ∧ B), cette tactique sépare le but en deux sous-buts :
1. Prouver que <code>x + y = S</code>
2. Prouver que <code>x * y = P</code></p>

<hr>

<pre><code class="language-lean">  · ring
</code></pre>

<p><strong>Première partie</strong> (prouver x + y = S) :
– <code>ring</code> — résout automatiquement l&#39;égalité algébrique</p>

<p>En effet : <code>(S/2 + √...) + (S/2 - √...) = S/2 + S/2 = S</code> ✓</p>

<hr>

<pre><code class="language-lean">  · have h1 : (S / 2 + Real.sqrt (S^2 / 4 - P)) * (S / 2 - Real.sqrt (S^2 / 4 - P))
            = (S / 2)^2 - (Real.sqrt (S^2 / 4 - P))^2 := by ring
    rw [h1, Real.sq_sqrt h]
    ring
</code></pre>

<p><strong>Deuxième partie</strong> (prouver x × y = P) :
– <code>have h1 : ... := by ring</code> — on établit un fait intermédiaire : le produit (a+b)(a-b) = a² – b² (identité remarquable), prouvé par <code>ring</code>
– <code>rw [h1, Real.sq_sqrt h]</code> — on réécrit en utilisant h1, puis on applique le lemme <code>Real.sq_sqrt</code> qui dit que (√z)² = z quand z ≥ 0 (ce qu&#39;on sait grâce à l&#39;hypothèse <code>h</code>)
– <code>ring</code> — on conclut que (S/2)² – (S²/4 – P) = S²/4 – S²/4 + P = P</p>

<h3 id="le-verdict">Le verdict</h3>

<pre><code>✓ Build successful
</code></pre>

<p><strong>Le code compile</strong> — donc la preuve est mathématiquement correcte. Le théorème de Jules est formellement vérifié : sa formule donne bien deux nombres dont la somme est S et le produit est P.</p>

<h2 id="contexte-historique">Contexte historique</h2>

<h3 id="françois-viète-1540-1603">François Viète (1540-1603)</h3>

<p>Jules a redécouvert ce qu&#39;on appelle les <strong>relations de Viète</strong>, du nom du mathématicien français <a href="https://www.bibmath.net/bios/index.php?action=affiche&amp;quoi=viete">François Viète</a>.</p>

<p>En 1591, Viète publie <em>In artem analyticem isagoge</em>, un ouvrage qui fonde l&#39;algèbre moderne. Il est le premier à utiliser des lettres pour désigner les inconnues (voyelles A, E, I...) et les constantes (consonnes B, C, D...). On lui doit l&#39;invention du <strong>calcul littéral</strong>.</p>

<p>Les <a href="https://fr.wikipedia.org/wiki/Relations_entre_coefficients_et_racines">formules de Viète</a> établissent que pour un polynôme du second degré x² – Sx + P = 0 :
– La somme des racines = S
– Le produit des racines = P</p>

<p>C&#39;est exactement le problème solutionné par mon fils.</p>

<h3 id="le-lien-précis-avec-la-formule-de-jules">Le lien précis avec la formule de Jules</h3>

<p>Jules a résolu le <strong>problème inverse</strong> de Viète : connaissant S et P, retrouver x et y.</p>

<p>Si x et y vérifient x + y = S et x × y = P, alors x et y sont les <strong>racines</strong> du polynôme :</p>

<pre><code>t² - St + P = 0
</code></pre>

<p>La formule quadratique classique donne :</p>

<pre><code>t = (S ± √(S² - 4P)) / 2
</code></pre>

<p>En réécrivant cette formule :</p>

<pre><code>t = S/2 ± √(S² - 4P)/2
t = S/2 ± √((S² - 4P)/4)
t = S/2 ± √(S²/4 - P)      ← C&#39;est la formule de Jules !
</code></pre>

<table>
<thead>
<tr>
<th>Viète (1591)</th>
<th>Jules (2024)</th>
</tr>
</thead>

<tbody>
<tr>
<td>Si x, y sont racines de t² – St + P = 0</td>
<td>Connaissant S et P</td>
</tr>

<tr>
<td>Alors x + y = S et xy = P</td>
<td>Trouver x = S/2 + √(S²/4 – P)</td>
</tr>

<tr>
<td><em>(sens direct)</em></td>
<td><em>(sens réciproque)</em></td>
</tr>
</tbody>
</table>

<p>Mon fils a donc retrouvé la <strong>formule quadratique</strong> par un raisonnement purement géométrique, ce qui est exactement dans l&#39;esprit des travaux de Viète — établir le lien entre les racines d&#39;un polynôme et ses coefficients.</p>

<p><strong>Sources :</strong>
– <a href="https://www.math93.com/annales-du-bac/bac-specialite-nsi/19-histoire-des-mathematiques/1183-formules-de-viete-relations-entre-racines-et-coefficients-des-polynomes.html">Formules de Viète – Math93</a>
– <a href="http://culturemath.ens.fr/node/2107">François Viète – CultureMath (ENS)</a>
– <a href="https://www.bibmath.net/bios/index.php?action=affiche&amp;quoi=viete">Biographie de Viète – Bibmath</a></p>

<h3 id="les-mathématiciens-grecs-et-l-algèbre-géométrique">Les mathématiciens grecs et l&#39;algèbre géométrique</h3>

<p>L&#39;approche de Jules — raisonner avec des carrés et des aires — rappelle celle des mathématiciens grecs anciens.</p>

<p>Le <a href="https://fr.wikipedia.org/wiki/Livre_II_des_%C3%89l%C3%A9ments_d%27Euclide">Livre II des Éléments d&#39;Euclide</a> (vers 300 av. J.-C.) contient ce qu&#39;on appelle l&#39;<strong>algèbre géométrique</strong>. Euclide y démontre les identités remarquables par des constructions de carrés et de rectangles, exactement comme Jules l&#39;a fait intuitivement.</p>

<p>Par exemple, la proposition 4 du Livre II énonce que « le carré de la droite entière est égal aux carrés des segments, et à deux fois le rectangle contenu sous les deux segments » — c&#39;est notre identité (a+b)² = a² + 2ab + b².</p>

<p>Cette tradition géométrique a influencé les mathématiciens arabes comme <a href="https://fr.wikipedia.org/wiki/Al-Khw%C3%A2rizm%C3%AE">Al-Khwarizmi</a> (VIIIe siècle), dont le traité <em>Abrégé du calcul par la restauration et la comparaison</em> utilise les identités remarquables d&#39;Euclide pour résoudre les équations du second degré.</p>

<p><strong>Sources :</strong>
– <a href="https://fr.wikipedia.org/wiki/Livre_II_des_%C3%89l%C3%A9ments_d%27Euclide">Livre II des Éléments d&#39;Euclide – Wikipédia</a>
– <a href="https://fr.wikipedia.org/wiki/Identit%C3%A9_remarquable">Identité remarquable – Wikipédia</a>
– <a href="https://automathssite.wordpress.com/2019/09/14/des-identites-remarquables/">Les identités remarquables – Automaths</a></p>

<h2 id="conclusion">Conclusion</h2>

<p>À 15 ans, Jules a redécouvert de manière autonome un résultat fondamental de l&#39;algèbre, en utilisant une intuition géométrique remarquable. Son approche — décomposer deux nombres symétriquement autour de leur moyenne — est élégante et montre une compréhension profonde de la structure mathématique.</p>

<p>Ce qui me frappe, c&#39;est que son raisonnement suit exactement la tradition des mathématiciens grecs : penser l&#39;algèbre géométriquement, avec des carrés et des aires. Sans le savoir, il a refait le chemin d&#39;Euclide.</p>

<p>Grâce à Lean 4, nous avons pu vérifier formellement que sa démonstration est mathématiquement correcte. Le théorème est prouvé.</p>

<hr>

<p><em>Preuve formellement vérifiée avec <a href="https://lean-lang.org/">Lean 4</a> et <a href="https://leanprover-community.github.io/">Mathlib</a></em></p>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/comment-jules-15-ans-a-redecouvert-les-relations-de-viete</guid>
      <pubDate>Thu, 01 Jan 2026 16:14:31 +0000</pubDate>
    </item>
    <item>
      <title>Migrer un Homelab Docker Compose vers un Nouveau Serveur : Guide Complet</title>
      <link>https://blog.ut0pia.org/migrer-un-homelab-docker-compose-vers-un-nouveau-serveur-guide-complet</link>
      <description>&lt;![CDATA[Après plusieurs années de bons et loyaux services, mon homelab tournant sur un Intel Atom N2800 montrait ses limites. Ce processeur de 2011 ne supportait pas les instructions AVX/AVX2 — une contrainte qui m&#39;empêchait d&#39;utiliser certains logiciels modernes comme Homarr v1.0+ ou la transcription automatique de PeerTube via Whisper.&#xA;&#xA;!--more--&#xA;&#xA;La migration vers un serveur OVH SYS-1 équipé d&#39;un Intel Xeon E-2136 était l&#39;occasion de moderniser l&#39;infrastructure tout en documentant le processus pour d&#39;autres administrateurs de homelab.&#xA;&#xA;Ce que vous allez apprendre&#xA;&#xA;Préparer un nouveau serveur Ubuntu 24 pour Docker&#xA;Configurer SSH &#xA;Utiliser rsync avec préservation des hardlinks&#xA;Adapter une stack Docker Compose à un nouvel environnement&#xA;Gérer la migration DNS avec zero downtime&#xA;&#xA;Spécifications des serveurs&#xA;&#xA;| Élément | Ancien serveur | Nouveau serveur |&#xA;|---------|----------------|-----------------|&#xA;| CPU | Intel Atom N2800 (2 cœurs, pas AVX) | Intel Xeon E-2136 (6 cœurs, AVX2) |&#xA;| RAM | 4 Go | 32 Go |&#xA;| Stockage | 2 x 2 To HDD | 2 x 2 To SSD |&#xA;| OS | Ubuntu 22.04 | Ubuntu 24.04 LTS |&#xA;| Hébergeur | Kimsufi | OVH SYS-1 |&#xA;&#xA;Services à migrer&#xA;&#xA;Mon infrastructure Docker Compose comprend :&#xA;&#xA;Reverse proxy : nginx-proxy + acme-companion (Let&#39;s Encrypt automatique)&#xA;Vidéo : PeerTube (plateforme vidéo fédérée)&#xA;Dashboard : Homarr&#xA;Workflow : n8n&#xA;Blog : WriteFreely (fédéré via ActivityPub)&#xA;&#xA;Volume total des données : ~935 Go (réduit à ~350 Go après nettoyage).&#xA;&#xA;---&#xA;&#xA;1. Préparation du Nouveau Serveur&#xA;&#xA;1.1 Installation de Docker avec stockage dédié&#xA;&#xA;Sur Ubuntu 24 Server, la partition root est souvent limitée (typiquement 20-50 Go sur un serveur OVH). Il est crucial de configurer Docker pour stocker ses images et volumes sur une partition de données séparée.&#xA;&#xA;Installation de Docker via le script officiel&#xA;curl -fsSL https://get.docker.com | sudo sh&#xA;&#xA;Ajout de l&#39;utilisateur au groupe docker&#xA;sudo usermod -aG docker $USER&#xA;&#xA;Arrêt de Docker pour la configuration&#xA;sudo systemctl stop docker&#xA;&#xA;Configuration du stockage Docker sur /data/docker :&#xA;&#xA;Création du répertoire de données&#xA;sudo mkdir -p /data/docker&#xA;&#xA;Configuration de Docker avec data-root personnalisé&#xA;et limitation des logs pour éviter de saturer le disque&#xA;sudo tee /etc/docker/daemon.json &lt;&lt; &#39;EOF&#39;&#xA;{&#xA;  &#34;data-root&#34;: &#34;/data/docker&#34;,&#xA;  &#34;log-driver&#34;: &#34;json-file&#34;,&#xA;  &#34;log-opts&#34;: {&#xA;    &#34;max-size&#34;: &#34;10m&#34;,&#xA;    &#34;max-file&#34;: &#34;3&#34;&#xA;  }&#xA;}&#xA;EOF&#xA;&#xA;Redémarrage de Docker&#xA;sudo systemctl start docker&#xA;&#xA;Vérification&#xA;docker info | grep &#34;Docker Root Dir&#34;&#xA;Doit afficher: Docker Root Dir: /data/docker&#xA;&#xA;Pourquoi cette configuration ?&#xA;&#xA;data-root : Évite de saturer la partition root avec les images Docker&#xA;log-driver + log-opts : Limite chaque fichier de log à 10 Mo avec 3 rotations maximum, évitant l&#39;accumulation de logs volumineux&#xA;&#xA;1.2 Installation des dépendances Python (PEP 668)&#xA;&#xA;Ubuntu 24 implémente strictement le PEP 668 qui empêche l&#39;installation de paquets Python via pip au niveau système. Cette décision vise à éviter les conflits entre pip et apt.&#xA;&#xA;INCORRECT sur Ubuntu 24 - génère une erreur&#xA;pip3 install requests python-dotenv&#xA;error: externally-managed-environment&#xA;&#xA;CORRECT - utiliser les paquets système&#xA;sudo apt install -y python3-requests python3-dotenv&#xA;&#xA;Alternatives si vous avez besoin de versions spécifiques :&#xA;&#xA;Option 1 : Environnement virtuel (recommandé pour le développement)&#xA;python3 -m venv ~/venv&#xA;source ~/venv/bin/activate&#xA;pip install requests python-dotenv&#xA;&#xA;Option 2 : pipx pour les outils CLI&#xA;pipx install package&#xA;&#xA;Pour mes scripts d&#39;organisation de médias, les versions système suffisent.&#xA;&#xA;1.3 Installation de Node.js via nvm&#xA;&#xA;NodeSource, longtemps la méthode recommandée, a changé ses conditions en 2023. nvm (Node Version Manager) offre plus de flexibilité :&#xA;&#xA;Installation de nvm&#xA;curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash&#xA;&#xA;Rechargement du shell&#xA;source ~/.bashrc&#xA;&#xA;Installation de la dernière LTS&#xA;nvm install --lts&#xA;&#xA;Vérification&#xA;node --version&#xA;npm --version&#xA;&#xA;Avantages de nvm :&#xA;Installation dans le home utilisateur (pas de sudo)&#xA;Possibilité de switcher entre versions&#xA;Mise à jour simple via nvm install --lts&#xA;&#xA;---&#xA;&#xA;2. Configuration SSH Bidirectionnelle&#xA;&#xA;Pour transférer les données de manière sécurisée, nous devons établir une connexion SSH de l&#39;ancien vers le nouveau serveur et inversement.&#xA;&#xA;2.1 Génération de clé sur l&#39;ancien serveur&#xA;&#xA;Sur l&#39;ANCIEN serveur&#xA;ssh-keygen -t ed25519 -N &#34;&#34; -f ~/.ssh/ided25519&#xA;&#xA;Afficher la clé publique à copier&#xA;cat ~/.ssh/ided25519.pub&#xA;&#xA;Pourquoi ed25519 ?&#xA;Plus sécurisé que RSA à taille de clé équivalente&#xA;Plus rapide pour la génération et l&#39;authentification&#xA;Clés plus courtes (plus faciles à copier/coller)&#xA;&#xA;2.2 Autorisation sur le nouveau serveur&#xA;&#xA;Sur le NOUVEAU serveur&#xA;Ajouter la clé publique de l&#39;ancien serveur&#xA;echo &#34;ssh-ed25519 AAAA... user@ancien-serveur&#34;     ~/.ssh/authorizedkeys&#xA;&#xA;Vérifier les permissions (crucial pour SSH)&#xA;chmod 700 ~/.ssh&#xA;chmod 600 ~/.ssh/authorizedkeys&#xA;&#xA;2.3 Test de connexion&#xA;&#xA;Depuis l&#39;ANCIEN serveur, tester la connexion&#xA;ssh newuser@nouveau-serveur &#34;hostname &amp;&amp; uptime&#34;&#xA;&#xA;---&#xA;&#xA;3. Synchronisation avec rsync : L&#39;Art de Préserver les Hardlinks&#xA;&#xA;3.1 Comprendre les hardlinks&#xA;&#xA;Dans un système de fichiers, un hardlink est une référence directe au même inode sur le disque. Le fichier n&#39;existe qu&#39;une fois physiquement, mais apparaît à plusieurs endroits dans l&#39;arborescence.&#xA;&#xA;/data/source/fichier.mkv  (fichier original)&#xA;         |&#xA;         +-- hardlink --  /data/organise/fichier.mkv&#xA;&#xA;Cela permet :&#xA;D&#39;organiser les fichiers de différentes manières&#xA;De ne pas doubler l&#39;espace disque&#xA;D&#39;avoir plusieurs &#34;vues&#34; sur les mêmes données&#xA;&#xA;3.2 L&#39;importance du flag -H&#xA;&#xA;Sans le flag -H, rsync copie chaque chemin comme un fichier séparé :&#xA;&#xA;MAUVAIS : duplique les données&#xA;rsync -avz /data/ user@serveur:/data/&#xA;Résultat : 350 Go de fichiers + 350 Go de hardlinks = 700 Go&#xA;&#xA;CORRECT : préserve les hardlinks&#xA;rsync -avzH /data/ user@serveur:/data/&#xA;Résultat : 350 Go total (hardlinks préservés)&#xA;&#xA;3.3 Commande de synchronisation complète&#xA;&#xA;Synchronisation avec préservation des hardlinks&#xA;rsync -avzH --progress \&#xA;  --exclude=&#39;.tmp&#39; \&#xA;  --exclude=&#39;.part&#39; \&#xA;  /data/ newuser@nouveau-serveur:/data/&#xA;&#xA;Pour les syncs subséquentes, ajouter --delete&#xA;pour supprimer les fichiers effacés sur la source&#xA;rsync -avzH --progress --delete \&#xA;  /data/ newuser@nouveau-serveur:/data/&#xA;&#xA;Explication des flags :&#xA;-a : Archive (préserve permissions, propriétaire, timestamps, liens symboliques)&#xA;-v : Verbose (affiche les fichiers transférés)&#xA;-z : Compression durant le transfert&#xA;-H : Préserve les hardlinks (crucial pour notre cas)&#xA;--progress : Affiche la progression&#xA;--delete : Supprime les fichiers absents de la source&#xA;&#xA;3.4 Vérification des hardlinks après transfert&#xA;&#xA;Sur le nouveau serveur, vérifier qu&#39;un fichier a plusieurs liens&#xA;ls -li /data/media//fichier.mkv&#xA;La première colonne (inode) et la troisième (link count) doivent montrer   1&#xA;&#xA;Exemple de sortie :&#xA;1234567 -rw-r--r-- 2 newuser newuser 5.0G Dec 31 fichier.mkv&#xA;^-- 2 = le fichier existe à 2 endroits (hardlink)&#xA;&#xA;---&#xA;&#xA;4. Nettoyage Pré-Migration : 550 Go Économisés&#xA;&#xA;4.1 Identification du problème&#xA;&#xA;En analysant l&#39;espace disque, j&#39;ai découvert que de nombreux hardlinks étaient devenus &#34;orphelins&#34; : les fichiers sources avaient été supprimés, mais les hardlinks restaient dans les répertoires organisés.&#xA;&#xA;Un hardlink orphelin (link count = 1) n&#39;est plus un hardlink — c&#39;est juste un fichier normal qui occupe de l&#39;espace.&#xA;&#xA;4.2 Script de nettoyage des hardlinks orphelins&#xA;&#xA;!/usr/bin/env python3&#xA;&#34;&#34;&#34;&#xA;clean-orphan-hardlinks.py&#xA;Supprime les fichiers qui étaient des hardlinks mais dont la source&#xA;a été supprimée (link count = 1).&#xA;&#34;&#34;&#34;&#xA;import os&#xA;import sys&#xA;&#xA;Répertoires à nettoyer&#xA;DIRS = [&#39;/data/perso&#39;, &#39;/data/pro&#39;]&#xA;DRYRUN = &#39;--dry-run&#39; in sys.argv&#xA;&#xA;deleted = 0&#xA;freed = 0&#xA;&#xA;for directory in DIRS:&#xA;    if not os.path.isdir(directory):&#xA;        continue&#xA;    &#xA;    for root, dirs, files in os.walk(directory, topdown=False):&#xA;        for filename in files:&#xA;            filepath = os.path.join(root, filename)&#xA;            try:&#xA;                stat = os.stat(filepath)&#xA;                # Un hardlink a un link count   1&#xA;                # Si link count = 1, la source a été supprimée&#xA;                if stat.stnlink == 1:&#xA;                    freed += stat.stsize&#xA;                    if DRYRUN:&#xA;                        print(f&#34;[DRY-RUN] Would delete: {filepath}&#34;)&#xA;                    else:&#xA;                        os.remove(filepath)&#xA;                    deleted += 1&#xA;            except OSError:&#xA;                pass&#xA;        &#xA;        # Nettoyer les répertoires vides&#xA;        if not DRYRUN:&#xA;            try:&#xA;                if not os.listdir(root):&#xA;                    os.rmdir(root)&#xA;            except OSError:&#xA;                pass&#xA;&#xA;print(f&#34;{&#39;[DRY-RUN] &#39; if DRYRUN else &#39;&#39;}&#34;&#xA;      f&#34;Deleted: {deleted} files, Freed: {freed / (10243):.2f} GB&#34;)&#xA;&#xA;Utilisation :&#xA;&#xA;Prévisualisation (recommandé d&#39;abord)&#xA;python3 clean-orphan-hardlinks.py --dry-run&#xA;&#xA;Exécution réelle&#xA;sudo python3 clean-orphan-hardlinks.py&#xA;&#xA;4.3 Résultats du nettoyage&#xA;&#xA;Le nettoyage a permis d&#39;économiser environ 550 Go d&#39;espace disque, réduisant le temps de transfert de ~15 heures à ~5 heures.&#xA;&#xA;4.4 Nettoyage des anciens services&#xA;&#xA;Profitez de la migration pour supprimer les données obsolètes :&#xA;&#xA;Exemples d&#39;anciens services remplacés&#xA;rm -rf /data/mysql        # Si remplacé par PostgreSQL&#xA;&#xA;---&#xA;&#xA;5. Création des Réseaux Docker&#xA;&#xA;Notre architecture utilise des réseaux Docker pour isoler les services :&#xA;&#xA;Réseau pour le reverse proxy (services publics)&#xA;docker network create proxy-tier&#xA;&#xA;Réseau interne pour les services avec bases de données&#xA;docker network create internal&#xA;&#xA;Architecture réseau :&#xA;&#xA;Internet&#xA;    |&#xA;    v&#xA;+-------+     proxy-tier     +-----------+&#xA;| nginx |------------------| Services  |&#xA;| proxy |                    | publics   |&#xA;+-------+                    +-----------+&#xA;                                   |&#xA;                             internal (isolé)&#xA;                                   |&#xA;                     +-------------+-------------+&#xA;                     |             |             |&#xA;                 +-------+   +---------+   +--------+&#xA;                 |Postgre|   |  Redis  |   | Autres |&#xA;                 +-------+   +---------+   +--------+&#xA;&#xA;---&#xA;&#xA;6. Adaptation des Chemins et Configurations&#xA;&#xA;6.1 Modification des volumes Docker Compose&#xA;&#xA;Les chemins diffèrent souvent entre les deux serveurs :&#xA;&#xA;| Élément | Ancien | Nouveau |&#xA;|---------|--------|---------|&#xA;| Utilisateur | ancienuser | newuser |&#xA;| Projet | /home/ancienuser/cloud | /home/newuser/services-web |&#xA;&#xA;Mise à jour automatique dans tous les fichiers de service&#xA;cd ~/services-web&#xA;&#xA;Remplacer les anciens chemins&#xA;find services/ -name &#34;.yml&#34; -exec \&#xA;  sed -i &#39;s|/home/ancienuser|/home/newuser|g&#39; {} \;&#xA;&#xA;Vérification&#xA;grep -r &#34;/home/ancienuser&#34; services/&#xA;Ne doit rien retourner&#xA;&#xA;6.2 Migration des fichiers hors /data&#xA;&#xA;Certains fichiers de configuration sont ailleurs :&#xA;&#xA;Cron jobs&#xA;scp ancienuser@ancien:/etc/cron.d/mon-cron /tmp/&#xA;sed -i &#39;s|/home/ancienuser/cloud|/home/newuser/services-web|g&#39; /tmp/mon-cron&#xA;sudo cp /tmp/mon-cron /etc/cron.d/&#xA;sudo chmod 644 /etc/cron.d/mon-cron&#xA;&#xA;Dotfiles importants&#xA;scp ancienuser@ancien:~/.ssh/authorizedkeys ~/.ssh/&#xA;&#xA;---&#xA;&#xA;7. Mise à Niveau des Services (Grâce au Nouveau CPU)&#xA;&#xA;7.1 Homarr : De la version legacy à v1.0+&#xA;&#xA;L&#39;Intel Atom N2800 ne supportant pas AVX, nous étions bloqués sur l&#39;ancienne version de Homarr. Le Xeon E-2136 supporte AVX2, permettant de passer à la nouvelle version.&#xA;&#xA;Ancien fichier (legacy) :&#xA;services:&#xA;  homarr:&#xA;    image: ghcr.io/ajnart/homarr:latest  # Version legacy&#xA;    environment:&#xA;      VIRTUALPORT=7575  # Port différent&#xA;    volumes:&#xA;      /data/homarr/configs:/app/data/configs&#xA;      /data/homarr/icons:/app/public/icons&#xA;      /data/homarr/data:/data&#xA;&#xA;Nouveau fichier (v1.0+) :&#xA;services:&#xA;  homarr:&#xA;    image: ghcr.io/homarr-labs/homarr:latest  # Nouvelle image&#xA;    environment:&#xA;      VIRTUALPORT=3000  # Nouveau port&#xA;      AUTHSECRET=${HOMARRAUTHSECRET}  # Nouveau : secret d&#39;authentification&#xA;    volumes:&#xA;      /data/homarr:/appdata  # Structure de volumes simplifiée&#xA;&#xA;Migration des données :&#xA;La structure de données a changé, il faut repartir de zéro&#xA;sudo rm -rf /data/homarr/&#xA;sudo mkdir -p /data/homarr&#xA;sudo chown newuser:newuser /data/homarr&#xA;&#xA;Générer le secret d&#39;authentification&#xA;openssl rand -hex 32&#xA;Ajouter dans .env : HOMARRAUTHSECRET=valeur&#xA;&#xA;7.2 PeerTube : Activation de la transcription Whisper&#xA;&#xA;Avec le support AVX2, nous pouvons maintenant activer la transcription automatique des vidéos :&#xA;&#xA;Éditer la configuration PeerTube&#xA;nano /data/peertube/config/local-production.json&#xA;&#xA;{&#xA;  &#34;videotranscription&#34;: {&#xA;    &#34;enabled&#34;: true,&#xA;    &#34;engine&#34;: &#34;whisper&#34;,&#xA;    &#34;engineoptions&#34;: {&#xA;      &#34;model&#34;: &#34;small&#34;&#xA;    }&#xA;  }&#xA;}&#xA;&#xA;Note : La transcription utilise CTranslate2 qui requiert AVX. Sur l&#39;ancien serveur, cette fonctionnalité causait un &#34;Illegal instruction (core dumped)&#34;.&#xA;&#xA;---&#xA;&#xA;8. Migration des Volumes Docker&#xA;&#xA;Certains volumes Docker ne sont pas dans /data mais dans le stockage Docker :&#xA;&#xA;Sur l&#39;ANCIEN serveur : export des volumes&#xA;cd /tmp&#xA;docker run --rm -v certs:/data -v /tmp:/backup alpine \&#xA;  tar cvf /backup/vol-certs.tar -C /data .&#xA;docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \&#xA;  tar cvf /backup/vol-vhost.tar -C /data .&#xA;docker run --rm -v html:/data -v /tmp:/backup alpine \&#xA;  tar cvf /backup/vol-html.tar -C /data .&#xA;&#xA;Transfert vers le nouveau serveur&#xA;scp /tmp/vol-.tar newuser@nouveau:/tmp/&#xA;&#xA;Sur le NOUVEAU serveur : import des volumes&#xA;docker volume create certs&#xA;docker volume create vhost.d&#xA;docker volume create html&#xA;&#xA;docker run --rm -v certs:/data -v /tmp:/backup alpine \&#xA;  tar xvf /backup/vol-certs.tar -C /data&#xA;docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \&#xA;  tar xvf /backup/vol-vhost.tar -C /data&#xA;docker run --rm -v html:/data -v /tmp:/backup alpine \&#xA;  tar xvf /backup/vol-html.tar -C /data&#xA;&#xA;Note sur le nommage : Les volumes sont préfixés par le nom du projet Docker Compose (le répertoire). Il faut adapter le nom si le répertoire change.&#xA;&#xA;---&#xA;&#xA;9. Basculement DNS&#xA;&#xA;9.1 Stratégie zero-downtime&#xA;&#xA;Notre approche :&#xA;Préparer tout sur le nouveau serveur (services prêts mais non exposés)&#xA;Faire une dernière synchronisation rsync&#xA;Arrêter les services sur l&#39;ancien serveur&#xA;Basculer le DNS&#xA;Démarrer les services sur le nouveau serveur&#xA;&#xA;9.2 Mise à jour DNS&#xA;&#xA;Obtenir la nouvelle IP publique&#xA;NEWIP=$(curl -s ifconfig.me)&#xA;echo &#34;Nouvelle IP: $NEWIP&#34;&#xA;&#xA;Mettre à jour l&#39;enregistrement A chez votre registrar&#xA;Les sous-domaines (CNAME) pointent vers le domaine principal&#xA;&#xA;Vérification de la propagation&#xA;watch -n 30 &#39;dig +short mondomaine.org&#39;&#xA;&#xA;9.3 Vérification des certificats SSL&#xA;&#xA;Les certificats Let&#39;s Encrypt ont été importés avec les volumes Docker. Ils seront automatiquement renouvelés par acme-companion :&#xA;&#xA;Vérifier les certificats existants&#xA;docker exec letsencrypt ls -la /etc/nginx/certs/&#xA;&#xA;Forcer le renouvellement si nécessaire&#xA;docker exec letsencrypt /app/signalleservice&#xA;&#xA;---&#xA;&#xA;10. Démarrage et Vérification&#xA;&#xA;10.1 Ordre de démarrage&#xA;&#xA;cd ~/services-web&#xA;&#xA;1. Infrastructure de base (reverse proxy + SSL)&#xA;docker compose up -d&#xA;sleep 30  # Attendre que nginx-proxy soit prêt&#xA;&#xA;2. Services avec dépendances (ex: PeerTube + PostgreSQL + Redis)&#xA;docker compose -f docker-compose.yml -f services/peertube.yml up -d&#xA;sleep 30&#xA;&#xA;3. Services média&#xA;docker compose -f docker-compose.yml -f services/media.yml up -d&#xA;&#xA;4. Autres services&#xA;docker compose -f docker-compose.yml -f services/homarr.yml up -d&#xA;docker compose -f docker-compose.yml -f services/n8n.yml up -d&#xA;docker compose -f docker-compose.yml -f services/writefreely.yml up -d&#xA;&#xA;10.2 Vérification des services&#xA;&#xA;État des conteneurs&#xA;docker ps --format &#34;table {{.Names}}\t{{.Status}}\t{{.Ports}}&#34;&#xA;&#xA;Tests HTTPS&#xA;for host in video www n8n blog; do&#xA;  echo -n &#34;$host.mondomaine.org: &#34;&#xA;  curl -sI --connect-timeout 5 &#34;https://$host.mondomaine.org&#34; 2  /dev/null | head -1&#xA;done&#xA;&#xA;10.3 Vérification des logs&#xA;&#xA;Logs du reverse proxy&#xA;docker logs nginx-proxy --tail=50&#xA;&#xA;Logs d&#39;un service spécifique&#xA;docker logs nom-conteneur --tail=50&#xA;&#xA;Logs en temps réel&#xA;docker compose -f docker-compose.yml -f services/service.yml logs -f&#xA;&#xA;---&#xA;&#xA;Bonnes Pratiques et Leçons Apprises&#xA;&#xA;Ce qui a bien fonctionné&#xA;&#xA;Toujours utiliser -H avec rsync quand des hardlinks sont impliqués. Sans ce flag, nous aurions doublé l&#39;espace disque.&#xA;&#xA;Nettoyer avant de migrer : 550 Go économisés = plusieurs heures de transfert en moins.&#xA;&#xA;Tester chaque service individuellement avant de basculer le DNS. Cela permet d&#39;identifier les problèmes de configuration.&#xA;&#xA;Garder l&#39;ancien serveur fonctionnel pendant quelques jours après la migration. Rollback facile en cas de problème.&#xA;&#xA;Documenter les chemins spécifiques : certains chemins sont des héritages d&#39;anciennes installations et peuvent surprendre.&#xA;&#xA;Pièges à éviter&#xA;&#xA;Ne pas oublier les volumes Docker : les certificats SSL et autres assets peuvent être dans des volumes, pas dans /data.&#xA;&#xA;Attention aux permissions : certains conteneurs tournent avec des UID spécifiques (ex: WriteFreely en UID 5000). Un chown -R newuser:new_user /data peut casser ces services.&#xA;&#xA;PEP 668 sur Ubuntu 24 : pip install au niveau système ne fonctionne plus. Utiliser apt ou venv.&#xA;&#xA;Noms des volumes Docker : ils sont préfixés par le nom du répertoire projet. Adapter si le répertoire change.&#xA;&#xA;Ports des nouvelles versions : vérifier la documentation quand on met à jour une image (ex: Homarr v1.0+ utilise le port 3000, pas 7575).&#xA;&#xA;Améliorations futures&#xA;&#xA;Automatiser la détection des hardlinks orphelins avec un cron job&#xA;Implémenter une sauvegarde 3-2-1 avec rsync vers un stockage distant&#xA;Ajouter du monitoring (Prometheus + Grafana) pour anticiper les problèmes d&#39;espace disque&#xA;!--more--&#xA;&#xA;---&#xA;&#xA;Conclusion&#xA;&#xA;Cette migration m&#39;a pris environ une journée complète, dont la majorité pour le transfert des données. Le nettoyage préalable des hardlinks orphelins a été la clé pour réduire ce temps de manière significative.&#xA;&#xA;Le passage à un CPU moderne ouvre de nouvelles possibilités : transcription automatique des vidéos PeerTube, utilisation de la dernière version de Homarr, et de la marge pour de futurs services.&#xA;&#xA;Les points les plus importants à retenir :&#xA;&#xA;rsync -H pour préserver les hardlinks&#xA;Nettoyer avant de migrer pour gagner du temps&#xA;Tester avant de basculer le DNS&#xA;Documenter les chemins spécifiques de votre configuration&#xA;&#xA;J&#39;espère que ce guide vous sera utile pour votre propre migration. &#xA;]]&gt;</description>
      <content:encoded><![CDATA[<p>Après plusieurs années de bons et loyaux services, mon homelab tournant sur un Intel Atom N2800 montrait ses limites. Ce processeur de 2011 ne supportait pas les instructions AVX/AVX2 — une contrainte qui m&#39;empêchait d&#39;utiliser certains logiciels modernes comme Homarr v1.0+ ou la transcription automatique de PeerTube via Whisper.</p>



<p>La migration vers un serveur OVH SYS-1 équipé d&#39;un Intel Xeon E-2136 était l&#39;occasion de moderniser l&#39;infrastructure tout en documentant le processus pour d&#39;autres administrateurs de homelab.</p>

<h3 id="ce-que-vous-allez-apprendre">Ce que vous allez apprendre</h3>
<ul><li>Préparer un nouveau serveur Ubuntu 24 pour Docker</li>
<li>Configurer SSH</li>
<li>Utiliser rsync avec préservation des hardlinks</li>
<li>Adapter une stack Docker Compose à un nouvel environnement</li>
<li>Gérer la migration DNS avec zero downtime</li></ul>

<h3 id="spécifications-des-serveurs">Spécifications des serveurs</h3>

<table>
<thead>
<tr>
<th>Élément</th>
<th>Ancien serveur</th>
<th>Nouveau serveur</th>
</tr>
</thead>

<tbody>
<tr>
<td><strong>CPU</strong></td>
<td>Intel Atom N2800 (2 cœurs, pas AVX)</td>
<td>Intel Xeon E-2136 (6 cœurs, AVX2)</td>
</tr>

<tr>
<td><strong>RAM</strong></td>
<td>4 Go</td>
<td>32 Go</td>
</tr>

<tr>
<td><strong>Stockage</strong></td>
<td>2 x 2 To HDD</td>
<td>2 x 2 To SSD</td>
</tr>

<tr>
<td><strong>OS</strong></td>
<td>Ubuntu 22.04</td>
<td>Ubuntu 24.04 LTS</td>
</tr>

<tr>
<td><strong>Hébergeur</strong></td>
<td>Kimsufi</td>
<td>OVH SYS-1</td>
</tr>
</tbody>
</table>

<h3 id="services-à-migrer">Services à migrer</h3>

<p>Mon infrastructure Docker Compose comprend :</p>
<ul><li><strong>Reverse proxy</strong> : nginx-proxy + acme-companion (Let&#39;s Encrypt automatique)</li>
<li><strong>Vidéo</strong> : PeerTube (plateforme vidéo fédérée)</li>
<li><strong>Dashboard</strong> : Homarr</li>
<li><strong>Workflow</strong> : n8n</li>
<li><strong>Blog</strong> : WriteFreely (fédéré via ActivityPub)</li></ul>

<p>Volume total des données : <strong>~935 Go</strong> (réduit à <strong>~350 Go</strong> après nettoyage).</p>

<hr>

<h2 id="1-préparation-du-nouveau-serveur">1. Préparation du Nouveau Serveur</h2>

<h3 id="1-1-installation-de-docker-avec-stockage-dédié">1.1 Installation de Docker avec stockage dédié</h3>

<p>Sur Ubuntu 24 Server, la partition root est souvent limitée (typiquement 20-50 Go sur un serveur OVH). Il est crucial de configurer Docker pour stocker ses images et volumes sur une partition de données séparée.</p>

<pre><code class="language-bash"># Installation de Docker via le script officiel
curl -fsSL https://get.docker.com | sudo sh

# Ajout de l&#39;utilisateur au groupe docker
sudo usermod -aG docker $USER

# Arrêt de Docker pour la configuration
sudo systemctl stop docker
</code></pre>

<p>Configuration du stockage Docker sur <code>/data/docker</code> :</p>

<pre><code class="language-bash"># Création du répertoire de données
sudo mkdir -p /data/docker

# Configuration de Docker avec data-root personnalisé
# et limitation des logs pour éviter de saturer le disque
sudo tee /etc/docker/daemon.json &lt;&lt; &#39;EOF&#39;
{
  &#34;data-root&#34;: &#34;/data/docker&#34;,
  &#34;log-driver&#34;: &#34;json-file&#34;,
  &#34;log-opts&#34;: {
    &#34;max-size&#34;: &#34;10m&#34;,
    &#34;max-file&#34;: &#34;3&#34;
  }
}
EOF

# Redémarrage de Docker
sudo systemctl start docker

# Vérification
docker info | grep &#34;Docker Root Dir&#34;
# Doit afficher: Docker Root Dir: /data/docker
</code></pre>

<p><strong>Pourquoi cette configuration ?</strong></p>
<ul><li><strong>data-root</strong> : Évite de saturer la partition root avec les images Docker</li>
<li><strong>log-driver</strong> + <strong>log-opts</strong> : Limite chaque fichier de log à 10 Mo avec 3 rotations maximum, évitant l&#39;accumulation de logs volumineux</li></ul>

<h3 id="1-2-installation-des-dépendances-python-pep-668">1.2 Installation des dépendances Python (PEP 668)</h3>

<p>Ubuntu 24 implémente strictement le <a href="https://peps.python.org/pep-0668/">PEP 668</a> qui empêche l&#39;installation de paquets Python via pip au niveau système. Cette décision vise à éviter les conflits entre pip et apt.</p>

<pre><code class="language-bash"># INCORRECT sur Ubuntu 24 - génère une erreur
pip3 install requests python-dotenv
# error: externally-managed-environment

# CORRECT - utiliser les paquets système
sudo apt install -y python3-requests python3-dotenv
</code></pre>

<p><strong>Alternatives si vous avez besoin de versions spécifiques :</strong></p>

<pre><code class="language-bash"># Option 1 : Environnement virtuel (recommandé pour le développement)
python3 -m venv ~/venv
source ~/venv/bin/activate
pip install requests python-dotenv

# Option 2 : pipx pour les outils CLI
pipx install &lt;package&gt;
</code></pre>

<p>Pour mes scripts d&#39;organisation de médias, les versions système suffisent.</p>

<h3 id="1-3-installation-de-node-js-via-nvm">1.3 Installation de Node.js via nvm</h3>

<p>NodeSource, longtemps la méthode recommandée, a <a href="https://github.com/nodesource/distributions#debian-and-ubuntu-based-distributions">changé ses conditions</a> en 2023. nvm (Node Version Manager) offre plus de flexibilité :</p>

<pre><code class="language-bash"># Installation de nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash

# Rechargement du shell
source ~/.bashrc

# Installation de la dernière LTS
nvm install --lts

# Vérification
node --version
npm --version
</code></pre>

<p><strong>Avantages de nvm :</strong>
– Installation dans le home utilisateur (pas de sudo)
– Possibilité de switcher entre versions
– Mise à jour simple via <code>nvm install --lts</code></p>

<hr>

<h2 id="2-configuration-ssh-bidirectionnelle">2. Configuration SSH Bidirectionnelle</h2>

<p>Pour transférer les données de manière sécurisée, nous devons établir une connexion SSH de l&#39;ancien vers le nouveau serveur et inversement.</p>

<h3 id="2-1-génération-de-clé-sur-l-ancien-serveur">2.1 Génération de clé sur l&#39;ancien serveur</h3>

<pre><code class="language-bash"># Sur l&#39;ANCIEN serveur
ssh-keygen -t ed25519 -N &#34;&#34; -f ~/.ssh/id_ed25519

# Afficher la clé publique à copier
cat ~/.ssh/id_ed25519.pub
</code></pre>

<p><strong>Pourquoi ed25519 ?</strong>
– Plus sécurisé que RSA à taille de clé équivalente
– Plus rapide pour la génération et l&#39;authentification
– Clés plus courtes (plus faciles à copier/coller)</p>

<h3 id="2-2-autorisation-sur-le-nouveau-serveur">2.2 Autorisation sur le nouveau serveur</h3>

<pre><code class="language-bash"># Sur le NOUVEAU serveur
# Ajouter la clé publique de l&#39;ancien serveur
echo &#34;ssh-ed25519 AAAA... user@ancien-serveur&#34; &gt;&gt; ~/.ssh/authorized_keys

# Vérifier les permissions (crucial pour SSH)
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
</code></pre>

<h3 id="2-3-test-de-connexion">2.3 Test de connexion</h3>

<pre><code class="language-bash"># Depuis l&#39;ANCIEN serveur, tester la connexion
ssh new_user@nouveau-serveur &#34;hostname &amp;&amp; uptime&#34;
</code></pre>

<hr>

<h2 id="3-synchronisation-avec-rsync-l-art-de-préserver-les-hardlinks">3. Synchronisation avec rsync : L&#39;Art de Préserver les Hardlinks</h2>

<h3 id="3-1-comprendre-les-hardlinks">3.1 Comprendre les hardlinks</h3>

<p>Dans un système de fichiers, un <strong>hardlink</strong> est une référence directe au même inode sur le disque. Le fichier n&#39;existe qu&#39;une fois physiquement, mais apparaît à plusieurs endroits dans l&#39;arborescence.</p>

<pre><code>/data/source/fichier.mkv  (fichier original)
         |
         +-- hardlink --&gt; /data/organise/fichier.mkv
</code></pre>

<p>Cela permet :
– D&#39;organiser les fichiers de différentes manières
– De <strong>ne pas doubler l&#39;espace disque</strong>
– D&#39;avoir plusieurs “vues” sur les mêmes données</p>

<h3 id="3-2-l-importance-du-flag-h">3.2 L&#39;importance du flag -H</h3>

<p>Sans le flag <code>-H</code>, rsync copie chaque chemin comme un fichier séparé :</p>

<pre><code class="language-bash"># MAUVAIS : duplique les données
rsync -avz /data/ user@serveur:/data/
# Résultat : 350 Go de fichiers + 350 Go de hardlinks = 700 Go

# CORRECT : préserve les hardlinks
rsync -avzH /data/ user@serveur:/data/
# Résultat : 350 Go total (hardlinks préservés)
</code></pre>

<h3 id="3-3-commande-de-synchronisation-complète">3.3 Commande de synchronisation complète</h3>

<pre><code class="language-bash"># Synchronisation avec préservation des hardlinks
rsync -avzH --progress \
  --exclude=&#39;*.tmp&#39; \
  --exclude=&#39;*.part&#39; \
  /data/ new_user@nouveau-serveur:/data/

# Pour les syncs subséquentes, ajouter --delete
# pour supprimer les fichiers effacés sur la source
rsync -avzH --progress --delete \
  /data/ new_user@nouveau-serveur:/data/
</code></pre>

<p><strong>Explication des flags :</strong>
– <code>-a</code> : Archive (préserve permissions, propriétaire, timestamps, liens symboliques)
– <code>-v</code> : Verbose (affiche les fichiers transférés)
– <code>-z</code> : Compression durant le transfert
– <code>-H</code> : <strong>Préserve les hardlinks</strong> (crucial pour notre cas)
– <code>--progress</code> : Affiche la progression
– <code>--delete</code> : Supprime les fichiers absents de la source</p>

<h3 id="3-4-vérification-des-hardlinks-après-transfert">3.4 Vérification des hardlinks après transfert</h3>

<pre><code class="language-bash"># Sur le nouveau serveur, vérifier qu&#39;un fichier a plusieurs liens
ls -li /data/media/*/fichier.mkv
# La première colonne (inode) et la troisième (link count) doivent montrer &gt; 1

# Exemple de sortie :
# 1234567 -rw-r--r-- 2 new_user new_user 5.0G Dec 31 fichier.mkv
#                    ^-- 2 = le fichier existe à 2 endroits (hardlink)
</code></pre>

<hr>

<h2 id="4-nettoyage-pré-migration-550-go-économisés">4. Nettoyage Pré-Migration : 550 Go Économisés</h2>

<h3 id="4-1-identification-du-problème">4.1 Identification du problème</h3>

<p>En analysant l&#39;espace disque, j&#39;ai découvert que de nombreux hardlinks étaient devenus “orphelins” : les fichiers sources avaient été supprimés, mais les hardlinks restaient dans les répertoires organisés.</p>

<p>Un hardlink orphelin (link count = 1) n&#39;est plus un hardlink — c&#39;est juste un fichier normal qui occupe de l&#39;espace.</p>

<h3 id="4-2-script-de-nettoyage-des-hardlinks-orphelins">4.2 Script de nettoyage des hardlinks orphelins</h3>

<pre><code class="language-python">#!/usr/bin/env python3
&#34;&#34;&#34;
clean-orphan-hardlinks.py
Supprime les fichiers qui étaient des hardlinks mais dont la source
a été supprimée (link count = 1).
&#34;&#34;&#34;
import os
import sys

# Répertoires à nettoyer
DIRS = [&#39;/data/perso&#39;, &#39;/data/pro&#39;]
DRY_RUN = &#39;--dry-run&#39; in sys.argv

deleted = 0
freed = 0

for directory in DIRS:
    if not os.path.isdir(directory):
        continue
    
    for root, dirs, files in os.walk(directory, topdown=False):
        for filename in files:
            filepath = os.path.join(root, filename)
            try:
                stat = os.stat(filepath)
                # Un hardlink a un link count &gt; 1
                # Si link count = 1, la source a été supprimée
                if stat.st_nlink == 1:
                    freed += stat.st_size
                    if DRY_RUN:
                        print(f&#34;[DRY-RUN] Would delete: {filepath}&#34;)
                    else:
                        os.remove(filepath)
                    deleted += 1
            except OSError:
                pass
        
        # Nettoyer les répertoires vides
        if not DRY_RUN:
            try:
                if not os.listdir(root):
                    os.rmdir(root)
            except OSError:
                pass

print(f&#34;{&#39;[DRY-RUN] &#39; if DRY_RUN else &#39;&#39;}&#34;
      f&#34;Deleted: {deleted} files, Freed: {freed / (1024**3):.2f} GB&#34;)
</code></pre>

<p><strong>Utilisation :</strong></p>

<pre><code class="language-bash"># Prévisualisation (recommandé d&#39;abord)
python3 clean-orphan-hardlinks.py --dry-run

# Exécution réelle
sudo python3 clean-orphan-hardlinks.py
</code></pre>

<h3 id="4-3-résultats-du-nettoyage">4.3 Résultats du nettoyage</h3>

<p>Le nettoyage a permis d&#39;économiser environ <strong>550 Go</strong> d&#39;espace disque, réduisant le temps de transfert de ~15 heures à ~5 heures.</p>

<h3 id="4-4-nettoyage-des-anciens-services">4.4 Nettoyage des anciens services</h3>

<p>Profitez de la migration pour supprimer les données obsolètes :</p>

<pre><code class="language-bash"># Exemples d&#39;anciens services remplacés
rm -rf /data/mysql        # Si remplacé par PostgreSQL
</code></pre>

<hr>

<h2 id="5-création-des-réseaux-docker">5. Création des Réseaux Docker</h2>

<p>Notre architecture utilise des réseaux Docker pour isoler les services :</p>

<pre><code class="language-bash"># Réseau pour le reverse proxy (services publics)
docker network create proxy-tier

# Réseau interne pour les services avec bases de données
docker network create internal
</code></pre>

<p><strong>Architecture réseau :</strong></p>

<pre><code>Internet
    |
    v
+-------+     proxy-tier     +-----------+
| nginx |&lt;------------------&gt;| Services  |
| proxy |                    | publics   |
+-------+                    +-----------+
                                   |
                             internal (isolé)
                                   |
                     +-------------+-------------+
                     |             |             |
                 +-------+   +---------+   +--------+
                 |Postgre|   |  Redis  |   | Autres |
                 +-------+   +---------+   +--------+
</code></pre>

<hr>

<h2 id="6-adaptation-des-chemins-et-configurations">6. Adaptation des Chemins et Configurations</h2>

<h3 id="6-1-modification-des-volumes-docker-compose">6.1 Modification des volumes Docker Compose</h3>

<p>Les chemins diffèrent souvent entre les deux serveurs :</p>

<table>
<thead>
<tr>
<th>Élément</th>
<th>Ancien</th>
<th>Nouveau</th>
</tr>
</thead>

<tbody>
<tr>
<td>Utilisateur</td>
<td>ancien_user</td>
<td>new_user</td>
</tr>

<tr>
<td>Projet</td>
<td>/home/ancien_user/cloud</td>
<td>/home/new_user/services-web</td>
</tr>
</tbody>
</table>

<pre><code class="language-bash"># Mise à jour automatique dans tous les fichiers de service
cd ~/services-web

# Remplacer les anciens chemins
find services/ -name &#34;*.yml&#34; -exec \
  sed -i &#39;s|/home/ancien_user|/home/new_user|g&#39; {} \;

# Vérification
grep -r &#34;/home/ancien_user&#34; services/
# Ne doit rien retourner
</code></pre>

<h3 id="6-2-migration-des-fichiers-hors-data">6.2 Migration des fichiers hors /data</h3>

<p>Certains fichiers de configuration sont ailleurs :</p>

<pre><code class="language-bash"># Cron jobs
scp ancien_user@ancien:/etc/cron.d/mon-cron /tmp/
sed -i &#39;s|/home/ancien_user/cloud|/home/new_user/services-web|g&#39; /tmp/mon-cron
sudo cp /tmp/mon-cron /etc/cron.d/
sudo chmod 644 /etc/cron.d/mon-cron

# Dotfiles importants
scp ancien_user@ancien:~/.ssh/authorized_keys ~/.ssh/
</code></pre>

<hr>

<h2 id="7-mise-à-niveau-des-services-grâce-au-nouveau-cpu">7. Mise à Niveau des Services (Grâce au Nouveau CPU)</h2>

<h3 id="7-1-homarr-de-la-version-legacy-à-v1-0">7.1 Homarr : De la version legacy à v1.0+</h3>

<p>L&#39;Intel Atom N2800 ne supportant pas AVX, nous étions bloqués sur l&#39;ancienne version de Homarr. Le Xeon E-2136 supporte AVX2, permettant de passer à la nouvelle version.</p>

<p><strong>Ancien fichier (legacy) :</strong></p>

<pre><code class="language-yaml">services:
  homarr:
    image: ghcr.io/ajnart/homarr:latest  # Version legacy
    environment:
      - VIRTUAL_PORT=7575  # Port différent
    volumes:
      - /data/homarr/configs:/app/data/configs
      - /data/homarr/icons:/app/public/icons
      - /data/homarr/data:/data
</code></pre>

<p><strong>Nouveau fichier (v1.0+) :</strong></p>

<pre><code class="language-yaml">services:
  homarr:
    image: ghcr.io/homarr-labs/homarr:latest  # Nouvelle image
    environment:
      - VIRTUAL_PORT=3000  # Nouveau port
      - AUTH_SECRET=${HOMARR_AUTH_SECRET}  # Nouveau : secret d&#39;authentification
    volumes:
      - /data/homarr:/appdata  # Structure de volumes simplifiée
</code></pre>

<p><strong>Migration des données :</strong></p>

<pre><code class="language-bash"># La structure de données a changé, il faut repartir de zéro
sudo rm -rf /data/homarr/*
sudo mkdir -p /data/homarr
sudo chown new_user:new_user /data/homarr

# Générer le secret d&#39;authentification
openssl rand -hex 32
# Ajouter dans .env : HOMARR_AUTH_SECRET=&lt;valeur&gt;
</code></pre>

<h3 id="7-2-peertube-activation-de-la-transcription-whisper">7.2 PeerTube : Activation de la transcription Whisper</h3>

<p>Avec le support AVX2, nous pouvons maintenant activer la transcription automatique des vidéos :</p>

<pre><code class="language-bash"># Éditer la configuration PeerTube
nano /data/peertube/config/local-production.json
</code></pre>

<pre><code class="language-json">{
  &#34;video_transcription&#34;: {
    &#34;enabled&#34;: true,
    &#34;engine&#34;: &#34;whisper&#34;,
    &#34;engine_options&#34;: {
      &#34;model&#34;: &#34;small&#34;
    }
  }
}
</code></pre>

<p><strong>Note :</strong> La transcription utilise CTranslate2 qui requiert AVX. Sur l&#39;ancien serveur, cette fonctionnalité causait un “Illegal instruction (core dumped)”.</p>

<hr>

<h2 id="8-migration-des-volumes-docker">8. Migration des Volumes Docker</h2>

<p>Certains volumes Docker ne sont pas dans /data mais dans le stockage Docker :</p>

<pre><code class="language-bash"># Sur l&#39;ANCIEN serveur : export des volumes
cd /tmp
docker run --rm -v certs:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-certs.tar -C /data .
docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-vhost.tar -C /data .
docker run --rm -v html:/data -v /tmp:/backup alpine \
  tar cvf /backup/vol-html.tar -C /data .

# Transfert vers le nouveau serveur
scp /tmp/vol-*.tar new_user@nouveau:/tmp/

# Sur le NOUVEAU serveur : import des volumes
docker volume create certs
docker volume create vhost.d
docker volume create html

docker run --rm -v certs:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-certs.tar -C /data
docker run --rm -v vhost.d:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-vhost.tar -C /data
docker run --rm -v html:/data -v /tmp:/backup alpine \
  tar xvf /backup/vol-html.tar -C /data
</code></pre>

<p><strong>Note sur le nommage :</strong> Les volumes sont préfixés par le nom du projet Docker Compose (le répertoire). Il faut adapter le nom si le répertoire change.</p>

<hr>

<h2 id="9-basculement-dns">9. Basculement DNS</h2>

<h3 id="9-1-stratégie-zero-downtime">9.1 Stratégie zero-downtime</h3>

<p>Notre approche :
1. Préparer tout sur le nouveau serveur (services prêts mais non exposés)
2. Faire une dernière synchronisation rsync
3. Arrêter les services sur l&#39;ancien serveur
4. Basculer le DNS
5. Démarrer les services sur le nouveau serveur</p>

<h3 id="9-2-mise-à-jour-dns">9.2 Mise à jour DNS</h3>

<pre><code class="language-bash"># Obtenir la nouvelle IP publique
NEW_IP=$(curl -s ifconfig.me)
echo &#34;Nouvelle IP: $NEW_IP&#34;

# Mettre à jour l&#39;enregistrement A chez votre registrar
# Les sous-domaines (CNAME) pointent vers le domaine principal

# Vérification de la propagation
watch -n 30 &#39;dig +short mondomaine.org&#39;
</code></pre>

<h3 id="9-3-vérification-des-certificats-ssl">9.3 Vérification des certificats SSL</h3>

<p>Les certificats Let&#39;s Encrypt ont été importés avec les volumes Docker. Ils seront automatiquement renouvelés par acme-companion :</p>

<pre><code class="language-bash"># Vérifier les certificats existants
docker exec letsencrypt ls -la /etc/nginx/certs/

# Forcer le renouvellement si nécessaire
docker exec letsencrypt /app/signal_le_service
</code></pre>

<hr>

<h2 id="10-démarrage-et-vérification">10. Démarrage et Vérification</h2>

<h3 id="10-1-ordre-de-démarrage">10.1 Ordre de démarrage</h3>

<pre><code class="language-bash">cd ~/services-web

# 1. Infrastructure de base (reverse proxy + SSL)
docker compose up -d
sleep 30  # Attendre que nginx-proxy soit prêt

# 2. Services avec dépendances (ex: PeerTube + PostgreSQL + Redis)
docker compose -f docker-compose.yml -f services/peertube.yml up -d
sleep 30

# 3. Services média
docker compose -f docker-compose.yml -f services/media.yml up -d

# 4. Autres services
docker compose -f docker-compose.yml -f services/homarr.yml up -d
docker compose -f docker-compose.yml -f services/n8n.yml up -d
docker compose -f docker-compose.yml -f services/writefreely.yml up -d
</code></pre>

<h3 id="10-2-vérification-des-services">10.2 Vérification des services</h3>

<pre><code class="language-bash"># État des conteneurs
docker ps --format &#34;table {{.Names}}\t{{.Status}}\t{{.Ports}}&#34;

# Tests HTTPS
for host in video www n8n blog; do
  echo -n &#34;$host.mondomaine.org: &#34;
  curl -sI --connect-timeout 5 &#34;https://$host.mondomaine.org&#34; 2&gt;/dev/null | head -1
done
</code></pre>

<h3 id="10-3-vérification-des-logs">10.3 Vérification des logs</h3>

<pre><code class="language-bash"># Logs du reverse proxy
docker logs nginx-proxy --tail=50

# Logs d&#39;un service spécifique
docker logs nom-conteneur --tail=50

# Logs en temps réel
docker compose -f docker-compose.yml -f services/service.yml logs -f
</code></pre>

<hr>

<h2 id="bonnes-pratiques-et-leçons-apprises">Bonnes Pratiques et Leçons Apprises</h2>

<h3 id="ce-qui-a-bien-fonctionné">Ce qui a bien fonctionné</h3>
<ol><li><p><strong>Toujours utiliser -H avec rsync</strong> quand des hardlinks sont impliqués. Sans ce flag, nous aurions doublé l&#39;espace disque.</p></li>

<li><p><strong>Nettoyer avant de migrer</strong> : 550 Go économisés = plusieurs heures de transfert en moins.</p></li>

<li><p><strong>Tester chaque service individuellement</strong> avant de basculer le DNS. Cela permet d&#39;identifier les problèmes de configuration.</p></li>

<li><p><strong>Garder l&#39;ancien serveur fonctionnel</strong> pendant quelques jours après la migration. Rollback facile en cas de problème.</p></li>

<li><p><strong>Documenter les chemins spécifiques</strong> : certains chemins sont des héritages d&#39;anciennes installations et peuvent surprendre.</p></li></ol>

<h3 id="pièges-à-éviter">Pièges à éviter</h3>
<ol><li><p><strong>Ne pas oublier les volumes Docker</strong> : les certificats SSL et autres assets peuvent être dans des volumes, pas dans /data.</p></li>

<li><p><strong>Attention aux permissions</strong> : certains conteneurs tournent avec des UID spécifiques (ex: WriteFreely en UID 5000). Un <code>chown -R new_user:new_user /data</code> peut casser ces services.</p></li>

<li><p><strong>PEP 668 sur Ubuntu 24</strong> : pip install au niveau système ne fonctionne plus. Utiliser apt ou venv.</p></li>

<li><p><strong>Noms des volumes Docker</strong> : ils sont préfixés par le nom du répertoire projet. Adapter si le répertoire change.</p></li>

<li><p><strong>Ports des nouvelles versions</strong> : vérifier la documentation quand on met à jour une image (ex: Homarr v1.0+ utilise le port 3000, pas 7575).</p></li></ol>

<h3 id="améliorations-futures">Améliorations futures</h3>
<ul><li>Automatiser la détection des hardlinks orphelins avec un cron job</li>
<li>Implémenter une sauvegarde 3-2-1 avec rsync vers un stockage distant</li>
<li>Ajouter du monitoring (Prometheus + Grafana) pour anticiper les problèmes d&#39;espace disque
</li></ul>

<hr>

<h2 id="conclusion">Conclusion</h2>

<p>Cette migration m&#39;a pris environ une journée complète, dont la majorité pour le transfert des données. Le nettoyage préalable des hardlinks orphelins a été la clé pour réduire ce temps de manière significative.</p>

<p>Le passage à un CPU moderne ouvre de nouvelles possibilités : transcription automatique des vidéos PeerTube, utilisation de la dernière version de Homarr, et de la marge pour de futurs services.</p>

<p>Les points les plus importants à retenir :</p>
<ol><li><strong>rsync -H</strong> pour préserver les hardlinks</li>
<li><strong>Nettoyer avant de migrer</strong> pour gagner du temps</li>
<li><strong>Tester avant de basculer le DNS</strong></li>
<li><strong>Documenter les chemins spécifiques</strong> de votre configuration</li></ol>

<p>J&#39;espère que ce guide vous sera utile pour votre propre migration.</p>
]]></content:encoded>
      <guid>https://blog.ut0pia.org/migrer-un-homelab-docker-compose-vers-un-nouveau-serveur-guide-complet</guid>
      <pubDate>Wed, 31 Dec 2025 13:00:48 +0000</pubDate>
    </item>
  </channel>
</rss>