← Retour aux tutoriels

Sécuriser des applis Docker avec Let’s Encrypt et Nginx : HTTPS de bout en bout + dépannage

dockernginxlet’s encryptcertbothttpsreverse proxytlsacmesécurité webdépannage

Sécuriser des applis Docker avec Let’s Encrypt et Nginx : HTTPS de bout en bout + dépannage

Ce tutoriel explique comment exposer plusieurs applications Docker en HTTPS de bout en bout (du navigateur jusqu’au conteneur applicatif si nécessaire), en s’appuyant sur Nginx comme reverse proxy et Let’s Encrypt pour des certificats TLS gratuits et renouvelés automatiquement. Il inclut des commandes réelles, des exemples de configurations, et une section de dépannage orientée terrain.


1) Objectif et architecture

Ce que l’on veut obtenir

Deux modèles TLS possibles

  1. TLS “terminé” au reverse proxy (le plus fréquent)

    • Internet → HTTPS → Nginx (décrypte)
    • Nginx → HTTP → conteneur applicatif
      Avantages : simple, performant, moins de complexité côté applis.
      Inconvénient : le trafic entre Nginx et l’appli n’est pas chiffré (mais reste sur un réseau Docker local).
  2. HTTPS de bout en bout (TLS jusqu’au conteneur)

    • Internet → HTTPS → Nginx
    • Nginx → HTTPS → conteneur applicatif
      Avantages : chiffrement complet même sur le réseau interne.
      Inconvénients : plus complexe (certificats internes, validation, SNI, etc.).
      Remarque : “de bout en bout” peut aussi signifier TLS jusqu’à Nginx + réseau interne isolé. Ici, on couvre les deux : terminaison TLS au proxy et option TLS vers le backend.

2) Prérequis

Côté DNS

Vérification :

dig +short app1.example.com A
dig +short app1.example.com AAAA

Côté serveur

Vérifier l’écoute :

sudo ss -lntp | grep -E ':80|:443' || true

Installer Docker + Compose (exemple Ubuntu) :

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

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  $(. /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 -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"

Déconnectez/reconnectez-vous, puis :

docker version
docker compose version

3) Choix de la stratégie : Nginx + Certbot en conteneurs

Il existe des stacks “tout-en-un” (ex. nginx-proxy + compagnon Let’s Encrypt). Elles sont pratiques mais parfois plus opaques à dépanner.

Ici, on fait volontairement explicite :

Avantage : vous comprenez précisément ce qui se passe, et le dépannage est plus simple.


4) Structure des fichiers

Créez un répertoire de projet :

mkdir -p ~/docker-nginx-letsencrypt
cd ~/docker-nginx-letsencrypt
mkdir -p nginx/conf.d
mkdir -p certbot/www
mkdir -p certbot/conf

On va utiliser :


5) docker compose : Nginx + Certbot + applis

Créez compose.yml :

cat > compose.yml <<'EOF'
services:
  nginx:
    image: nginx:1.27-alpine
    container_name: nginx
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d:ro
      - ./certbot/www:/var/www/certbot:ro
      - ./certbot/conf:/etc/letsencrypt:ro
    networks:
      - web

  # Conteneur "outil" : on l'exécute à la demande (pas besoin qu'il tourne en permanence)
  certbot:
    image: certbot/certbot:latest
    container_name: certbot
    volumes:
      - ./certbot/www:/var/www/certbot
      - ./certbot/conf:/etc/letsencrypt
    networks:
      - web

  # Exemple d'appli 1 (HTTP)
  app1:
    image: nginxdemos/hello:plain-text
    container_name: app1
    restart: unless-stopped
    networks:
      - web

  # Exemple d'appli 2 (HTTP)
  app2:
    image: nginxdemos/hello:plain-text
    container_name: app2
    restart: unless-stopped
    networks:
      - web

networks:
  web:
    name: web
EOF

Démarrez les services :

docker compose up -d
docker compose ps

6) Nginx : configuration HTTP minimale + endpoint ACME

Avant d’avoir des certificats, Nginx doit répondre en HTTP pour que Certbot puisse valider le domaine (challenge HTTP-01).

Créez un vhost HTTP pour app1.example.com et app2.example.com (remplacez par vos domaines) :

