devops

Déployer une application avec Docker Compose, Traefik et Let's Encrypt

Guide complet pour déployer une application en production avec Docker Compose, Traefik comme reverse proxy et des certificats SSL automatiques via Let's Encrypt.

10 minutes de lecture

Dans mon article précédent, on a déployé une infrastructure Scaleway avec Terraform. Aujourd'hui, on passe à l'étape suivante : déployer une vraie application avec Docker Compose, Traefik comme reverse proxy, et des certificats SSL automatiques via Let's Encrypt.

Ce qu'on va construire

À la fin de cet article, tu auras :

  • Traefik v3 configuré comme reverse proxy
  • Let's Encrypt pour des certificats SSL automatiques et gratuits
  • Un Docker Socket Proxy pour sécuriser l'accès au Docker socket
  • Des middlewares de sécurité (headers, redirection HTTPS)
  • Une application exemple exposée en HTTPS
  • Le dashboard Traefik sécurisé avec authentification

Prérequis

  • Un serveur avec Docker installé (voir mon article sur Scaleway)
  • Un nom de domaine pointant vers l'IP de ton serveur
  • Les ports 80 et 443 ouverts dans ton firewall

Pourquoi Traefik ?

Traefik est un reverse proxy moderne conçu pour le cloud. Ses avantages :

  • Découverte automatique : Traefik détecte les containers Docker et les expose automatiquement
  • Let's Encrypt intégré : Génération et renouvellement automatique des certificats SSL
  • Configuration dynamique : Pas besoin de redémarrer pour ajouter un nouveau service
  • Dashboard : Interface web pour visualiser tes routes

Architecture des fichiers

1/opt/docker/
2├── traefik/
3│   ├── docker-compose.yml     # Stack Traefik
4│   ├── acme.json              # Certificats Let's Encrypt (généré)
5│   └── dynamic/
6│       └── middlewares.yml    # Middlewares réutilisables
7└── apps/
8    └── whoami/
9        └── docker-compose.yml # Application exemple

On sépare Traefik des applications. Ça permet de redémarrer une app sans toucher au reverse proxy.

Étape 1 : Créer le réseau Docker

Traefik et les applications doivent partager un réseau Docker pour communiquer :

1docker network create proxy

Ce réseau proxy sera utilisé par tous les services exposés publiquement.

Étape 2 : Configurer Traefik

docker-compose.yml

Crée le fichier /opt/docker/traefik/docker-compose.yml :

1services:
2    # Proxy sécurisé pour le socket Docker
3    socket-proxy:
4        image: tecnativa/docker-socket-proxy:0.4
5        restart: unless-stopped
6        environment:
7            CONTAINERS: 1
8            NETWORKS: 1
9            SERVICES: 0
10            TASKS: 0
11            POST: 0
12        volumes:
13            - /var/run/docker.sock:/var/run/docker.sock:ro
14        networks:
15            - socket-proxy
16
17    traefik:
18        image: traefik:v3.6
19        container_name: traefik
20        restart: unless-stopped
21        depends_on:
22            - socket-proxy
23        security_opt:
24            - no-new-privileges:true
25        command:
26            # API et Dashboard
27            - "--api.dashboard=true"
28
29            # Provider Docker via socket proxy
30            - "--providers.docker=true"
31            - "--providers.docker.endpoint=tcp://socket-proxy:2375"
32            - "--providers.docker.exposedbydefault=false"
33            - "--providers.docker.network=proxy"
34
35            # Provider fichiers pour middlewares
36            - "--providers.file.directory=/etc/traefik/dynamic"
37            - "--providers.file.watch=true"
38
39            # Entrypoints
40            - "--entrypoints.web.address=:80"
41            - "--entrypoints.websecure.address=:443"
42
43            # Redirection HTTP -> HTTPS globale
44            - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
45            - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
46
47            # Let's Encrypt
48            - "--certificatesresolvers.letsencrypt.acme.email=ton@email.com"
49            - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
50            - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
51
52            # Logs
53            - "--log.level=INFO"
54        ports:
55            - "80:80"
56            - "443:443"
57        volumes:
58            - ./acme.json:/letsencrypt/acme.json
59            - ./dynamic:/etc/traefik/dynamic:ro
60        networks:
61            - proxy
62            - socket-proxy
63        labels:
64            # Dashboard Traefik
65            - "traefik.enable=true"
66            - "traefik.http.routers.traefik.rule=Host(`traefik.ton-domaine.com`)"
67            - "traefik.http.routers.traefik.entrypoints=websecure"
68            - "traefik.http.routers.traefik.tls=true"
69            - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
70            - "traefik.http.routers.traefik.service=api@internal"
71            - "traefik.http.routers.traefik.middlewares=auth@file,security-headers@file"
72
73networks:
74    proxy:
75        external: true
76    socket-proxy:
77        driver: bridge

