Un client m'appelle un mardi matin, paniqué. Son application tourne sur un serveur qu'il a monté lui-même il y a deux ans, et depuis la dernière mise à jour de la librairie système, plus rien ne démarre. « Ça marchait hier soir », qu'il me dit. On a passé trois heures au téléphone. À la fin, je lui ai dit la vérité : le problème n'était pas la librairie, c'était qu'il n'avait jamais vraiment déployé — il avait copié des fichiers par FTP en croisant les doigts.
Docker ne résout pas tout. Mais il rend ce genre de soirée beaucoup plus rare. Voici comment je déploie aujourd'hui une application web avec Docker, de la machine locale au serveur de production, avec les étapes que j'ai apprises à la dure.
Points clés à retenir
- Un Dockerfile décrit comment construire votre image ; il doit être court et lisible.
- Le déploiement réel se joue sur le serveur : SSH, image tirée depuis un registre, redémarrage maîtrisé.
- Un reverse proxy (Traefik ou Nginx) gère le HTTPS et les noms de domaine, pas votre application.
- Ne déployez jamais en tant que root. Jamais.
- Le dépannage commence par
docker logsetdocker ps -a, pas par Google.
Déployer une application web avec Docker : ce qui se passe vraiment
Déployer, ce n'est pas « faire tourner l'appli sur ma machine ». C'est la rendre accessible à des inconnus, de manière reproductible, sans que vous soyez connecté en SSH à 23 h pour réparer.
La chaîne est toujours la même, et si un maillon manque, ça casse :
- Écrire un Dockerfile qui construit votre application
- Construire l'image localement et la tester
- Publier cette image dans un registre (Docker Hub, GHCR, registre privé)
- Sur le serveur : tirer l'image et lancer le conteneur
- Placer un reverse proxy devant, avec un certificat HTTPS
- Automatiser tout ça avec une CI/CD
Comment conteneuriser une application avec Docker ?
Vous avez besoin d'un fichier Dockerfile à la racine du projet. Pour une application Node.js, ça ressemble à ceci :
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Trois choses à remarquer. L'utilisateur final n'est pas root (USER node). Les dépendances de dev sont exclues (--omit=dev). Et le COPY . . vient après l'installation des packages — ça permet à Docker de mettre en cache l'étape coûteuse.
Ce cache, franchement, je l'ai sous-estimé pendant longtemps. Sur mon tout premier projet conteneurisé, je rebuildais l'image complète à chaque changement de code : deux minutes et demie à chaque fois. Après avoir réordonné les couches, je suis tombé à 11 secondes. Même image, même résultat.
Un point que personne ne m'avait expliqué au début : ajoutez un .dockerignore. Sinon, votre dossier node_modules local de 400 Mo et vos .env se retrouvent embarqués dans l'image. Pas glorieux.
Construire et publier l'image sans se tromper
Construire et tester en local
Deux commandes suffisent pour valider que l'image fonctionne :
docker build -t monapp:1.0 .— construit l'image avec un tag explicitedocker run -p 3000:3000 monapp:1.0— lance le conteneur sur le port 3000
Ne testez jamais directement en production. Ça paraît évident, mais j'ai déjà vu quelqu'un (moi, donc) pousser une image non testée sur un serveur parce que « c'est juste un petit changement ». Le résultat : une application down pendant 40 minutes un vendredi soir.
Publier une image Docker : le registre
Le registre, c'est le dépôt central où votre image attend. Docker Hub fonctionne pour du public, mais pour un projet sérieux, je préfère le registre GitHub (GHCR) ou un registre privé type GitLab Container Registry. Question de confidentialité, et de vitesse.
Exporter une image Docker vers un serveur sans registre, c'est aussi possible :
docker save monapp:1.0 | gzip > monapp.tar.gz
scp monapp.tar.gz user@serveur:/tmp/
ssh user@serveur "docker load < /tmp/monapp.tar.gz"
C'est ce que je fais quand le client a un réseau fermé, sans accès externe. Moins élégant qu'un docker pull, mais ça sauve la mise.
Le déploiement sur un serveur distant
Sécuriser le serveur avant tout le reste
La grande majorité des tutoriels sautent cette étape. C'est une erreur. Avant même d'installer Docker :
- Créez un utilisateur non-root avec accès sudo
- Désactivez la connexion SSH root et la connexion par mot de passe (clés uniquement)
- Activez un pare-feu (
ufwpar exemple) en n'ouvrant que les ports 22, 80 et 443 - Installez Docker et ajoutez votre utilisateur au groupe
docker
Un serveur sans pare-feu, exposé avec Docker directement à l'écoute sur un port haut, c'est un serveur scanné et trouvé en quelques heures. Docker modifie les règles iptables, ce qui a pour effet pervers de contourner ufw si l'on publie mal les ports. Détail vicieux, mais à connaître :
docker run -p 127.0.0.1:3000:3000 monapp:1.0
En bindant sur 127.0.0.1, l'application n'est accessible que depuis la machine locale — donc uniquement via le reverse proxy.
Reverse proxy et HTTPS
Votre application ne doit jamais gérer elle-même les certificats TLS. C'est le rôle du proxy. Traefik s'occupe des certificats Let's Encrypt automatiquement, Nginx demande un peu plus de configuration, mais les deux fonctionnent. J'ai une préférence marquée pour Traefik dans une stack Docker Compose — je défendrai ce choix bec et ongles — parce qu'il lit directement les labels des conteneurs : pas besoin de recharger sa configuration quand un service est ajouté.
| Outil | Certificats automatiques | Configuration des routes | Complexité |
|---|---|---|---|
| Traefik | Oui | Labels Docker | Moyenne |
| Nginx + Certbot | Via script | Fichiers de conf | Élevée |
| Caddy | Oui | Fichier Caddyfile | Faible |
Automatiser le déploiement avec une CI/CD
À la main, un déploiement prend entre 5 et 10 minutes si tout va bien. Automatisé, il prend 90 secondes. La différence n'est pas le gain de temps : c'est la reproductibilité. Le même chemin, à chaque fois, sans oubli.
Un workflow GitHub Actions minimaliste :
name: deploy
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: docker build -t ghcr.io/user/monapp:${{ github.sha }} .
- run: docker push ghcr.io/user/monapp:${{ github.sha }}
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: deploy
key: ${{ secrets.SSH_KEY }}
script: |
docker pull ghcr.io/user/monapp:${{ github.sha }}
docker stop monapp || true
docker rm monapp || true
docker run -d --name monapp --restart unless-stopped \
-p 127.0.0.1:3000:3000 \
ghcr.io/user/monapp:${{ github.sha }}
« Redémarrage zéro-downtime » ici est un abus de langage : il y a une coupure de quelques secondes. Pour un vrai zéro-downtime, il faut deux conteneurs derrière le proxy et un basculement progressif. J'ai essayé de mettre ça en place sur un projet à faible trafic. Spoiler : c'était totalement disproportionné. Faites-le quand vous avez des utilisateurs qui râlent quand ça tombe, pas avant.
Sauvegarde Docker : ne l'oubliez pas
Les images se reconstruisent. Les volumes, non. Tout ce qui contient des données persistantes — base de données, fichiers uploadés — doit être dans un volume nommé, et ce volume doit être sauvegardé. Un docker volume ls de temps en temps vous rappellera l'existence de ces volumes que vous avez oubliés.
Et si vous voulez une interface graphique pour voir ce qui tourne, Portainer est le choix le plus courant. Je l'installe systématiquement sur les projets dont je ne suis pas le seul administrateur : ça évite qu'on vienne me demander la commande à taper. Sur mes projets solo, honnêtement, un alias dans le shell me suffit.
Comment puis-je héberger une application Web ?
Trois grandes familles. Un serveur privé (VPS) chez un fournisseur, où vous contrôlez tout via Docker : c'est ce que je décris ici, et c'est le plus formateur. Une plateforme managée type PaaS qui construit l'image à partir de votre dépôt et gère l'infrastructure : rapide, mais souvent plus chère et moins flexible. Ou un hébergement mutualisé, qui reste utile pour du contenu statique mais se prête mal à Docker.
Pour un projet qui doit encaisser du trafic variable, un orchestrateur (Kubernetes) prend le relais, mais il faut une équipe pour le tenir. Un serveur unique avec Docker Compose suffit à la grande majorité des applications que je croise.
Dépannage : les erreurs que j'ai rencontrées le plus souvent
Voici ce que je vérifie en premier quand un conteneur ne démarre pas :
- Le conteneur s'arrête immédiatement →
docker logs monapp. C'est neuf fois sur dix un problème de variables d'environnement manquantes ou un port déjà occupé. - Accessible en local, pas depuis l'extérieur → l'application écoute-t-elle sur
0.0.0.0ou seulement surlocalhostdans le conteneur ? Erreur classique, y compris pour moi. - Erreur de permissions sur un volume → l'utilisateur dans le conteneur n'a pas les droits sur le dossier monté. Vérifiez les UID.
- L'image ne se met pas à jour → vous avez réutilisé le tag
latest. Utilisez le hash du commit, toujours.
Et un dernier réflexe qui m'a coûté une soirée entière sur mon premier déploiement Traefik : les labels sont sensibles à la casse et à l'indentation dans le fichier Compose. Une espace en trop, et le conteneur est simplement invisible pour le proxy. Pas d'erreur, pas de message. Rien.
Ce que Docker change vraiment, au fond, ce n'est pas la technique. C'est que le prochain qui reprendra votre serveur n'aura pas à deviner ce qui tourne dessus. Tout est dans un fichier, versionné, relisible. Le mardi matin du client dont je parlais au début aurait ressemblé à un café normal, pas à trois heures de panique.