Techniques Avancées de Dockerfile

DockerBeginner
Pratiquer maintenant

Introduction

Dans cet atelier, nous allons approfondir les techniques liées au Dockerfile en explorant des concepts avancés qui vous aideront à créer des images Docker plus légères, sécurisées et flexibles. Nous aborderons en détail les instructions Dockerfile, les constructions multi-étapes (multi-stage builds) et l'utilité des fichiers .dockerignore. Nous étudierons également le concept crucial de couches (layers) dans les images Docker. À l'issue de cet atelier, vous aurez une compréhension complète de ces méthodes avancées et saurez comment les appliquer à vos propres projets.

Ce laboratoire est conçu pour les débutants, offrant des explications détaillées et anticipant les points de confusion courants. Nous utiliserons l'IDE Web (basé sur VS Code) pour toutes les tâches d'édition, ce qui facilite la création et la modification de fichiers directement depuis votre navigateur.

Comprendre les Instructions Dockerfile et les Couches

Commençons par créer un Dockerfile utilisant diverses instructions. Nous allons construire une image pour une application web Python utilisant Flask, et nous verrons comment chaque instruction contribue aux couches de notre image Docker.

  1. Tout d'abord, créons un nouveau répertoire pour notre projet. Dans le terminal de l'IDE Web, exécutez :
mkdir -p ~/project/advanced-dockerfile && cd ~/project/advanced-dockerfile

Cette commande crée un dossier nommé advanced-dockerfile à l'intérieur du dossier project, puis se déplace dedans.

  1. Créons maintenant notre fichier d'application. Dans l'explorateur de fichiers de l'IDE (généralement à gauche), faites un clic droit sur le dossier advanced-dockerfile et sélectionnez "New File". Nommez ce fichier app.py.

  2. Ouvrez app.py et ajoutez le code Python suivant :

from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return f"Hello from {os.environ.get('ENVIRONMENT', 'unknown')} environment!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Il s'agit d'une application Flask simple qui renvoie un message de bienvenue incluant le nom de l'environnement dans lequel elle s'exécute.

  1. Ensuite, nous devons créer un fichier requirements.txt pour spécifier nos dépendances Python. Créez un nouveau fichier nommé requirements.txt dans le même répertoire et ajoutez le contenu suivant :
Flask==2.0.1
Werkzeug==2.0.1

Nous spécifions ici des versions exactes pour Flask et Werkzeug afin de garantir la compatibilité.

  1. Créons maintenant notre Dockerfile. Créez un nouveau fichier nommé Dockerfile (avec un 'D' majuscule) dans le même répertoire et ajoutez le contenu suivant :
## Use an official Python runtime as the base image
FROM python:3.9-slim

## Set the working directory in the container
WORKDIR /app

## Set an environment variable
ENV ENVIRONMENT=production

## Copy the requirements file into the container
COPY requirements.txt .

## Install the required packages
RUN pip install --no-cache-dir -r requirements.txt

## Copy the application code into the container
COPY app.py .

## Specify the command to run when the container starts
CMD ["python", "app.py"]

## Expose the port the app runs on
EXPOSE 5000

## Add labels for metadata
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo for advanced Dockerfile techniques"

Analysons ces instructions pour comprendre comment elles génèrent les couches de notre image :

  • FROM python:3.9-slim : C'est toujours la première instruction. Elle définit l'image de base. Cela crée la première couche de notre image, incluant l'environnement d'exécution Python.
  • WORKDIR /app : Définit le répertoire de travail pour les instructions suivantes. Cela ne crée pas de nouvelle couche de données, mais modifie le comportement des instructions suivantes.
  • ENV ENVIRONMENT=production : Définit une variable d'environnement. Les variables d'environnement ne créent pas de couches de fichiers, mais sont stockées dans les métadonnées de l'image.
  • COPY requirements.txt . : Copie le fichier des dépendances depuis notre machine hôte vers l'image. Cela crée une nouvelle couche contenant uniquement ce fichier.
  • RUN pip install --no-cache-dir -r requirements.txt : Exécute une commande dans le conteneur pendant la construction. Cela installe nos dépendances Python et crée une nouvelle couche contenant tous les paquets installés.
  • COPY app.py . : Copie le code de notre application dans l'image, créant une autre couche.
  • CMD ["python", "app.py"] : Spécifie la commande à exécuter au démarrage du conteneur. Cela ne crée pas de couche, mais définit la commande par défaut.
  • EXPOSE 5000 : Il s'agit en réalité d'une forme de documentation. Cela indique à Docker que le conteneur écoutera sur ce port, mais ne publie pas réellement le port. Aucune couche n'est créée.
  • LABEL ... : Ajoute des métadonnées à l'image. Comme l'instruction ENV, les labels ne créent pas de couches mais sont stockés dans les métadonnées.

