Profile Floris Robart

Docker Docker est une plateforme open-source de conteneurisation qui permet de créer, déployer et exécuter des applications dans des conteneurs.

Ma définition

Docker est une plateforme de conteneurisation qui permet de créer, déployer et exécuter des applications dans des conteneurs. Un conteneur est une unité légère et portable qui contient tout ce dont une application a besoin pour fonctionner, y compris le code, les bibliothèques, les dépendances et les fichiers de configuration. Docker permet aux développeurs de créer des applications de manière plus rapide et plus efficace, en éliminant les problèmes liés à la compatibilité des environnements de développement et de production. Avec Docker, les développeurs peuvent facilement partager leurs applications avec d'autres personnes, que ce soit pour le développement collaboratif ou pour le déploiement en production.

Docker fonctionne selon deux concepts complémentaires : les images Docker et les conteneurs Docker. Il est important de comprendre la différence entre les deux, car elles sont souvent confondues alors qu'il s'agit de la base du fonctionnement de Docker. Je précise toutefois que Docker est un outil vaste et complexe. Je n'expliquerai pas tout en détail, mais je vais tout de même expliquer, de manière simplifiée, les principes essentiels pour comprendre les images et les conteneurs.

Pour commencer, les images. Une image Docker contient tout le code, les bibliothèques, les dépendances et parfois un système minimal nécessaire pour exécuter l'application. Pour créer l'image d'une application, on choisit une image de base adaptée, puis on ajoute le code, on installe les dépendances et on configure l'environnement nécessaire au bon fonctionnement de l'application. Le but étant d'obtenir une image complète et autonome qui peut être exécutée sur n'importe quelle machine disposant de Docker, sans se soucier des différences d'environnement ou de configuration.

Une fois l'image créée, on peut l'exécuter dans un conteneur Docker. Peu importe que le conteneur soit lancé sur un serveur, un ordinateur Windows, Linux ou macOS, tant que Docker est installé, l'application va se comporter de la même façon. C'est l'un des principaux intérêts de Docker, car cela facilite considérablement le développement et surtout le déploiement.

Le conteneur Docker, quant à lui, est l'instance d'une image en cours d'exécution. Pour exécuter plusieurs instances d'une application, il suffit de lancer plusieurs conteneurs à partir de la même image. Chaque conteneur fonctionne de manière isolée, avec sa propre configuration et ses propres données. Cela rend la gestion d'instances multiples simple et fiable.

En bref, l'image décrit l'état de votre application (fichiers, dépendances, configuration) tandis que le conteneur est l'environnement d'exécution créé à partir de cette image. L'image est la base, le conteneur est l'instance en fonctionnement.

Toutes les données que l'application enregistre (fichiers, etc.) peuvent être écrites dans le conteneur. Vous pouvez arrêter puis redémarrer un conteneur : l'application retrouvera généralement le même état si des volumes persistants sont utilisés. En revanche, si vous supprimez un conteneur sans avoir externalisé les données grâce à des volumes, vous allez perdre ces données. Les volumes sont des fichiers ou des répertoires sur l'hôte qui sont rendus accessibles au conteneur. Ils permettent de stocker les données de manière persistante, même si le conteneur est supprimé ou recréé.

En plus de ses avantages fonctionnels, Docker apporte un niveau d'isolation qui contribue à la sécurité. Un conteneur isole l'application du système d'exploitation hôte. En cas de faille, l'attaque reste confinée au conteneur et n'affecte pas directement l'hôte ou les autres conteneurs. Cette isolation réduit les risques, mais ne doit pas remplacer d'autres bonnes pratiques de sécurité.

Mes éléments de preuve

Docker est, au même titre que Git, un outil incontournable pour de nombreux développeurs. Aujourd'hui, il est difficile d'envisager le déploiement sans Docker. Pour ma part, je l'ai utilisé sur absolument tous mes projets, en développement comme en production, car il est à la fois puissant et pratique pour créer des environnements reproductibles.

Pour être plus précis, j'utilise Docker Compose, un outil qui permet de définir et de gérer des applications multi-conteneurs. Autrement dit, il permet de décrire plusieurs services (base de données, serveur web, frontend, etc.) qui fonctionnent ensemble pour former une application complète. C'est très pratique pour orchestrer un ensemble de conteneurs.

Cela m'est utile car chacune de mes applications repose généralement sur trois conteneurs : frontend, backend et base de données. Grâce à Docker Compose, je peux définir ces conteneurs dans un seul fichier et les lancer simultanément avec une commande, ce qui me fait gagner beaucoup de temps et simplifie la configuration.

