<?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>Homelab on Guillaume Delré</title><link>https://guillaumedelre.github.io/fr/tags/homelab/</link><description>Recent content in Homelab 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/homelab/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></channel></rss>