cat > nginx/conf.d/00-http-acme.conf <<'EOF'
server {
  listen 80;
  listen [::]:80;

  server_name app1.example.com app2.example.com;

  # Répertoire webroot pour le challenge ACME
  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    try_files $uri =404;
  }

  # Pour l'instant, on ne force pas encore HTTPS tant que les certifs ne sont pas prêts.
  location / {
    return 200 "Nginx HTTP OK (en attente des certificats Let's Encrypt)\n";
    add_header Content-Type text/plain;
  }
}
EOF

Rechargez Nginx :

docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload

Test depuis votre machine :

curl -i http://app1.example.com/

Vous devez obtenir 200 avec le message.


7) Obtenir les certificats Let’s Encrypt (webroot)

Pourquoi “webroot” ?

Certbot va déposer un fichier temporaire sous :

http://app1.example.com/.well-known/acme-challenge/<token>

Let’s Encrypt vérifie qu’il est accessible. Comme Nginx sert ce chemin depuis /var/www/certbot, et que ce volume est partagé, la validation fonctionne.

Commande Certbot

Remplacez email et domaines :

docker compose run --rm certbot certonly \
  --webroot \
  --webroot-path /var/www/certbot \
  -d app1.example.com \
  -d app2.example.com \
  --email admin@example.com \
  --agree-tos \
  --no-eff-email

Si tout se passe bien, vous verrez un message de succès et des fichiers apparaîtront dans ./certbot/conf.

Vérifiez :

ls -la certbot/conf/live/app1.example.com/ || true
ls -la certbot/conf/live/app2.example.com/ || true

Remarque : Let’s Encrypt crée souvent un seul certificat multi-domaines si vous les demandez ensemble. Dans ce cas, vous aurez un répertoire live/app1.example.com/ contenant un cert valable aussi pour app2.example.com. C’est normal.


8) Nginx HTTPS : redirection + reverse proxy vers les conteneurs

Maintenant, on met en place :

Créez nginx/conf.d/10-app1.conf :

cat > nginx/conf.d/10-app1.conf <<'EOF'
# HTTP -> HTTPS (en laissant passer le challenge ACME)
server {
  listen 80;
  listen [::]:80;

  server_name app1.example.com;

  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    try_files $uri =404;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}

# HTTPS
server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name app1.example.com;

  ssl_certificate     /etc/letsencrypt/live/app1.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/app1.example.com/privkey.pem;

  # Bonnes pratiques TLS (base)
  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_prefer_server_ciphers off;

  # En-têtes utiles
  add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;
  add_header X-Content-Type-Options nosniff always;
  add_header X-Frame-Options DENY always;
  add_header Referrer-Policy no-referrer always;

  # Reverse proxy vers le service Docker "app1" (HTTP)
  location / {
    proxy_pass http://app1:80;
    proxy_http_version 1.1;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;

    # Timeouts raisonnables
    proxy_connect_timeout 5s;
    proxy_send_timeout 60s;
    proxy_read_timeout 60s;
  }
}
EOF

Créez nginx/conf.d/20-app2.conf :

cat > nginx/conf.d/20-app2.conf <<'EOF'
server {
  listen 80;
  listen [::]:80;

  server_name app2.example.com;

  location /.well-known/acme-challenge/ {
    root /var/www/certbot;
    try_files $uri =404;
  }

  location / {
    return 301 https://$host$request_uri;
  }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;

  server_name app2.example.com;

  ssl_certificate     /etc/letsencrypt/live/app1.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/app1.example.com/privkey.pem;

  ssl_session_cache shared:SSL:10m;
  ssl_session_timeout 10m;
  ssl_protocols TLSv1.2 TLSv1.3;

  add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

  location / {
    proxy_pass http://app2:80;

    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Real-IP $remote_addr;
  }
}
EOF

Notez que app2 réutilise le certificat multi-domaines stocké sous live/app1.example.com/ (si vous avez demandé les deux domaines ensemble). Si vous avez des répertoires distincts, adaptez les chemins.

Rechargez Nginx :

docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload

Test :

curl -I http://app1.example.com/
curl -I https://app1.example.com/
curl -I https://app2.example.com/

Vous devez voir une redirection 301 en HTTP et une réponse 200/301 côté HTTPS selon l’appli.


9) Renouvellement automatique des certificats

Let’s Encrypt délivre des certificats valables ~90 jours. Il faut renouveler régulièrement (tous les jours ou toutes les semaines) et recharger Nginx après renouvellement.

