Fabriquer son propre container

Dans cette partie nous allons voir comment fabriquer son propre container à partir d’un container de base et comment le distribuer facilement en crééant un Dockerfile. Pour illustrer ceci nous allons partir d’un container contenant une ditribution minimale Ubuntu et le transformer en un container contenant la derière version de Python et de la librairie NumPy ainsi que de l’éditeur de texte Nano.

Une première approche

Commençons par récupérer le container ubuntu.

% docker pull ubuntu
Using default tag: latest
latest: Pulling from library/ubuntu
...
...
Digest: sha256:868fd30a0e47b8d8ac485df174795b5e2fe8a6c8f056cc707b232d65b8a1ab68
Status: Downloaded newer image for ubuntu:latest
% docker images
REPOSITORY       TAG        IMAGE ID         CREATED          SIZE
ubuntu           latest     1d9c17228a9e     2 weeks ago      86.7MB

Installons ensuite python3 dans le container.

% docker run -ti ubuntu --name ubuntu
root@281030b0bef9:/# apt-get update
Get:1 http://security.ubuntu.com/ubuntu bionic-security InRelease [83.2 kB]
Fetched 15.4 MB in 22s (704 kB/s)
Reading package lists... Done
root@281030b0bef9:/# apt-get install python3
Reading package lists... Done
Building dependency tree
Reading state information... Done
...
...
After this operation, 33.5 MB of additional disk space will be used.
Do you want to continue? [Y/n] Y
...
...
Setting up python3 (3.6.7-1~18.04) ...
running python rtupdate hooks for python3.6...
running python post-rtupdate hooks for python3.6...
Processing triggers for libc-bin (2.27-3ubuntu1) ...

Procédons ensuite à l’installation de numpy.

root@281030b0bef9:/# apt-get install python3-numpy
Reading package lists... Done
Building dependency tree
Reading state information... Done
...
...
Setting up python3-numpy (1:1.13.3-2ubuntu1) ...
Processing triggers for libc-bin (2.27-3ubuntu1) ...

Terminons par installer l’éditeur nano.

root@281030b0bef9:/# apt-get install nano
Reading package lists... Done
Building dependency tree
Reading state information... Done
...
...
Unpacking nano (2.9.3-2) ...
Setting up nano (2.9.3-2) ...
...

Nous avons à présent un container disposant de tous les outils qui étaient recquis, nous pouvons en créer une image en utilisant comme dans la section précédente l’option commit.

% docker commit -m "Mon container python+numpy" ubuntu veron/monpythonnumpy:v1
...

% docker images
REPOSITORY             TAG         IMAGE ID         CREATED           SIZE
veron/monpythonnumpy   v1          7169b3b425ba     2 minutes ago     172MB
ubuntu                 latest      1d9c17228a9e     2 weeks ago       86.7MB

L’image obtenue fait 172 méga-octets au lieu des 86.7 méga-octets du container initial. On voit donc que selon l’environnement de travail que l’on souhaite transmettre à un étudiant, la taille de l’image peut très vite devenir assez imposante. De plus si on souhaite mettre à jour certaines composantes de l’image, il faudra par la suite redistribuer cette image à tous les étudiants. Finalement, un autre inconvénient est que dans cette image les étudiants seront administrateurs systèmes et donc potentiellement, ils ne sont pas à l’abri d’une mauvaise manipulation pouvant compromettre le bon fonctionnement de l’environnement que l’enseignant aura mis en place dans le container. Nous allons voir par la suite, comment créer un container personnalisé et restreindre son utilisation à un utilisateur non privilégié, tout ceci en ne distribuant qu’un simple fichier de configuration.

Le fichier Dockerfile

Dans un premier temps, on pourrait définier le fichier Dockerfile comme un fichier contenant toutes les commandes qu’il faut exécuter à l’intérieur d’un container afin d’y rajouter de nouveaux éléments. Voici à titre d’exemple, le fichier Dockerfile permettant d’obtenir le container que nous avons créé dans le paragraphe précédent.

FROM ubuntu:latest
MAINTAINER Pascal Véron <veron@univ-tln.fr>
RUN apt-get -y update && \\
apt-get -y install python3 && \\
apt-get -y install python3-numpy && \\
apt-get -y install nano

Ce fichier est composé de 3 lignes :

  • la ligne FROM indique le container initial que l’on souhaite enrichir et qui sera automatiquement récupéré sur le portail Docker Hub,
  • la ligne MAINTAINER permet de connaître le concepteur du container modifié,
  • la ligne RUN indique les commandes à exécuter dans le container afin de créer le container modifié. Remarquons qu’ici nous avons plusieurs commandes à exécuter, nous aurions pu ajouter une ligne RUN par commande à exécuter mais il faut savoir que pour chaque ligne de type RUN, Docker lance un container en interne pour exécuter la commande associée. Ceci alourdit le procédé de création de l’image. Ici, nous utilisons l’attribut && du shell permettant d’exécuter la commande suivant cet attribut que si la commande le précédent s’est terminiée normalement. Nous pouvons donc intégrer toute nos commandes dans une seule directive RUN.