Points importants

Docker Socket Proxy : Au lieu de monter directement /var/run/docker.sock dans Traefik (risque de sécurité majeur), on utilise un proxy qui limite l'accès à l'API Docker. Seules les opérations de lecture des containers et réseaux sont autorisées.

exposedbydefault=false : Les containers ne sont exposés que s'ils ont explicitement le label traefik.enable=true. Plus sûr.

Redirection HTTPS globale : Configurée au niveau de l'entrypoint web, toutes les requêtes HTTP sont automatiquement redirigées vers HTTPS.

Préparer le fichier acme.json

Let's Encrypt stocke les certificats dans ce fichier. Il doit avoir des permissions restrictives :

1touch /opt/docker/traefik/acme.json
2chmod 600 /opt/docker/traefik/acme.json

Étape 3 : Configurer les middlewares

Les middlewares sont des composants réutilisables qui modifient les requêtes. Crée le fichier /opt/docker/traefik/dynamic/middlewares.yml :

1http:
2    middlewares:
3        # Headers de sécurité
4        security-headers:
5            headers:
6                browserXssFilter: true
7                contentTypeNosniff: true
8                frameDeny: true
9                stsIncludeSubdomains: true
10                stsPreload: true
11                stsSeconds: 31536000
12                customFrameOptionsValue: "SAMEORIGIN"
13                referrerPolicy: "strict-origin-when-cross-origin"
14
15        # Authentification basique pour le dashboard
16        auth:
17            basicAuth:
18                users:
19                    - "admin:$2y$05$your_hashed_password_here"

Générer le mot de passe hashé

Pour l'authentification du dashboard, génère un hash avec htpasswd :

1# Installer htpasswd si nécessaire
2sudo apt-get install apache2-utils
3
4# Générer le hash (remplace 'admin' et 'motdepasse')
5htpasswd -nB admin

La commande te demande un mot de passe et affiche quelque chose comme :

admin:$2y$05$LqKVSNp...

Copie cette ligne dans le fichier middlewares.yml.

Note : Les $ ne doivent PAS être doublés dans les fichiers YAML externes. Le doublement ($$) n'est nécessaire que dans les labels docker-compose pour éviter l'interprétation comme variable d'environnement.

Étape 4 : Démarrer Traefik

1cd /opt/docker/traefik
2docker compose up -d

Vérifie les logs :

1docker compose logs -f traefik

Tu devrais voir Traefik démarrer et se connecter au socket proxy.

Étape 5 : Déployer une application

Testons avec l'image whoami, un service simple qui affiche des infos sur la requête.

Crée /opt/docker/apps/whoami/docker-compose.yml :

1services:
2    whoami:
3        image: traefik/whoami
4        container_name: whoami
5        restart: unless-stopped
6        networks:
7            - proxy
8        labels:
9            - "traefik.enable=true"
10
11            # Router HTTPS
12            - "traefik.http.routers.whoami.rule=Host(`whoami.ton-domaine.com`)"
13            - "traefik.http.routers.whoami.entrypoints=websecure"
14            - "traefik.http.routers.whoami.tls=true"
15            - "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
16            - "traefik.http.routers.whoami.middlewares=security-headers@file"
17
18networks:
19    proxy:
20        external: true

