SSD Nodes Learn Hosting plans →
Guides Matt ConnorPar Matt Connor · Mis à jour le 2026-08-22

Traefik avec Docker Compose : 5 apps sur un seul VPS

Configurez Traefik v3 avec Docker Compose pour router 5 apps par Host, activer Let's Encrypt et éviter l'erreur acme.json qui bloque le démarrage.

Une adresse IP, cinq applications, un port 443

Votre VPS dispose d’une seule adresse IPv4 publique et d’un seul port TCP 443. Vous voulez y héberger Gitea, une copie de staging de votre application, un tableau de bord interne, une page d’état et un récepteur de webhooks : cinq noms d’hôte sur une seule machine. Un reverse proxy est le processus qui écoute sur :80 et :443, lit l’en-tête Host de chaque requête et la transmet au bon conteneur. Traefik s’en charge et obtient puis renouvelle un certificat pour chaque nom d’hôte, sans que vous ayez à exécuter certbot manuellement. Nginx et Caddy pourraient tout aussi bien assurer la terminaison TLS pour ces cinq noms d’hôte. Si le choix n’est pas encore arrêté, il est donc utile de comparer les trois reverse proxies selon la gestion des certificats et le coût de configuration par application avant de tout connecter à l’un d’eux.

Ce qui distingue Traefik d’un bloc server {} nginx, c’est l’origine de sa configuration. Avec nginx, vous modifiez un fichier puis rechargez le service, tandis que le cycle de vie des certificats reste une tâche distincte. C’est le fonctionnement que vous suivez lorsque vous obtenez des certificats Let's Encrypt avec certbot sur nginx : le timer de renouvellement se trouve entièrement en dehors du serveur web. Le provider Docker de Traefik surveille le flux d’événements Docker et lit les labels de vos conteneurs : démarrez un conteneur avec un label de règle Host() et il devient routable en moins d’une seconde ; arrêtez-le et la route disparaît. C’est aussi le piège. La configuration stockée dans les labels se retrouve à cinq endroits différents, et un label incorrect ne produit aucun message : le conteneur n’est tout simplement pas routé et Traefik ne signale rien.

Les quatre notions

  • Entrypoints sont des sockets en écoute. Vous en définirez deux : web sur :80 et websecure sur :443.
  • Les routers font correspondre une requête (Host(...)) et l’associent à un service. Les certificats sont demandés pour chaque router, via tls.certresolver.
  • Les services sont les backends : un conteneur et le port sur lequel il écoute à l’intérieur du réseau Docker.
  • Les middlewares s’intercalent entre le router et le service : authentification basique, listes d’adresses IP autorisées, réécriture d’en-têtes, redirections.

Ces quatre notions correspondent aux noms utilisés par Traefik pour désigner des éléments que vous devriez sinon configurer manuellement : le router est un server_name, le service est une cible proxy_pass, et les middlewares regroupent les directives d’en-têtes et d’authentification que vous définiriez vous-même lorsque vous construisez un server block nginx de reverse proxy ligne par ligne.

La configuration statique (entrypoints, providers, ACME) est passée à Traefik sur sa ligne de commande ou dans traefik.yml. Toute modification nécessite de redémarrer Traefik. La configuration dynamique (routers, services, middlewares) provient des labels des conteneurs et est rechargée à chaud. Confondre les deux est la cause habituelle du message « mon flag ne fait rien ».

Le fichier Compose

Un réseau Docker partagé nommé proxy constitue l’ossature de l’ensemble. Traefik n’atteint un conteneur que si les deux sont connectés à ce réseau.

name: edge

networks:
  proxy:
    name: proxy

services:
  traefik:
    image: traefik:v3.5
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedByDefault=false
      - --providers.docker.network=proxy
      - --entryPoints.web.address=:80
      - --entryPoints.websecure.address=:443
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --certificatesresolvers.le.acme.email=you@example.com
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # while you iterate, point at staging so a mistake costs nothing:
      # - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
      - --api.dashboard=true
      - --log.level=INFO
      - --accesslog=true
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./letsencrypt:/letsencrypt
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS

  gitea:
    image: gitea/gitea:1  # major-only pin keeps this demo copy-pasteable; pin an exact release in production
    restart: unless-stopped
    volumes:
      - ./gitea:/data
    networks:
      - proxy
    labels:
      - traefik.enable=true
      - traefik.http.routers.gitea.rule=Host(`git.example.com`)
      - traefik.http.routers.gitea.entrypoints=websecure
      - traefik.http.routers.gitea.tls.certresolver=le
      - traefik.http.services.gitea.loadbalancer.server.port=3000

