# Docker: précisions sur le Dockerfile {{INLINETOC}} 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 layers : Dans le Dockerfile, chaque commande (`ADD`, `COPY`, `CMD`, `ENTRYPOINT`, …) 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, 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 : * Placer les lignes qui changent souvent le plus bas possible (ajout d’un jar par exemple, COPY). * 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). ## Les Directives La directive `FROM` spécifie une image à partir de laquelle Toute référence d'image valide peut être utilisée. ``` FROM node:5 ``` La directive `WORKDIR` définit le répertoire de travail actuel dans le conteneur, ce qui équivaut à exécuter cd dans le conteneur. RUN cd ne changera pas le répertoire de travail en cours. ``` WORKDIR /usr/src/app ``` La directive `RUN` exécute la commande donnée à l'intérieur du conteneur. ``` RUN npm install cowsay knock-knock-jokes ``` La directive `COPY` copie le fichier ou le répertoire spécifié dans le premier argument à partir du contexte de génération (le path transmis au docker build path ) vers l'emplacement du conteneur spécifié par le deuxième argument. ``` COPY cowsay-knockknock.js . ``` La directive `CMD` spécifie une commande à exécuter lorsque l'image est exécutée et qu'aucune commande n'est donnée. Il peut être remplacé en transmettant une commande à docker run . ``` CMD node cowsay-knockknock.js ``` Il existe de nombreuses autres instructions et options. # Organisation d'un dockerfile ## Exemple simple ``` # Base image FROM python:2.7-alpine # Metadata MAINTAINER John Doe # System-level dependencies RUN apk add --update \ ca-certificates \ && update-ca-certificates \ && rm -rf /var/cache/apk/* # App dependencies COPY requirements.txt /requirements.txt RUN pip install -r /requirements.txt # App codebase WORKDIR /app COPY . ./ # Configs ENV DEBUG true EXPOSE 5000 CMD ["python", "app.py"] ``` ## Classement du contenu Le `dokerfile` présenté ci-dessus est organisé de la maière suivante * Déclaration d'image de base (`FROM`) * Métadonnées (par exemple `MAINTAINER` , `LABEL` ) * Installation des dépendances du système (`RUN`: apt-get install , apk add ) * Copie du fichier de dépendances de l'application (`COPY`: bower.json , package.json , build.gradle , requirements.txt ) * Installation de dépendances d'application (`RUN`: npm install pip install ) * Copier l'intégralité du code * Configuration des configurations d'exécution par défaut (`CMD`, `ENTRYPOINT` ,`ENV` ,`EXPOSE` ) Ces commandes sont destinées à optimiser le temps de création à l'aide du mécanisme de mise en cache intégré de Docker. **Règle de base**: Les parties qui changent souvent (par exemple, la base de code) doivent être placées au bas du fichier Dockerfile et vice-versa. Les parties qui changent rarement (par exemple les dépendances) doivent être placées en haut. # Directives 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 relatif au `contextDir`. * **dest**: un nom de fichier (/var/opt/fileName), ou un répertoire (/var/opt/) 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) ``` ## ADD 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/). ## COPY COPY se contente tout simplement de prendre un fichier (ou répertoire) du host et de le mettre dans le conteneur. 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. # Directives CMD et ENTRYPOINT Il existe deux directives Dockerfile pour spécifier quelle commande exécuter par défaut dans les images construites. Si vous spécifiez uniquement `CMD` alors docker exécutera cette commande en utilisant le `ENTRYPOINT` par défaut, à savoir `/bin/sh -c`. On peut remplacer soit le point d’entrée, soit la commande lorsqu'on démarre l’image construite. Si on spécifie les deux, alors `ENTRYPOINT` spécifie l'exécutable du processus de conteneur et `CMD` sera fourni comme paramètre de cet exécutable. ## CMD Par exemple, si `Dockerfile` contient ``` FROM ubuntu:16.04 CMD ["/bin/date"] ``` On utilise la directive `ENTRYPOINT` par défaut de `/bin/sh -c` avec `/bin/date` (par défaut). La commande du processus de conteneur sera `/bin/sh -c /bin/date` . Lorsqu'on exécutera cette image, elle affichera par défaut la date actuelle ``` $ docker build -t test . $ docker run test Tue Jul 19 10:37:43 UTC 2016 ``` On peut spécifier la commande à passer sur la ligne de commande, auquel cas il exécutera la commande spécifiée. ``` $ docker run test /bin/hostname bf0274ec8820 ``` ## ENTRYPOINT Lorsqu'on spécifie une directive ENTRYPOINT , Docker utilise cet exécutable et la directive CMD spécifie le ou les paramètres par défaut de la commande. Donc, si votre Dockerfile contient: ``` FROM ubuntu:16.04 ENTRYPOINT ["/bin/echo"] CMD ["Hello"] ``` Alors lord fe l'exécution ça va produire ``` $ docker build -t test . $ docker run test Hello ``` On peut fournir différents paramètres, mais ils seront tous exécutés dans `/bin/echo` ``` $ docker run test Hi Hi ``` Pour remplacer le point d'entrée répertorié dans le fichier Docker (par exemple, si on souhaite exécuter une commande différente de echo dans ce conteneur), il faut spécifier le paramètre `--entrypoint` sur la ligne de commande: ``` $ docker run --entrypoint=/bin/hostname test b2c70e74df18 ``` Généralement, on utilise la directive `ENTRYPOINT` pour pointer vers uns script que l'on vous souhaite exécuter et `CMD` vers les paramètres par défaut. # Directive EXPOSE Expose un port dans le fichier `Dockerfile` ## Description ``` EXPOSE [...] ``` `EXPOSE` informe Docker que le conteneur écoute les ports réseau spécifiés au moment de l'exécution. `EXPOSE` ne rend pas les ports du conteneur accessibles à l'hôte. Pour ce faire, vous devez utiliser l'option `-p` pour publier une plage de ports ou l'option `-P` pour publier tous les ports exposés. On peut exposer un numéro de port et le publier en externe sous un autre numéro. ## Exemple: À l'intérieur du fichier Docker: ``` EXPOSE 8765 ``` Pour accéder à ce port depuis la machine hôte, inclure cet argument dans le commande `docker run` : `` -p 8765:8765 ``