Blog Arolla

Quelques précisions sur le Dockerfile

Précédemment, dans la présentation de Docker et de la technologie des conteneurs, on avait parlé des features sur lesquelles reposent Docker. Le Cgroup et le Namespaces pour l’isolation de processus et le système de fichier COW pour l’optimisation de l’espace disque. Dans cet article, on passera à la loupe le Dockerfile, le fichier de description d’une image et d’un conteneur Docker . Même si le nombre de commandes est réduit, leur subtilité n’apparaît pas à la première utilisation. Le site officiel demeure la meilleure source de documentation. On ne s’attardera que sur les commandes de base - ADD, COPY, CMD et ENTRYPOINT.

Les exemples ont été utilisés avec la version 1.11 de Docker. Des mises à jours seront éventuellement apportées si nécessaire.

Comprendre les layers :

Dans le Dockerfile, chaque commande (ADD, COPY, ENTRYPOINT, CMD, …) fait l’objet d’ajout d’une nouvelle couche (COW) à l’image. Et les couches, moins on en a, mieux c’est pour la taille des images. Sachant qu'en plus, le nombre de couches a une limite (42 auparavant et 127 depuis peu). Même si la contrainte du nombre de lignes (ou couches) est quasiment écartée (si vous avez un Dockerfile avec 127 lignes, c'est qu'il y a un vrai problème !), regrouper les commandes facilite la compréhension du Dockerfile.

Tip : Regrouper les commandes autant que possible

Example 1. Dockerfile (avec 4 couches)
RUN curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz
RUN tar xzf apache-maven-$MAVEN_VERSION-bin.tar.gz  - -C /usr/share
RUN mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven
RUN ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
Example 2. Dockerfile “refactore” en une seule commande
RUN curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \
  && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \
  && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn

On réduit le nombre de couches a une et on a gagne en lisibilité.

Le cache

Toutes les couches d’une image sont mises dans le cache. Quand on lance le build d’une image Docker, il faut d’abord commencer par chercher dans le cache des couches qu’il pourrait réutiliser pour optimiser le temps de construction de l’image en évitant les téléchargements inutiles. De ce fait, en cas de modification, seules les lignes concernées par la modification seront réexecutées.

FROM java:openjdk-8-jdk
MAINTAINER Yakhya DABO
ENV MAVEN_VERSION 3.3.3
ENV M2_HOME /usr/share/maven
ENV PROJECT_DIR /usr/src/app
...

...
8. RUN mkdir -p $PROJECT_DIR
9. COPY config/settings.xml $M2_HOME/conf/
10. RUN curl -fsSL http://archive.apache.org/dist/maven/maven-3/$MAVEN_VERSION/binaries/apache-maven-$MAVEN_VERSION-bin.tar.gz | tar xzf - -C /usr/share \
  && mv /usr/share/apache-maven-$MAVEN_VERSION /usr/share/maven \
  && ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
11. VOLUME $PROJECT_DIR
12. WORKDIR $PROJECT_DIR

Si on apporte une modification sur la ligne 9, le cache invalide toutes ses filles, les lignes 10, 11, … qui seront réexecutées. Les lignes 0, 1, … 8 n'étant pas des filles de 9, elles ne seront pas concernées par les modifications, c'est le cache qui sera utilisé.

Deux astuces pour bien profiter du cache :

  1. Placer les lignes qui changent souvent le plus bas possible (ajout d’un jar par exemple, COPY).
  2. Mettre les opérations coûteuses le plus haut possible dans le Dockerfile afin d'éviter de les réexécuter à chaque modification (téléchargement par exemple, RUN curl).

Commandes ADD et COPY

Ces deux commandes sont facilement sujet à confusion, de par leur nom mais aussi de par leur syntaxe : elles semblent faire la même chose, sauf que dans la pratique, ce n'est pas tout le temps le cas.

 ADD  src dest
 COPY   src dest

src : un répertoire (ou fichier) du host
dest : un répertoire (ou fichier) du conteneur

Une petite précision : dans la commande “docker build”, on spécifie le context du Dockerfile.

 $ docker build -f dockerfileDir contextDir

ou

 $ docker build contextDir  // (si contextDir = buildDir)

src est relatif au contextDir.

dest est soit un nom de fichier (/var/opt/fileName), soit un répertoire (/var/opt/).

COPY se contente tout simplement de prendre un fichier (ou répertoire) du host et de le mettre dans le conteneur.

ADD peut aussi faire la même chose, mais le src peut être une URL. Dans ce cas, docker se charge du téléchargement et de placer le fichier téléchargé dans dest. Si src est un fichier zippé et dest un répertoire docker dezzipe src (ADD file.zip /var/opt/). Un peu confus tout ça …

La documentation de Docker conseille d’utiliser COPY au profit de ADD, sauf pour des cas spécifiques. Mais pour quelqu'un qui a un peu baigné dans la philosophie Unix (faire une et une seule chose) ou le principe SRP, on voit que ADD c’est du legacy, donc inutile. Mieux vaut ne pas l’avoir dans son Dockerfile.

On peut se contenter de COPY pour les opérations simples de copie du host vers le conteneur, et de coupler RUN avec les utilitaires existants tels que tar, unzip, wget, curl, … si on a besoin de zipper ou de télécharger des fichiers.

