DEV Community

Cover image for Gérez vos sites de production avec Traefik, Docker & Let's Encrypt
Guillaume Sainthillier for SILARHI

Posted on Originally published at blog.silarhi.fr on

Gérez vos sites de production avec Traefik, Docker & Let's Encrypt

Vous avez une vieille app legacy en PHP 5 sur votre serveur de production qui vous empĂȘche de dĂ©ployer votre belle app Symfony 5 ? Vous montez un nouveau serveur de production et vous vous demandez comment gĂ©rer facilement des stacks diffĂ©rentes et l'ajout de nouveaux sites ? Vous voulez faire la fĂȘte comme cet ananas sur la cover ? Docker est peut-ĂȘtre la solution. En le combinant avec Traefik, vous avez une solution simple et rapide pour gĂ©rer vos apps de production et mĂȘme le renouvellement automatique de vos certificats SSL avec Let's Encrypt.

PrĂ©-requis : Je pars du principe que vous avez un minimum de notions avec Docker et docker-compose (au moins des notions de Dockerfile et de routage de ports entre le conteneur et le host). Je pars aussi du principe que vous ĂȘtes quelqu'un de bien et que votre serveur de production est sous Linux.

Traefik, c'est quoi ?

Schéma d'architecture Traefik

Crédits : https://traefik.io/

En fait, Traefik va jouer le rĂŽle de reverse proxy. C'est Ă  dire qu'il va ĂȘtre le rĂ©cepteur frontal de toutes les requĂȘtes HTTP qui vont venir des Internets et va les rediriger vers les bonnes instances de vos conteneurs Docker. Et c'est bien normal de mettre en place un reverse proxy quand vous avez plus d'une app Docker Ă  exposer : vous ne pouvez pas toutes les faire Ă©couter sur le port 80 ou 443. Vous voulez aussi un joli nom de domaine associĂ© Ă  votre instance Docker.

Installation de Traefik

La premiÚre étape est bien sûr d'installer Docker. Une fois chose faite, on va gérer tout le reste avec Docker Compose, désormais un plugin de Docker : la commande devient docker compose, sans tiret. L'installation est assez simple selon la documentation de Docker :

sudo apt-get update
sudo apt-get install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# Add the repository to Apt sources:
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
docker compose version
Enter fullscreen mode Exit fullscreen mode

On va commencer par crĂ©er un network qui sera partagĂ© entre tous les conteneurs de nos applications. Je l'ai appelĂ© "web" parce que j'aime ĂȘtre original mais aussi parce que c'est dur de trouver des noms, comme le rappelle ce brave Phil :

There are only two hard things in Computer Science: cache invalidation and naming things.

-- Phil Karlton

docker network create web
Enter fullscreen mode Exit fullscreen mode

On crée ensuite notre fichier docker-compose.yml (j'ai pris pour habitude de mettre la configuration des conteneurs directement dans un dossier /apps à la racine du serveur de production, mais vous pouvez bien sûr changer l'emplacement selon vos habitudes) :

# /apps/traefik/docker-compose.yml
services:
  reverse-proxy:
    image: traefik:v3
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/etc/traefik/traefik.toml
      - $PWD/acme.json:/acme.json
    restart: always
    networks:
      - web
networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

Les directives vont créer une instance à partir de l'image officielle de Traefik. On veut qu'il réponde à la fois pour HTTP et HTTPS parce qu'en 2025, y'a plus d'excuses pour ne pas proposer par défaut des apps sécurisées.

Le conteneur est intégré au sein du network "web" que l'on vient de créer juste avant (c'est pourquoi on le tag comme external car il est géré en dehors de docker-compose).

đŸ€“ « C'est quoi ce port 8080 d'ouvert ? »

Bien vu. Traefik dispose aussi d'une petite interface d'administration pour les services qu'il gÚre. Nous verrons en fin d'article comment sécuriser ce dashboard.

Pour l'explication des volumes utilisés, il y a 3 raisons.

La premiĂšre : on utilise Traefik avec Docker. Traefik peut Ă©galement fonctionner avec d'autres providers (Kubernetes pour les chauds, Consul, une configuration par fichiers, etc), mais dans notre cas on va directement se brancher sur le socket interne de l'engine Docker pour ĂȘtre notifiĂ© Ă  chaque crĂ©ation / destruction de conteneurs. Ça va ĂȘtre super pratique pour monter un nouveau site en moins de 5 secondes.

La deuxiÚme : il nous faut modifier la configuration par défaut de Traefik pour qu'elle colle parfaitement à nos besoins. C'est pourquoi j'ai créé un mapping pour le fichier traefik.toml, que je vais détailler juste aprÚs.