docker compose up -d, puis docker compose logs -f traefik. Chaque application supplémentaire reprend le bloc gitea, avec son propre nom de router, sa propre Host() et son propre port interne. Une installation de Nextcloud dans Docker avec TLS et sauvegardes s’intègre de la même manière : supprimez ses ports publiés, connectez-la à proxy et laissez les labels du router gérer le nom d’hôte et le certificat.

Cinq détails sont importants.

exposedByDefault=false rend un conteneur invisible pour Traefik tant qu’il ne porte pas traefik.enable=true. Si vous l’omettez, tous les conteneurs que vous démarrerez, y compris le conteneur temporaire postgres utilisé pour vérifier un point, recevront une route.

providers.docker.network=proxy indique à Traefik quel réseau utiliser lorsqu’un conteneur est connecté à plusieurs réseaux. Si vous l’omettez, Traefik peut sélectionner la mauvaise adresse IP du conteneur. Le problème se manifeste alors par une erreur 502 qui ressemble à un dysfonctionnement de l’application.

loadbalancer.server.port=3000 désigne le port à l’intérieur du conteneur ; Gitea y écoute sur 3000. Notez qu’aucun conteneur d’application ne publie de port. Seul Traefik en publie.

La redirection sur l’entrypoint web transforme les requêtes en clair en réponses 308 vers HTTPS. Le port 80 reste néanmoins ouvert : le challenge HTTP ACME en a besoin, tout comme les utilisateurs qui saisissent un nom d’hôte sans protocole.

Le double $$ dans le hash basic-auth correspond à l’échappement de Compose, et non à une erreur. Générez-le avec htpasswd -nbB admin 'your-password' (paquet apache2-utils), puis doublez chaque $.

Le certificat et le piège de acme.json

tlschallenge=true sélectionne TLS-ALPN-01 : Let’s Encrypt se connecte à votre serveur sur 443, et Traefik répond au défi pendant la négociation TLS. L’alternative est HTTP-01, sur le port 80. Remplacez la ligne tlschallenge dans la liste command: de Traefik par ces deux lignes :

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

Les deux méthodes fonctionnent. Elles nécessitent toutes deux que le DNS public du nom d’hôte pointe déjà vers votre VPS, afin que l’autorité de certification puisse résoudre le nom et se connecter depuis l’extérieur. Créez d’abord l’enregistrement A (et AAAA), vérifiez-le avec dig +short git.example.com, puis démarrez Traefik.

Voici maintenant le piège qui peut vous faire perdre une soirée. Traefik conserve la clé de compte ACME et chaque certificat délivré dans un seul fichier acme.json. Si ce fichier est lisible par le groupe ou par tous les utilisateurs, Traefik affiche un message très proche de celui-ci, puis s’arrête :

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

La correction propre est celle indiquée plus haut : liez le répertoire en montage bind et laissez Traefik créer lui-même le fichier avec les bonnes permissions. Si vous avez créé acme.json avec touch, votre umask lui a attribué les permissions 644. Corrigez-les sur l’hôte :

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

Sauvegardez ce répertoire avec les volumes de vos applications. Sa perte reste récupérable : les certificats seront délivrés à nouveau. Toutefois, renouveler cinq noms d’hôte en même temps peut vous faire atteindre les limites de débit.

Utilisez l’autorité de certification de staging pendant vos essais. Décommentez la ligne caserver, faites fonctionner toutes les routes, puis commentez-la et supprimez acme.json afin que les certificats de production soient demandés à nouveau. L’environnement de production de Let’s Encrypt autorise cinq certificats dupliqués par semaine pour un ensemble identique de noms d’hôte et limite les validations échouées à répétition pour un même nom. Le staging délivre des certificats non approuvés : votre navigateur affiche un avertissement. Cet avertissement confirme que la configuration fonctionne, avec des limites beaucoup moins strictes.

Le tableau de bord est une interface de contrôle, pas une démonstration

La plupart des guides de démarrage rapide définissent --api.insecure=true, ce qui expose le tableau de bord sur le port 8080 sans authentification. Sur un serveur doté d’une adresse IP publique, cette configuration révèle votre topologie de routage, vos noms d’hôte, les noms des middlewares et les ports des backends à toute personne qui effectue un scan.

Les labels du service traefik ci-dessus sont l’alternative : le tableau de bord est routé comme n’importe quelle autre application, sur un véritable nom d’hôte, via TLS et derrière basicauth. service=api@internal relie le routeur à l’API intégrée de Traefik. Vous pouvez renforcer cette protection en enchaînant une liste d’autorisation IP, appliquée de gauche à droite. Si l’adresse de votre bureau est dynamique, définissez la plage sur le sous-réseau distribué par un VPN WireGuard que vous hébergez vous-même sur le même VPS et accédez au tableau de bord uniquement par le tunnel :

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