Démarre l'application :

1cd /opt/docker/apps/whoami
2docker compose up -d

Ce qui se passe

  1. Traefik détecte le nouveau container via le socket proxy
  2. Il lit les labels et crée automatiquement une route
  3. Il demande un certificat à Let's Encrypt via le HTTP-01 challenge
  4. Le service est accessible en HTTPS avec un certificat valide

Visite https://whoami.ton-domaine.com pour vérifier.

Exemple concret : déployer une app web

Voici un exemple plus réaliste avec une application Node.js et sa base de données :

1services:
2    app:
3        image: node:20-alpine
4        container_name: myapp
5        restart: unless-stopped
6        working_dir: /app
7        volumes:
8            - ./src:/app
9        command: node server.js
10        environment:
11            - DATABASE_URL=postgresql://user:password@db:5432/myapp
12        networks:
13            - proxy
14            - internal
15        labels:
16            - "traefik.enable=true"
17            - "traefik.http.routers.myapp.rule=Host(`app.ton-domaine.com`)"
18            - "traefik.http.routers.myapp.entrypoints=websecure"
19            - "traefik.http.routers.myapp.tls=true"
20            - "traefik.http.routers.myapp.tls.certresolver=letsencrypt"
21            - "traefik.http.routers.myapp.middlewares=security-headers@file"
22            - "traefik.http.services.myapp.loadbalancer.server.port=3000"
23
24    db:
25        image: postgres:16-alpine
26        container_name: myapp-db
27        restart: unless-stopped
28        environment:
29            - POSTGRES_USER=user
30            - POSTGRES_PASSWORD=password
31            - POSTGRES_DB=myapp
32        volumes:
33            - db-data:/var/lib/postgresql/data
34        networks:
35            - internal
36
37networks:
38    proxy:
39        external: true
40    internal:
41        driver: bridge
42
43volumes:
44    db-data:

Points clés :

  • L'app est sur deux réseaux : proxy (pour Traefik) et internal (pour la DB)
  • La DB n'est que sur internal, donc pas accessible depuis l'extérieur
  • Le label loadbalancer.server.port indique sur quel port l'app écoute

Environnement de staging Let's Encrypt

Let's Encrypt a des limites de rate strictes. Pendant tes tests, utilise le serveur staging :

1command:
2    # ... autres options ...
3    - "--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory"

Les certificats staging ne sont pas valides (ton navigateur affichera un avertissement), mais tu ne risques pas d'être bloqué par les rate limits.

Une fois tes tests terminés, supprime la ligne --certificatesresolvers.letsencrypt.acme.caserver=... de ton docker-compose.yml. Sans cette option, Traefik utilisera automatiquement le serveur de production Let's Encrypt et générera des certificats valides.

Monitoring et dépannage

Vérifier les routes actives

Accède au dashboard Traefik via https://traefik.ton-domaine.com (protégé par authentification).

Tu peux aussi inspecter les logs :

1# Logs Traefik
2docker compose logs -f traefik
3
4# Logs du socket proxy
5docker compose logs -f socket-proxy

Problèmes courants

Le certificat n'est pas généré :

  • Vérifie que le port 80 est accessible depuis Internet
  • Vérifie les logs : docker compose logs traefik
  • Assure-toi que le DNS pointe vers ton serveur

Le service n'est pas découvert :

  • Vérifie que le container est sur le réseau proxy
  • Vérifie que le label traefik.enable=true est présent
  • Vérifie les logs du socket-proxy

Erreur 502 Bad Gateway :

  • Le container de l'app n'est pas démarré
  • Le port spécifié dans loadbalancer.server.port est incorrect
  • L'app n'écoute pas sur le bon port