DerniÚre raison : Let's Encrypt génÚre des "challenges" pour les domaines qu'il a réussi à authentifier. C'est pourquoi il nous faut conserver le résultat de ces challenges en dehors de la portée éphémÚre d'un conteneur Docker.

Configuration de Traefik

Pour gĂ©rer HTTPS une fois pour toutes avec Let's Encrypt, on va modifier lĂ©gĂšrement la configuration de base de Traefik. Ça se traduit par la crĂ©ation d'un fichier traefik.toml :

# /apps/traefik/traefik.toml
[api]
  dashboard = true
  insecure = true

[entryPoints]
  [entryPoints.web]
    address = ":80"
    [entryPoints.web.http]
      [entryPoints.web.http.redirections]
        [entryPoints.web.http.redirections.entryPoint]
          to = "websecure"
          scheme = "https"
          permanent = true

  [entryPoints.websecure]
    address = ":443"
      [entryPoints.websecure.http.tls]
        certResolver = "default"

[providers]
  [providers.docker]
    watch = true
    exposedByDefault = false
    network = "web"

[certificatesResolvers]
  [certificatesResolvers.default]
    [certificatesResolvers.default.acme]
      email = "contact@superman.com"
      storage = "acme.json"
    [certificatesResolvers.default.acme.tlsChallenge]
Enter fullscreen mode Exit fullscreen mode

Attention, ne recopiez pas cette configuration sans d'abord comprendre ces lignes ! Personnellement, je ne suis pas fan du format TOML mais le contenu reste plutĂŽt simple Ă  lire.

Dans ce setup tout le trafic HTTP est automatiquement redirigĂ© vers HTTPS. Si vous avez encore des apps que vous ne pouvez pas simplement passer en HTTPS (apps legacy, url claquĂ©e en dur dans le programme, etc), vous pouvez faire sauter les lignes sous [entryPoints.web.http]. Vous pouvez Ă©galement commenter temporairement la ligne permanent = true le temps de faire vos tests si vous n'ĂȘtes pas trop sĂ»r.e de ce que vous faites !

Traefik va gérer automatiquement pour nous la communication avec les serveurs de Let's encrypt et le renouvellement des certificats. C'est principalement les lignes sous [certificatesResolvers] qui s'occupent de ce job.

La section [providers] indique à Traefik d'aller écouter le socket Docker.

Allez, il nous faut encore créer le fichier acme.json avant de pouvoir admirer le résultat :

touch /apps/traefik/acme.json
chmod 600 /apps/traefik/acme.json
Enter fullscreen mode Exit fullscreen mode

Le plus dur est fait ! Il ne nous reste plus qu'à builder et démarrer notre conteneur :

docker compose up -d
Enter fullscreen mode Exit fullscreen mode

Si tout s'est bien passé, rendez-vous sur votresupersite.fr:8080. Vous tombez alors sur l'interface d'admin de Traefik. L'infrastructure de base est mise en place. YAY.

L'ajout des apps Docker dans Traefik

Traefik utilise avec les labels de Docker que l'on positionne dans le fichier docker-compose.yml de notre app pour lui indiquer comment gérer le trafic de cette instance.

Du concret

Parce qu'un exemple vaut 1000 lignes d'explications, je vais prendre l'exemple de la configuration de ce blog (qui tourne avec Ghost 6) et qui tient en moins de 30 lignes :