Pour construire le container décrit par le fichier Dockerfile, il suffit de palcer ce fichier dans un répertoire et de lancer la commande docker avec l’option build et l’option -t permettant de spécifier le nom du nouveau container qui par convention est de la forme concepteur/nom_container:version.

% mkdir DOCK
% mv Dockerfile DOCK
% docker build -t veron/monpythonnumpy:v1 .
  Sending build context to Docker daemon  2.048kB
  Step 1/3 : FROM ubuntu:latest
   ---> 1d9c17228a9e
  Step 2/3 : MAINTAINER Pascal Véron <veron@univ-tln.fr>
   ---> Running in 509a5dd12fac
  Removing intermediate container 509a5dd12fac
   ---> 41c6e112988d
  Step 3/3 : RUN apt-get -y update && \apt-get -y install python3 && \apt-get -y install python3-numpy && \apt-get -y install nano
    ---> Running in 92d44d59ff8e
  ...
  ...
  Successfully tagged veron/monpythonnumpy:v1

En distribuant le fichier Dockerfile à chaque étudiant, ce dernier sera donc en mesure de contruire son container de travail. L’inconvénient de ce container est qu’à son lancement on se retrouve par défaut à la racine du système de fichiers du container en tant qu’administrateur

%docker run -ti veron/monpythonnumpy:v1
root@a9fda2927e90:/# ls
bin  boot  dev  etc  home  lib  lib64  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Plus loin avec Dockerfile

Dans cette partie, nous allons voir comment modifier le fichier Dockerfile afin que le container possède par défaut un utilisateur numpython dont le répertoire de travail sera le répertoire partagé avec le système hôte. De plus, au lancement du container, on se retrouvera par défaut dans le répertoire de travail de numpython en tant que l’utilisateur numpython. Pour cela, lors de la création du container, nous allons utiliser les commandes groupadd et useradd afin de créer un nouvel utilisateur, puis nous utiliserons les directives USER et WORKDIR du fichier Dockerfile afin de spécifier un utilisateur par défaut et un répertoire de travail par défaut lors du lancement du container. Le contenu du fichier est donc le suivant :

  FROM ubuntu:latest
  MAINTAINER Pascal Véron <veron@univ-tln.fr>
  RUN apt-get -y update && \\
  apt-get -y install python3 && \\
  apt-get -y install python3-numpy && \\
  apt-get -y install nano && \\
  groupadd -g 1000 developpeur && \\
  useradd -u 1000 -g 1000 -m numpython
  USER numpython:developpeur
  WORKDIR /home/numpython


Nous pouvons à présent vérifier après avoir relancé la génération du *container* que nous avons obtenu la configuration souhaitée.
% docker build -t veron/monpythonnumpy:v1 .
  ...
  ...
% docker run -ti veron/monpythonnumpy:v1
     numpython@0cb9e4f9e929:~$ pwd
     /home/numpython
     numpython@0cb9e4f9e929:~$ id
     uid=1000(numpython) gid=1000(developpeur) groups=1000(developpeur)

On peut lier le répertroire de travail de l’utilisateur numpython à un répertoire de la machine hôte comme vu dans la précédente section :

% docker run -ti --volume /home/pascal/PYTHON:/home/numpython veron/monpythonnumpy:v1
numpython@cc0d4f235d89:~$ ls
matrice.py

Encore plus loin

Avec le container que nous venons de fabriquer, l’utilisateur peut toujours exécuter la commande de son choix en passant le nom de cette commande en argument de la commande docker.

% docker run -ti veron/monpythonnumpy:v1 /bin/pwd
/home/numpython
%

Si on désire désactiver cette fonctionnalité et forcer l’exécution d’une commande particulière au lancement du container, il faut rajouter la directive ENTRYPOINT dans le fichier Dokerfile et spécifier la commande devant être exécutée par défaut lors du lancement du container. Pour notre exemple, on souhaite que l’utilisateur ait accès à un interprète de commandes afin de pouvoir gérer ces fichiers à partir de commandes du container et lancer l’interprète python.

