Ecrire un fichier Dockerfile pour Code Engine
Avant de générer votre code dans une image de conteneur, apprenez quelques éléments de base de la manière dont la génération d'un Docker fonctionne dans IBM Cloud® Code Engine. Examinez ensuite certaines bonnes pratiques qui permettront à votre fichier Dockerfile d'atteindre ces objectifs. Ces exemples se concentrent sur la réduction de la taille de l'image, l'amélioration de l'interopérabilité avec des applications Code Engine et l'exécution de conteneurs en tant qu'utilisateur non superutilisateur.
Lorsque vous créez votre configuration de génération, vous choisissez laquelle des deux stratégies disponibles utiliser.
-
Cloud Native Buildpack inspecte votre code source et détecte sur quel environnement d'exécution votre code est basé et comment une image de conteneur est générée à partir de vos sources. Il est pris en charge pour plusieurs environnements de programmation. La liste prise en charge se trouve sous Choix d'une stratégie de génération.
-
Une génération de fichier Docker crée un conteneur en fonction de la manière dont vous le décrivez dans un fichier Dockerfile. Le fichier Dockerfile est ensuite validé avec votre code source pour créer le conteneur.
Bien que vous puissiez utiliser l'une ou l'autre stratégie pour votre génération, vous pouvez choisir le fichier Dockerfile si, par exemple,
- votre environnement de programmation n'est pas pris en charge par les packs de construction,
- votre génération de projet doit installer des packages supplémentaires dans le conteneur.
Concepts de base des fichiers Dockerfile
Un fichier Dockerfile décrit de quelle manière un conteneur est généré. Dans votre fichier Dockerfile, vous choisissez une image de base qui inclut les outils nécessaires dont vous avez besoin lors de la génération et de l'exécution. Vous pouvez
copier des fichiers à partir d'un contexte de génération dans l'image, exécuter des commandes, définir le comportement d'exécution, tels que les variables d'environnement, les ports exposés, et définirENTRYPOINT
. La commandeENTRYPOINT
est définie par la commande appelée lors du démarrage du conteneur. Pour plus d'informations sur la manière dont les instructions Dockerfile peuvent être spécifiées, voir Dockerfile reference.
Dans une génération Code Engine, définissez une source qui pointe vers un référentiel Git. Le contexte disponible pour la génération de fichier Docker est, par défaut, le répertoire principal de votre référentiel Git. Par exemple, si vous disposez
d'un répertoire nommé src
dans votre référentiel, vous pouvez utiliser l'instruction COPY
dans le fichier Dockerfile pour copier ce répertoire dans votre image. Par exemple :
COPY src /app/src
Pour copier l'ensemble du référentiel Git, indiquez l’instruction COPY
suivante :
COPY . /app/src
Si vous copiez l'intégralité du référentiel Git, mais que vous souhaitez exclure certains fichiers, par exemple le fichier README.md
du référentiel, vous pouvez ajouter un fichier Fichier .dockerignore
. Utilisez le même fichier pour ignorer également les fichiers et les répertoires que vous spécifiez dans votre fichier .gitignore. L'utilisation du même fichier permet de s'assurer qu'une génération exécutée en local possède le même ensemble de fichiers que la génération en cours d'exécution dans Code Engine.
Copiez toujours vos fichiers d'application dans un sous-répertoire de la racine (/
) plutôt que directement dans la racine afin d'éviter tout conflit avec les fichiers du système d'exploitation. Lorsque vous nommez votre répertoire
d'application, n'utilisez pas un répertoire qui est réservé par les systèmes d'exploitation basés sur Unix, Kubernetes ou les générations Code Engine, comme /bin
, /dev
, /etc
, /lib
, /proc
,
/run
, /sys
, /usr
, /var
ou /workspace
. Il est recommandé de nommer votre répertoire d'application /app
.
Si votre référentiel de code source contient les sources de différentes applications organisées en répertoires, comme dans le référentiel d'exemplesCode Engine,
vous pouvez utiliser un sous-répertoire comme contexte. Dans la commande ibmcloud ce build create
, spécifiez des sous-répertoires à l'aide de
l'option --context-dir
.
Si votre fichier Dockerfile ne se trouve pas dans le répertoire de contexte, vous pouvez pointer vers ce dernier à l'aide de l'argument --dockerfile
.
Pour tester une génération Docker sur votre système local avant de la générer dans Code Engine, vous pouvez utiliser Docker Desktop.
Réduction de la taille d'une image de conteneur
La réduction de la taille d'une image de conteneur apporte une valeur à plusieurs aspects.
- Moins d'espace est nécessaire pour stocker l'image dans le registre de conteneurs. Vous économisez une partie du quota pour d'autres images et économisez de l'argent.
- Parfois, l'exécution de la génération a besoin de moins de temps pour se terminer car une image plus petite peut être transférée plus rapidement au registre de conteneurs qu'une image beaucoup plus grande. Là encore, vous économisez de l'argent.
- L'application ou le travail qui utilise l'image démarre plus rapidement car le temps nécessaire pour extraire l'image est plus court. Comme les ressources nécessaires pour exécuter votre application ou votre travail sont réservées lors de l'extraction de l'image, là encore vous économisez de l'argent. Un temps de démarrage rapide est particulièrement pertinent pour les applications dans Code Engine car il garantit un temps de réponse acceptable pour les demandes de vos utilisateurs, même si l'application est réduite à zéro.
Prenez en compte les meilleures pratiques suivantes pour réduire la taille de votre génération.
Combinaison de plusieurs commandes en une seule instruction RUN
afin de réduire la taille de l'image
Dans cet exemple, vous devez installer le logiciel dans l'image du conteneur, par exemple, Node.js. Utilisez les images de base pour Node.js afin de générer une application Node.js .
Pour installer manuellement Node.js, vous pouvez utiliser l'exemple de fichier Dockerfile suivant :
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y nodejs
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
Dans cet exemple, plusieurs instructions RUN
sont utilisées pour la mise à jour, la mise à niveau, l'installation de Node.js, le nettoyage et la suppression du cache des fichiers. Bien que cette séquence d'instructions soit correcte,
l'utilisation de plusieurs instructions RUN
n'est pas le meilleur moyen de l'implémenter. Lorsqu'un fichier Dockerfile est traité, chaque instruction du fichier Dockerfile crée une couche. Chaque couche contient les fichiers
qui ont été créés ou mis à jour ainsi que des informations sur ce qui a été supprimé. Une image de conteneur est la collection de toutes les couches. Si un fichier est ajouté dans une instruction et supprimé dans une autre, l'image obtenue
contient toujours le fichier dans la couche de la première instruction, et le marque comme supprimé dans la seconde couche.
Pour sauvegarder l'espace disque, indiquez une seule instruction RUN
pour créer une seule couche pour ce jeu de commandes :
FROM ubuntu
RUN apt update && apt upgrade -y && apt install -y nodejs && apt clean && rm -rf /var/lib/apt/lists/\*
Pour conserver un fichier Dockerfile lisible avec une ligne par commande, ajoutez des retours à la ligne à votre code :
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
L'utilisation de ce type d'instruction vous permet de réduire la taille de l'image de conteneur de 174 Mo à environ 147 Mo. Notez que les tailles de vos exemples peuvent différer car l'image de base Ubuntu et le package Node.js peuvent changer.
Ce type de meilleure pratique s'applique non seulement aux installations de packages, mais également à des tâches similaires. Dans l'exemple suivant, le téléchargement et l'extraction d'un progiciel, à nouveau Node.js, peuvent ressembler à l'exemple suivant :
FROM ubuntu
RUN apt update
RUN apt upgrade -y
RUN apt install -y curl
RUN curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz
RUN mkdir /opt/node
RUN tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1
RUN rm /tmp/nodejs.tar.gz
RUN apt remove -y curl
RUN apt clean
RUN rm -rf /var/lib/apt/lists/\*
La commande rm
supprime le package nodejs
téléchargé, mais les nombreuses instructions RUN
créent à nouveau des couches distinctes. En outre, pour télécharger le package nodejs
,
le package curl
est temporairement installé. Bien que le package curl lui-même soit supprimé ultérieurement, ses dépendances qui ont été implicitement installées sont toujours présentes. Un meilleur fichier Dockerfile ressemble
à l'exemple suivant :
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y curl && \
curl https://nodejs.org/dist/v16.14.2/node-v16.14.2-linux-x64.tar.gz -o /tmp/nodejs.tar.gz && \
mkdir /opt/node && \
tar -xzf /tmp/nodejs.tar.gz -C /opt/node --strip 1 && \
rm /tmp/nodejs.tar.gz && \
apt remove -y curl && \
apt auto-remove -y && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
Les commandes connexes sont combinées en une seule instruction RUN
et la commande apt auto-remove
est ajoutée pour supprimer les dépendances. Cet exemple réduit la taille de l'image de 222 Mo à environ
162 Mo.
Utilisation d'une petite image de base
Les exemples précédents utilisent Ubuntu comme image de base. Bien que cette image contienne de nombreux utilitaires utiles, plus il y a d'utilitaires dans une image de base, plus la taille de cette dernière est grande. De plus, en incluant plus d'utilitaires, vous augmentez le risque de faille de sécurité, vous obligeant à régénérer votre image. Pour éviter ces deux problèmes, utilisez une image de base plus petite. Par exemple :
-
Alpine est une image Docker officielle de petite taille. Pour les environnements de programmation tels que Java ou Node.js, vous trouverez souvent des balises basées sur Alpine.
-
Les images Distroless de Google Container Tools ne contiennent aucun outil de système d'exploitation mais l'environnement d'exécution nécessaire pour différents langages tels que Java et Node.js.
Comparez ces images de base en générant une image de conteneur qui exécute un programme Node.js dans un fichier program.js. Pour Ubuntu, le fichier Dockerfile ressemble à l'exemple suivant :
FROM ubuntu
RUN \
apt update && \
apt upgrade -y && \
apt install -y nodejs && \
apt clean && \
rm -rf /var/lib/apt/lists/\*
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
Pour Alpine, l'image est prise directement à partir de Node.js :
FROM node:16-alpine
COPY program.js /app/program.js
WORKDIR /app
ENTRYPOINT ["node", "program.js"]
Pour Distroless, utilisez l'exemple suivant :
FROM gcr.io/distroless/nodejs:16
COPY program.js /app/program.js
WORKDIR /app
CMD ["program.js"]
Alors que l'image basée sur Ubuntu fait 147 Mo, l'image basée sur Alpine fait 90 Mo et l'image basée sur Distroless 94 Mo.
Ne pas inclure de sources et d'outils de génération pour réduire la taille de l'image
Dans les exemples précédents basés sur Node.js, un seul fichier source est ajouté à l'image de conteneur. Cet exemple peut utiliser un fichier source unique car aucune compilation n'était nécessaire. Toutefois, si une compilation est requise, utilisez les outils nécessaires à la génération uniquement, mais ne les incluez pas à l'image obtenue. Par exemple, indiquez une application Java qui utilise Maven comme exemple. Un fichier Dockerfile mal codé ressemble à l'exemple suivant :
FROM maven:3-jdk-11-openj9
WORKDIR /app
COPY . /app
RUN mvn package
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/target/java-application-1.0-SNAPSHOT-fat.jar"]
L'image de conteneur qui en résulte contient tout le code source et tous les fichiers intermédiaires de Maven (tels que le cache d'artéfacts, les fichiers de classe qui sont ensuite intégrés dans un fichier JAR) ainsi que l'outil de génération Maven. De plus, le kit Java Development Kit (JDK) est également inclus lorsqu'un environnement JRE beaucoup plus petit est requis à l'exécution. En conséquence, la taille de l'image est de 466 Mo.
Pour réduire la taille de l'image, utilisez une fonctionnalité dite de générations à plusieurs étapes. Chaque étape d'une génération de fichier Docker possède sa propre image de base et peut exécuter des commandes à son étape. A la fin, une image finale est générée et copie les artefacts des étapes précédentes. Pour générer une image de conteneur à partir du code source, un modèle commun doit avoir deux étapes :
- L'étape de génération qui utilise une image de base qui contient tous les outils nécessaires pour compiler le code source dans le fichier binaire de l'environnement d'exécution.
- L'étape d'exécution, qui utilise une image de base avec l'environnement d'exécution nécessaire pour exécuter le fichier binaire. Le fichier binaire de l'étape de génération est copié dans cette étape.
Pour le projet Maven, le résultat ressemble à l'exemple suivant :
FROM maven:3-jdk-11-openj9 AS builder
WORKDIR /app
COPY . /app
RUN mvn package
FROM adoptopenjdk:11-jre-openj9
WORKDIR /app
COPY --from=builder /app/target/java-application-1.0-SNAPSHOT-fat.jar /app/java-application.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/java-application.jar"]
Dans cet exemple, l'étape de génération utilise l'image de base Maven pour générer le fichier JAR de l'application. L'étape d'exécution utilise l'image de base JRE plus petite. L'utilisation de la commande COPY --from=STAGE_NAME
permet de copier le fichier JAR de l'étape de génération à l'étape d'exécution. L'image obtenue a une taille de 242 Mo, permettant d'économiser environ 48 %.
Ce modèle peut également être utilisé pour d'autres langages de programmation dans lesquels une compilation doit être effectuée.
- Des applications de noeud nécessitant une génération, par exemple une génération Angular ou React. Ici, les images de base de génération et d'exécution peuvent finir par être identiques (pour les deux noeuds), mais tous les artefacts et sources de temps de génération n'ont pas besoin d'être copiés dans l'image d'exécution.
- Tout langage de programmation qui compile du code source dans un exécutable natif qui s'exécute sans environnement d'exécution, par exemple Go ou Rust.
Pour les langages qui produisent un exécutable natif et qui n'ont pas du tout besoin d'un environnement d'exécution, utilisez une autre fonctionnalité Docker pour l'étape d'exécution : Scratch. Scratch peut être utilisé en tant que base dans
la commande FROM
, mais il ne s'agit pas d'une image de conteneur finale. Au lieu de cela, il indique à la génération de fichier Docker de ne pas du tout utiliser d'image de base. Sans aucun fichier de système
d'exploitation provenant d'une image de base, l'image obtenue peut ne contenir qu'un seul fichier : votre fichier binaire copié à partir de l'étape de génération. Notez qu'en fonction du langage de programmation et de votre code, vous devrez
peut-être effectuer d'autres ajustements sur les options de compilation car les fichiers binaires peuvent dépendre de certains fichiers du système d'exploitation.
Garder votre image propre
Lorsque vous développez pour la première fois votre fichier Dockerfile et que vous le déboguez, vous pouvez installer certains outils temporaires ou appliquer d'autres charges de travail qui ne doivent pas nécessairement figurer dans l'image de conteneur finale. La suppression de ces fichiers temporaires et charges de travail permet de garder votre image propre, petite et plus sécurisée.
Amélioration de l'heure de démarrage de votre image
Pour une efficacité maximale, votre image de conteneur doit démarrer le plus rapidement possible. Pour les applications, la métrique pertinente est le temps nécessaire pour que le noeud final HTTP soit disponible et prêt à accepter et traiter les demandes HTTP entrantes. La vitesse de démarrage est particulièrement importante pour les applications Code Engine basées sur Knative. Ces applications peuvent être configurées pour réduire à zéro les instances en cours d'exécution lorsqu'il n'y a pas de trafic, ne consommant ainsi aucune ressource et ne coûtant pas d'argent. Lorsqu'une demande arrive, une instance est démarrée pour la traiter. Votre conteneur doit démarrer aussi rapidement que possible pour répondre à la nouvelle demande.
Pour améliorer le démarrage de votre application, étudiez son implémentation et déterminez si vous pouvez appliquer des modèles tels que les suivants :
- Parallélisation des travaux d'initialisation indépendants, par exemple pour établir une connexion à une base de données et lire le fichier de configuration pour communiquer avec un serveur de messagerie à partir de variables d'environnement.
- Report des travaux d'initialisation qui ne sont pas nécessaires pour le démarrage de l'application et exécution en fonction des besoins.
De plus, vous pouvez également éviter un écueil courant lorsque vous implémentez une application Web qui utilise une infrastructure telle que Angular, React ou Vue. Chacune de ces infrastructures est basée sur Node.js avec NPM et inclut une
interface de ligne de commande qui peut faciliter la configuration d'un projet. Par exemple, une application React qui est créée à l'aide de la commande create-react-app
configure un fichier package.json
qui inclut certains scripts prédéfinis. L'un de ces scripts est start
, qui active un serveur Web avec votre application Web. Votre fichier Dockerfile peut ressembler à l'exemple suivant
:
FROM nodejs:16-alpine
COPY . /app
WORKDIR /app
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "run", "start"]
Bien que ce type de fichier Dockerfile fonctionne, il n'est pas rapide, car chaque fois que la commande npm run start
est appelée, l'application est compilée puis démarrée. Ce retard est particulièrement perceptible
avec les applications qui dépassent une petite taille d'échantillon. L'approche correcte consiste à compiler l'application au moment de la génération et à l'utiliser uniquement au démarrage :
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
Vous voyez à nouveau le modèle en deux étapes de génération et d'exécution. Notez également que le modèle mis à jour utilise un autre port, 8080
. Bien que cet exemple fonctionne avec n'importe quel port, 8080
est le
port par défaut pour les applications Code Engine. De plus, en utilisant la génération compilée, toutes les sources et les outils installés dans node_modules
ne sont pas inclus dans l'image du conteneur final, ce qui réduit sa
taille de 281 à 97 Mo.
Exécution d'un conteneur non root
Les systèmes bien conçus suivent le principe du moindre privilège - une application ou un utilisateur n'obtient que les privilèges dont il a besoin pour effectuer une action spécifique. Dans Code Engine, vous exécutez un serveur d'applications ou une logique de traitement par lots, qui n'exige généralement pas un accès administratif au système. Par conséquent, il ne doit pas s'exécuter en tant que root dans son conteneur. Une bonne pratique consiste à configurer l'image de conteneur avec un utilisateur défini et à l'exécuter en tant que non root. Par exemple, selon notre précédent scénario :
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
Le fichier Dockerfile utilise la commande USER
pour indiquer qu'il souhaite s'exécuter en tant qu'utilisateur et groupe 1100. Notez que cette commande ne crée pas implicitement un utilisateur et un groupe nommés
dans l'image de conteneur. Généralement, cette structure est acceptable, mais si votre logique d'application requiert l'existence de l'utilisateur et de son répertoire de base, vous devez créer l'utilisateur et le groupe de manière explicite
:
FROM node:16-alpine AS builder
COPY . /app
WORKDIR /app
RUN npm install && npm run build
FROM node:16-alpine
RUN npm install -g serve && \
addgroup nonroot --gid 1100 && \
adduser nonroot --ingroup nonroot --uid 1100 --home /home/nonroot --disabled-password
COPY --from=builder /app/build /app
USER 1100:1100
EXPOSE 8080
ENTRYPOINT [ "serve", "--single", "--no-clipboard", "--listen", "8080", "/app" ]
La commande RUN
de l'étape d'exécution a été étendue pour appeler les commandes addgroup
et adduser
afin de créer un groupe et un utilisateur avec un répertoire
principal.