# /apps/blog.silarhi.fr/docker-compose.yml
services:
  app:
    image: ghost:6-alpine
    container_name: blog
    environment:
        url: https://blog.silarhi.fr
        database__client: mysql
        database__connection__host: mysql
        database__connection__user: root
        database__connection__password: supermotdepasse
        database__connection__database: blog
    volumes:
        - content:/var/lib/ghost/content
    networks:
        - web
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`)"
        - "traefik.http.routers.blog.entrypoints=websecure"
    restart: always

volumes:
  content:

networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

Les informations vraiment importantes ici sont celles des labels (le reste étant standard).

Depuis Ghost 5, MySQL est obligatoire en production : les variables database__* pointent vers l'instance MySQL mutualisĂ©e que l'on va crĂ©er juste aprĂšs (le service mysql, sur le mĂȘme network web).

Le premier label traefik.enable=true indique à Traefik qu'il doit gérer ce conteneur. Cette ligne est à ajouter si vous avez laissé la configuration exposedByDefault = false dans votre fichier TOML. Dans le cas contraire, vous n'avez pas à vous en occuper.

Le label traefik.http.routers.blog.rule=Host('blog.silarhi.fr') permet de créer un routeur Traefik (que j'ai appelé blog) qui redirige vers CE conteneur TOUT le trafic qu'il reçoit venant des milliards de visiteurs quotidiens de blog.silarhi.fr.

Le dernier label traefik.http.routers.blog.entrypoints=websecure indique que ce conteneur sera disponible sur le port HTTPS (étant donné que Traefik redirige déjà le traffic HTTP vers HTTPS).

đŸ€“ « Et si j'ai une base de donnĂ©es ? »

Facile ! LĂ  encore tout se passe grĂące Ă  Docker. Vous pouvez choisir d'utiliser une instance de base de donnĂ©es par app ou bien de mutualiser l'instance pour toutes vos apps. Personnellement, j'ai fais le choix de mutualiser l'instance de MySQL Ă©tant donnĂ© que j'utilise exclusivement ce SGBD et la mĂȘme version d'une app Ă  l'autre.

# /apps/database/docker-compose.yml
services:
  mysql:
    image: mysql:8
    container_name: mysql
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
    volumes:
        - data:/var/lib/mysql
    networks:
        - web
    restart: always

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: phpmyadmin
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
        PMA_HOST: mysql
        UPLOAD_LIMIT: 128M
    labels:
        - "traefik.enable=true"
        - "traefik.http.routers.phpmyadmin.rule=Host(`pma.monsupersite.fr`)"
        - "traefik.http.routers.phpmyadmin.entrypoints=websecure"
    restart: always
    volumes:
        - /sessions
    networks:
        - web

volumes:
  data:

networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

En prime, je vous ai ajoutĂ© un conteneur phpMyAdmin (mĂȘme si c'est vivement dĂ©conseillĂ© pour la production). Pour la partie MySQL, on utilise lĂ  encore l'image officielle basĂ©e sur la version 8.4.

Il ne faut pas ajouter le label traefik.enable=true dans la configuration MySQL ! Le but étant d'exclure ce conteneur de la gestion de Traefik. En effet, on ne veut pas que MySQL soit accessible depuis l'extérieur, on va donc le laisser tranquillement dans son network local "web".

Modifier les informations de connexion à la base de données

La petite modification à faire dans le code de vos apps est de modifier les informations de connexion à la base. Vous devez remplacer en effet 127.0.0.1 par mysql (ou le nom que vous avez donné à votre service dans le fichier docker-compose.yml de MySQL). Eh oui, les conteneurs Docker sont isolés les uns des autres, par conséquence 127.0.0.1 fait référence au localhost du conteneur et non plus à celui du host.

Aller plus loin avec Traefik

Pour celles et ceux qui veulent pousser un peu l'utilisation de Traefik, on va voir ensemble quelques patterns communs que vous pourrez utiliser et reproduire.

Sécuriser le dashboard Traefik

Avec la configuration utilisée plus haut, on a autorisé l'accÚs publique au dashboard de Traefik sur le port 8080. Voyons un peu comment améliorer tout ça !

On va d'abord commencer par enlever le mode insecure et modifier la configuration :

# /apps/traefik/traefik.toml
[api]
  dashboard = true
- insecure = true
Enter fullscreen mode Exit fullscreen mode
# /apps/traefik/docker-compose.yml 
 services:
   reverse-proxy:
     image: traefik:v3
     container_name: traefik
     ports:
       - "80:80"
       - "443:443"
-      - "8080:8080"
     volumes:
       - /var/run/docker.sock:/var/run/docker.sock
       - $PWD/traefik.toml:/etc/traefik/traefik.toml
       - $PWD/acme.json:/acme.json
     restart: always
+    labels:
+      - "traefik.enable=true"
+      - "traefik.http.routers.api.rule=Host(`monsupersite.fr`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
+      - "traefik.http.routers.api.service=api@internal"
+      - "traefik.http.routers.api.entrypoints=websecure"
     networks:
       - web
 networks:
   web:
     external: true
Enter fullscreen mode Exit fullscreen mode

Relancez le conteneur Traefik (docker compose up -d) et vous devriez pouvoir accéder à https://monsupersite.fr/dashboard/. On progresse, mais le dashboard est toujours accessible publiquement, ce qui n'est pas terrible.

Les middlewares Ă  la rescousse

On va sécuriser l'accÚs au dashboard en utilisant un middleware. Vous avez déjà du entendre ce nom, c'est un pattern qui revient souvent en programmation. Dans notre cas on va placer une logique qui s'execute entre le routeur du dashboard Traefik (api) et le service associé (api@internal).

Il y a plusieurs maniĂšres d'ajouter des middlewares. Pour ma part, je suis partisan de la configuration via les labels de Docker, directement dans le docker-compose.yml de Traefik.

Forcer l'authentification avec auth

# /apps/traefik/docker-compose.yml
services:
  reverse-proxy:
    image: traefik:v3
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/etc/traefik/traefik.toml
      - $PWD/acme.json:/acme.json
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`monsupersite.fr`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.routers.api.entrypoints=websecure"
+     - "traefik.http.routers.api.middlewares=auth"
+     - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$6ydic0em$ZAhWtxDpkgZhpBOGT7ibu."
    networks:
      - web
networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

Avec cette configuration, vous avez protégé l'accÚs au dashboard avec l'équivalent d'un fichier htpasswd. Veillez à bien doubler les $ dans la chaßne de caractÚre du mot de passe !

Rediriger www vers non-www avec redirectregex

Vous l'avez compris, on va Ă  nouveau ajouter un middleware dans le docker-compose.yml de Traefik !

# /apps/traefik/docker-compose.yml
services:
  reverse-proxy:
    image: traefik:v3
    container_name: traefik
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/traefik.toml:/etc/traefik/traefik.toml
      - $PWD/acme.json:/acme.json
    restart: always
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`silarhi.fr`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.middlewares=auth"
      - "traefik.http.middlewares.auth.basicauth.users=admin:$$apr1$$6ydic0em$ZAhWtxDpkgZhpBOGT7ibu."
+     - "traefik.http.middlewares.strip-www.redirectregex.regex=^https?://(www\\.)(.+)"
+     - "traefik.http.middlewares.strip-www.redirectregex.replacement=https://$${2}"
+     - "traefik.http.middlewares.strip-www.redirectregex.permanent=true"
    networks:
      - web
networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

On va maintenant appeler le middleware dans notre site (j'ai repris l'exemple du blog) et ajouter la gestion du www (depuis Traefik v3, une rĂšgle Host ne prend plus qu'un seul domaine : on en combine deux avec ||) :

# /apps/blog.silarhi.fr/docker-compose.yml
services:
  app:
    image: ghost:6-alpine
    container_name: blog
    environment:
        url: https://blog.silarhi.fr
        database__client: mysql
        database__connection__host: mysql
        database__connection__user: root
        database__connection__password: supermotdepasse
        database__connection__database: blog
    volumes:
        - content:/var/lib/ghost/content
    networks:
        - web
    labels:
         - "traefik.enable=true"
-        - "traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`)"
+        - "traefik.http.routers.blog.rule=Host(`blog.silarhi.fr`) || Host(`www.blog.silarhi.fr`)"
         - "traefik.http.routers.blog.entrypoints=websecure"