Chaque instruction RUN, COPY et ADD dans un Dockerfile crée une nouvelle couche. Les couches sont un concept fondamental de Docker permettant un stockage et un transfert efficaces des images. Lorsque vous modifiez votre Dockerfile et reconstruisez l'image, Docker réutilise les couches en cache qui n'ont pas changé, accélérant ainsi le processus.

  1. Maintenant que nous comprenons le rôle de notre Dockerfile, construisons l'image. Dans le terminal, exécutez :
docker build -t advanced-flask-app .

Cette commande construit une nouvelle image Docker taguée advanced-flask-app. Le . à la fin indique à Docker de chercher le Dockerfile dans le répertoire courant.

Vous verrez défiler les étapes du processus de construction. Notez comment chaque étape correspond à une instruction et comment Docker mentionne "Using cache" pour les étapes inchangées si vous relancez la commande.

  1. Une fois la construction terminée, nous pouvons lancer un conteneur basé sur notre nouvelle image :
docker run -d -p 5000:5000 --name flask-container advanced-flask-app

Explication des options :

  • -d lance le conteneur en mode détaché (en arrière-plan).
  • -p 5000:5000 mappe le port 5000 de votre hôte sur le port 5000 du conteneur.
  • --name flask-container donne un nom personnalisé à notre conteneur.
  • advanced-flask-app est le nom de l'image utilisée.

Vérifiez que le conteneur est bien actif :

docker ps
  1. Pour tester si l'application fonctionne, utilisez la commande curl :
curl http://localhost:5000

Vous devriez voir le message : "Hello from production environment!"

Si vous préférez, vous pouvez aussi ouvrir un nouvel onglet dans votre navigateur et accéder à http://localhost:5000.

En cas de problème, consultez les journaux du conteneur :

docker logs flask-container

Cela affichera les messages d'erreur ou les sorties de votre application Flask.

Constructions Multi-étapes (Multi-stage Builds)

Maintenant que nous maîtrisons les bases, explorons une technique plus avancée : les builds multi-étapes. Cette méthode permet d'utiliser plusieurs instructions FROM dans un seul Dockerfile. C'est particulièrement utile pour réduire la taille de l'image finale en ne copiant que les fichiers nécessaires d'une étape à l'autre.

Modifions notre Dockerfile pour utiliser cette technique :

  1. Dans l'IDE Web, ouvrez le Dockerfile créé précédemment.
  2. Remplacez tout son contenu par celui-ci :
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

WORKDIR /app

## Copy only the installed packages from the builder stage
COPY --from=builder /root/.local /root/.local
COPY app.py .

ENV PATH=/root/.local/bin:$PATH
ENV ENVIRONMENT=production

CMD ["python", "app.py"]

EXPOSE 5000

LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="1.0"
LABEL description="Flask app demo with multi-stage build"

Analysons ce qui se passe ici :

  1. Nous commençons par une étape nommée builder :

    • Nous utilisons l'image Python 3.9-slim pour rester léger.
    • Nous installons les dépendances avec pip install --user. Cela installe les paquets dans le répertoire personnel de l'utilisateur.
  2. Ensuite, nous définissons l'étape finale :

    • Nous repartons d'une image Python 3.9-slim toute neuve.
    • Nous copions uniquement les paquets installés depuis l'étape builder, spécifiquement depuis /root/.local.
    • Nous copions notre code source.
    • Nous ajoutons le répertoire des binaires locaux au PATH pour que Python trouve les paquets.
    • Nous configurons le reste (ENV, CMD, etc.).