La première fois que j'ai réellement utilisé Docker en profondeur, c'était pour Econoris, lors de la mise en production. Auparavant, je n'avais fait que lancer des conteneurs existants. Pour Econoris, j'ai dû créer moi-même les images Docker et les configurer pour la production. Cette expérience m'a permis de comprendre réellement comment Docker fonctionne et comment l'utiliser efficacement pour le développement et le déploiement.

Le premier concept que j'ai appris concerne évidemment les images. J'ai appris à créer des images optimisées en partant d'images de base légères et en n'incluant que les dépendances nécessaires. Le Dockerfile d'Econoris, que nous présentons ci-dessous, illustre ces bonnes pratiques. Il a d'ailleurs servi de base pour d'autres projets comme FlorAccess et Genesis.

Econoris API Dockerfile
# Étape 1 : Build avec TypeScript
FROM node:24-alpine AS builder
WORKDIR /app
# Copier le code source
COPY tsconfig.json ./
COPY package*.json ./
COPY ./src ./src
COPY ./public ./public
# Supprimer les fichiers sensibles s'ils existent
RUN rm -f .env* public/.env* src/.env*
# Installer les dépendances
RUN npm ci
# Build TypeScript → JavaScript
RUN npm run build
# Étape 2 : Image finale
FROM node:24-alpine AS runner
WORKDIR /app
# Copier uniquement les fichiers nécessaires à l'exécution
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
# Ajout d'un utilisateur non-root avec UID/GID fixes
# Utiliser les options longues pour éviter les ambiguïtés entre différentes variantes d'adduser/addgroup
RUN addgroup --gid 1800 --system econorisgroup && adduser --uid 1800 --system --ingroup econorisgroup --disabled-password --gecos "" --no-create-home econorisuser
USER econorisuser
# Par défaut : lance le serveur
CMD ["node", "dist/server.js"]

Cet exemple est relativement simple, mais il montre à quel point Docker est puissant et flexible pour créer des images optimisées et sécurisées.

Le Dockerfile bien fait est souvent séparé en deux étapes. L'étape de build et l'étape de production. La première installe les dépendances, compile le projet, exécute éventuellement des tests et peut contenir des secrets nécessaires au build. La seconde crée l'image finale destinée à la production. Elle ne contient que les fichiers nécessaires à l'exécution et est optimisée pour la sécurité, la performance et la fiabilité.

Cette architecture multi-étapes est puissante car tout ce qui se passe dans l'étape de build est supprimé de l'image finale. On peut donc utiliser des outils de build et inclure des secrets temporaires sans les intégrer à l'image finale. En copiant uniquement l'essentiel dans l'étape de production, on obtient une image plus légère, plus rapide à démarrer et moins exposée aux risques de sécurité.

Vous remarquerez aussi que, dans l'étape de production, j'exécute l'application avec un utilisateur non-root (sans privilèges). Cette bonne pratique réduit les risques de sécurité. Si l'application est compromise, l'attaquant n'obtient pas automatiquement des droits d'administrateur au sein du conteneur, ce qui lui rend plus difficile l'exploitation de la faille.

