Jusqu’ici nous avons utilisé des images toutes prêtes.
Une des fonctionnalités principales de Docker est de pouvoir facilement construire des images à partir d’un simple fichier texte : le Dockerfile.
Un image Docker ressemble un peu à une VM car on peut penser à un Linux “freezé” dans un état.
En réalité c’est assez différent : il s’agit uniquement d’un système de fichier (par couches ou layers) et d’un manifeste JSON (des méta-données).
Les images sont créés en empilant de nouvelles couches sur une image existante grâce à un système de fichiers qui fait du union mount.
Chaque nouveau build génère une nouvelle image dans le répertoire des images (/var/lib/docker/images
) (attention ça peut vite prendre énormément de place)
On construit les images à partir d’un fichier Dockerfile
en décrivant procéduralement (étape par étape) la construction.
FROM debian:latest
RUN apt update && apt install htop
CMD ['sleep 1000']
docker build [-t tag] [-f dockerfile] <build_context>
généralement pour construire une image on se place directement dans le dossier avec le Dockerfile
et les élements de contexte nécessaire (programme, config, etc), le contexte est donc le caractère .
, il est obligatoire de préciser un contexte.
exemple : docker build -t mondebian .
Le Dockerfile est un fichier procédural qui permet de décrire l’installation d’un logiciel (la configuration d’un container) en enchaînant des instructions Dockerfile (en MAJUSCULE).
Exemple:
# our base image
FROM alpine:3.5
# Install python and pip
RUN apk add --update py2-pip
# upgrade pip
RUN pip install --upgrade pip
# install Python modules needed by the Python app
COPY requirements.txt /usr/src/app/
RUN pip install --no-cache-dir -r /usr/src/app/requirements.txt
# copy files required for the app to run
COPY app.py /usr/src/app/
COPY templates/index.html /usr/src/app/templates/
# tell the port number the container should expose
EXPOSE 5000
# run the application
CMD ["python", "/usr/src/app/app.py"]
FROM
RUN
ADD
CMD
Dockerfile
: elle permet de préciser la commande par défaut lancée à la création d’une instance du conteneur avec docker run
. on l’utilise avec une liste de paramètresCMD ["echo 'Conteneur démarré'"]
ENTRYPOINT
ENTRYPOINT ["/usr/bin/python3"]
CMD
et ENTRYPOINT
RUN
qui exécute une commande Bash uniquement pendant la construction de l’image.L’instruction CMD
a trois formes :
CMD ["executable","param1","param2"]
(exec form, forme à préférer)CMD ["param1","param2"]
(combinée à une instruction ENTRYPOINT
)CMD command param1 param2
(shell form)Si l’on souhaite que notre container lance le même exécutable à chaque fois, alors on peut opter pour l’usage d'ENTRYPOINT
en combination avec CMD
.
ENV
HEALTHCHECK
HEALTHCHECK
permet de vérifier si l’app contenue dans un conteneur est en bonne santé.
HEALTHCHECK CMD curl --fail http://localhost:5000/health || exit 1
On peut utiliser des variables d’environnement dans les Dockerfiles. La syntaxe est ${...}
.
Exemple :
FROM busybox
ENV FOO=/bar
WORKDIR ${FOO} # WORKDIR /bar
ADD . $FOO # ADD . /bar
COPY \$FOO /quux # COPY $FOO /quux
Se référer au mode d’emploi pour la logique plus précise de fonctionnement des variables.
docker build [-t <tag:version>] [-f <chemin_du_dockerfile>] <contexte_de_construction>
Lors de la construction, Docker télécharge l’image de base. On constate plusieurs téléchargements en parallèle.
Il lance ensuite la séquence des instructions du Dockerfile.
Observez l’historique de construction de l’image avec docker image history <image>
Il lance ensuite la série d’instructions du Dockerfile et indique un hash pour chaque étape.
Docker construit les images comme une série de “couches” de fichiers successives.
On parle d'Union Filesystem car chaque couche (de fichiers) écrase la précédente.
Chaque couche correspond à une instruction du Dockerfile.
docker image history <conteneur>
permet d’afficher les layers, leur date de construction et taille respectives.
Ce principe est au coeur de l'immutabilité des images Docker.
Au lancement d’un container, le Docker Engine rajoute une nouvelle couche de filesystem “normal” read/write par dessus la pile des couches de l’image.
docker diff <container>
permet d’observer les changements apportés au conteneur depuis le lancement.
Les images Docker ont souvent une taille de plusieurs centaines de mégaoctets voire parfois gigaoctets. docker image ls
permet de voir la taille des images.
Or, on construit souvent plusieurs dizaines de versions d’une application par jour (souvent automatiquement sur les serveurs d’intégration continue).
Le principe de Docker est justement d’avoir des images légères car on va créer beaucoup de conteneurs (un par instance d’application/service).
De plus on télécharge souvent les images depuis un registry, ce qui consomme de la bande passante.
La principale bonne pratique dans la construction d’images est de limiter leur taille au maximum.
Choisir une image Linux de base minimale:
ubuntu
complète pèse déjà presque une soixantaine de mégaoctets.busybox
) est difficile à débugger et peu bloquer pour certaines tâches à cause de binaires ou de bibliothèques logicielles qui manquent (compilation par exemple).alpine
qui est un bon compromis (6 mégaoctets seulement et un gestionnaire de paquets apk
).python3
est fourni en version python:alpine
(99 Mo), python:3-slim
(179 Mo) et python:latest
(918 Mo).Quand on tente de réduire la taille d’une image, on a recours à un tas de techniques. Avant, on utilisait deux Dockerfile
différents : un pour la version prod, léger, et un pour la version dev, avec des outils en plus. Ce n’était pas idéal.
Par ailleurs, il existe une limite du nombre de couches maximum par image (42 layers). Souvent on enchaînait les commandes en une seule pour économiser des couches (souvent, les commandes RUN
et ADD
), en y perdant en lisibilité.
Maintenant on peut utiliser les multistage builds.
Avec les multi-stage builds, on peut utiliser plusieurs instructions FROM
dans un Dockerfile. Chaque instruction FROM
utilise une base différente.
On sélectionne ensuite les fichiers intéressants (des fichiers compilés par exemple) en les copiant d’un stage à un autre.
Exemple de Dockerfile
utilisant un multi-stage build :
FROM golang:1.7.3 AS builder
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]
Il n’est pas nécessaire de partir d’une image Linux vierge pour construire un conteneur.
On peut utiliser la directive FROM
avec n’importe quelle image.
De nombreuses applications peuvent être configurées en étendant une image officielle
Exemple : une image Wordpress déjà adaptée à des besoins spécifiques.
L’intérêt ensuite est que l’image est disponible préconfigurée pour construire ou mettre à jour une infrastructure, ou lancer plusieurs instances (plusieurs containers) à partir de cette image.
C’est grâce à cette fonctionnalité que Docker peut être considéré comme un outil d'infrastructure as code.
On peut également prendre une sorte de snapshot du conteneur (de son système de fichiers, pas des processus en train de tourner) sous forme d’image avec docker commit <image>
et docker push
.
Généralement les images spécifiques produites par une entreprise n’ont pas vocation à finir dans un dépôt public.
On peut installer des registries privés.
On utilise alors docker login <adresse_repo>
pour se logger au registry et le nom du registry dans les tags
de l’image.
Exemples de registries :