Docker expliqué simplement – ce que vous devez savoir avant de l’utiliser
Il y a quelques mois, je vous proposais un guide étape par étape pour déployer une application AdonisJS sur un VPS. Nous avions configuré le serveur, sécurisé SSH, installé PostgreSQL, Nginx, Volta… puis déployé notre application.
Une vraie aventure manuelle. Efficace, mais pas idéale sur la durée.
Aujourd’hui, nous posons tout ça de côté pour nous plonger dans Docker. L’idée ? Comprendre l’outil, savoir pourquoi il existe, comment il fonctionne et vous initier à son usage en local, avant de l’utiliser pour déployer vos applications en production.
Pourquoi Docker existe ?
Quand nous avons déployé notre application sur le VPS dans l’article précédent, nous avons tout installé directement sur le serveur : Node.js, PostgreSQL, Nginx… Cette méthode fonctionne, mais elle présente plusieurs limites.
D’abord, tout est installé sur le système hôte. Tant que vous n’avez qu’une seule application ou un seul projet, ça peut fonctionner. Mais les problèmes commencent quand vous devez faire cohabiter plusieurs besoins différents sur le même serveur.
Imaginez : un projet A utilise PostgreSQL 14, un projet B doit tourner sur PostgreSQL 16. Ou encore, un client exige Node.js 18, tandis qu’un autre a absolument besoin de Node.js 20.
Sur un VPS classique, sans conteneurisation, c’est quasi impossible :
- Soit vous installez une seule version pour tout le monde, et un projet casse.
- Soit vous essayez de jongler avec des hacks pour installer plusieurs versions, et votre système devient vite fragile.
Et ça ne s’arrête pas là. Si demain vous devez tester une autre stack pour un nouveau client ou migrer un projet progressivement, ça peut devenir ingérable. Les dépendances, les configurations et les binaires finissent par s’empiler sur le serveur, et la moindre erreur peut casser une autre application en place.
C’est souvent là qu’apparaît le fameux problème du « ça marche sur ma machine ». La solution la plus radicale ? Envoyer directement l’ordinateur du développeur en production.
Les solutions historiques : les machines virtuelles
Pour résoudre ces problèmes, on a longtemps utilisé des machines virtuelles (VM). Une VM embarque son propre système d’exploitation complet. On pourrait donc avoir une VM Ubuntu pour un projet, une VM Debian pour un autre et une VM CentOS pour un troisième, toutes tournant sur le même serveur.
Cette approche a permis de résoudre trois problèmes majeurs :
- L’isolation : chaque VM tourne dans son propre environnement, avec son OS, ses bibliothèques et ses dépendances.
- La portabilité : une fois la VM configurée, on peut la déplacer et la relancer sur n’importe quel serveur.
- La standardisation : avec des outils comme Vagrant, il devenait possible de partager une configuration identique pour tous les développeurs.
Le problème ? C’était lourd. Chaque VM embarque un OS complet, ce qui les rend longues à démarrer, très consommatrices en ressources et compliquées à maintenir.
C’est là que Docker arrive.
Docker : une réponse plus moderne
Docker reprend exactement ces trois avantages : isolation, portabilité et standardisation, mais les rend beaucoup plus légers et accessibles
Plutôt que de virtualiser un système complet, Docker isole uniquement votre application et ses dépendances dans un conteneur.
Ces conteneurs partagent le noyau de l’hôte, ce qui les rend beaucoup plus rapides et moins gourmands que les VMs.
Résultat :
- Démarrage quasi instantané.
- Consommation minimale de ressources.
- Création, suppression ou mise à jour d’un environnement complet en quelques secondes.
Et surtout, comme chaque service vit dans son propre conteneur, vous pouvez faire tourner plusieurs versions d’un même outil sur la même machine, sans conflit : PostgreSQL 14 et 16, Node.js 18 et 20, PHP 7.4 et 8.3… tout fonctionne en parallèle, indépendamment.
Docker n’a pas inventé l’isolation, la portabilité ou la standardisation : il les a simplement rendues plus rapides, plus simples et plus accessibles. C’est ce qui explique son adoption massive.
Comment fonctionne Docker ?
Pour comprendre Docker, il faut le voir comme un « emballeur d’applications ». Il prend votre code, vos dépendances, votre configuration et met tout ça dans une image. Cette image peut ensuite être exécutée sous forme de conteneur.
Les images
Une image Docker est comme une recette de cuisine. Elle définit tous les ingrédients nécessaires pour exécuter votre application :
- le code source
- les dépendances
- les bibliothèques système
- la configuration
Ces images sont créées à partir d’un Dockerfile : un simple fichier texte qui décrit les étapes pour construire votre image.
Une fois créée, une image ne change pas : elle est immuable. Si vous voulez mettre à jour votre application, vous devez construire une nouvelle image.
Les conteneurs
Si l’image est la recette, le conteneur est le gâteau !
Un conteneur est une instance en cours d’exécution d’une image. Vous pouvez lancer autant de conteneurs que vous voulez :
- un conteneur PostgreSQL 14 pour le projet A
- un conteneur PostgreSQL 16 pour le projet B
- un conteneur Node.js 18 pour le projet C
- etc.
Chaque conteneur tourne dans son environnement isolé. Il a son propre système de fichiers, ses processus, ses variables d’environnement.
Un point important : un conteneur est éphémère.
Par défaut, si vous le supprimez ou le recréez, toutes les données qu’il contenait disparaissent. C’est parfait pour lancer, tester, arrêter et redémarrer des services rapidement. Mais dès que vous voulez conserver des données (par exemple une base PostgreSQL), il faudra utiliser les volumes Docker pour les stocker de manière persistante. Nous y reviendrons plus loin dans l’article.
Les couches (layers)
Les images Docker ne sont pas de simples fichiers statiques. Elles sont construites en couches (layers), comme certains gâteaux !
Chaque instruction du Dockerfile crée une nouvelle couche. Si une couche n’a pas changé, Docker la réutilise lors du prochain build. Ce mécanisme permet à vos images de se construire plus rapidement et d’occuper moins d’espace disque.
Par exemple, imaginons une image qui installe Node.js, copie les fichiers de dépendances (package.json
, package-lock.json
, etc.), installe ces dépendances, puis copie votre code source.
Si vous changez uniquement votre code, Docker garde en cache la couche d’installation et ne reconstruit que la partie nécessaire.
Les registres
Une fois une image créée, vous pouvez la stocker et la partager via un registre. Le plus connu est Docker Hub, mais il existe aussi des registres privés comme GitHub Packages, GitLab Registry, AWS ECR, etc.
L'idée est simple :
- Vous construisez votre image localement (ou dans votre CI)
- Vous la poussez sur un registre
- Un autre serveur (ou développeur) peut la récupérer et l’exécuter exactement comme chez vous
C’est ce mécanisme qui permet de garantir la reproductibilité : le même code, la même configuration, le même environnement
Les orchestrateurs
Dans la plupart des projets, vous aurez besoin de gérer plusieurs conteneurs : par exemple, une API, une base de données, un frontend et un cache.
C’est là qu’interviennent les orchestrateurs : des outils qui permettent de lancer, arrêter, superviser et mettre à jour plusieurs conteneurs en même temps.
Les principaux orchestrateurs sont :
- Docker Compose : parfait pour le développement local, permet de définir plusieurs services dans un fichier unique et de les lancer en une seule commande
- Docker Swarm : intégré directement à Docker, il est simple à prendre en main et permet de déployer et gérer vos conteneurs sur plusieurs serveurs tout en répartissant automatiquement la charge
- Kubernetes (K8s) : plus avancé et plus complexe, il est conçu pour les environnements à grande échelle. Il bénéficie d’une très grande communauté, d’un écosystème riche et de nombreux automatismes puissants, comme les Operators, qui permettent d’automatiser la gestion d’applications complexes et d’étendre facilement les fonctionnalités de la plateforme.
Premiers pas avec Docker
Maintenant que nous avons compris les concepts fondamentaux, voyons comment utiliser Docker en pratique.
Avant de commencer, il faut installer Docker sur votre machine. Plutôt que de détailler les instructions pour chaque système d’exploitation ici, je vous recommande de suivre le guide officiel.
Docker propose un installateur graphique Docker Desktop pour macOS et Windows, ainsi que des instructions détaillées pour Linux. Une fois installé, vous pouvez vérifier que tout fonctionne avec :
docker --version
docker compose --version
Il existe des alternatives à Docker Desktop comme OrbStack ou Colima, souvent recommandées sur macOS pour de meilleures performances.
Nous allons commencer simplement : lancer un conteneur existant, créer notre propre image, puis découvrir Docker Compose pour gérer plusieurs services.
Lancer son premier conteneur
Commençons par lancer notre premier conteneur avec l’image officielle hello-world :
docker run hello-world
Cette commande demande à Docker de lancer l'image nommée hello-world
. Il va :
- Télécharger l’image depuis le registre par défaut Docker Hub si elle n’est pas déjà disponible en local
- Créer un conteneur à partir de cette image
- Exécuter le script de l’image, en l’occurrence, il affiche un message de confirmation
Hello from Docker!
This message shows that your installation appears to be working correctly.
…
Félicitations, vous venez de lancer votre premier conteneur !
Lancer un service réel (PostgreSQL)
Lancer un simple conteneur "Hello World", c'est bien pour tester Docker, mais voyons maintenant un exemple plus concret :
docker run --name my-database \
-e POSTGRES_PASSWORD=app \
-p 5432:5432 \
-d postgres:latest
Cette commande est déjà un peu plus complexe que notre exemple précédent. Décortiquons-la :
--name
: donne un nom au conteneur, pour le retrouver facilement par la suite-e
: définit une variable d’environnement, iciPOSTGRES_PASSWORD
, qui permet de définir le mot de passe de l’utilisateurpostgres
par défaut-p
: expose un port à l’intérieur du conteneur sur la machine hôte. Ici, le port5432
(celui par défaut de PostgreSQL) est exposé sur le même port localement-d
: exécute le conteneur en arrière-plan (detached mode)postgres:latest
: c’est le nom de l’image à lancer
Une fois exécutée, vous pouvez vérifier que votre conteneur est bien démarré avec :
docker ps
Pour l’arrêter, utilisez le nom donné plus haut :
docker stop my-database
Et pour le supprimer :
docker rm my-database
Persistance des données avec les volumes
Rappelez-vous : un conteneur est éphémère.
Par défaut, si vous supprimez un conteneur, toutes les données qu’il contient disparaissent. Pour une base de données, ce n’est évidemment pas acceptable.
C’est là qu’interviennent les volumes Docker.
Un volume permet de stocker les données en dehors du conteneur pour qu’elles soient conservées, même si le conteneur est supprimé ou recréé.
Pour PostgreSQL, on peut monter un volume comme ceci :
docker run --name my-database \
-e POSTGRES_PASSWORD=app \
-p 5432:5432 \
-v my-database-data:/var/lib/postgresql/data \
-d postgres:latest
L’option -v my-database-data:/var/lib/postgresql/data
crée un volume nommé my-database-data
, monté dans le dossier /var/lib/postgresql/data
(là où PostgreSQL stocke ses données).
Vous pouvez lister les volumes existants avec :
docker volume ls
Même si vous supprimez le conteneur :
docker rm -f my-database
… le volume my-database-data
reste intact. Vous pouvez relancer un nouveau conteneur PostgreSQL en pointant sur ce même volume, et vos données seront toujours disponibles.
Construire votre propre image Docker
Jusqu’ici, nous avons lancé des images existantes depuis Docker Hub. Mais dans la majorité des cas, vous aurez besoin de construire votre propre image, par exemple pour votre application Node.js.
Voici un exemple de Dockerfile
minimal pour une application Node.js :
# Utiliser l'image officielle Node.js
FROM node:lts-bookworm-slim
# Définir le dossier de travail
WORKDIR /app
# Copier uniquement les fichiers de dépendances
COPY package.json package-lock.json* ./
# Installer les dépendances
RUN npm ci
# Copier le reste du code
COPY . .
# Exposer le port 3000
EXPOSE 3000
# Lancer l'application
CMD ["npm", "start"]
Construisons maintenant l’image :
docker build -t my-application .
Puis lançons-la avec :
docker run --name my-application \
-p 3000:3000
my-application
Votre application est maintenant accessible sur http://localhost:3000.
Cet exemple suppose que vous avez déjà une application Node.js existante dans le dossier courant.
Optimiser la taille des images
Lorsque vous construisez une image Docker, l’un des objectifs principaux est de la garder aussi légère que possible.
Une image plus petite :
- se télécharge plus vite,
- se déploie plus rapidement,
- embarque moins de dépendances inutiles, ce qui améliore également la sécurité.
Aller plus loin : le multi-stage build
Notre Dockerfile
minimal fonctionne, mais il n’est pas optimal pour la production : il embarque toutes les dépendances et tous les fichiers source, même ceux dont l’application n’a pas besoin pour tourner.
Le multi-stage build permet de séparer la phase de construction et la phase d’exécution.
L’image finale ne contient que ce qui est nécessaire pour exécuter l’application, ce qui la rend plus légère
Voici un exemple de Dockerfile
tiré de la documentation officielle d’AdonisJS.
FROM node:lts-bookworm-slim AS base
# Étape 1 : Installation de toutes les dépendances (prod + dev)
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Étape 2 : Installation uniquement des dépendances de production
FROM base AS production-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Étape 3 : Construction de l'application
FROM base AS build
WORKDIR /app
COPY --from=deps /app/node_modules /app/node_modules
COPY . .
RUN node ace build
# Étape 4 : Image finale de production
FROM base
ENV NODE_ENV=production
WORKDIR /app
COPY --from=production-deps /app/node_modules /app/node_modules
COPY --from=build /app/build /app
EXPOSE 8080
CMD ["node", "./bin/server.js"]
Dans cet exemple, le Dockerfile
utilise quatre étapes :
- Stage
deps
: installe toutes les dépendances (prod + dev), nécessaires pour construire l’application. - Stage
production-deps
: installe uniquement les dépendances de production pour obtenir une image finale plus légère. - Stage
build
: copie les dépendances complètes, ajoute le code source et exécutenode ace build
pour compiler l’application dans/app/build
. - Stage final : définit
NODE_ENV=production
, copie uniquement les fichiers compilés et les dépendances nécessaires, expose le port8080
et lance le serveur.
Grâce à cette approche, l’image finale ne contient que le strict nécessaire pour exécuter l’application.
Les étapes 2 et 3 peuvent être exécutées en parallèle par Docker, car elles ne dépendent pas l’une de l’autre. Cela peut réduire le temps total de construction de l’image.
Exporter plusieurs images depuis un seul Dockerfile
Dans certains projets, vous pouvez avoir besoin de générer plusieurs images Docker à partir d’un seul Dockerfile.
Un exemple classique : un frontend Nuxt et une API AdonisJS dans un même monorepo
Plutôt que de maintenir deux fichiers séparés, vous pouvez définir plusieurs targets dans le même Dockerfile.
Ces targets correspondent simplement aux stages définis plus haut avec AS :
FROM base AS api
…
FROM base AS front
…
Pour construire une image spécifique, vous pouvez lancer :
docker build -t my-api --target=api .
docker build -t my-front --target=front .
Le problème de cette méthode, c’est que les builds sont indépendants : si la compilation échoue pour l’un des services, l’autre peut quand même être généré, ce qui peut entraîner des incohérences.
Pour éviter ça, vous pouvez utiliser docker buildx bake
: il permet de construire tous les targets en une seule commande et de rendre le build atomique.
Ainsi, si la compilation échoue, aucune image n’est générée et vous êtes certain que vos services utilisent le même code et les mêmes dépendances.
Orchestrer plusieurs services avec Docker Compose
Pour le moment, nous avons lancé nos conteneurs un par un avec docker run
. C’est suffisant pour tester un service isolé, mais les choses se compliquent dès qu’il faut gérer plusieurs services qui dépendent les uns des autres. Par exemple, une API AdonisJS qui a besoin d'une base de données PostgreSQL.
C’est là qu’intervient Docker Compose : un outil qui permet de définir, configurer et lancer plusieurs conteneurs en une seule commande, grâce à un fichier compose.yml
.
Voici un exemple simple :
services:
api:
build: .
container_name: my-api
ports:
- "3333:3333"
environment:
NODE_ENV: development
depends_on:
- db
db:
image: postgres:latest
container_name: my-database
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:
Dans cet exemple, nous définissons deux services : api
et db
.
api
: construit une image à partir duDockerfile
du projetdb
: utilise l'image officielle PostgreSQL
L’option depends_on
garantit que la base de données démarre avant l’API, et la section volumes permet de stocker les données PostgreSQL en dehors du conteneur, afin qu’elles soient conservées même après suppression.
Pour démarrer tous les services définis dans compose.yml
:
docker compose up -d
Pour les arrêter :
docker compose down
Avec une seule commande, vous pouvez donc gérer tous les services et leurs dépendances
Ajouter de nouveaux services
Si, par exemple, votre projet a besoin de Redis, il suffit d’ajouter un nouveau service dans le fichier compose.yml
:
services:
…
redis:
image: redis:7.2.4-alpine
ports:
- "6379:6379"
…
Votre nouveau service sera automatiquement intégré à l’ensemble.
Les limites de Docker Compose
Docker Compose simplifie énormément la gestion des conteneurs en local, mais il reste limité dans certains cas :
- Il fonctionne sur une seule machine : pas de gestion native de plusieurs serveurs.
- Pas de scaling automatique : vous pouvez démarrer plusieurs instances d’un service, mais la répartition de la charge n’est pas gérée.
- Pas de déploiements blue/green ni de stratégie intégrée pour assurer du zéro-downtime.
- Pas de gestion fine des ressources (CPU, mémoire) comme peuvent le faire des orchestrateurs plus avancés.
Pour la production, il existe des solutions plus puissantes, comme Docker Swarm ou Kubernetes.
Dans le prochain article, nous verrons comment passer de Docker Compose à Docker Swarm pour déployer votre application sur plusieurs serveurs, gérer la répartition de charge et garantir une haute disponibilité, le tout sans downtime.