Lors des tutoriels précédents pour débuter docker, nous avions vu le lancement de conteneurs à partir d’images déjà existantes. Ces images sont stockées sur ce que l’on appelle des registries. C’est une sorte de dépôt pour stocker des images de type OCI (Open Container Initiative).
Mais finalement comment créer une image docker ? Quelles bonnes pratiques mettre en oeuvre ?
Créer une image par un commit de conteneur, c’est possible ??!!
Alors autant le dire tout de suite c’est possible mais ce n’est pas une grande idée. En effet, comme nous allons le voir par la suite il vaut mieux privilégier les Dockerfile. Ces fichiers descriptifs de la constitution d’une image permettent de les créer et s’agissant de fichiers, on peut donc les gitter et ainsi les versionner et les partager. Ce n’est pas le cas avec les commit de conteneurs.
Néanmoins, cela reste très intéressant de savoir que l’on peut partir d’un conteneur docker pour créer une image. Parfois, cela peut même vous enlever une sacrée épingle du pied.
Alors comment faire ?
Vous devez vous en douter, la CLI de docker va nous fournir une commande magique : docker commit.
Donc partons d’un conteneur existant :
docker run -d --name c1 nginx:latest
Puis on va faire une modification anodine dedans :
docker exec -ti c1 bash
touch /xavki.txt
Maintenant resortons du conteneur puis créons une image à l’aide de la commande docker commit. Pour cela, il nous faut l’id du conteneur et on définira un nom d’image et un tag.
docker commit <id_conteneur> <nom_image:tag_image>
docker commit a5zea5ef myimage:v1.0.0
Et maintenant vous pouvez vérifier que votre moteur docker local trouve bien cette nouvelle image docker avec un docker ps.
Puis vous pouvez lancer un nouveau conteneur avec cette image :
docker run -d --name c2 myimage:v1.0.0
Et vous rendre dedans pour lancer un ls du fichier que nous avions créé dansnotre conteneur c1 et donc présent de base dans notre nouvelle image.
docker exec -ti c1 ls /xavki.txt
Bingo !!! Vous venez de créer votre première image docker !!!
Comme je vous disez il vaut mieux utiliser le Dockerfile mais néanmoins docker commit est assez pratique :
- pour débuguer
- pour tester
- pour recréer une image quand on a perdu le Dockerfile
- … bref pour bidouiller
Mais le mieux reste le Dockerfile
Layers d’une image docker
Les images docker repose sur le principe de base des images et des conteneurs : le Copy On Write
C’est quoi ce truc ??
Considérons qu’une image pour être crée doit se faire au fur et à mesure d’instruction (cf ci-dessous avec le Dockerfile). Au fur et à mesure que l’on joue ces instructions on empile des couches successivent, les unes sur les autres.
C’est ce que l’on appelle les layers. Une fois que l’on a dit se principe il est facile de comprendre une différence importante entre une image et un conteneur d’un point de vue filesystème (en plus du principe de base de processus que l’on a déjà vu précédemment).
Une image docker est une succession de couches en Lecture seule. Et un conteneur est un layer en écriture sur ces couches en lecture. D’où le docker commit qui est finalement la validation (le commit) de cette couche en écriture pour le rendre en lecture seul et ainsi constituer une image.
Pour mieux s’en rendre compte il existe une commande peu utilisé : docker diff <nom_conteneur>. Avec cette commande lancé sur un conteneur vous pourrez voir les modifications qui votn être commitées. Magique ;).
Comment créer une image docker avec un Dockerfile ?
Alors une fois que l’on a dit cela on se dit ok mais c’est quoi ces instructions ? C’est la que l’on retrouve le Dockerfile (avec un D majuscule hein 😉 ).
Un dockerfile c’est un fichier plat avec en début de ligne une instruction écrite en majuscule puis un espace suivi de ces paramètres par exemple :
RUN apt install -y git
Donc maintenant voyons les principales instructions que vous pouvez retrouver facilement dans la documentation docker ici :
- RUN : lancer des commandes shell
- FROM déclarer une image source
- ARG déclarer une variable utilisable uniquement durant le build docker
- ENV : délcarer une variable d’environnement utilisable durant le build et lors de l’utilisation du conteneur
- COPY : pour copier des fichiers et des répertoires de l’extérieur de l’image (ou d’une autre image) dans l’image en cours de création
- ADD : idem COPY mais avec la possiblité de télécharger des documents par url
- CMD : définitions des options de la commandes principales du conteneurs (processus du conteneur)
- ENTRYPOINT : définition de la commande principale du conteneur issu de l’image
Par exemple :
FROM python@sha256:2659ee0e84fab5bd62a4d9cbe5b6750285e79d6d6ec00d8e128352dad956f096
# Labels
LABEL version=v1.0.0
LABEL owner=xavki-app
# Env vars
ENV FLASK_APP=app.py
ENV FLASK_ENV=dev
ARG APP_USER=xavki
# Create a dedicated user
RUN adduser -D ${APP_USER}
# Directory
WORKDIR /app
# Copy & install requirements
RUN apk add --no-cache curl=8.0.1-r0
USER ${APP_USER}:${APP_USER}
COPY --chown=xavki requirements.txt requirements.txt
RUN pip3 install --no-cache-dir -r requirements.txt
# Copy all files
COPY --chown=xavki . .
# Expose port
EXPOSE 5000
CMD ["python", "-m", "flask", "run", "--host=0.0.0.0"]
Et je vous propose de mettre en place de bonnes pratiques pour créer vos images docker
- Choisir une image légère && minimalist (multistage plus tard)
- Définir précisément le tag de image ou le digest
- Suppression des caches APT, APK… && /var/cache/xxx
- Grouper les layers (surtout pour les installations et utiliser && )
- Utiliser .dockerignore (secrets, fichiers sensibles et inutiles…)
- Utiliser plutôt COPY à la place de ADD (datas exterieures)
- Créer un utilisateur par défaut
- Utiliser cet utilisateur aux bons endroits USER && CMD
- Supprimer éventuellement des éléments installés (avec précision)
- Eviter à tout prix le tag latest
- Vérifier le FROM (registries vérifiées, image maintenue, contrôle des couches)
- Ou pousser ? registry publique ou privée
- Eviter le monolith : bases de données + apps + statics…
- Utiliser un linter (hadolint, clair…) – docker run –rm -i hadolint/hadolint < Dockerfile
- Ne pas pousser des secrets dedans (mot de passe, clefs, certificats…)
- Variabiliser qunad même…
- Eviter les outils de debug qui souvent sont néfastes pour la sécu (telnet, tcpdump, netcat…)
- Définir des règles sur les labels (standardiser: équipes, langage, version…)
- Vérifier la vulnérabilité (Clair, Falco…)
- Méfiez vous des COPY vraiment…
- Définir un WORKDIR
- Vérifier la possiblité du multistage-build
- Versionner c’est bien… et maintenir c’est mieux !!!
Mais quand même la commande pour créer notre image ??
Alors on a un dockerfile mais comment créer une image ?
C’est très simple avec docker build. Cette commande est assez magique et contient notamment de très bonnes fonctionnalités de cache par défaut pour éviter de reconstruire certains layers déjà existants.
Basiquement on peut lancer :
docker build -t myimage:v.1.0.0 .
Et voilà !!! Vous avez maintenant localement votre image myimage avec le tag v1.0.0
Et bien sûr pour découvrir les plus de 1500 tutos, rendez-vous sur la chaine Xavki