<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Traefik on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/traefik/</link><description>Recent content in Traefik on Guillaume Delré</description><generator>Hugo</generator><language>fr-FR</language><lastBuildDate>Tue, 17 Feb 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://guillaumedelre.github.io/fr/tags/traefik/index.xml" rel="self" type="application/rss+xml"/><item><title>Construire un homelab self-hosted avec Docker Compose et Traefik</title><link>https://guillaumedelre.github.io/fr/2026/02/17/construire-un-homelab-self-hosted-avec-docker-compose-et-traefik/</link><pubDate>Tue, 17 Feb 2026 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2026/02/17/construire-un-homelab-self-hosted-avec-docker-compose-et-traefik/</guid><description>Tuto complet pour monter un homelab Docker avec Traefik et sslip.io : stacks indépendants, dashboard auto-configuré, pièges documentés.</description><content:encoded><![CDATA[<p>Ça fait des années que j&rsquo;avais envie d&rsquo;un homelab à la maison. Un endroit à moi pour héberger mes outils de développement, surveiller mes machines, faire tourner de la domotique, tester des trucs sans risquer de casser quoi que ce soit d&rsquo;important. L&rsquo;idée est simple. La mise en place un peu moins.</p>
<p>À l&rsquo;époque, Kubernetes n&rsquo;existait pas encore. Les options pour faire tourner plusieurs services sur une machine se résumaient à du scripting bash, des configurations Nginx écrites à la main, et beaucoup de café. Les tutoriels &ldquo;homelab pour les humains&rdquo; brillaient par leur absence.</p>
<p>Ce tuto, c&rsquo;est ce que j&rsquo;aurais voulu trouver à l&rsquo;époque. Ça tourne depuis plusieurs années maintenant. Pas sans évoluer : des services ajoutés, d&rsquo;autres abandonnés, des choix revisités. Mais la base est là, stable, et c&rsquo;est bien ça le succès en self-hosting.</p>
<p>Le setup : dix services web auto-hébergés sur une machine locale, accessibles depuis un navigateur via des URLs lisibles, sans toucher à la configuration DNS, sans louer un VPS, sans certificat TLS à gérer. L&rsquo;ingrédient qui rend ça possible : <a href="https://sslip.io" target="_blank" rel="noopener noreferrer">sslip.io</a>
, un service DNS public qui encode l&rsquo;IP directement dans le nom de domaine. <code>service.192.168.1.10.sslip.io</code> résout vers <code>192.168.1.10</code>, sans rien configurer, depuis n&rsquo;importe quelle machine du réseau local.</p>
<p>Ce tutoriel s&rsquo;adresse à quelqu&rsquo;un qui connaît Docker mais qui part de zéro sur l&rsquo;orchestration de services self-hosted.</p>
<hr>
<h2 id="table-des-matières">Table des matières</h2>
<ol>
<li><a href="#1-philosophie-et-choix-darchitecture">Philosophie et choix d&rsquo;architecture</a>
</li>
<li><a href="#2-les-briques-fondamentales">Les briques fondamentales</a>
</li>
<li><a href="#3-mise-en-place-pas-%c3%a0-pas">Mise en place pas à pas</a>
</li>
<li><a href="#4-ajouter-un-nouveau-service">Ajouter un nouveau service</a>
</li>
<li><a href="#5-patterns-et-conventions">Patterns et conventions</a>
</li>
<li><a href="#6-pi%c3%a8ges-courants">Pièges courants</a>
</li>
<li><a href="#conclusion">Conclusion</a>
</li>
<li><a href="#r%c3%a9f%c3%a9rences">Références</a>
</li>
</ol>
<hr>
<h2 id="1-philosophie-et-choix-darchitecture">1. Philosophie et choix d&rsquo;architecture</h2>
<h3 id="objectif">Objectif</h3>
<p>Faire tourner plusieurs services web sur une machine locale, accessibles depuis un navigateur via des URLs lisibles, sans toucher à la configuration DNS, sans louer un VPS, sans certificat TLS à gérer.</p>
<h3 id="pourquoi-docker-compose-et-pas-autre-chose-">Pourquoi Docker Compose et pas autre chose ?</h3>
<p>Docker Compose est le bon niveau de complexité pour un homelab personnel. Kubernetes est trop lourd pour une seule machine. Docker Swarm est en déclin. Compose est simple, lisible, versionnable, et suffisant pour des dizaines de services.</p>
<h3 id="pourquoi-traefik-et-pas-nginx-proxy-manager-">Pourquoi Traefik et pas Nginx Proxy Manager ?</h3>
<p><strong>Nginx Proxy Manager (NPM)</strong> est une interface graphique pour configurer Nginx comme reverse proxy. Les routes sont stockées dans une base de données et configurées via une UI.</p>
<p><strong><a href="https://github.com/traefik/traefik" target="_blank" rel="noopener noreferrer">Traefik</a>
</strong> lit automatiquement les labels Docker des containers et génère sa configuration à la volée. Quand on démarre un container avec les bons labels, Traefik le découvre et crée la route immédiatement, sans redémarrage, sans UI à ouvrir.</p>
<p>Ce comportement &ldquo;configuration as code&rdquo; a deux avantages majeurs :</p>
<ul>
<li>La configuration d&rsquo;un service est dans son <code>compose.yaml</code>, au même endroit que tout le reste.</li>
<li>Ajouter un service ne nécessite pas de toucher à Traefik.</li>
</ul>
<h3 id="pourquoi-dockge-et-pas-portainer-">Pourquoi Dockge et pas Portainer ?</h3>
<p><strong>Portainer</strong> est un outil de gestion Docker complet : images, volumes, réseaux, containers individuels&hellip; puissant mais complexe.</p>
<p><strong><a href="https://github.com/louislam/dockge" target="_blank" rel="noopener noreferrer">Dockge</a>
</strong> est focalisé sur une seule chose : gérer des stacks Docker Compose. Son UI est minimaliste et intuitive. Pour un homelab où tout est géré en Compose, c&rsquo;est suffisant et bien plus agréable à utiliser.</p>
<h3 id="pourquoi-sslipio-">Pourquoi sslip.io ?</h3>
<p>Les services web ont besoin d&rsquo;un nom d&rsquo;hôte (ex: <code>dozzle.monserveur.local</code>) pour que Traefik puisse les router correctement. Les options habituelles :</p>
<ul>
<li>Modifier <code>/etc/hosts</code> sur chaque machine : fastidieux, non partageable.</li>
<li>Configurer un vrai DNS local (Pi-hole, AdGuard) : nécessite une infrastructure supplémentaire.</li>
<li>Acheter un domaine et configurer les DNS : coûte de l&rsquo;argent et du temps.</li>
</ul>
<p><strong>sslip.io</strong> est un service DNS public qui résout automatiquement <code>&lt;anything&gt;.&lt;IP&gt;.sslip.io</code> vers <code>&lt;IP&gt;</code>. Exemple : <code>dozzle.192.168.1.10.sslip.io</code> résout vers <code>192.168.1.10</code>. Il n&rsquo;y a rien à configurer, le DNS fonctionne partout sans toucher à quoi que ce soit.</p>
<hr>
<h2 id="2-les-briques-fondamentales">2. Les briques fondamentales</h2>
<h3 id="le-réseau-docker-partagé">Le réseau Docker partagé</h3>
<p>Tous les services et Traefik doivent partager le même réseau Docker pour que Traefik puisse communiquer avec eux. Ce réseau s&rsquo;appelle <code>traefik</code> et est créé une seule fois :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker network create traefik
</span></span></code></pre></div><p>C&rsquo;est un réseau <strong>externe</strong> (créé hors de tout Compose). Chaque <code>compose.yaml</code> le déclare comme externe :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">traefik</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">external</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Pourquoi externe plutôt qu&rsquo;interne à un Compose ? Parce que plusieurs stacks indépendants doivent tous y être connectés. Un réseau interne à un Compose n&rsquo;est accessible qu&rsquo;aux services de ce Compose.</p>
<h3 id="traefik--le-reverse-proxy">Traefik : le reverse proxy</h3>
<p>Traefik écoute sur le port 80 et route les requêtes HTTP vers le bon container selon le <code>Host</code> header.</p>
<p>Sa configuration principale est dans <code>stacks/traefik/docker/traefik/traefik.yaml</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">api</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">dashboard</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">insecure</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">entryPoints</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">web</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">address</span>: :<span style="color:#ae81ff">80</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ping</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">address</span>: :<span style="color:#ae81ff">8082</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">providers</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">docker</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">endpoint</span>: <span style="color:#ae81ff">unix:///var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">exposedByDefault</span>: <span style="color:#66d9ef">false</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">log</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">level</span>: <span style="color:#ae81ff">INFO</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">global</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">sendAnonymousUsage</span>: <span style="color:#66d9ef">false</span>
</span></span></code></pre></div><p><code>exposedByDefault: false</code> est important : Traefik ignore tous les containers par défaut. Un container doit explicitement s&rsquo;exposer avec le label <code>traefik.enable: true</code>. Cela évite d&rsquo;exposer accidentellement des services.</p>
<p>L&rsquo;entrypoint <code>ping</code> sur le port 8082 est dédié aux health checks. Le séparer de l&rsquo;entrypoint <code>web</code> évite que les checks apparaissent dans les logs d&rsquo;accès.</p>
<p>Pour accéder au daemon Docker, Traefik monte le socket :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span></code></pre></div><h3 id="dockge--le-gestionnaire-de-stacks">Dockge : le gestionnaire de stacks</h3>
<p>Dockge tourne lui-même dans un container (le <code>compose.yaml</code> à la racine du repo). Il a besoin de deux choses :</p>
<ol>
<li>Accès au socket Docker pour piloter les autres containers.</li>
<li>Accès aux dossiers des stacks pour lire et modifier les <code>compose.yaml</code>.</li>
</ol>
<p>Le point critique est le montage des stacks. Dockge lance les stacks en passant des chemins absolus au daemon Docker. Ces chemins doivent être identiques dans le container Dockge et sur le host. La solution :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">${PWD}/stacks:${PWD}/stacks</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">DOCKGE_STACKS_DIR</span>: <span style="color:#ae81ff">${PWD}/stacks</span>
</span></span></code></pre></div><p><code>${PWD}</code> est une variable shell résolue au moment du <code>docker compose up</code>. Elle vaut le répertoire courant. Si on lance Dockge depuis <code>/home/user/homelab</code>, le dossier stacks sera monté à <code>/home/user/homelab/stacks</code> des deux côtés. C&rsquo;est la seule façon d&rsquo;éviter que Docker crée des répertoires fantômes au mauvais endroit.</p>
<p><strong>Conséquence pratique</strong> : toujours lancer <code>docker compose up -d</code> depuis la racine du repo.</p>
<p>La donnée persistante de Dockge (configuration, historique) est dans un volume nommé créé à l&rsquo;avance :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker volume create homelab_dockge_data
</span></span></code></pre></div><p>Un volume nommé survit à un <code>docker compose down -v</code>. Un volume anonyme serait détruit avec la stack.</p>
<hr>
<h2 id="3-mise-en-place-pas-à-pas">3. Mise en place pas à pas</h2>
<h3 id="étape-1--cloner-et-configurer">Étape 1 : cloner et configurer</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git clone &lt;repo&gt; homelab
</span></span><span style="display:flex;"><span>cd homelab
</span></span></code></pre></div><p>Trouver l&rsquo;IP locale de la machine :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>hostname -I | awk <span style="color:#e6db74">&#39;{print $1}&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># ex: 192.168.1.10</span>
</span></span></code></pre></div><p>Créer et éditer le <code>.env</code> racine :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cp .env.example .env
</span></span><span style="display:flex;"><span><span style="color:#75715e"># Éditer .env :</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># IP=192.168.1.10</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># DOMAIN=sslip.io</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># COMPOSE_PROJECT_NAME=dockge  ← important, voir section conventions</span>
</span></span></code></pre></div><h3 id="étape-2--prérequis-docker">Étape 2 : prérequis Docker</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker network create traefik
</span></span><span style="display:flex;"><span>docker volume create homelab_dockge_data
</span></span></code></pre></div><h3 id="étape-3--démarrer-dockge">Étape 3 : démarrer Dockge</h3>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>echo <span style="color:#e6db74">&#34;STACKS_DIR=</span><span style="color:#66d9ef">$(</span>pwd<span style="color:#66d9ef">)</span><span style="color:#e6db74">/stacks&#34;</span> &gt;&gt; .env
</span></span><span style="display:flex;"><span>docker compose up -d
</span></span></code></pre></div><p>Dockge est accessible sur <code>http://&lt;IP&gt;:5001</code>. Il est exposé directement sur le port 5001, pas via Traefik (Traefik n&rsquo;est pas encore démarré à ce stade). Créer un compte admin à la première ouverture.</p>
<h3 id="étape-4--configurer-les-stacks">Étape 4 : configurer les stacks</h3>
<p>Pour chaque dossier dans <code>stacks/</code>, copier le <code>.env.example</code> :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> stack in stacks/*/; <span style="color:#66d9ef">do</span>
</span></span><span style="display:flex;"><span>    cp <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>stack<span style="color:#e6db74">}</span><span style="color:#e6db74">.env.example&#34;</span> <span style="color:#e6db74">&#34;</span><span style="color:#e6db74">${</span>stack<span style="color:#e6db74">}</span><span style="color:#e6db74">.env&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">done</span>
</span></span></code></pre></div><p>Puis éditer chaque <code>.env</code> pour renseigner <code>IP</code> et <code>DOMAIN</code> avec les mêmes valeurs qu&rsquo;à l&rsquo;étape 1. La valeur <code>COMPOSE_PROJECT_NAME</code> est pré-remplie avec le nom du dossier, ne pas la changer (voir section conventions).</p>
<p>Pour <code>filebrowser</code>, renseigner aussi <code>FILEBROWSER_ROOT</code> avec le chemin local à exposer.</p>
<h3 id="étape-5--lancer-les-stacks-depuis-dockge">Étape 5 : lancer les stacks depuis Dockge</h3>
<p>Depuis l&rsquo;interface Dockge (<code>http://&lt;IP&gt;:5001</code>), dans cet ordre :</p>
<p><strong>1. Traefik en premier</strong></p>
<p>Traefik doit être actif avant les autres services. Sans Traefik, les routes n&rsquo;existent pas et les services sont inaccessibles via leur URL.</p>
<p>Après démarrage, vérifier que Traefik est healthy :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker ps --filter name<span style="color:#f92672">=</span>traefik
</span></span></code></pre></div><p><strong>2. Les autres stacks dans n&rsquo;importe quel ordre</strong></p>
<p>Chaque stack se déclare automatiquement auprès de Traefik via ses labels Docker. Traefik découvre les nouveaux containers en temps réel.</p>
<p><strong>3. Homepage en dernier</strong></p>
<p>Homepage lit les labels Docker de tous les containers au démarrage pour construire le dashboard. Le démarrer en dernier garantit qu&rsquo;il découvre tous les services actifs dès le premier lancement.</p>
<hr>
<h2 id="4-ajouter-un-nouveau-service">4. Ajouter un nouveau service</h2>
<p>Voici le template de <code>compose.yaml</code> pour tout nouveau service :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">monservice</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">image</span>: <span style="color:#ae81ff">editeur/monservice:latest</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">restart</span>: <span style="color:#ae81ff">unless-stopped</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">healthcheck</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">test</span>: [<span style="color:#e6db74">&#34;CMD-SHELL&#34;</span>, <span style="color:#e6db74">&#34;wget -qO- http://127.0.0.1:&lt;PORT&gt;/ || exit 1&#34;</span>]
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">interval</span>: <span style="color:#ae81ff">30s</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">timeout</span>: <span style="color:#ae81ff">10s</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">retries</span>: <span style="color:#ae81ff">3</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">start_period</span>: <span style="color:#ae81ff">10s</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Homepage - apparition automatique dans le dashboard</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">homepage.group</span>: <span style="color:#ae81ff">outils</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">homepage.name</span>: <span style="color:#ae81ff">Mon Service</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">homepage.icon</span>: <span style="color:#ae81ff">https://cdn.jsdelivr.net/gh/selfhst/icons/webp/monservice.webp</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">homepage.href</span>: <span style="color:#ae81ff">http://${COMPOSE_PROJECT_NAME}.${IP}.${DOMAIN}</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>            <span style="color:#75715e"># Traefik - routage HTTP</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">traefik.enable</span>: <span style="color:#66d9ef">true</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">traefik.http.routers.monservice.entrypoints</span>: <span style="color:#ae81ff">web</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">traefik.http.routers.monservice.rule</span>: <span style="color:#ae81ff">Host(`${COMPOSE_PROJECT_NAME}.${IP}.${DOMAIN}`)</span>
</span></span><span style="display:flex;"><span>            <span style="color:#f92672">traefik.http.services.monservice.loadbalancer.server.port</span>: <span style="color:#ae81ff">&lt;PORT&gt;</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>            - <span style="color:#ae81ff">traefik</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">traefik</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">external</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>Et le <code>.env.example</code> associé :</p>
<pre tabindex="0"><code>COMPOSE_PROJECT_NAME=monservice
IP=127.0.0.1
DOMAIN=sslip.io
</code></pre><p><strong>Le nom du dossier détermine le sous-domaine.</strong> Si le dossier s&rsquo;appelle <code>monservice</code>, le service sera accessible sur <code>monservice.&lt;IP&gt;.&lt;DOMAIN&gt;</code>. C&rsquo;est tout.</p>
<p>Pour trouver des services à ajouter, <a href="https://selfh.st" target="_blank" rel="noopener noreferrer">selfh.st</a>
 est une excellente ressource : c&rsquo;est un catalogue de logiciels self-hosted organisé par catégorie (media, sécurité, productivité, monitoring&hellip;), avec pour chacun une description, une capture d&rsquo;écran et le lien GitHub. Le site publie aussi une newsletter hebdomadaire sur les nouvelles releases.</p>
<h3 id="checklist-pour-un-nouveau-service">Checklist pour un nouveau service</h3>
<ul>
<li><input disabled="" type="checkbox"> Créer <code>stacks/&lt;nom-du-sous-domaine&gt;/compose.yaml</code></li>
<li><input disabled="" type="checkbox"> Créer <code>stacks/&lt;nom-du-sous-domaine&gt;/.env.example</code> avec <code>COMPOSE_PROJECT_NAME=&lt;nom&gt;</code></li>
<li><input disabled="" type="checkbox"> Copier <code>.env.example</code> en <code>.env</code> et renseigner IP/DOMAIN</li>
<li><input disabled="" type="checkbox"> Vérifier le port dans les labels Traefik</li>
<li><input disabled="" type="checkbox"> Choisir le groupe Homepage : <code>infra</code>, <code>observabilité</code>, ou <code>outils</code></li>
<li><input disabled="" type="checkbox"> Trouver l&rsquo;icône sur <a href="https://github.com/selfhst/icons" target="_blank" rel="noopener noreferrer">selfhst/icons</a>
</li>
<li><input disabled="" type="checkbox"> Ajouter les données persistantes dans un volume si nécessaire</li>
<li><input disabled="" type="checkbox"> Lancer depuis Dockge et vérifier que le container est <code>healthy</code></li>
</ul>
<hr>
<h2 id="5-patterns-et-conventions">5. Patterns et conventions</h2>
<h3 id="la-variable-compose_project_name">La variable <code>${COMPOSE_PROJECT_NAME}</code></h3>
<p>Docker Compose valorise automatiquement <code>COMPOSE_PROJECT_NAME</code> avec le nom du dossier du stack. On l&rsquo;utilise pour construire dynamiquement les URLs :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">traefik.http.routers.dozzle.rule</span>: <span style="color:#ae81ff">Host(`${COMPOSE_PROJECT_NAME}.${IP}.${DOMAIN}`)</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">homepage.href</span>: <span style="color:#ae81ff">http://${COMPOSE_PROJECT_NAME}.${IP}.${DOMAIN}</span>
</span></span></code></pre></div><p>Avantage : pas de variable <code>*_HOST</code> à maintenir dans chaque <code>.env</code>. Renommer le dossier change automatiquement le sous-domaine.</p>
<p><strong>Attention</strong> : dans le <code>.env</code>, il faut définir <code>COMPOSE_PROJECT_NAME</code> explicitement avec le nom du dossier du stack. Si on ne le définit pas, Docker Compose utilise le nom du répertoire courant au moment du lancement, ce qui peut donner des valeurs inattendues selon d&rsquo;où on lance la commande.</p>
<h3 id="les-groupes-homepage">Les groupes Homepage</h3>
<p>Les services sont organisés en trois groupes dans le dashboard :</p>
<table>
  <thead>
      <tr>
          <th>Groupe</th>
          <th>Services</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>infra</code></td>
          <td><a href="https://github.com/traefik/traefik" target="_blank" rel="noopener noreferrer">Traefik</a>
, <a href="https://github.com/louislam/dockge" target="_blank" rel="noopener noreferrer">Dockge</a>
, <a href="https://github.com/containrrr/watchtower" target="_blank" rel="noopener noreferrer">Watchtower</a>
, <a href="https://github.com/gethomepage/homepage" target="_blank" rel="noopener noreferrer">Homepage</a>
</td>
      </tr>
      <tr>
          <td><code>observabilité</code></td>
          <td><a href="https://github.com/amir20/dozzle" target="_blank" rel="noopener noreferrer">Dozzle</a>
, <a href="https://github.com/nicolargo/glances" target="_blank" rel="noopener noreferrer">Glances</a>
, <a href="https://github.com/louislam/uptime-kuma" target="_blank" rel="noopener noreferrer">Uptime Kuma</a>
</td>
      </tr>
      <tr>
          <td><code>outils</code></td>
          <td><a href="https://github.com/gtsteffaniak/filebrowser" target="_blank" rel="noopener noreferrer">FileBrowser</a>
, <a href="https://github.com/CorentinTh/it-tools" target="_blank" rel="noopener noreferrer">IT-Tools</a>
, <a href="https://github.com/Stirling-Tools/Stirling-PDF" target="_blank" rel="noopener noreferrer">Stirling PDF</a>
</td>
      </tr>
  </tbody>
</table>
<p>Ce découpage est celui de ce homelab, pas une convention imposée. Homepage accepte n&rsquo;importe quelle valeur dans <code>homepage.group</code> : on peut créer autant de groupes que nécessaire et les nommer comme on veut (<code>media</code>, <code>domotique</code>, <code>dev</code>&hellip;). Le dashboard se réorganise automatiquement.</p>
<h3 id="health-checks">Health checks</h3>
<p>Tous les services ont un health check. C&rsquo;est crucial car <strong>Traefik ignore silencieusement les containers <code>unhealthy</code></strong> : un service avec un health check défaillant n&rsquo;apparaît pas dans le routage, même avec <code>traefik.enable: true</code>.</p>
<p>Trois cas particuliers rencontrés en pratique :</p>
<p><strong>1. <code>localhost</code> ne résout pas toujours en <code>127.0.0.1</code></strong></p>
<p>Dans certaines images minimalistes, <code>localhost</code> n&rsquo;est pas résolu. Utiliser <code>127.0.0.1</code> explicitement :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">test</span>: [<span style="color:#e6db74">&#34;CMD-SHELL&#34;</span>, <span style="color:#e6db74">&#34;wget -qO- http://127.0.0.1:8080/ || exit 1&#34;</span>]
</span></span></code></pre></div><p><strong>2. Images sans shell (<code>scratch</code>-based)</strong></p>
<p>Les images basées sur <code>scratch</code> (ex: Dozzle) ne contiennent pas <code>/bin/sh</code>. <code>CMD-SHELL</code> échoue. Utiliser le binaire embarqué :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">test</span>: [<span style="color:#e6db74">&#34;CMD&#34;</span>, <span style="color:#e6db74">&#34;/dozzle&#34;</span>, <span style="color:#e6db74">&#34;healthcheck&#34;</span>]
</span></span></code></pre></div><p><strong>3. Images sans <code>wget</code> ni <code>curl</code></strong></p>
<p>Certaines images Node.js ou JVM n&rsquo;ont ni wget ni curl. Solutions possibles :</p>
<ul>
<li>Si Node.js est disponible : <code>node -e &quot;require('http').get('http://localhost:PORT', r =&gt; process.exit(r.statusCode &lt; 400 ? 0 : 1)).on('error', () =&gt; process.exit(1))&quot;</code></li>
<li>Si curl est disponible : <code>curl -fs http://127.0.0.1:PORT/</code></li>
<li>Si le binaire de l&rsquo;app expose une sous-commande healthcheck : l&rsquo;utiliser directement.</li>
</ul>
<h3 id="persistance-des-données">Persistance des données</h3>
<p>Pour les services qui ont des données (configuration, base utilisateurs, base de données) :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#ae81ff">./docker/data:/chemin/dans/container</span>
</span></span></code></pre></div><p>Le dossier <code>./docker/</code> est dans le dossier du stack et peut être versionné, à l&rsquo;exception des données runtime qui vont dans <code>.gitignore</code>.</p>
<p><strong>Règle</strong> : ajouter <code>stacks/&lt;service&gt;/docker/</code> dans <code>.gitignore</code> si le dossier contient des données qui ne doivent pas être committées (base SQLite, uploads&hellip;).</p>
<h3 id="organisation-des-labels-traefik">Organisation des labels Traefik</h3>
<p>Par convention, le nom utilisé dans les labels Traefik (<code>traefik.http.routers.&lt;nom&gt;</code>) correspond au nom du service Docker dans le <code>compose.yaml</code>. En pratique on les aligne avec le nom du dossier :</p>
<pre tabindex="0"><code>stacks/it-tools/    →    service: ittools    →    traefik.http.routers.ittools.*
</code></pre><p>Ce n&rsquo;est pas une contrainte technique de Traefik, juste une convention de lisibilité.</p>
<hr>
<h2 id="6-pièges-courants">6. Pièges courants</h2>
<h3 id="dockge--stop-puis-start-pas-restart">Dockge : Stop puis Start, pas Restart</h3>
<p>Quand on modifie un <code>compose.yaml</code> depuis l&rsquo;IDE et qu&rsquo;on veut appliquer les changements, il faut faire <strong>Stop + Start</strong> depuis Dockge, pas &ldquo;Restart&rdquo;. Le Restart redémarre le container existant sans relire le <code>compose.yaml</code>. Le Stop + Start recrée le container avec la nouvelle configuration.</p>
<h3 id="labels-modifiés--redémarrer-homepage">Labels modifiés : redémarrer Homepage</h3>
<p>Homepage lit les labels Docker <strong>au démarrage</strong>. Si on change le <code>homepage.group</code> ou <code>homepage.name</code> d&rsquo;un service, Homepage ne le voit pas tant qu&rsquo;il n&rsquo;est pas redémarré.</p>
<h3 id="le-container-démarre-mais-nest-pas-routable">Le container démarre mais n&rsquo;est pas routable</h3>
<p>Vérifier dans l&rsquo;ordre :</p>
<ol>
<li><code>docker ps</code> : le container est-il <code>healthy</code> ? Traefik ignore les containers <code>unhealthy</code>.</li>
<li>Le container est-il sur le réseau <code>traefik</code> ?</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker inspect &lt;container&gt; --format <span style="color:#e6db74">&#39;{{json .NetworkSettings.Networks}}&#39;</span>
</span></span></code></pre></div><ol start="3">
<li>Le label <code>traefik.enable: true</code> est-il présent ?</li>
<li>La règle <code>Host(...)</code> correspond-elle à l&rsquo;URL testée ?</li>
</ol>
<h3 id="montage-de-fichiers-inexistants-sous-docker-desktop--wsl">Montage de fichiers inexistants sous Docker Desktop / WSL</h3>
<p>Quand Docker Desktop (WSL) monte un <strong>fichier</strong> qui n&rsquo;existe pas encore sur le host, il crée un <strong>répertoire</strong> à la place. Ce répertoire fantôme bloque ensuite le montage du vrai fichier. Symptôme : le container refuse de démarrer avec une erreur de montage.</p>
<p>Solution : s&rsquo;assurer que le fichier existe sur le host avant de démarrer le container, ou utiliser un montage de répertoire plutôt que de fichier.</p>
<h3 id="watchtower--api-docker-trop-ancienne">Watchtower : API Docker trop ancienne</h3>
<p>Sur certaines configurations, Watchtower tente de communiquer avec le daemon en commençant la négociation à l&rsquo;API v1.25 (son minimum historique). Les versions récentes de Docker refusent cette version. Symptôme : le container redémarre en boucle avec <code>client version 1.25 is too old. Minimum supported API version is 1.40</code>.</p>
<p>Fix dans le <code>compose.yaml</code> de Watchtower :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">environment</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">DOCKER_API_VERSION</span>: <span style="color:#e6db74">&#34;1.40&#34;</span>
</span></span></code></pre></div><p><code>1.40</code> est la valeur à mettre, quelle que soit ta version de Docker. Ce n&rsquo;est pas ta version exacte, c&rsquo;est le minimum que le daemon accepte, indiqué dans le message d&rsquo;erreur. Pour vérifier la version d&rsquo;API réelle de ton daemon :</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker version --format <span style="color:#e6db74">&#39;{{.Server.APIVersion}}&#39;</span>
</span></span></code></pre></div><h3 id="pwd-dans-le-compose-de-dockge"><code>${PWD}</code> dans le compose de Dockge</h3>
<p><code>${PWD}</code> n&rsquo;est pas une variable <code>.env</code>, c&rsquo;est une variable shell résolue au moment du <code>docker compose up</code>. Elle vaut le répertoire courant du terminal. Lancer <code>docker compose up -d</code> depuis n&rsquo;importe quel autre répertoire donnera une mauvaise valeur et cassera les montages de volumes des stacks.</p>
<hr>
<p><em>Ce homelab est conçu pour tourner sur une machine Linux ou WSL. Toutes les commandes sont testées sur Ubuntu/WSL2 avec Docker Desktop.</em></p>
<hr>
<h2 id="conclusion">Conclusion</h2>
<p>J&rsquo;ai bien conscience que ce tuto ne couvre pas tout. On aurait pu ajouter de l&rsquo;authentification devant chaque service, faire tourner l&rsquo;ensemble en HTTPS, mettre en place un socket proxy pour limiter l&rsquo;exposition du daemon Docker, ou épingler précisément chaque version d&rsquo;image. Mais chacun de ces points aurait considérablement allongé l&rsquo;article et la complexité de mise en place. L&rsquo;objectif était de démarrer avec quelque chose de fonctionnel et maintenable, pas de construire une forteresse dès le premier jour.</p>
<p>Le homelab parfait n&rsquo;existe pas. Celui qui tourne, si.</p>
<div style="border: 1px solid #e8e8e8; padding: 16px; margin-top: 2em; border-radius: 3px;">
  <img src="https://cdn.simpleicons.org/github" width="20" style="vertical-align: middle; margin-right: 8px;" />
  <strong><a href="https://github.com/guillaumedelre/homelab" target="_blank" rel="noopener noreferrer">guillaumedelre/homelab</a></strong>
  <p style="margin: 8px 0 0; color: #828282; font-size: 14px;">Homelab Docker Compose avec Traefik — stacks indépendants, dashboard auto-configuré, et zéro configuration DNS grâce à sslip.io.</p>
</div>
<h2 id="références">Références</h2>
<table>
  <thead>
      <tr>
          <th>Projet</th>
          <th>GitHub</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>sslip.io</td>
          <td><a href="https://sslip.io" target="_blank" rel="noopener noreferrer">sslip.io</a>
</td>
      </tr>
      <tr>
          <td>selfh.st</td>
          <td><a href="https://selfh.st" target="_blank" rel="noopener noreferrer">selfh.st</a>
</td>
      </tr>
      <tr>
          <td>Traefik</td>
          <td><a href="https://github.com/traefik/traefik" target="_blank" rel="noopener noreferrer">github.com/traefik/traefik</a>
</td>
      </tr>
      <tr>
          <td>Dockge</td>
          <td><a href="https://github.com/louislam/dockge" target="_blank" rel="noopener noreferrer">github.com/louislam/dockge</a>
</td>
      </tr>
      <tr>
          <td>Homepage</td>
          <td><a href="https://github.com/gethomepage/homepage" target="_blank" rel="noopener noreferrer">github.com/gethomepage/homepage</a>
</td>
      </tr>
      <tr>
          <td>Dozzle</td>
          <td><a href="https://github.com/amir20/dozzle" target="_blank" rel="noopener noreferrer">github.com/amir20/dozzle</a>
</td>
      </tr>
      <tr>
          <td>Glances</td>
          <td><a href="https://github.com/nicolargo/glances" target="_blank" rel="noopener noreferrer">github.com/nicolargo/glances</a>
</td>
      </tr>
      <tr>
          <td>FileBrowser</td>
          <td><a href="https://github.com/gtsteffaniak/filebrowser" target="_blank" rel="noopener noreferrer">github.com/gtsteffaniak/filebrowser</a>
</td>
      </tr>
      <tr>
          <td>IT-Tools</td>
          <td><a href="https://github.com/CorentinTh/it-tools" target="_blank" rel="noopener noreferrer">github.com/CorentinTh/it-tools</a>
</td>
      </tr>
      <tr>
          <td>Stirling PDF</td>
          <td><a href="https://github.com/Stirling-Tools/Stirling-PDF" target="_blank" rel="noopener noreferrer">github.com/Stirling-Tools/Stirling-PDF</a>
</td>
      </tr>
      <tr>
          <td>Uptime Kuma</td>
          <td><a href="https://github.com/louislam/uptime-kuma" target="_blank" rel="noopener noreferrer">github.com/louislam/uptime-kuma</a>
</td>
      </tr>
      <tr>
          <td>Watchtower</td>
          <td><a href="https://github.com/containrrr/watchtower" target="_blank" rel="noopener noreferrer">github.com/containrrr/watchtower</a>
</td>
      </tr>
      <tr>
          <td>selfhst/icons</td>
          <td><a href="https://github.com/selfhst/icons" target="_blank" rel="noopener noreferrer">github.com/selfhst/icons</a>
</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>HTTPS local avec Traefik: traefik.me est mort, vive sslip.io</title><link>https://guillaumedelre.github.io/fr/2025/04/17/https-local-avec-traefik-traefik.me-est-mort-vive-sslip.io/</link><pubDate>Thu, 17 Apr 2025 00:00:00 +0000</pubDate><guid>https://guillaumedelre.github.io/fr/2025/04/17/https-local-avec-traefik-traefik.me-est-mort-vive-sslip.io/</guid><description>Le certificat wildcard de traefik.me a été révoqué en 2025. Voici comment le remplacer avec sslip.io, mkcert et une configuration Traefik locale.</description><content:encoded><![CDATA[<p>La configuration semblait parfaite. Pointer <code>*.traefik.me</code> sur 127.0.0.1, télécharger un certificat wildcard depuis le même domaine, le déposer dans Traefik, et chaque service local obtient une URL HTTPS propre sans IP dans la barre d&rsquo;adresse. Pas de limites de débit Let&rsquo;s Encrypt, pas de <code>mkcert</code> à expliquer aux collègues, pas d&rsquo;avertissements de certificat auto-signé à contourner. Juste <code>https://myapp.traefik.me</code> et un cadenas vert.</p>
<p>Puis en mars 2025, Let&rsquo;s Encrypt a révoqué le certificat. Le wildcard cert pour traefik.me est parti et il ne reviendra pas.</p>
<h2 id="ce-que-traefikme-vendait-vraiment">Ce que traefik.me vendait vraiment</h2>
<p>traefik.me est un résolveur DNS wildcard. Tapez <code>anything.traefik.me</code> et ça résout vers 127.0.0.1. Tapez <code>anything.10.0.0.1.traefik.me</code> et ça résout vers 10.0.0.1. Aucun compte, aucune configuration, aucune infrastructure à maintenir. La partie DNS fonctionne toujours bien, soit dit en passant.</p>
<p>Le certificat était le bonus: un wildcard cert pour <code>*.traefik.me</code> que pyrou, le mainteneur, avait généré avec Let&rsquo;s Encrypt et distribué sur <code>https://traefik.me/cert.pem</code> et <code>https://traefik.me/privkey.pem</code>. C&rsquo;était pratique précisément parce que c&rsquo;était partagé: télécharger, déposer dans Traefik, terminé.</p>
<p>Partager une clé privée, c&rsquo;est ce qui l&rsquo;a tué.</p>
<p>Les Baseline Requirements du CA/Browser Forum, section 9.6.3, exigent que les souscripteurs &ldquo;maintiennent le contrôle exclusif&rdquo; de leur clé privée. La distribuer à quiconque visite une URL, c&rsquo;est exactement le contraire du contrôle exclusif. Let&rsquo;s Encrypt a envoyé une notification, bloqué toute future émission pour le domaine, et révoqué le certificat existant. Pyrou a confirmé la situation et recommandé mkcert comme alternative. Le projet survivra uniquement en tant que résolveur DNS.</p>
<p>Le cert avait déjà été révoqué deux fois avant 2025. La troisième était la dernière.</p>
<h2 id="sslipio-fait-la-même-chose-différemment">sslip.io fait la même chose, différemment</h2>
<p>sslip.io est aussi un résolveur DNS wildcard, avec une différence: l&rsquo;IP est encodée dans le hostname plutôt que résolue depuis un fallback. <code>10-0-0-1.sslip.io</code> résout vers <code>10.0.0.1</code>. <code>myapp.192-168-1-10.sslip.io</code> résout vers <code>192.168.1.10</code>. IPv6 fonctionne aussi.</p>
<p>L&rsquo;infrastructure derrière sslip.io est aussi plus visible: trois serveurs de noms à Singapour, aux États-Unis et en Pologne, traitant plus de 10 000 requêtes par seconde, avec un monitoring public. Environ 1 000 étoiles GitHub et une maintenance active sous licence Apache 2.0.</p>
<p>En mettant de côté l&rsquo;histoire des certificats, la comparaison est assez directe:</p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>traefik.me</th>
          <th>sslip.io</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DNS wildcard</td>
          <td>oui</td>
          <td>oui</td>
      </tr>
      <tr>
          <td>Fallback vers 127.0.0.1</td>
          <td>oui</td>
          <td>non</td>
      </tr>
      <tr>
          <td>IPv6</td>
          <td>non</td>
          <td>oui</td>
      </tr>
      <tr>
          <td>Certificat wildcard</td>
          <td><del>oui</del> révoqué</td>
          <td>non</td>
      </tr>
      <tr>
          <td>Infrastructure</td>
          <td>opaque</td>
          <td>documentée</td>
      </tr>
      <tr>
          <td>Activité du projet</td>
          <td>au point mort</td>
          <td>active</td>
      </tr>
  </tbody>
</table>
<p>Le seul avantage restant de traefik.me est le fallback vers 127.0.0.1: des URLs sans segment IP. Ça compte si on tient vraiment à <code>myapp.traefik.me</code> plutôt que <code>myapp.127-0-0-1.sslip.io</code>. La question est de savoir si cette différence vaut l&rsquo;incertitude sur l&rsquo;infrastructure.</p>
<h2 id="mkcert-comble-le-vide">mkcert comble le vide</h2>
<p>mkcert crée une autorité de certification locale, l&rsquo;installe dans le trust store système et dans les navigateurs qu&rsquo;il trouve, puis émet des certificats signés par cette CA. Les navigateurs voient une chaîne de confiance valide. Aucun avertissement, aucun clic, aucun &ldquo;continuer quand même&rdquo;.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkcert -install
</span></span></code></pre></div><p>C&rsquo;est la configuration unique. Ensuite, générer un certificat se fait en une commande:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkcert <span style="color:#e6db74">&#34;*.127-0-0-1.sslip.io&#34;</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e"># produit _wildcard.127-0-0-1.sslip.io.pem</span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">#         _wildcard.127-0-0-1.sslip.io-key.pem</span>
</span></span></code></pre></div><p>La limitation: la CA de mkcert est locale. Les autres machines du réseau ne lui feront pas confiance par défaut. Pour un setup solo, c&rsquo;est très bien. Pour un environnement d&rsquo;équipe partagé, il faudrait distribuer la CA root, ce qui est essentiellement le même problème opérationnel que traefik.me tentait d&rsquo;éviter, juste à plus petite échelle.</p>
<h2 id="la-configuration-traefik">La configuration Traefik</h2>
<p>Le setup est le même quelle que soit la solution DNS choisie. Traefik a besoin du certificat monté en volume et d&rsquo;un file provider statique pointant vers un fichier de configuration TLS.</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#75715e"># traefik/config/tls.yml</span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">tls</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">certificates</span>:
</span></span><span style="display:flex;"><span>    - <span style="color:#f92672">certFile</span>: <span style="color:#ae81ff">/certs/cert.pem</span>
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">keyFile</span>: <span style="color:#ae81ff">/certs/key.pem</span>
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">stores</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">default</span>:
</span></span><span style="display:flex;"><span>      <span style="color:#f92672">defaultCertificate</span>:
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">certFile</span>: <span style="color:#ae81ff">/certs/cert.pem</span>
</span></span><span style="display:flex;"><span>        <span style="color:#f92672">keyFile</span>: <span style="color:#ae81ff">/certs/key.pem</span>
</span></span></code></pre></div><p>La bonne pratique: faire tourner Traefik dans son propre projet Compose, séparé des services qu&rsquo;il route. Chaque projet de service se connecte à Traefik via un réseau externe partagé. On démarre et arrête les services indépendamment sans toucher au reverse proxy.</p>
<p>On commence par créer le réseau externe une seule fois:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker network create traefik-public
</span></span></code></pre></div><p><strong><code>traefik/compose.yml</code></strong> - Traefik seul, propriétaire du réseau:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">traefik</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">traefik:v3</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">ports</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;80:80&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;443:443&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">volumes</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">/var/run/docker.sock:/var/run/docker.sock</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./config:/etc/traefik/config</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">./certs:/certs</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">command</span>:
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.web.address=:80</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">entrypoints.websecure.address=:443</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker=true</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.docker.network=traefik-public</span>
</span></span><span style="display:flex;"><span>      - --<span style="color:#ae81ff">providers.file.directory=/etc/traefik/config</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">traefik-public</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">traefik-public</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">external</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><p>On copie la sortie de mkcert dans <code>./certs/</code>, on renomme en <code>cert.pem</code> et <code>key.pem</code>, puis:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose -f traefik/compose.yml up -d
</span></span></code></pre></div><p>Traefik est lancé, il écoute sur le port 80 et 443, et surveille Docker pour les nouveaux containers. Aucune route n&rsquo;est encore configurée.</p>
<p><strong><code>whoami/compose.yml</code></strong> - un service qui rejoint le même réseau:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#f92672">services</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">whoami</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">image</span>: <span style="color:#ae81ff">traefik/whoami</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">labels</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;traefik.enable=true&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;traefik.http.routers.whoami.rule=Host(`whoami.127-0-0-1.sslip.io`)&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;traefik.http.routers.whoami.tls=true&#34;</span>
</span></span><span style="display:flex;"><span>      - <span style="color:#e6db74">&#34;traefik.http.routers.whoami.entrypoints=websecure&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>      - <span style="color:#ae81ff">traefik-public</span>
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#f92672">networks</span>:
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">traefik-public</span>:
</span></span><span style="display:flex;"><span>    <span style="color:#f92672">external</span>: <span style="color:#66d9ef">true</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker compose -f whoami/compose.yml up -d
</span></span></code></pre></div><p>Traefik détecte le nouveau container via le Docker provider, lit ses labels, et ajoute la route. <code>https://whoami.127-0-0-1.sslip.io</code> répond immédiatement. Arrêter <code>whoami</code> et la route disparaît. Traefik continue de tourner sans s&rsquo;en apercevoir.</p>
<p>La déclaration <code>external: true</code> est la ligne qui porte tout le poids. Sans elle, Compose crée un réseau limité au périmètre du projet: Traefik et <code>whoami</code> se retrouvent sur des réseaux différents et ne peuvent pas communiquer, même s&rsquo;ils tournent tous les deux. Le réseau externe est le bus partagé auquel chaque projet de service doit explicitement adhérer.</p>
<p>Si on préfère les URLs traefik.me, il suffit de remplacer la commande mkcert et le label de host:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkcert <span style="color:#e6db74">&#34;*.traefik.me&#34;</span>
</span></span></code></pre></div><div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span>- <span style="color:#e6db74">&#34;traefik.http.routers.whoami.rule=Host(`whoami.traefik.me`)&#34;</span>
</span></span></code></pre></div><p>Le fallback DNS vers 127.0.0.1 gère le reste.</p>
<h2 id="ce-que-lhistoire-traefikme-enseigne-vraiment">Ce que l&rsquo;histoire traefik.me enseigne vraiment</h2>
<p>Le modèle de distribution de certificats a toujours été fragile. Une &ldquo;paire clé publique-clé privée&rdquo; est une contradiction dans les termes. Chaque révocation était un avertissement que la suivante pourrait être définitive. Finalement, ça l&rsquo;a été.</p>
<p>La leçon ne se limite pas à traefik.me. Tout service qui apporte de la commodité en supprimant discrètement une frontière de sécurité finira par se heurter à cette frontière. mkcert est le bon outil pour ce problème parce qu&rsquo;il opère entièrement dans votre propre domaine de confiance: on génère la CA, on l&rsquo;installe, on émet les certificats. Rien ne dépend de la volonté continue d&rsquo;un tiers de contourner les règles d&rsquo;émission de certificats.</p>
<p>sslip.io résout proprement la partie DNS. mkcert résout proprement la partie TLS. Ils se combinent bien. Le setup traefik.me était plus simple, pendant un temps. Jusqu&rsquo;à ce que ce ne soit plus le cas.</p>
]]></content:encoded></item></channel></rss>