L'avantage majeur est que l'image finale ne contient aucun outil de compilation ni cache généré par pip. Elle ne contient que le strict nécessaire, ce qui réduit sa taille.

  1. Construisons cette nouvelle image :
docker build -t multi-stage-flask-app .
  1. Comparons maintenant la taille de nos deux images :
docker images | grep flask-app
multi-stage-flask-app         latest     7bdd1be2d1fb   10 seconds ago   129MB
advanced-flask-app            latest     c59d6fa303cc   10 minutes ago   136MB

Vous constaterez que multi-stage-flask-app est plus légère que advanced-flask-app.

  1. Lançons un conteneur avec cette image optimisée :
docker run -d -p 5001:5000 --name multi-stage-container multi-stage-flask-app

Notez que nous utilisons le port 5001 pour éviter tout conflit avec le conteneur précédent.

  1. Testez l'application :
curl http://localhost:5001

Le message "Hello from production environment!" doit toujours s'afficher.

  1. Pour mieux comprendre les différences de structure, utilisez la commande docker history :
docker history advanced-flask-app
docker history multi-stage-flask-app

En comparant les sorties, vous remarquerez que le build multi-étapes comporte moins de couches et que certaines sont beaucoup plus petites. C'est une technique indispensable pour les langages compilés ou les applications ayant des processus de construction complexes.

Utilisation du Fichier .dockerignore

Lors de la construction d'une image, Docker envoie tous les fichiers du répertoire courant au démon Docker (le "build context"). Si vous avez des fichiers volumineux inutiles, cela ralentit le processus. Le fichier .dockerignore permet d'exclure certains fichiers ou répertoires.

Créons-en un pour voir son fonctionnement :

  1. Dans l'IDE Web, créez un nouveau fichier dans le dossier advanced-dockerfile nommé .dockerignore.
  2. Ajoutez le contenu suivant :
**/.git
**/.gitignore
**/__pycache__
**/*.pyc
**/*.pyo
**/*.pyd
**/.Python
**/env
**/venv
**/ENV
**/env.bak
**/venv.bak

Signification des motifs :

  • **/.git : Ignore le répertoire .git et tout son contenu, peu importe où il se trouve.
  • **/.gitignore : Ignore les fichiers .gitignore.
  • **/__pycache__ : Ignore les caches Python.
  • **/*.pyc, **/*.pyo, **/*.pyd : Ignore les fichiers Python compilés.
  • **/venv, **/env : Ignore les environnements virtuels locaux.

Le préfixe ** signifie "dans n'importe quel sous-répertoire".

  1. Pour tester l'effet, créons des fichiers factices que nous souhaitons ignorer :
mkdir venv
touch venv/ignore_me.txt
touch .gitignore

Ces fichiers sont courants dans les projets Python mais n'ont pas leur place dans une image Docker de production.

  1. Reconstruisez l'image :
docker build -t ignored-flask-app .
  1. Pour vérifier que ces fichiers n'ont pas été inclus, examinez l'historique :
docker history ignored-flask-app

Vous ne devriez voir aucune étape copiant le répertoire venv ou le fichier .gitignore. Le fichier .dockerignore est essentiel pour garder vos images propres et vos builds rapides.

Instructions Dockerfile Avancées

Dans cette dernière étape, nous allons explorer des instructions qui renforcent la sécurité, la maintenabilité et la robustesse de vos images.

  1. Dans l'IDE Web, ouvrez à nouveau le Dockerfile.
  2. Remplacez le contenu par celui-ci :
## Build stage
FROM python:3.9-slim AS builder

WORKDIR /app

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

## Final stage
FROM python:3.9-slim

## Create a non-root user
RUN useradd -m appuser

## Install curl for healthcheck
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*

WORKDIR /app

## Dynamically determine Python version and site-packages path
RUN PYTHON_VERSION=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")') && \
    SITE_PACKAGES_PATH="/home/appuser/.local/lib/python${PYTHON_VERSION}/site-packages" && \
    mkdir -p "${SITE_PACKAGES_PATH}" && \
    chown -R appuser:appuser /home/appuser/.local