Test “dry-run”

docker compose run --rm certbot renew --dry-run

Renouvellement réel + reload Nginx

Vous pouvez créer un script :

cat > renew.sh <<'EOF'
#!/usr/bin/env sh
set -eu

docker compose run --rm certbot renew

# Recharger Nginx pour prendre en compte les nouveaux certificats
docker compose exec nginx nginx -s reload
EOF

chmod +x renew.sh

Lancer manuellement :

./renew.sh

Planification via cron (sur l’hôte)

Éditez le crontab :

crontab -e

Ajoutez (tous les jours à 03:17) :

17 3 * * * cd /home/<votre_user>/docker-nginx-letsencrypt && ./renew.sh >> renew.log 2>&1

Remplacez le chemin. Vérifiez que docker est accessible au cron (souvent oui si vous utilisez l’utilisateur correct).


10) HTTPS de bout en bout : Nginx → backend en HTTPS

Dans beaucoup de cas, chiffrer entre Nginx et les conteneurs n’apporte pas grand-chose si tout est sur le même hôte et le même réseau Docker. Mais si vous avez :

alors vous pouvez faire Nginx → backend en HTTPS.

Option A : backend expose déjà HTTPS (certificat interne)

Supposons que app1 écoute en HTTPS sur 443 avec un certificat interne (auto-signé ou CA interne). Vous modifiez :

proxy_pass https://app1:443;

Et vous gérez la validation :

Exemple désactivation (à éviter si possible) :

proxy_ssl_verify off;

Exemple avec CA interne :

  1. Placez votre CA (ex. ca.pem) dans un volume monté dans Nginx, par exemple ./nginx/ca/ca.pem :
mkdir -p nginx/ca
# cp ca.pem nginx/ca/ca.pem

Modifiez compose.yml pour ajouter :

Puis dans le vhost :

proxy_ssl_trusted_certificate /etc/nginx/ca/ca.pem;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
proxy_ssl_server_name on;

proxy_ssl_server_name on; est important si le backend sert plusieurs noms (SNI).

Option B : mTLS (TLS mutuel) entre Nginx et backend

C’est le niveau supérieur : Nginx présente un certificat client, et le backend le vérifie.

proxy_ssl_certificate     /etc/nginx/mtls/client.crt;
proxy_ssl_certificate_key /etc/nginx/mtls/client.key;
proxy_ssl_trusted_certificate /etc/nginx/mtls/ca.pem;
proxy_ssl_verify on;

C’est très robuste, mais demande une PKI interne et une gestion de rotation.


11) Durcissement TLS et sécurité HTTP

HSTS : attention

L’en-tête :

add_header Strict-Transport-Security "max-age=15552000; includeSubDomains" always;

force les navigateurs à utiliser HTTPS pendant 180 jours. C’est bien, mais :

Vous pouvez commencer plus prudemment :

add_header Strict-Transport-Security "max-age=86400" always;

Puis augmenter.

Taille upload et websockets

Pour des applis type API / websockets :

client_max_body_size 50m;

proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";

À mettre dans le location / si nécessaire.

Logs utiles

Par défaut, Nginx loggue dans stdout/stderr du conteneur. Consultez :

docker logs nginx --tail=200

12) Dépannage (cas réels et solutions)

12.1 Erreur Let’s Encrypt : “Timeout during connect” / “Connection refused”

Symptômes : Certbot échoue à valider le domaine.

Causes fréquentes :

Vérifications :

  1. DNS :
dig +short app1.example.com A
  1. Ports ouverts côté serveur :
sudo ss -lntp | grep ':80'
sudo ss -lntp | grep ':443'
  1. Firewall (UFW) :
sudo ufw status verbose || true

Ouvrir si nécessaire :

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
  1. Test HTTP depuis l’extérieur :
curl -i http://app1.example.com/.well-known/acme-challenge/test

Vous devriez obtenir 404 (c’est normal si le fichier n’existe pas), mais pas un timeout.


12.2 Certbot : “Invalid response” sur le challenge

Symptôme : Let’s Encrypt reçoit une page HTML inattendue, ou une redirection, ou un 404 alors que le fichier existe.

Cause : le bloc location /.well-known/acme-challenge/ n’est pas correct, ou un autre vhost matche avant.

Diagnostic :