Commandes CMD et ENTRYPOINT

Dans la pratique, les deux commandes peuvent avoir le même résultat: exécuter le script de démarrage du conteneur. Mais d’après la doc, Entrypoint sert à configurer un container au démarrage, et CMD est utilisé pour définir la commande de démarrage par défaut du conteneur.

Example 3. Utilisation de CMD
FROM maven:3.3.3-jdk-8
WORKDIR projectDir
...
CMD ["mvn clean install”]
$ docker run my_maven_image va exécuter mvn clean install

… et si on veut surcharger cette commande …

$ docker run my_maven_image mvn clean verify
Example 4. Utilisation de ENTRYPOINT

Pour mon conteneur git, j'aurais besoin des paramètres du committeur (user.name et user.email) au moment de lancer le container. Je peux donc utiliser un entrypoint pour fixer ces deux paramètres.

FROM git:2.0
….
COPY entrypoint.sh /var/lib
ENTRYPOINT [“/var/lib/entrypoint.sh”]
$ docker run -e GIT_USER_NAME=username -e GIT_USER_EMAIL=email my_git_image git commit -m “xxxxx”

Le contenu de mon fichier entrypoint.sh :

 #!/bin/bash

 set -e

 git config user.name "$GIT_USER_NAME"
 git config user.email "$GIT_USER_EMAIL"

 exec "$@"

Il est important de noter que ENTRYPOINT utilise CMD comme argument ("$@"). Sa valeur par défaut est /bin/sh -c, qui prend en paramètre une commande. Ce qui fait que quand on ne définit pas notre propre ENTRYPOINT (dans le Dockerfile ou en paramètre a docker run avec –entrypoint), CMD devient la commande a exécuter (éventuellement avec ses paramètres).

Exemple 5. Avec New Relic

J’utilise New Relic en Prod pour le monitoring de la JVM de mon conteneur. Mais je veux aussi avoir le choix de m'en passer quand je n'en ai pas besoin, en Dev par exemple.

  • La solution la plus simple pourrait être d’avoir deux images différentes, une pour la Prod, avec New Relic, et la seconde pour le Dev, sans New Relic. Mais cette option ne respecte pas les principes du Continuous Delivery, “le livrable doit être le même dans tous les envs”.
  • Une deuxième solution, celle que je préfère, sera d’utiliser ENTRYPOINT pour décider de lancer ou non New Relic selon que les variables NEWRELIC_KEY et NEWRELIC_APP_NAME sont spécifiées ou non.

Pour lancer le container en Prod :

 $ docker run -e  NEWRELIC_KEY=XXXXXXXX -e  NEWRELIC_APP_NAME=my_app_name my_service_image

… et en Dev :

 $ docker run -e my_service_image

Dans mon ENTRYPOINT, je peux avoir le script d’initialisation de l'environnement d’exécution pour positionner les paramètres des fichiers de config avec les variables d'environnements passées en paramètres (nom, cle, url, mot de passe, login, …) et CMD pour spécifier la commande à exécuter après l’initialisation de l’environnement.

Dockerfile
….
ENTRYPOINT [“entrypoint.sh”]
CMD […...........]

entrypoint.sh

#!/bin/sh

set -e


if [ -z "$NEWRELIC_KEY" ]; then
        java -Djava.security.egd=file:/dev/./urandom -jar app.jar
else
        if [ -z "$NEWRELIC_APP_NAME" ]; then
                echo >&2 'error: missing required environment variable'
                echo >&2 'error: NEWRELIC_APP_NAME must be set when using New Relic'
                exit 1
        fi

        NEW_RELIC_CONFIG_FILE=$NEW_RELIC_DIR/newrelic.yml
       cp $NEW_RELIC_CONFIG_FILE $NEW_RELIC_CONFIG_FILE.original

        # Override key and app_name
        sed -i -e "s/app_name:\ My\ Application/app_name:\ ${NEWRELIC_APP_NAME}/g" $NEW_RELIC_CONFIG_FILE
        sed -i -e "s/'<\%= license_key \%>'/${NEWRELIC_KEY}/g" $NEW_RELIC_CONFIG_FILE

        exec "$@"
fi

Ce qu’il faut retenir …

Comprendre le fonctionnement du cache est important pour réduire le temps de build des images, une contrainte essentielle pour faire du Continuous Delivery.

Toujours utiliser la commande COPY à la place de ADD.

Se limiter à CMD pour les commandes simples, sans besoin de configuration du conteneur. Et utiliser ENTRYPOINT + CMD quand on a besoin d’appliquer des configurations au conteneur avant de le lancer.

 

Plus de publications

3 comments for “Quelques précisions sur le Dockerfile

  1. remi
    31 août 2017 at 22 h 18 min

    Bonjour;
    Un grand merci!
    Je viens de comprendre plein de petits détails pour docker!

    Cordialement.

  2. John
    29 décembre 2017 at 17 h 30 min

    Bonjour,

    Merci beaucoup pour cet article très intéressant!

    Cordialement,

  3. Farah
    6 février 2018 at 13 h 31 min

    Bonjour,

    Super article sur les commandes Docker qui éclaircit un peu ma lanterne concernant les Dockerfiles ainsi que les configurations possibles !

    Merci beaucoup !