## Copy site-packages and binaries using the variable
COPY --from=builder /root/.local/lib/python3.9/site-packages "${SITE_PACKAGES_PATH}"
COPY --from=builder /root/.local/bin /home/appuser/.local/bin
COPY app.py .

ENV PATH=/home/appuser/.local/bin:$PATH
ENV ENVIRONMENT=production

## Set the user to run the application
USER appuser

## Use ENTRYPOINT with CMD
ENTRYPOINT ["python"]
CMD ["app.py"]

EXPOSE 5000

HEALTHCHECK --interval=30s --timeout=3s \
  CMD curl -f http://localhost:5000/ || exit 1

ARG BUILD_VERSION
LABEL maintainer="Your Name <your.email@example.com>"
LABEL version="${BUILD_VERSION:-1.0}"
LABEL description="Flask app demo with advanced Dockerfile techniques"

Décortiquons les nouveaux concepts :

  • RUN useradd -m appuser : Crée un utilisateur non-root. C'est une règle de sécurité majeure : si votre application est compromise, l'attaquant n'aura pas les privilèges root sur le système.
  • RUN apt-get update && ... && rm -rf /var/lib/apt/lists/* : Installe curl pour le test de santé (healthcheck) et nettoie immédiatement les fichiers temporaires pour ne pas alourdir l'image.
  • USER appuser : Indique à Docker d'exécuter les instructions suivantes (et le processus final) avec cet utilisateur restreint.
  • ENTRYPOINT ["python"] avec CMD ["app.py"] : ENTRYPOINT définit l'exécutable principal, tandis que CMD fournit les arguments par défaut. Cela permet à l'utilisateur de passer facilement d'autres arguments au démarrage s'il le souhaite.
  • HEALTHCHECK : Configure un test périodique pour vérifier que l'application répond toujours. Si la commande curl échoue, Docker marquera le conteneur comme "unhealthy".
  • ARG BUILD_VERSION : Définit une variable disponible uniquement pendant la construction.
  • LABEL version="${BUILD_VERSION:-1.0}" : Utilise l'argument de build pour étiqueter l'image. Si aucune valeur n'est fournie, elle prend "1.0" par défaut.
  1. Construisons cette image en passant une version spécifique :
docker build -t advanced-flask-app-v2 --build-arg BUILD_VERSION=2.0 .
  1. Vérifiez la création de l'image :
docker images | grep advanced-flask-app-v2
  1. Lancez le conteneur :
docker run -d -p 5002:5000 --name advanced-container-v2 advanced-flask-app-v2
  1. Vérifiez son état :
docker ps | grep advanced-container-v2

Si le conteneur ne semble pas actif, utilisez docker ps -a pour voir s'il s'est arrêté prématurément, puis consultez les logs avec docker logs advanced-container-v2.

  1. Après quelques secondes (le temps que le premier test de santé s'exécute), vérifiez le statut de santé :
docker inspect --format='{{.State.Health.Status}}' advanced-container-v2

Vous devriez voir "healthy".

  1. Vérifiez que le label de version a bien été appliqué :
docker inspect -f '{{.Config.Labels.version}}' advanced-container-v2

La sortie devrait être "2.0".

  1. Enfin, testez l'accès final :
curl http://localhost:5002

Ces techniques avancées transforment un simple conteneur en une image prête pour la production, sécurisée et facile à surveiller.

Résumé

Dans cet atelier, nous avons exploré des techniques avancées de Dockerfile pour créer des images plus performantes, sécurisées et maintenables. Nous avons appris à :

  1. Analyser l'impact des instructions sur les couches de l'image pour optimiser le cache et le stockage.
  2. Utiliser les constructions multi-étapes (multi-stage builds) pour séparer l'environnement de compilation de l'environnement d'exécution, réduisant ainsi drastiquement la taille des images.
  3. Configurer un fichier .dockerignore pour exclure les fichiers inutiles du contexte de construction.
  4. Implémenter des instructions avancées comme USER pour la sécurité, ENTRYPOINT pour la flexibilité, HEALTHCHECK pour la surveillance et ARG pour la personnalisation lors du build.

Ces compétences vous permettent désormais de produire des conteneurs de qualité professionnelle, adaptés aux exigences des environnements de production modernes.