Econoris App Dockerfile
# Étape 1 : Builder Flutter Web
FROM dart:stable AS builder
WORKDIR /app
# Installer Flutter SDK (branche stable) et précharger le SDK web
ENV CI=true FLUTTER_SUPPRESS_ANALYTICS=true PUB_ENVIRONMENT=docker
RUN apt-get update && apt-get install -y --no-install-recommends git curl unzip xz-utils ca-certificates
RUN git clone --branch stable --depth 1 https://github.com/flutter/flutter.git /flutter
RUN chmod -R a+rX /flutter
RUN /flutter/bin/flutter config --no-analytics
RUN /flutter/bin/flutter --suppress-analytics --version
RUN /flutter/bin/flutter --suppress-analytics precache --web
# Activer Flutter Web et mettre le PATH
ENV PATH="/flutter/bin:/flutter/bin/cache/dart-sdk/bin:${PATH}"
RUN flutter --suppress-analytics config --enable-web
# Copier le code source de l'application dans le repertoire courent (/app)
COPY . .
# Suppression des fichiers inutiles pour le build
RUN rm -rf .env .git .github .vscode .idea .dart_tool build
# Installer les dépendances du projet de manière déterministe
RUN flutter --suppress-analytics pub get
# Build web
RUN flutter --suppress-analytics build web --release
# Étape 2 : Image finale avec NGINX
FROM nginx:alpine
# Remove default nginx website
RUN rm -rf /usr/share/nginx/html/*
# Copier le build Flutter web
COPY --from=builder /app/build/web /usr/share/nginx/html
# Optionnel : config nginx custom
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Ajout d'un utilisateur non-root avec UID/GID fixes
RUN addgroup -g 1800 -S econorisgroup && adduser -u 1800 -S econorisuser -G econorisgroup && mkdir -p /run/nginx /var/cache/nginx /var/run /var/log/nginx && chown -R econorisuser:econorisgroup /var/cache/nginx /var/run /var/log/nginx /run/nginx && chmod 0755 /run/nginx
USER econorisuser

Nous pouvons également voir ci-dessus le Dockerfile de l'application frontend d'Econoris, qui est une application Flutter. Ce Dockerfile permet de créer une image optimisée pour exécuter une application Flutter web en production, en utilisant NGINX comme serveur web pour servir les fichiers statiques de l'application Flutter web.

Vous pouvez voir que le Dockerfile est également séparé en deux étapes. La première étape va copier le code source de l'application, installer les dépendances et build l'application Flutter web pour générer les fichiers statiques qui seront servis par NGINX. La deuxième étape va utiliser une image NGINX légère pour servir les fichiers statiques de l'application Flutter web, et elle est également configurée pour être optimisée pour la production, avec des bonnes pratiques de sécurité, de performance et de fiabilité.

Ce qui est intéressant dans le Dockerfile de l'application frontend d'Econoris, c'est l'utilisation d'images différentes entre l'étape de build et l'étape de production. Par ailleurs, on peut créer autant d'étapes que nécessaire, seule la dernière étape est conservée dans l'image finale.

Un autre point important est l'optimisation du build. Le build peut être long, plusieurs minutes. Docker dispose donc d'un mécanisme de cache efficace. En ajoutant le code source relativement tard dans le Dockerfile, on maximise l'utilisation du cache pour les étapes précédentes qui changent rarement. Ainsi, lors d'un changement de code, Docker n'a pas à réexécuter toutes les instructions et peut éviter de réinstaller Flutter depuis Internet, ce qui permet de gagner plusieurs minutes par build.

Sur l'aspect sécurité, si un attaquant parvenait à compromettre l'application depuis le conteneur, l'impact reste limité au conteneur. De plus, la configuration (par exemple, l'exécution en read-only, l'impossibilité d'augmentation de privilèges) limite la capacité d'escalade des privilèges, ce qui renforce grandement la sécurité en production.

Grâce à Docker et à une configuration soignée, on ajoute plusieurs niveaux d'isolation en complément des sécurités applicatives, ce qui renforce la fiabilité et la sécurité pour la production. C'est en partie pour ces raisons que j'utilise systématiquement Docker pour le déploiement.

Voyons maintenant Docker Compose, qui permet de paramétrer tous les conteneurs d'une application et de les lancer simultanément avec une seule commande. C'est un outil puissant pour gérer les services d'une application, en développement comme en production.

Econoris Docker Compose
services:
# Econoris Database #
econoris-db:
image: florobart/econoris-db:latest
restart: always
env_file:
- ./config/.env.econoris_db
volumes:
- econoris_db_data:/var/lib/postgresql/econoris_db_data
networks:
- econoris-net
security_opt:
- no-new-privileges:true
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${ECONORIS_DB_USER} -d ${ECONORIS_DB_NAME} || exit 1"]
interval: 5s
timeout: 30s
retries: 5
# Econoris Server #
econoris-server:
image: florobart/econoris-server:latest
working_dir: /app
restart: always
env_file:
- ./config/.env.econoris_server
depends_on:
econoris-db:
condition: service_healthy
networks:
- flower-garden-net
- econoris-net
ports:
- ${ECONORIS_SERVER_PORT:-}:80
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
read_only: true
user: "1800:1800"
tmpfs:
- /tmp:exec,nosuid,nodev
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://econoris-server:80/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5
# Econoris App Web #
econoris-app-web:
image: florobart/econoris-app-web:latest
restart: always
env_file:
- ./config/.env.econoris_app
volumes:
- ./config/.env.econoris_app:/usr/share/nginx/html/assets/.env:ro
networks:
- flower-garden-net
ports:
- ${ECONORIS_APP_PORT:-}:80
depends_on:
econoris-server:
condition: service_healthy
floraccess-server:
condition: service_healthy
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
- CHOWN
- SETGID
- SETUID
read_only: true
user: "1800:1800"
tmpfs:
- /tmp:exec,nosuid,nodev,uid=1800,gid=1800
- /run:rw,mode=0755,uid=1800,gid=1800
- /run/nginx:rw,mode=0755,uid=1800,gid=1800
- /var/cache/nginx:rw,uid=1800,gid=1800
- /var/cache/nginx/client_temp:rw,uid=1800,gid=1800
- /var/cache/nginx/proxy_temp:rw,uid=1800,gid=1800
- /var/cache/nginx/fastcgi_temp:rw,uid=1800,gid=1800
- /var/cache/nginx/uwsgi_temp:rw,uid=1800,gid=1800
- /var/cache/nginx/scgi_temp:rw,uid=1800,gid=1800
healthcheck:
test: ["CMD-SHELL", "wget -q --spider http://econoris-app-web:80/ || exit 1"]
interval: 10s
timeout: 5s
retries: 5

Vous pouvez voir ci-dessus le Docker Compose complet d'Econoris. Chaque service est un conteneur (base de données, backend, frontend). Les conteneurs sont configurés pour la production : healthchecks, volumes pour la persistance, réseaux internes pour la communication entre services, et options de sécurité visant à limiter les privilèges.

Je ne vais pas passer en revue l'ensemble du fichier de configuration, mais voici quelques points intéressants, notamment sur la sécurité. J'ai défini des réseaux personnalisés pour limiter la communication aux services nécessaires. Par exemple, un réseau partagé avec le reverse-proxy ("flower-garden-net") permet d'exposer le backend via un nom de domaine, tandis qu'un réseau privé ("econoris-net") permet au backend de communiquer avec la base de données sans exposer cette dernière sur Internet.

Seuls le backend et la base de données sont présents sur le réseau privé "econoris-net", ce qui évite d'exposer la base de données sur Internet et limite l'accès à cette dernière. De plus, certains conteneurs sont configurés en read-only, de sorte qu'un attaquant ayant compromis un conteneur ne puisse pas modifier ses fichiers, ce qui renforce encore la sécurité.

En résumé, grâce à Docker et à une configuration adaptée, mes applications évoluent dans des environnements isolés. Même si une vulnérabilité applicative est exploitée, l'impact reste limité. Il est impossible d'écrire dans le conteneur, d'escalader les privilèges ou d'accéder à l'hôte ou à d'autres conteneurs. Le code source étant généralement public, l'accès aux fichiers ne suffit pas à compromettre l'infrastructure.

L'attaquant pourrait éventuellement accéder au fichier d'environnement contenant des secrets (clés d'accès), mais si la base de données n'est pas exposée et si les accès sont correctement restreints, ces secrets restent difficilement exploitables depuis le conteneur compromis.

Autrement dit, même si l'application fait l'objet d'attaques ciblées et sophistiquées, la configuration et l'isolation limitent considérablement l'accès aux données, ce qui protège les utilisateurs et les données sensibles.

Mon autocritique

J'ai choisi Econoris comme exemple car c'est le premier projet pour lequel j'ai appliqué systématiquement les bonnes pratiques Docker. Cela m'a permis d'acquérir une solide expérience. J'ai appliqué des pratiques similaires sur d'autres projets, qu'il soit personnel ou professionnel, notamment sur Genesis, où j'ai pu présenter les automatisations et mesures de sécurité mises en place sur Econoris à d'autres développeurs plus expérimentés qui travaillent sur Genesis.

Ils ont pu vérifier ce que j'avais réalisé et me donner des conseils pour aller plus loin, notamment sur l'utilisation du cache Docker. Au final, la configuration de mes images et conteneurs s'est révélée de très bonne qualité, hormis l'utilisation du cache, j'avais appliqué des techniques d'optimisation et de sécurité pertinentes, comme la séparation d'une étape de build permettant d'inclure des secrets sans les intégrer à l'image finale.

Grâce à cette expérience, j'ai une bonne maîtrise de Docker. Maintenant, pour continuer de progresser, je compte m'intéresser à Kubernetes. Kubernetes reprend les concepts d'images et de conteneurs, mais ajoute une couche d'orchestration pour gérer les déploiements à grande échelle. Il propose un gestionnaire de secrets, souvent chiffrés, et des fonctionnalités d'auto-scaling pour adapter automatiquement les ressources en fonction de la charge. C'est la prochaine étape logique pour approfondir la conteneurisation.

Attention toutefois, pour aborder Kubernetes il faut maîtriser Docker, car Kubernetes repose sur les mêmes concepts, mais il est plus complexe et nécessite une bonne connaissance préalable des images et conteneurs, ce qui le rend plus facile à apprendre avec Docker.

Mon évolution dans cette compétence

Pour moi, Docker fait partie du trio d'outils parfait avec Git et Linux. Je compte continuer à l'utiliser sur mes projets actuels et futurs, en développement comme en production. Même si j'estime avoir une bonne maîtrise, je resterai attentif aux nouveautés et aux bonnes pratiques pour l'utiliser de façon toujours plus efficace et sûre.

Je compte également me former à Kubernetes, qui représente selon moi l'évolution logique après Docker pour le déploiement à grande échelle, notamment grâce à son gestionnaire de secrets et à ses fonctionnalités d'auto-scaling. Pour cela, je vais suivre des formations en ligne, lire la documentation officielle et expérimenter Kubernetes sur les trois serveurs de test que j'ai mis en place chez moi.