+        - "traefik.http.routers.blog.middlewares=strip-www"
    restart: always

volumes:
  content:

networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

Gérer des sous-repertoires avec stripprefix

Tout à l'heure, je vous ai montré comment exposer un conteneur phpMyAdmin avec un sous-domaine particulier. On va voir ici comment se passer de la création d'un sous-domaine en le placant plutÎt dans un sous repertoire (en plus de le sécuriser avec une authentification !).

# /apps/database/docker-compose.yml

services:
  mysql:
    image: mysql:8
    container_name: mysql
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
    volumes:
        - data:/var/lib/mysql
    networks:
        - web
    restart: always

  phpmyadmin:
    image: phpmyadmin/phpmyadmin
    container_name: phpmyadmin
    environment:
        MYSQL_ROOT_PASSWORD: supermotdepasse
        PMA_HOST: mysql
+       PMA_ABSOLUTE_URI: "https://monsupersite.fr/_phpmyadmin/"
        UPLOAD_LIMIT: 128M
     labels:
         - "traefik.enable=true"
-        - "traefik.http.routers.phpmyadmin.rule=Host(`pma.monsupersite.fr`)"
+        - "traefik.http.routers.phpmyadmin.rule=Host(`monsupersite.fr`) && PathPrefix(`/_phpmyadmin/`)"
         - "traefik.http.routers.phpmyadmin.entrypoints=websecure"
+        - "traefik.http.routers.phpmyadmin.middlewares=phpmyadmin,auth"
+        - "traefik.http.middlewares.phpmyadmin.stripprefix.prefixes=/_phpmyadmin"
    restart: always
    volumes:
        - /sessions
    networks:
        - web

volumes:
  data:

networks:
  web:
    external: true
Enter fullscreen mode Exit fullscreen mode

Vous ĂȘtes maintenant un pro de Docker et Traefik ! N'hĂ©sitez pas Ă  me faire part de vos retours / succĂšs / Ă©checs dans les commentaires.

Ressources

Top comments (0)