Bonnes pratiques

1. Un réseau par contexte

1networks:
2    proxy: # Pour les services exposés
3        external: true
4    internal: # Pour la communication interne
5        driver: bridge

2. Toujours utiliser security_opt

1security_opt:
2    - no-new-privileges:true

Empêche les containers d'acquérir de nouveaux privilèges.

3. Labels explicites

Nomme clairement tes routers et middlewares :

1labels:
2    - "traefik.http.routers.myapp-secure.rule=..." # Nom explicite
3    - "traefik.http.routers.myapp-secure.middlewares=security-headers@file,rate-limit@file"

4. Healthchecks

Ajoute des healthchecks pour que Traefik route seulement vers des containers sains :

1services:
2    app:
3        healthcheck:
4            test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
5            interval: 30s
6            timeout: 10s
7            retries: 3

5. Limiter les ressources

1services:
2    app:
3        deploy:
4            resources:
5                limits:
6                    cpus: "0.5"
7                    memory: 512M

Intégration avec Terraform

Si tu as suivi mon article sur Scaleway avec Terraform, tu peux automatiser l'installation de Traefik directement au provisioning de l'instance.

Cloud-init amélioré

Remplace le cloud-init de l'article précédent par cette version qui prépare l'environnement Traefik :

1#cloud-config
2package_update: true
3package_upgrade: true
4
5packages:
6    - apt-transport-https
7    - ca-certificates
8    - curl
9    - git
10    - htop
11    - docker.io
12    - docker-compose-plugin
13    - apache2-utils
14
15runcmd:
16    # Activer Docker
17    - systemctl enable docker
18    - systemctl start docker
19
20    # Créer la structure
21    - mkdir -p /opt/docker/traefik/dynamic /opt/docker/apps
22    - touch /opt/docker/traefik/acme.json
23    - chmod 600 /opt/docker/traefik/acme.json
24
25    # Créer le réseau Docker
26    - docker network create proxy
27
28write_files:
29    - path: /opt/docker/traefik/docker-compose.yml
30      permissions: "0644"
31      content: |
32          services:
33              socket-proxy:
34                  image: tecnativa/docker-socket-proxy:0.4
35                  restart: unless-stopped
36                  environment:
37                      CONTAINERS: 1
38                      NETWORKS: 1
39                      SERVICES: 0
40                      TASKS: 0
41                      POST: 0
42                  volumes:
43                      - /var/run/docker.sock:/var/run/docker.sock:ro
44                  networks:
45                      - socket-proxy
46
47              traefik:
48                  image: traefik:v3.6
49                  container_name: traefik
50                  restart: unless-stopped
51                  depends_on:
52                      - socket-proxy
53                  security_opt:
54                      - no-new-privileges:true
55                  command:
56                      - "--api.dashboard=true"
57                      - "--providers.docker=true"
58                      - "--providers.docker.endpoint=tcp://socket-proxy:2375"
59                      - "--providers.docker.exposedbydefault=false"
60                      - "--providers.docker.network=proxy"
61                      - "--providers.file.directory=/etc/traefik/dynamic"
62                      - "--providers.file.watch=true"
63                      - "--entrypoints.web.address=:80"
64                      - "--entrypoints.websecure.address=:443"
65                      - "--entrypoints.web.http.redirections.entrypoint.to=websecure"
66                      - "--entrypoints.web.http.redirections.entrypoint.scheme=https"
67                      - "--certificatesresolvers.letsencrypt.acme.email=${email}"
68                      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
69                      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
70                      - "--log.level=INFO"
71                  ports:
72                      - "80:80"
73                      - "443:443"
74                  volumes:
75                      - ./acme.json:/letsencrypt/acme.json
76                      - ./dynamic:/etc/traefik/dynamic:ro
77                  networks:
78                      - proxy
79                      - socket-proxy
80                  labels:
81                      - "traefik.enable=true"
82                      - "traefik.http.routers.traefik.rule=Host(`traefik.${domain}`)"
83                      - "traefik.http.routers.traefik.entrypoints=websecure"
84                      - "traefik.http.routers.traefik.tls=true"
85                      - "traefik.http.routers.traefik.tls.certresolver=letsencrypt"
86                      - "traefik.http.routers.traefik.service=api@internal"
87                      - "traefik.http.routers.traefik.middlewares=auth@file,security-headers@file"
88
89          networks:
90              proxy:
91                  external: true
92              socket-proxy:
93                  driver: bridge
94
95    - path: /opt/docker/traefik/dynamic/middlewares.yml
96      permissions: "0644"
97      content: |
98          http:
99              middlewares:
100                  security-headers:
101                      headers:
102                          browserXssFilter: true
103                          contentTypeNosniff: true
104                          frameDeny: true
105                          stsIncludeSubdomains: true
106                          stsPreload: true
107                          stsSeconds: 31536000
108                          customFrameOptionsValue: "SAMEORIGIN"
109                          referrerPolicy: "strict-origin-when-cross-origin"
110                  auth:
111                      basicAuth:
112                          users:
113                              - "${traefik_auth}"