Un mot de passe basicauth partagé n’est plus défendable dès que cinq applications doivent avoir leurs propres comptes. Le même emplacement de middleware peut recevoir un forwardauth qui délègue la décision à Authentik, un serveur d’authentification unique auto-hébergé. Le tableau de bord et toutes les routes voisines sont ainsi protégés par une seule connexion, que vous pouvez révoquer depuis un emplacement central.

Le socket Docker est root

/var/run/docker.sock est une API qui peut créer un conteneur montant / depuis l’hôte. Y accéder revient à disposer des privilèges root sur la machine. Traefik en a besoin pour lire les labels.

Conservez :ro sur le montage, mais précisez ce que cela apporte : le fichier socket devient accessible en lecture seule. Cela n’empêche pas les requêtes POST adressées à l’API Docker de transiter par ce socket. La véritable mesure d’atténuation consiste à ne jamais donner le socket à Traefik et à intercaler un proxy de filtrage :

  dockerproxy:
    image: tecnativa/docker-socket-proxy   # pin the current tag
    restart: unless-stopped
    environment:
      CONTAINERS: 1
      NETWORKS: 1
      POST: 0
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
    networks:
      - proxy

Supprimez le volume du socket de Traefik et indiquez au provider d’utiliser le proxy :

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik conserve l’accès en lecture aux conteneurs et aux réseaux, mais ne peut plus rien créer.

Pare-feu, ports et la règle que tout le monde comprend mal

Deux ports ouverts, plus SSH :

sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

Les ports publiés par Docker contournent ufw. Docker ajoute ses propres règles iptables. Elles sont évaluées avant les chaînes de ufw. Un conteneur démarré avec ports: ["3000:3000"] reste donc accessible depuis Internet, même si une règle ufw deny se trouve juste devant. La protection repose sur la structure, pas sur la configuration du pare-feu : publiez les ports uniquement depuis Traefik et donnez à tous les autres conteneurs uniquement networks: [proxy]. Si un service doit réellement être accessible depuis l’hôte, liez-le à l’interface loopback avec "127.0.0.1:3000:3000".

Résolution des problèmes : erreurs que vous rencontrerez réellement

404 page not found, servie par Traefik. Aucun router ne correspond à la requête. Par ordre de probabilité : le conteneur ne contient pas traefik.enable=true (avec exposedByDefault=false défini) ; la règle Host() ne correspond pas au nom saisi ; le nom du router indiqué dans un label diffère de celui indiqué dans un autre (routers.gitea.rule et routers.gitea.entrypoints doivent être le même mot) ; ou le hostname est placé entre guillemets au lieu d’être placé entre backticks. Traefik v3 exige des backticks dans les matchers.

502 Bad Gateway. Un router correspond, mais le backend est inaccessible. Dans presque tous les cas, le conteneur n’est pas connecté au réseau proxy ; vérifiez docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. L’autre possibilité est un loadbalancer.server.port incorrect : vous avez indiqué un port publié ou l’application écoute sur un autre port. Le journal indique la tentative concernée : dial tcp 172.18.0.5:8080: connect: connection refused.

Le navigateur affiche un avertissement et le certificat est délivré à TRAEFIK DEFAULT CERT. Aucun certificat n’existe pour ce hostname et Traefik a servi son certificat autosigné par défaut. Consultez les lignes ACME :

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

Le DNS ne pointe pas encore vers le serveur. Corrigez l’enregistrement, attendez l’expiration du TTL, puis redémarrez Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... lors du challenge HTTP : le port 80 n’atteint pas Traefik depuis l’extérieur. Il s’agit généralement d’un firewall géré par le fournisseur devant le VPS, et non d’ufw.

Les certificats ne sont jamais délivrés et votre DNS est hébergé chez Cloudflare, avec le nuage orange activé. Cloudflare termine TLS sur son edge et TLS-ALPN-01 ne peut pas fonctionner au travers de celui-ci. Passez l’enregistrement en mode DNS-only pendant la délivrance, ou utilisez le challenge DNS-01 avec un token d’API. DNS-01 est également le seul challenge qui permet de délivrer des certificats wildcard.

Boucle de redirection. Un composant placé devant Traefik termine déjà TLS et transmet du trafic en clair vers :80 ; la redirection de l’entrypoint le renvoie vers HTTPS. Supprimez l’une des deux redirections.

Maintenir le service actif

L’unité Docker doit être activée au démarrage (systemctl is-enabled docker), et restart: unless-stopped relance la stack après un redémarrage. Pour un contrôle explicite, une petite unité systemd qui exécute docker compose -f /srv/edge/compose.yml up -d avec RemainAfterExit=yes vous donne systemctl status edge et le contrôle de l’ordre de démarrage.

