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 4 ? 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 ?

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 des 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 pour habitude de mettre la configuration des conteneurs directement dans un dossier /apps à la racine du serveur, mais vous pouvez bien sûr changer l'emplacement selon vos habitudes) :

# /apps/traefik/docker-compose.yml
version: '3'

services:
  traefik:
    image: traefik:1.7
    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 un conteneur à partir de l'image officielle de Traefik. On veut qu'il réponde à la fois pour HTTP et HTTPS parce qu'en 2019, 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). Si vous êtes attentif, vous avez remarqué qu'il y a aussi un port 8080 d'ouvert. C'est normal, Traefik dispose aussi d'une petite interface d'administration pour les conteneurs qu'il gère.

Pour l'explication des volumes utilisés, il y a 3 raisons.
La première : on utilise Traefik avec Docker. Traefik peut aussi être utilisé sans Docker, mais dans notre cas il va se brancher sur le socket interne de l'engine Docker pour être notifié à chaque création / destruction de conteneurs. C'est très 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 on a 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
defaultEntryPoints = ["http", "https"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"
  [entryPoints.https]
  address = ":443"
  [entryPoints.https.tls]

[docker]
domain = "docker.localhost"
watch = true
network = "web"

[acme]
email = "contact@masuperboite.fr"
storage = "acme.json"
caServer = "https://acme-v01.api.letsencrypt.org/directory"
entryPoint = "https"
onHostRule = true
  [acme.httpChallenge]
  entryPoint = "http"

Dans cette configuration tout le trafic HTTP sera 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 [entryPoints.http.redirect] et entryPoint = "https".
Traefik va gérer automatiquement pour nous la communication avec les serveurs de Let's encrypt et le renouvellement des certificats. C'est principalement la ligne onHostRule = true qui s'occupe de ce job.
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 fonctionne avec des labels que l'on positionne dans le fichier docker-compose.yml de notre app pour lui indiquer comment gérer le trafic de ce conteneur.

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 2.0) et qui tient en moins de 20 lignes :

# /apps/blog.silarhi.fr/docker-compose.yml
version: '3'

services:
  app:
    image: ghost:2-alpine
    container_name: blog
    labels:
        - "traefik.frontend.rule=Host:blog.silarhi.fr"
        - "traefik.frontend.port=2368"
    networks:
        - web
    restart: always

networks:
  web:
    external: true

Les informations vraiment importantes ici sont celles des labels. Le premier label traefik.frontend.rule définit que Traefik doit rediriger vers CE conteneur TOUT le trafic qu'il reçoit venant des milliards de visiteurs quotidiens de blog.silarhi.fr. Le second label traefik.frontend.port définit que le trafic sera redirigé vers le port 2368 du conteneur (qui est le port d'écoute par défaut de Ghost).

🤓 « 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, je 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:5.7
    container_name: mysql
    environment:
        MYSQL_ROOT_PASSWORD: supersecret
    labels:
        - "traefik.enable=false"
    volumes:
        - data:/var/lib/mysql
    networks:
        - web
    restart: always
    
  phpmyadmin:
    image: phpmyadmin/phpmyadmin:4.8.5
    container_name: phpmyadmin
    labels:
        - "traefik.frontend.rule=Host:monsupersite.fr;PathPrefix:/phpmyadmin"
    environment:
        MYSQL_ROOT_PASSWORD: supersecret
        PMA_HOST: mysql
    volumes:
        - /sessions
    networks:
        - web
    restart: always

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 5.7. Le label traefik.enable=false est utilisé pour exclure ce conteneur 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 "web".

Modifier les informations de connexion

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.

Vous êtes maintenant un pro de Docker et Traefik ! N'hésitez pas à faire part de vos retours dans la section commentaires.

Pour aller plus loin

D'autres exemples concrets de configuration Traefik
Vous êtes d'attaque ? Créez vos images PHP
La documentation des labels Traefik