docker compose exec nginx nginx -T | sed -n '1,200p'
mkdir -p certbot/www/.well-known/acme-challenge
echo "ok" > certbot/www/.well-known/acme-challenge/ping
curl -i http://app1.example.com/.well-known/acme-challenge/ping

Vous devez voir 200 et ok.

Si vous voyez une redirection vers HTTPS, ce n’est pas forcément un problème (Let’s Encrypt suit parfois), mais il est plus fiable de ne pas rediriger ce chemin.


12.3 Nginx ne démarre pas après ajout HTTPS

Symptôme : docker compose up -d puis docker logs nginx montre une erreur de certificat introuvable.

Cause : chemins ssl_certificate incorrects ou certificats non encore générés.

Solution :

Vérifiez les fichiers :

ls -la certbot/conf/live/app1.example.com/fullchain.pem
ls -la certbot/conf/live/app1.example.com/privkey.pem

Test config Nginx :

docker compose exec nginx nginx -t

12.4 Boucle de redirection (ERR_TOO_MANY_REDIRECTS)

Symptôme : navigateur indique trop de redirections.

Cause classique : l’appli derrière Nginx force HTTPS mais ne sait pas qu’elle est déjà derrière un proxy TLS, car X-Forwarded-Proto est absent ou mal interprété.

Solution :

proxy_set_header X-Forwarded-Proto https;

Testez :

curl -I https://app1.example.com/

Regardez la chaîne de Location:.


12.5 Erreur 502 Bad Gateway

Symptôme : Nginx répond 502.

Causes :

Vérifications :

docker compose ps
docker compose logs app1 --tail=200
docker compose exec nginx getent hosts app1
docker compose exec nginx wget -qO- http://app1:80/ | head

Si wget depuis le conteneur Nginx échoue, ce n’est pas un problème TLS mais un problème réseau/port.


12.6 Certificat “valide” mais navigateur affiche encore l’ancien

Cause : Nginx n’a pas été rechargé après renouvellement.

Solution :

docker compose exec nginx nginx -s reload

Vérifiez la date d’expiration :

echo | openssl s_client -servername app1.example.com -connect app1.example.com:443 2>/dev/null | openssl x509 -noout -dates

12.7 Rate limits Let’s Encrypt

Symptôme : vous avez trop tenté et Let’s Encrypt refuse temporairement.

Solution :

docker compose run --rm certbot certonly \
  --webroot --webroot-path /var/www/certbot \
  -d app1.example.com -d app2.example.com \
  --email admin@example.com --agree-tos --no-eff-email \
  --staging

13) Aller plus loin : multi-domaines, wildcard, DNS-01

Le challenge HTTP-01 nécessite le port 80 accessible. Si vous voulez :

il faut utiliser le challenge DNS-01 (création d’un enregistrement TXT). Avec Certbot, cela dépend du provider DNS (plugins). Exemple conceptuel (varie selon fournisseur) :

docker compose run --rm certbot certonly \
  --dns-<provider> \
  --dns-<provider>-credentials /run/secrets/dnscreds \
  -d "*.example.com" -d example.com

C’est puissant mais plus long à mettre en place (gestion des credentials, secrets, etc.).


14) Récapitulatif des commandes clés

docker compose up -d
docker compose run --rm certbot certonly --webroot --webroot-path /var/www/certbot \
  -d app1.example.com -d app2.example.com --email admin@example.com --agree-tos --no-eff-email
docker compose exec nginx nginx -t
docker compose exec nginx nginx -s reload
docker compose run --rm certbot renew
docker compose exec nginx nginx -s reload

Conclusion

Vous avez maintenant une base solide pour exposer des applications Docker via Nginx avec des certificats Let’s Encrypt, avec redirection HTTP→HTTPS, en-têtes de sécurité, renouvellement automatisé et une méthode claire de dépannage. Pour aller vers un “HTTPS de bout en bout” strict, vous pouvez chiffrer aussi la liaison Nginx→backend (avec CA interne ou mTLS), au prix d’une complexité supplémentaire mais avec un gain en isolation cryptographique.

Si vous me donnez vos domaines, le type d’app (Node, Django, Spring, etc.) et si vous souhaitez TLS jusqu’au backend ou seulement jusqu’au proxy, je peux proposer une configuration Nginx adaptée (websockets, gros uploads, HTTP/2, cache, etc.).