FROM ubuntu:latest
MAINTAINER Pascal Véron <veron@univ-tln.fr>
RUN apt-get -y update && \\
apt-get -y install python3 && \\
apt-get -y install python3-numpy && \\
apt-get -y install nano && \\
groupadd -g 1000 developpeur && \\
useradd -u 1000 -g 1000 -m numpython
USER numpython:developpeur
WORKDIR /home/numpython
ENTRYPOINT ["/bin/bash"]
% docker build -t veron/monpythonnumpy:v1 .
...
...
% docker run -ti veron/monpythonnumpy:v1 /bin/pwd
/bin/pwd: /bin/pwd: cannot execute binary file
% docker run -ti veron/monpythonnumpy:v1
numpython@a9f4ba6d716d:~$

On voit bien qu’ici il n’est plus possible de lancer de commande particulière à partir de la ligne de commande docker.

Et voilà comment transformer notre container en une sorte « d’exécutable » python :

FROM ubuntu:latest
MAINTAINER Pascal Véron <veron@univ-tln.fr>
RUN apt-get -y update && \\
apt-get -y install python3 && \\
apt-get -y install python3-numpy && \\
apt-get -y install nano && \\
groupadd -g 1000 developpeur && \\
useradd -u 1000 -g 1000 -m numpython
USER numpython:developpeur
WORKDIR /home/numpython
ENTRYPOINT ["/usr/bin/python3"]
% docker build -t monpython3:latest .
...
...
% cd /home/pascal/PYTHON
% ls
matrice.py
% docker run -ti --volume /home/pascal/PYTHON:/home/numpython monpython3 matrice.py
[[1 5]
 [2 6]
 [3 7]
 [4 8]]

Dans ce dernier exemple, l’étudiant n’a plus aucun accès au contenu du container, il s’en sert comme d’un exécutable.

Sur certains systèmes, il se peut que le fait que l’uid et le gid de l’utilisateur numpython soit différent de ceux sur la machine hôte de l’utilisateur du container pose problème lors du montage de volumes. Nous allons terminer cette partie en expliquant comment passer des arguments au fichier Dockerfile.

Passage d’arguments

Nous allons voir dans cette partie comment modifier le fichier Dockerfile afin qu’il prenne en compte l’uid, le gid et le login de l’étudiant effectuant la création du container. Nous allons de plus configurer l’environnement du compte local créé dans le container de façon à ce que python aille chercher les modules définis par l’utilisateur dans le répertoire modules situé dans son répertoire de travail. Ceci nécessite de modifier la variable d’envitonnement PYTHONPATH. Nous allons utiliser pour cela les directives ARG et ENV du fichier Dockerfile.

Pour illuster que les modifications souhaitées sont effectives, nous allons construire un container donnant accès à un shell. Le fichier Dockerfile est modifié comme suit :

FROM ubuntu:latest
MAINTAINER Pascal Véron <veron@univ-tln.fr>
ARG uid
ARG gid
ARG login
RUN apt-get -y update && \\
apt-get -y install python3 && \\
apt-get -y install python3-numpy && \\
apt-get -y install nano && \\
groupadd -g $gid developpeur && \\
useradd -u $uid -g $gid -m $login
USER $login:developpeur
WORKDIR /home/$login
ENV PYTHONPATH="/home/$login/modules:${PYTHONPATH}"
ENTRYPOINT ["/bin/bash"]

La directive ARG permet de spécifier des variables utilisables dans le fichier Dockerfile, et dont la valeur sera déterminée lors de la construction du container. L’attribut ENV permet de définir une variable d’environnement pour l’utilisateur spécifié par la directive USER. On peut lancer à présent la construction de l’image en utilisant l’option –build-arg pour définir les variables uid, gid et login.

% id
uid=501(pascal) gid=501(staff) ....
% docker build --build-arg uid=`id -u` --build-arg gid=`id -g` --build-arg login=$LOGNAME -t monpython3:latest .
...
Successfully built 19099e3b91f5
Successfully tagged monpython3:latest

Créons alors (sur la machine hôte) dans notre répertoire PYTHON, un répertoire modules et plaçons-y le fichier hello.py qui contient :

print("Bonjour le monde")
% cd /home/pascal/PYTHON
% mkdir modules
% echo 'print("Bonjour le monde")' > modules/hello.py

Lançons à présent notre container

% docker run -ti --volume /home/pascal/PYTHON:/home/pascal monpython3
pascal@f14d0538b7a1:~$ id
uid=501(pascal) gid=501(developpeur) groups=501(developpeur)

pascal@f14d0538b7a1:~$ env
HOSTNAME=f14d0538b7a1
PWD=/home/pascal
HOME=/home/pascal
TERM=xterm
SHLVL=1
PYTHONPATH=/home/pascal/modules:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
_=/usr/bin/env

pascal@f14d0538b7a1:~$ ls modules/
hello.py

pascal@f14d0538b7a1:~$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hello
Bonjour le monde
>>>