Variables Terraform

Ajoute ces variables dans ton variables.tf :

1variable "domain" {
2    description = "Nom de domaine principal"
3    type        = string
4}
5
6variable "email" {
7    description = "Email pour Let's Encrypt"
8    type        = string
9}
10
11variable "traefik_password" {
12    description = "Mot de passe pour le dashboard Traefik"
13    type        = string
14    sensitive   = true
15}

Générer le hash du mot de passe

Dans ton main.tf, utilise le provider htpasswd pour générer le hash :

1terraform {
2    required_providers {
3        # ... autres providers ...
4        htpasswd = {
5            source  = "loafoe/htpasswd"
6            version = "~> 1.0"
7        }
8    }
9}
10
11resource "htpasswd_password" "traefik" {
12    password = var.traefik_password
13    salt     = substr(sha256(var.traefik_password), 0, 8)
14}
15
16locals {
17    cloud_init_content = templatefile("${path.module}/templates/cloud-init.yml", {
18        domain       = var.domain
19        email        = var.email
20        traefik_auth = "admin:${htpasswd_password.traefik.bcrypt}"
21    })
22}

Démarrer Traefik automatiquement

Ajoute cette commande à la fin de la section runcmd du cloud-init :

1runcmd:
2    # ... commandes précédentes ...
3
4    # Démarrer Traefik (après un délai pour s'assurer que Docker est prêt)
5    - sleep 10
6    - cd /opt/docker/traefik && docker compose up -d

Résultat

Après terraform apply, ton instance Scaleway démarre avec :

  • Docker et Docker Compose installés
  • La structure de fichiers créée
  • Le réseau proxy configuré
  • Traefik lancé et prêt à recevoir des certificats SSL

Il ne te reste plus qu'à pointer ton DNS vers l'IP de l'instance et déployer tes applications.

TLDR

1# 1. Créer le réseau partagé
2docker network create proxy
3
4# 2. Créer la structure
5mkdir -p /opt/docker/traefik/dynamic /opt/docker/apps
6touch /opt/docker/traefik/acme.json
7chmod 600 /opt/docker/traefik/acme.json
8
9# 3. Créer docker-compose.yml et middlewares.yml
10# (voir contenu ci-dessus)
11
12# 4. Générer le mot de passe pour le dashboard
13htpasswd -nB admin
14
15# 5. Démarrer Traefik
16cd /opt/docker/traefik && docker compose up -d
17
18# 6. Déployer une app
19cd /opt/docker/apps/whoami && docker compose up -d

Pour aller plus loin

Tu as maintenant une stack de production complète : reverse proxy, SSL automatique, et sécurité renforcée. La prochaine étape serait d'ajouter du monitoring avec Prometheus et Grafana, mais ça c'est pour un prochain article.