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 ?
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. À l'heure où j'écris ces lignes, la version de docker-compose est la 1.23.2. L'installation est assez simple selon la documentation de Docker :
curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose
docker-compose --version
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
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
version: '3'
services:
reverse-proxy:
image: traefik:v2.4
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
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 2021, Coronavirus ou pas, 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"
caServer = "https://acme-v01.api.letsencrypt.org/directory"
[certificatesResolvers.default.acme.tlsChallenge]
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
Le plus dur est fait ! Il ne nous reste plus qu'à builder et démarrer notre conteneur :
docker-compose up -d
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 4) et qui tient en moins de 25 lignes :
# /apps/blog.silarhi.fr/docker-compose.yml
version: '3'
services:
app:
image: ghost:4-alpine
container_name: 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
Les informations vraiment importantes ici sont celles des labels
(le reste étant standard).
Le premier label traefik.frontend.enable
définit que Traefik doit manager ce traefik. 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
version: '3'
services:
mysql:
image: mysql:8
command: --default-authentication-plugin=mysql_native_password
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
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.0.
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
# /apps/traefik/docker-compose.yml
version: '3'
services:
reverse-proxy:
image: traefik:v2.4
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:
networks:
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
version: '3'
services:
reverse-proxy:
image: traefik:v2.4
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
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
version: '3'
services:
reverse-proxy:
image: traefik:v2.4
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
On va maintenant appeler le middleware dans notre site (j'ai repris l'exemple du blog) et ajouter la gestion du www :
# /apps/blog.silarhi.fr/docker-compose.yml
version: '3'
services:
app:
image: ghost:4-alpine
container_name: 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`, `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
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
version: '3'
services:
mysql:
image: mysql:8
command: --default-authentication-plugin=mysql_native_password
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
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.