Contenu

Astuces sur l'écriture de commandes shell dans les RUN de Dockerfile

Contenu

Les fichiers Dockerfile de Docker permettent la création d’images Docker, qui deviendront par la suite des conteneurs.

Dans ces fichiers il est possible d’indiquer des commandes CLI à exécuter lors de la phase de construction (build) de l’image via l’instruction RUN.

Je vais partager quelques petites astuces concernant l’écriture de commandes pour RUN.

Cela ne les rendra pas plus performantes, mais plus lisibles, y compris dans les historiques git.

L’instruction RUN s’utilise de 2 façons :

  • La forme shell, qui utilise l’interpréteur de commande /bin/sh -c ou cmd /S /C selon la plateforme (ou ce que l’éventuelle instruction SHELL aura configuré) :

    1
    
    RUN <command>
    
  • La forme exec, qui n’utilise pas de shell mais directement l’exécutable :

    1
    
    RUN ["executable", "param1", "param2"]
    

L’écriture de longues commandes sous la forme shell peut être un peu difficile à lire, suivre et/ou maintenir.

1
2
3
RUN apt-get update
RUN apt-get --assume-yes --quiet --no-install-recommends install apache2=2.4.* curl=7.74.* git=1:2.30.*
RUN a2enmod deflate expires headers http2 fcgid mpm_event proxy rewrite ssl status suexec

Surtout qu’il est conseillé, pour limiter la taille des images Docker, d’exécuter plusieurs commandes au sein de la même instruction RUN et de faire du ménage dans les dossiers de cache APT du système, ce qui donne quelque chose comme ça :

1
RUN apt-get update && apt-get [] install apache2=2.4.* [] && a2enmod [] && apt-get [] clean && apt-get [] autoremove && rm -rf /var/lib/apt/lists/*

Je propose d’écrire cette longue commande d’une autre façon.

On peut déjà aérer les commandes de l’instruction en la répartissant sur plusieurs lignes via des \ et des sauts de ligne, comme suit :

1
2
3
4
5
6
RUN apt-get update && \
    apt-get [] install apache2=2.4.* [] && \
    a2enmod [] && \
    apt-get [] clean && \
    apt-get [] autoremove && \
    rm -rf /var/lib/apt/lists/*

On peut aussi aérer à l’intérieur de chaque commande :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
RUN apt-get update && \
    apt-get [] install \
        apache2=2.4.* \
        curl=7.74.* \
        git=1:2.30.* && \
    a2enmod \
        deflate \
        expires \
        headers \
        http2 \
        fcgid \
        mpm_event \
        proxy \
        rewrite \
        ssl \
        status \
        suexec && \
    apt-get --assume-yes --quiet clean && \
    apt-get --assume-yes --quiet autoremove && \
    rm -rf /var/lib/apt/lists/*

C’est déjà plus lisible et plus facile à maintenir au fil du temps.

Mais, par exemple, ajouter un module à la commande a2enmod peut, dans certains cas demander de modifier 2 lignes.

Ici j’ai ajouté le module userdir, et comme je les organise par ordre alphabétique j’ai dû également modifier la ligne de suexec pour retirer le && causant un diff assez verbeux :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@@ -14,7 +14,8 @@ RUN apt-get update && \
         rewrite \
         ssl \
         status \
-        suexec && \
+        suexec \
+        userdir && \
     apt-get --assume-yes --quiet clean && \
     apt-get --assume-yes --quiet autoremove && \
     rm -rf /var/lib/apt/lists/*

Le même problème se poserait si je voulais :

  • rajouter une commande au tout début (avant apt-get update)
  • rajouter un paramètre ou une commande à la toute fin

C’est, d’après moi quelque chose de dérangeant à voir dans l’historique du code d’un projet (e.g. git) : le nombre de lignes modifiée est plus élevé que ce qui est réellement changé, les revues de codes sont impactées et les recherches via git pickaxe sont encombrées de lignes non pertinentes.

Afin de palier ces soucis, j’utilise la forme suivante :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
RUN \
    commande_1 && \
    commande_2 \
        --option_1 \
        --option_2 \
        parametre_1 \
        parametre_2 \
        && \
    commande_3 && \
    commande_4 \
        parametre_1 \
        && \
    true

Explications :

  • Ajouter un \ + saut de ligne dès le début de l’instruction RUN me permet de pouvoir rajouter un commande commande_0 à exécuter en premier sans toucher à la ligne RUN ni à celle de commande_1.

  • Chaque commande est suivie de && (car ici on veut enchaîner toutes les commandes en cas de succès : à adapter selon vos besoins) puis d’un \ + saut de ligne.

    Ainsi ajouter une commande entre 2 commandes ou après la dernière (ici commande_4) peut se faire sans toucher aucune autre ligne.

  • La même logique s’applique aux options et paramètres d’une même commande sans le && bien entendu). Je conseille d’utiliser cette forme multi-ligne pour toute commande pouvant avoir un nombre variable d’options et/ou paramètres de se laisser la possibilité d’en ajouter sans toucher aux autres lignes de la même commande.

  • Le true en dernière ligne est là pour aller avec le && de la ligne précédente.

Au final, cela donne ceci :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
RUN \
    apt-get --assume-yes --quiet update && \
    apt-get --assume-yes --quiet --no-install-recommends install \
        apache2=2.4.* \
        curl=7.74.* \
        git=1:2.30.* \
        && \
    a2enmod \
        deflate \
        expires \
        headers \
        http2 \
        fcgid \
        mpm_event \
        proxy \
        rewrite \
        ssl \
        status \
        suexec \
        && \
    apt-get --assume-yes --quiet clean && \
    apt-get --assume-yes --quiet autoremove && \
    rm -rf /var/lib/apt/lists/* && \
    true

Notez qu’il est aussi possible d’utiliser des chaînes de texte heredocs dans la forme shell avec BuildKit comme l’annonçait cet article de Docker et la documentation depuis que cette fonctionnalité a été ajoutée dans la version stable de la syntaxe syntax=docker/dockerfile:1 (et sans syntaxe spéciale dès Docker v23.0).

Cela permet d’écrire les commandes de RUN sans \ ni && :

1
2
3
4
RUN <<EOT
    apt-get update
    apt-get --assume-yes install vim
EOT