Épinglez le tag Traefik (traefik:v3.5, jamais latest). La mise à niveau de v2 vers v3 a modifié la syntaxe des règles et les noms des providers. Un latest sans supervision rechargera sans problème une configuration qu’il ne comprend plus. Effectuez la mise à niveau délibérément : lisez les notes de migration, augmentez le tag, docker compose up -d traefik, puis surveillez le journal. Si vous utilisez encore un tag v2, le guide de migration de Traefik v2 vers v3 détaille chaque renommage, le mode de compatibilité et une procédure de rollback qui conserve vos certificats.

Sauvegardez ./letsencrypt et le volume de données de chaque application. Traefik ne conserve aucun autre état que vous ne pourriez reconstruire à partir du fichier Compose.

Ce qui casse à grande échelle

La première limite n’est pas le débit, mais le serveur unique : un Traefik sur un seul VPS constitue un point de défaillance unique pour cinq applications, et acme.json utilise un fichier plat comme stockage ; deux instances de Traefik qui l’écrivent le corrompront. Pour répartir la charge, il faut déplacer le stockage des certificats hors d’un fichier ou terminer TLS ailleurs.

Le deuxième problème concerne les connexions longue durée. Les Server-Sent Events, les téléchargements volumineux et les clients lents se heurtent aux délais d’expiration de réponse de l’entrypoint ; --entryPoints.websecure.transport.respondingTimeouts.readTimeout, ainsi que ses équivalents writeTimeout et idleTimeout, permettent de les régler. Les WebSockets passent sans configuration supplémentaire.

Le troisième problème est l’espace disque. --accesslog=true écrit sur la sortie standard, et le driver Docker json-file conserve ces données indéfiniment tant qu’aucune limite n’est définie. Définissez logging.options.max-size sur le service Traefik, ou écrivez l’access log dans un fichier et faites-le tourner.

Aucun orchestrateur n’est nécessaire. Il faut en revanche un serveur que vous contrôlez, avec une véritable adresse IP et les ports 80 et 443 ouverts sur Internet. Un petit VPS suffit à couvrir toutes les dépendances.

FAQ

Ai-je encore besoin de certbot si j’utilise Traefik ?

Non. Le resolver ACME de Traefik demande et renouvelle le certificat pour chaque nom d’hôte qu’il route, puis stocke l’ensemble dans acme.json. Certbot reste l’outil adapté lorsque nginx ou un autre serveur assure lui-même la terminaison TLS ; exécuter les deux pour les mêmes noms d’hôte consomme inutilement les limites de débit de Let’s Encrypt.

Pourquoi mon conteneur renvoie-t-il une erreur 404 via Traefik ?

Une erreur 404 renvoyée par Traefik signifie qu’aucun router n’a correspondu à la requête. Vérifiez que le conteneur porte traefik.enable=true (obligatoire dès que exposedByDefault=false est défini), que la valeur de Host() correspond au nom saisi et que le nom du router est identique dans tous les labels de cette application. Traefik v3 attend également des backticks dans le matcher, et non des guillemets.

Quelle est la différence entre une erreur 404 et une erreur 502 ici ?

Une erreur 404 signifie que le routage n’a pas eu lieu ; une erreur 502 signifie qu’un router a correspondu, mais que le backend a refusé la connexion. Les causes habituelles d’une erreur 502 sont un conteneur qui n’est pas connecté au réseau proxy et un loadbalancer.server.port qui pointe vers un port publié au lieu du port sur lequel l’application écoute dans le conteneur. L’access log indique l’adresse exacte appelée par Traefik.

Monter le socket Docker en lecture seule suffit-il ?

L’option :ro rend le fichier socket accessible en lecture seule, mais pas l’API derrière ce fichier ; les requêtes POST continuent d’y transiter, et l’accès à l’API Docker équivaut à un accès root sur l’hôte. La solution plus sûre est le conteneur docker-socket-proxy présenté plus haut. Il n’expose à Traefik que les opérations de lecture sur les conteneurs et les réseaux, et bloque complètement les écritures.

Traefik peut-il émettre un certificat wildcard ?

Oui, mais uniquement avec le challenge DNS-01 et un token d’API pour votre fournisseur DNS. TLS-ALPN-01 et HTTP-01 valident chacun un seul nom d’hôte et ne peuvent pas produire de certificat wildcard. DNS-01 est également nécessaire lorsqu’un CDN comme Cloudflare assure la terminaison TLS devant votre VPS et que les deux autres challenges n’aboutissent jamais.