Le fichier Dockerfile va servir d'instruction afin de construire une image docker. Le fichier va contenir toutes les commandes afin de démarrer correctement l'application et donc, le container.
Voici un exemple de Dockerfile :
FROM ubuntu:20.04
COPY . /app
RUN make /app
CMD python /app/app.py
Si on décompose par couche :
Voici quelques "best practices" :
Voici les instructions les plus utilisées dans un dockerfile :
Instruction | Utilité |
---|---|
FROM | Initialise la nouvelle étape du dockerfile et défini l'image de base |
RUN | Exécute une commande dans une nouvelle couche |
CMD | Défini une commande par défaut lors de l'exécution d'un container. Il ne peut y avoir qu'une seule instruction CMD |
LABEL | Ajoute des métadatas à l'image |
EXPOSE | Documente les ports utilisés par l'application. |
ENV | Défini une variable d'environnement <key> <value> |
ADD | Copie les fichiers/dossiers ou les URLs distants depuis une <src> et les ajoutent au container (<dst>) |
WORKDIR | Défini le path par défaut du shell |
ARG | Défini une variable que l'utilisateur peut passer lors du build via la commande --build-arg VARNAME=VALUE |
ONBUILD | Ajoute un déclencheur qui exécute une instruction lorsque l'image est utilisé en tant que base pour un autre build exemple : ONBUILD COPY . /usr/src/app |
HEALTHCHECK | Indique à Docker comment tester si un container fonctionne toujours |
SHELL | Permet de remplacer le shell par défaut de l'image (utiliser bash au lieu de sh par exemple) |
COPY | Copie les dossiers/fichiers depuis une <src> vers un container <dst> |
ENTRYPOINT | Exécute un script en tant qu'exécutable |
VOLUME | Créer un volume avec un nom aléatoire |
USER | Défini un utilisateur (ou UID) and un groupe optionnel (ou GID) à utiliser pour les commandes RUN, CMD et ENTRYPOINT qui suivent. |
Voici un exemple de Dockerfile pour une application Flask python avec la structure suivante :
python-docker
|____ app.py
|____ requirements.txt
|____ Dockerfile
FROM python:3.12-slim-bullseye # Image python 3.12
WORKDIR /app # Le shell sera dans /app par défaut
COPY requirements.txt requirements.txt # On copie uniquement requirements.txt
RUN pip3 install -r requirements.txt # On lance la commande d'installation des paquets
COPY . . # On copie tous les fichiers vers /app du container
EXPOSE 5000 # On indique que l'application écoute sur le port 5000
CMD ["python3", "main.py"] # On défini la commande par défaut de l'image
Code python:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello_world():
return 'Hello, World!'
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0')
Requirements.txt:
flask
Afin de transformer ce Dockerfile en image :
docker image build . -t <NAME>:<VERSION>
docker image build . -t python-flask:v1
Le build va automatiquement chercher le fichier dockerfile. On peut cependant préciser le fichier via la commande -f.
Afin de sécurisé au mieux l'application, il est préférable de passer les données sensibles via les variables d'environnement et non de laisser un fichier de config en dur dans le code. Cela est expliqué dans la partie 3 du 12factor.
Il y a plusieurs façons de passer des variables d'environnement :
ENV <KEY> <VALUE>
ENV <KEY>=<VALUE>
En reprenant l'exemple fictif au-dessus on peut déclarer les variables comme ceci :
FROM python:3.12-slim-bullseye
ENV PYTHON_ENV="development"
ENV PORT 5000
WORKDIR /app
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
EXPOSE $PORT
CMD ["python3", "main.py"]
On va donc build cette image :
$ docker image build . -t python-flask:v2
On peut écraser les valeurs d'environnement en démarrant le container :
$ docker container run -d --name app -p 8080:3000 --env PORT=3000 --env PYTHON_ENV="production" python-flask:v2
On peut aussi passer un fichier d'environnement grâce au paramètre --env-file soit :
$ docker container run -d --name app -p 8080:5000 --env-file config/.env python-flask:v2
Comme vu plus haut nous pouvons passer des arguments directement via le build --build-arg, on peut aussi les passer via l'instruction ARG dans un dockerfile. L'instruction ARG peut être remplacée par une autre valeur via le paramètre --build-arg. Contrairement à ENV, ARG n'est disponible que lors du build. La variable n'est plus présent lorsque le container tourne.
On reprend notre dockerfile fictif et on y ajoute l'instruction ARG :
FROM python:3.12-slim-bullseye
ARG SRC_DIR=/var/python
RUN mkdir -p $SRC_DIR
WORKDIR $SRC_DIR
COPY requirements.txt requirements.txt
RUN pip3 install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python3", "main.py"]
On peut remplacer l'argument via la commande :
$ docker image build . -t python-flask:v3 --build-arg SRC_DIR=/var/test
Grace à l'instruction USER on peut passer de l'utilisateur root (par défaut) à un utilisateur crée au préalable. Soit :
FROM centos:latest
RUN useradd -ms /bin/bash antoine # -m crée le home, -s permet de spécifier le shell
USER antoine
On va maintenant build l'image puis exécuter un shell de celle-ci :
$ docker build . -t non-root:v1
Successfully built c94cb2d012a5
Successfully tagged non-root:v1
$ docker run -it --rm non-root:v1 /bin/bash
[antoine@c53421643c3f /]$ sudo su
bash: sudo: command not found
[antoine@c53421643c3f /]$ su
Password:
On voit que l'utilisateur "antoine" est bien utilisé par défaut. Cependant, on ne peut pas passer root depuis cet utilisateur. Pour se faire nous devons spécifier un paramètre lors du docker run :
$ docker run -it --rm -u 0 non-root:v1 /bin/bash
[root@a1c2446739e4 /]# whoami
root
-u va nous permettre de spécifier un utilisateur via son UID ou son login.
Un dockerfile s'exécute de haut vers le bas. Il est donc important d'ordonner les instructions.
CMD : défini une commande par défaut pour un container. CMD peut être facilement remplacer (docker run ubuntu:latest new_cmd).
ENTRYPOINT : défini un script spécifique par défaut pour un container. Il est un peu plus compliqué de remplacer un entrypoint. Quand on démarre un container, il faut spécifier --entrypoint afin d'écraser l'entrypoint actuel.
ENTRYPOINT se cumbine avec CMD. On peut par exemple appeler un script bash avec ENTRYPOINT et spécifier les paramètres avec CMD.
On peut par exemple regarder le dockerfile de postgresql. L'ENTRYPOINT appel le script "docker-entrypoint.sh" et CMD passe le paramètre "postgres". Lors du lancement du container, celui-ci exécutera donc la commande docker-entrypoint.sh postgres
Afin d'ignorer certains fichiers/dossiers lors du build on va créer un .dockerignore.
$ vim .dockerignore
*/*.md # Ignore tous les fichiers avec l'extension .md dans tous les dossiers
venv/ # Ignore le dossier venv/ et tous ses fichiers
*/.git # Ignore tous les dossiers .git
Pour rappel, le but d'une image est d'être le plus léger possible (poids). Il faut donc bien utiliser le .dockerignore afin de ne copier que les fichiers/dossiers essentiels !