---
# System prepended metadata

title: TP Docker
tags: [Assistants]

---

---
title: TP Docker
tags: Assistants
---

<!-- TODO: Choisir entre conteneurs et containers pour être logique tout du long -->

### Requirements:
 - avoir `docker` et `docker-compose` [Explications ici :)](https://blog.moulard.org/installer-docker/)
 - avoir testé son installation:
     - `docker run hello-world` (pas de `sudo` (c'est relou mais sécurisé))
     - `docker-compose --version`

# Le déroulement du TP
Le TP va se dérouler en deux étapes, un peu comme les TPs de l'ING1 : il y aura une partie de cours/TD et un exercice à faire à la fin, à rendre avant le 8/11/2020 à 23h42.

# Cours
## Docker

![](https://logos-download.com/wp-content/uploads/2016/09/Docker_logo.png)

> Selon la firme de recherche sur l'industrie 451 Research, « Docker est un outil qui peut impacter une application et ses dépendances dans un conteneur isolé, qui pourra être exécuté sur n'importe quel serveur ».
>
> -- <cite>[wikipedia](https://fr.wikipedia.org/wiki/Docker_%28logiciel%29)</cite>

### Qu'est ce que docker et les conteneurs ?

Docker est un outil permettant de faciliter le développement et le déploiement de conteneur. Un conteneur est une enveloppe virtuelle qui permet de distribuer une application avec tous les éléments dont elle a besoin pour fonctionner : fichiers sources, environnement d'exécution, librairies, outils et fichiers. Il est tout à fait possible de réaliser des conteneurs sans l'aide de docker ou autres outils de ce type. Les conteneurs sont une forme de virtualisations qui est intégrée au Kernel Linux. Contrairement aux `Machines Virtuelles` qui virtualisent un OS entier, les conteneurs vont utiliser l'OS sur lequel ils sont lancés afin de limiter l'impact sur les performances. Les applications à l'intérieur des conteneurs sont "sur" l'OS mais sont séparées du reste des processus par divers moyens comme les namespaces.

Étant donné que les conteneurs contiennent tout ce qui est nécessaire pour leurs usages (en général une application), ils permettent de limiter le besoin de configuration nécessaire afin de faire tourner ladite application. En effet, il suffit de télécharger l'image, d'avoir un outil comme docker pour la lancer et rien de plus, contrairement à une application directement sur l'OS qui pourrait nécessiter l'installation de plusieurs dépendances. Ces dépendances sont souvent installées via un package-manager ce qui rend l'étape de configuration propre à un système et peu portable. Les conteneurs permettent donc de s'affranchir de toutes ces complications. Cela a aussi pour effet d'améliorer le temps de déploiement d'une application.

![](https://i.imgur.com/zNfaqy3.png)

A noter que docker n'est pas le seul outil de ce type, on peut aussi citer `LXC` et `Podman` par exemple.

### Pourquoi faire de la conteneurisation ? (avec Docker dans notre exemple)
<!--
 - virtualisation
 - registery
 - versionning
 @sevan
 -->

Les conteneurs sont principalement utilisés car ils permettent d'avoir un environnement reproductible. Cela permet de faciliter le développement et les tests d'une application. Dans la majorité des cas, le comportement sera identique que ce soit sur le PC du développeur ou sur le serveur de production. Cela permet d'éviter les "Mais ça marche sur ma machine" lorsqu'il y a un problème en production.

 ![](https://i.imgur.com/IJUGRcS.jpg)

Un développeur peut créer une image docker (nous verrons comment par la suite), cette image sera la même sur son PC et sur la production. Une image est versionnée, il est donc facile de revenir en arrière s'il y a le moindre soucis. Cette image pourra ensuite être partagée via un `registry` cela permet de rendre la rendre (et donc une application) disponible pour tout le monde très facilement.

De plus, un conteneur isole l'application du reste de l'OS, cela présente un avantage de sécurité si le conteneur est lancé avec les bons paramètres. Si un attaquant prend le contrôle de l'application, il ne pourra pas sortir du conteneur (sauf cas particulier), il ne pourra donc pas altérer les autres applications, ni le système.

Cet aspect de visualisation permet aussi de faire tourner plusieurs applications similaires sur un même OS. Ce qui n'est pas trivial avec des applications directement installées sur l'OS. Par exemple, si l'on veut plusieurs base de données PGSQL, cela serait moins évident car l'application se bind sur un port donné (par défaut 5432), on ne peut pas lancer 2 fois l'application sans changer la configuration d'au moins l'une des deux, cela n'est pas très pratique. Avec les conteneurs, il est très simple de changer le port sur lequel l'application va se bind sans modifier la configuration de l'application.

### Comment faire du docker ?
![Cycle de vie d'un produit dans Docker](https://i.imgur.com/nfV8066.png)


Dans cette partie du TP, vous allez apprendre à faire du docker: comment créer le fichier `Dockerfile` et l'utiliser.

La première chose à savoir est que Docker utilise des fichiers appelés `Dockerfile` pour créer ses conteneurs.

Ces `Dockerfile` sont composés d'une suite d'instructions/commandes qui correspondent à toutes les commandes que voudrait faire un utilisateur pour créer une image.
Ces instructions sont de la forme:
```dockerfile
INSTRUCTION argument
```

Lors du build de l'image, Docker va créer des couches(`layers`) pour chacune des instructions spécifiés.

Ces couches vont permettre d'optimiser les prochains build.
En effet, Docker ne va pas recréer les `layers` qui n'ont pas changés: par exemple, Docker ne va pas installer une dependence à chaque build, seulement lors du premier ou lorsque les dépendances ont changé (il est possible de le forcer avec `--no-cache`).
D'autre part, si un layer remarque un changement, et donc une obsolescence de cache, les layers d'après vont aussi être rebuild.

![Schema de fonctionnement des layers](https://i.imgur.com/n2nptfV.png)

Pour build une image Docker, il faut un fichier `dockerfile` et build:
```bash
docker build .
```

Le `.` permet de donner à Docker l'environnement de build (AKA le dossier courant).

On peut aussi build une image avec un Dockerfile n'ayant pas un nom conventionnel: `-f ./Dockerfile_with_a_special_name`.

Il est recommandé de nommer les versions de nos images docker. Pour cela, on peut lui donner un nom (cf les `tags` vu avant) \o/:
```bash
docker build -t my_awesome_image:version .
```

#### Dockerfile
Explorons un peu les différentes instructions qui sont utiles. Vous pourrez trouver la [spec](https://docs.docker.com/engine/reference/builder/) online.

##### `FROM`
Pour faire un `Dockerfile` valide, cette instruction est **obligatoire**. Une image docker a toujours besoin de partir d'un layer existant.
Elle permet de connaitre l'image de base sur laquelle se situe la future image. (Vous pouvez trouver une liste des images de base disponible sur le [repository docker](https://hub.docker.com/search/?q=&type=image)).
Et ça ressemble à ça:
```dockerfile
FROM <image>[:<tag>] [AS <name>]
```

Essayons:
 - On va mettre dans un fichier `Dockerfile` ceci:
     ```dockerfile
     FROM ubuntu:19.04
     ```
 - Puis build l'image:
    ```bash
    docker build .
    ```

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon  2.048kB
Step 1/1 : FROM ubuntu:19.04
 ---> 9f3d7c446553
Successfully built 9f3d7c446553
```
On peut observer que docker va "envoyer" le Dockerfile au daemon docker pour qu'il puisse build.
Ce daemon va exécuter la seule étape, retourner le digest du layer et finir de build l'image.

<!-- TODO: Parler de la taille d'une image-->

##### `RUN`
C'est bien joli d'utiliser une image de base, mais comment spécifiez des instructions pendant le build me direz vous, c'est avec l'instruction `RUN` : Elle permet d'exécuter une commande shell lors du build.

On peut utiliser la version "simple":
```dockerfile
RUN <command>
```
Cela va exécuter la commande avec le shell par défaut de l'OS : `/bin/sh -c` pour Linux ou `cmd /S /C ` sur windows.

On peut aussi utiliser la version "exec":
```dockerfile
RUN ["executable", "param1", "param2", ...]
```

Tips: On peut utiliser des backslash `\` pour faire des `RUN` sur plusieurs lignes.

Essayons:
 - On va repartir du fichier `Dockerfile` de la dernière fois:
     ```dockerfile
     FROM ubuntu:19.04
     ```
 - Et y ajouter une commande:
     ```dockerfile
     FROM ubuntu:19.04
     RUN cat /etc/os-release
     ```
 - Puis build l'image:
    ```bash
    docker build .
     ```

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:19.04
 ---> 9f3d7c446553
Step 2/2 : RUN cat /etc/os-release
 ---> Running in 41ff7a13ca27
NAME="Ubuntu"
VERSION="19.04 (Disco Dingo)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 19.04"
VERSION_ID="19.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=disco
UBUNTU_CODENAME=disco
Removing intermediate container 41ff7a13ca27
 ---> 92b8b7f95f77
Successfully built 92b8b7f95f77
```

On peut observer le caching des instructions en faisant un rebuild:
```bash
> docker build .
Sending build context to Docker daemon  2.048kB
Step 1/2 : FROM ubuntu:19.04
 ---> 9f3d7c446553
Step 2/2 : RUN cat /etc/os-release
 ---> Using cache
 ---> 92b8b7f95f77
Successfully built 92b8b7f95f77
```

##### `COPY`, `ADD`
Maintenant qu'on peut exécuter des commandes shell dans le container, autant ne pas rester dans un monde fonctionnel pur et faisons des effets de bords sur des fichiers.

```dockerfile
# ADD
ADD <src>... <dest>
ADD ["<src>",... "<dest>"]

# COPY
COPY <src>... <dest>
COPY ["<src>",... "<dest>"]
```

La différence entre les deux est que `ADD` permet l'ajout de fichier depuis des sources différentes : `COPY` ajoute des fichiers ou dossiers alors que `ADD` peut aussi manipuler des fichiers distants (via URLs).

Essayons de faire un hello world en python:
 - On va créer un ficher python basique:
     ```bash
     echo "print('hello world')" > app.py
     ```
 - On va partir d'une image `python` pour le `Dockerfile`:
     ```dockerfile
     FROM python:3.8
     COPY app.py .
     RUN python app.py
     ```
 - Puis build l'image:
    ```bash
    docker build .
     ```

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon   5.12kB
Step 1/3 : FROM python:3.8
 ---> 28a4c88cdbbf
Step 2/3 : COPY app.py .
 ---> f6db079cdc6c
Step 3/3 : RUN python app.py
 ---> Running in 972191f3bec2
Hello, World!
Removing intermediate container 972191f3bec2
 ---> be8feedcb09d
Successfully built be8feedcb09d
```

##### `ENV` vs `ARG`
Pour de la configuration plus flexible, Docker est friand des variables d'environnement.
Ce sont des variables dont on peut avoir la valeur au runtime.

Par exemple, on pourrait stoker en variable d'environnement le login de la base de données ou le numéro de port à bind.

Dans le Docker file, ces variables sont indiquées avec la commande `ENV`:
```dockerfile
ENV DB_PASS=super_secure_password
ENV PORT=8080
```

Ces variables vont aussi pouvoir être changées à l'utilisation du container.

Tips: Pour des variables uniquement utiles au build de l'image, on peut utiliser `ARG`

Essayons de faire un hello world en rust :
 - On va créer un ficher rust basique `app.rs`:
     ```rust
     use std::env;

     fn main() {
         match env::var("LANG") {
             Ok(lang) => {
                 if lang.eq(&String::from("EN")) {
                     println!("Hello World");
                 } else {
                     println!("Bonjour Monde");
                 }
             }
             Err(e) => println!("Couldn't read LANG ({})", e),
         };
     }
     ```
 - On va partir d'une image `rust` pour le `Dockerfile`:
     ```dockerfile
     FROM rust:1.31
     COPY app.rs .
     RUN rustc app.rs -o app
     ENV LANG=EN
     RUN ./app
     ```
 - Puis build l'image:
    ```bash
    docker build .
     ```

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon   16.9kB
Step 1/5 : FROM rust:1.31
 ---> 6f61eb35ad91
Step 2/5 : COPY app.rs .
 ---> d5b1570b2c63
Step 3/5 : RUN rustc app.rs -o app
 ---> Running in e6354d1ff410
Removing intermediate container e6354d1ff410
 ---> 24d81400a54e
Step 4/5 : ENV LANG=EN
 ---> Running in 401cf76f6475
Removing intermediate container 401cf76f6475
 ---> ac92e5f19357
Step 5/5 : RUN ./app
 ---> Running in 5d3c41a3a85e
Hello World
Removing intermediate container 5d3c41a3a85e
 ---> d8349d6d185c
Successfully built d8349d6d185c
```

Et si on change la valeur de la variable, on obtient ça:
```bash
> docker build .
Sending build context to Docker daemon   16.9kB
Step 1/5 : FROM rust:1.31
 ---> 6f61eb35ad91
Step 2/5 : COPY app.rs .
 ---> Using cache
 ---> d5b1570b2c63
Step 3/5 : RUN rustc app.rs -o app
 ---> Using cache
 ---> 24d81400a54e
Step 4/5 : ENV LANG=FR
 ---> Running in 59fc11b76b80
Removing intermediate container 59fc11b76b80
 ---> 5535e8d40a4c
Step 5/5 : RUN ./app
 ---> Running in 16d58e234947
Bonjour Monde
Removing intermediate container 16d58e234947
 ---> 44f72d0290f1
Successfully built 44f72d0290f1
```

##### `CMD` vs `ENTRYPOINT`
Maintenant qu'on a pu compiler, on va lancer le binaire au démarrage du container.
On verra comment lancer le container plus tard.

Pour ça, on met:
```dockerfile
ENTRYPOINT ["executable", "param1", "param2"]
CMD <command>
```

`ENTRYPOINT` permet de définir un point d'entrée pour le container.

Dans le cas où un `ENTRYPOINT` est défini, il est possible de définir un `CMD` afin de créer des options supplémentaires. Dans la pratique, `ENTRYPOINT` et `CMD` ont un rôle très similaire. Cependant, `ENTRYPOINT` est statique alors que `CMD` est dynamique. Il est alors possible de créer un `ENTRYPOINT` couplé à un `CMD` afin d'avoir une part d'arguments statiques et dynamiques. Attention, cette pratique n'est pas simple à gérer et demande de la pratique.

Par exemple, pour le Reverse Proxy Traefik, son [`Dockerfile`](https://github.com/traefik/traefik/blob/master/Dockerfile) utilise `ENTRYPOINT` pour démarrer le binaire et `CMD` pour lui donner des paramètres.

Essayons de faire un hello world en rust :
 - On va partir d'une image `ubuntu` pour le `Dockerfile`:
     ```dockerfile
     FROM ubuntu:19.04
     ENTRYPOINT ["bash"]
     CMD ["--posix"]
     ```
 - Puis build l'image:
    ```bash
    docker build .
     ```

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM ubuntu:19.04
 ---> 9f3d7c446553
Step 2/3 : ENTRYPOINT ["bash"]
 ---> Running in d7c726e2c46d
Removing intermediate container d7c726e2c46d
 ---> ef0aacedc3fb
Step 3/3 : CMD ["--posix"]
 ---> Running in 41fcaa66d49b
Removing intermediate container 41fcaa66d49b
 ---> 439853250ab9
Successfully built 439853250ab9
```

A noter que lors du run du container, l'instruction `CMD` va pouvoir pouvoir être remplacée. Alors que l'instruction `ENTRYPOINT` non.

##### `WORKDIR`
On a réussi à lancer des commandes, mais on a jamais spécifié d'où on partait. On va maintenant utiliser WORKDIR pour le faire.

```dockerfile
# WORKDIR
WORKDIR /path/to/workdir
```

Un exemple de Dockerfile :

```dockerfile=
FROM ubuntu:19.04
WORKDIR a
WORKDIR b
WORKDIR c
RUN pwd

WORKDIR /d/e/f
RUN pwd
```

Et sa sortie :
```bash
> docker build .
docker build .
Sending build context to Docker daemon  3.072kB
Step 1/7 : FROM ubuntu:19.04
 ---> 9f3d7c446553
Step 2/7 : WORKDIR a
 ---> Running in 862c9c25b001
Removing intermediate container 862c9c25b001
 ---> b5182f87efaf
Step 3/7 : WORKDIR b
 ---> Running in a025044916fb
Removing intermediate container a025044916fb
 ---> 00639a0c768a
Step 4/7 : WORKDIR c
 ---> Running in f7636684c808
Removing intermediate container f7636684c808
 ---> c443fe035762
Step 5/7 : RUN pwd
 ---> Running in 666de2796715
/a/b/c
Removing intermediate container 666de2796715
 ---> 933bc24f7abd
Step 6/7 : WORKDIR /d/e/f
 ---> Running in 6a73fa4b4b1a
Removing intermediate container 6a73fa4b4b1a
 ---> 81d89d1f7712
Step 7/7 : RUN pwd
 ---> Running in df4d54248466
/d/e/f
Removing intermediate container df4d54248466
 ---> cd3537b0b0a7
Successfully built cd3537b0b0a7
```

##### `EXPOSE`
Les étapes précédentes vous ont permis de créer un Dockerfile et de faire un docker pouvant faire tourner une application. Il faut maintenant laisser les utilisateurs accéder à votre application. Pour cela, on utilise la commande EXPOSE.


En Dockerfile cela se traduit simplement :
```dockerfile
# EXPOSE
EXPOSE <port> [<port>/<protocol>...]

#En pratique
EXPOSE 80/tcp
```

```dockerfile=
FROM python:3.8
EXPOSE 8000
CMD python -m http.server 8000
```

Avec ce Dockerfile, le docker aura un port ouvert qu'il faudra ensuite bind à un de vos ports. Cela sera vu et expliqué plus tard.

On obtient ça:
```bash
> docker build .
Sending build context to Docker daemon  3.072kB
Step 1/3 : FROM python:3.8
 ---> 28a4c88cdbbf
Step 2/3 : EXPOSE 8000
 ---> Running in 74ed5b622239
Removing intermediate container 74ed5b622239
 ---> 7eeb65ba6434
Step 3/3 : CMD python -m http.server 8000
 ---> Running in c3d9cee464b8
Removing intermediate container c3d9cee464b8
 ---> c82182474b7f
Successfully built c82182474b7f
```

##### Aller plus loin
 - [`HEALTHCHECK`](https://docs.docker.com/engine/reference/builder/#healthcheck)
 - [`USER`](https://docs.docker.com/engine/reference/builder/#user)
 - [`.dockerignore`](https://docs.docker.com/engine/reference/builder/#dockerignore-file)
 - multi layer images: On peut faire des images plus légère en utilisant des images temporaires pour compiler les binaires:
     ```dockerfile
     # image pour compiler le go
     FROM golang:1.13.4 AS builder
     # ...
     RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

     # image plus légére pour run le go
     FROM scratch
     COPY --from=builder app .
     # ....
     ```

#### CLI
Après la création de `Dockerfile`, utilisons les vraiment !

##### `build`
```bash
docker build [OPTIONS] PATH | URL | -
```

La commande build de Docker permet de créer une image qui va ensuite pouvoir être lancée et transformée en conteneur.

Quelques options utiles :

- `-t <name>` permet de donner un nom à l'image créée (tag)
- `-f <path>` permet de spécifier un Dockerfile
- `--no-cache` permet de build sans utiliser le cache de docker

Pour en savoir plus sur les options de build : [Spec](https://docs.docker.com/engine/reference/commandline/build/)

Essayons avec l'image que nous avons fait

##### `tag`
```bash
docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
```

Docker tag permet de donner un nom à une image afin de la retrouver plus facilement.
Une image qui a été build ou une image modifiée peut alors être nommée facilement.
On se sert souvent de cette command en entreprise pour s'y retrouver parmi les différentes images créées.


Les Tags sont composés de 4 parties:
 - `registery` : correspond au serveur d'images à utiliser(comme [docker.io](hub.docker.io), [goharbor.io](https://goharbor.io/))
 - `author` : C'est l'auteur de l'image. Si l'auteur est `_` (c'est-à-dire Docker), il peut être omis. Par exemple, l'image `nginx` qui peut être utilisée dans auteur.
 - `name` : C'est le nom de l'image : `wordpress` par exemple.
 - `tag` : (oui, le tag du tag) permet de connaitre la version de l'image. Le tag par défaut est `latest`. Pour les images qui sont associées à :
     - des langages, comme l'image [haskell](https://hub.docker.com/_/haskell?tab=tags), le tag correspond à la version du langage : `haskell:8.8.4`
     - des services, comme l'image [gitlab-runner](https://hub.docker.com/r/gitlab/gitlab-runner/tags), le tag correspond à la version du service et à la version des layers sous-jacents : `gitlab/gitlab-runner:alpine-bleeding`, `gitlab/gitlab-runner:alpine-v13.5.0-rc2`, `gitlab/gitlab-runner:ubuntu-v13.4.0-rc1`

Une liste non exhaustive de tags valides:
 - `docker.io/michel/awesomeapp:latest`
 - `michel/awesomeapp`
 - `mysql`

```bash
# Depuis un ID de votre image locale
> docker tag 0e5574283393 image/httpd:version1.0

# Depuis une autre image locale nommée
> docker tag my-image epita/my-image:version1.0

# Depuis une image et un tag
> docker tag my-image:beta0.1 epita/my-image:version1.0.test
```

##### `push`
```bash
docker push [OPTIONS] NAME[:TAG]
```

Cette commande permet de mettre en ligne dans le répertoire d'image de votre choix.
Cela peut être le DockerHub ou une registry privée.

```bash
# On push une rhel (image red hat) dans une registry
> docker push registry-host:5000/myadmin/rhel-httpd
```

Tip: Les registry utilisent des comptes pour repérer les images, il faut donc faire un `docker login` pour se connecter et pouvoir push l'image voulue:
```bash
docker login [OPTIONS] [SERVER]
```

##### `run`
```bash
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
```

Docker run permet de lancer des conteneurs à partir d'une image.

``` bash
# Exemple simple docker run
> docker run -d -p 80:80 my_image service nginx start
```

Expliquons maintenant quelques options utiles :
- `-p` permet de bind un port du conteneur à un des ports de votre PC
- `-d`  permet de lancer le conteneur en mode détaché (en gros vous le lancez en background)
- `-it` afin d'avoir des process interactifs (et du coup lancer des commandes en plus dans votre docker)
- `-e` définit des variables d'environnement dans le container
- `-v` mount un volume dans le container: permet d'avoir de la persistance de données
- `--name` donne un nom au container
- `--rm` supprime le container au moment où il ne fonctionne plus

Un autre exemple d'utilisation du run pour avoir un container Ubuntu pour faire des test:
```bash
docker run -it --rm ubuntu bash
```

Si on ferme le tty de ce container (ctrl-d ou `exit`), il sera supprimé ; vous pouvez essayer de le casser ;)

##### `exec`
``` bash
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
```
Docker exec permet de lancer des commandes dans des conteneurs existant.

Avec Docker exec, il est possible de rentrer dans le conteneur afin de faire du debug (car comme tout épitéen normalement constitué, vous n'êtes pas des dieux du code).

Quelques exemples de Docker exec :
``` bash
# Afficher des informations internes au docker
> docker exec -ti my_container sh -c "echo a && echo b"

# Entrer dans le conteneur
> docker exec -ti ubuntu bash
```


##### `ps`
```bash
docker ps [OPTIONS]
```

Docker ps permet de lister les conteneurs déployés

```bash
# Les conteneurs qui tournent
> docker ps
CONTAINER ID IMAGE         COMMAND     CREATED      STATUS     PORTS  NAMES
2db9bb3bc2ea ubuntu:latest "/bin/bash" 8 months ago Up 9 hours        Ubuntrash

# tous les conteneurs (même ceux à l'arret)
> docker ps -a
CONTAINER ID IMAGE         COMMAND     CREATED        STATUS                      PORTS NAMES
45866acdddd6 ubuntu        "bash"      13 minutes ago Exited (0) 12 minutes ago         nifty_aryabhata
2db9bb3bc2ea ubuntu:latest "/bin/bash" 8 months ago   Up 9 hours                        Ubuntrash
```

Plus d'options disponibles ici : [link](https://docs.docker.com/engine/reference/commandline/ps/)

Tip: Pour arrêter tous les container qui sont en train de tourner, on peut faire : `docker stop $(docker ps -q)`

##### `stop`
```bash
docker stop [OPTIONS] CONTAINER [CONTAINER...]
```

Cette commande va envoyer un `SIGTERM` au process du container puis un `SIGKILL` après une période de grâce pour lui laisser le temps de s'éteindre.

##### network
```bash
docker network COMMAND
docker network create [OPTIONS] NETWORK
docker network connect [OPTIONS] NETWORK CONTAINER
docker network disconnect [OPTIONS] NETWORK CONTAINER
docker network inspect [OPTIONS] NETWORK [NETWORK...]
docker network ls [OPTIONS]
docker network rm NETWORK [NETWORK...]
```

Vous êtes des malades si vous utilisez ces commandes à la main !

Mais les commandes sont suffisamment simples pour comprendre ce qu'elles font:
 - `create` permet de créer un network
 - `connect` permet de connecter un container à un network
 - `disconnect` permet de déconnecter un container à un network
 - `inspect` permet d'inspecter un network pour avoir des informations dessus
 - `ls` permet de lister les networks
 - `rm` permet de supprimer un/des network(s).

## docker-compose
Woaw, vous avez atteint la moité de la partie cours du TP \o/

Vous savez maintenant utiliser docker (théoriquement) avec plein d'options.

### Pourquoi faire du docker-compose ?
Mais utiliser docker en CLI c'est gentil 2 minutes (qui compile son C sans un `Makefile` ?).

`docker-compose` est une sur-couche de docker qui permet de mettre dans un fichier tous les arguments qu'on aurait donné à docker. Mais aussi de gérer de multiples conteneurs en simultané plus facilement.

Le but est donc de transformer un :
```bash=
docker run \
  --name=transmission \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Europe/London \
  -e TRANSMISSION_WEB_HOME=/combustion-release/ `#optional` \
  -e USER=username `#optional` \
  -e PASS=password `#optional` \
  -p 9091:9091 \
  -p 51413:51413 \
  -p 51413:51413/udp \
  -v <path to data>:/config \
  -v <path to downloads>:/downloads \
  -v <path to watch folder>:/watch \
  --restart unless-stopped \
  linuxserver/transmission
```
en :
```bash
docker-compose up
```

Ca vous parait magique ? :mage: Alors apprenons tous ensemble comment ça marche !

### Comment faire du docker-compose ?
Encore une fois, la [spec](https://docs.docker.com/compose/compose-file/) est disponible en ligne.

#### docker-compose.yml
Comme pour utiliser docker, docker-compose utilise des fichiers `docker-compose.yml` avec un formal [yaml](https://yaml.org/) ([wiki](https://fr.wikipedia.org/wiki/YAML)).

Il sont composés de plusieurs parties qui décrivent des requirements à match:
 - Des services (expliqué plus loin)
 - Des networks pour lier de containers
 - Des volumes pour garder des fichiers persistants
 - Des secrets (peut utilisés) pour avoir des données chiffrée
 - Soon des imports

##### version
Pour chaque version de docker-compose, la version que le programme va utiliser va changer.

En effet, la version `'1'` n'est plus utilisée, la `'2'` continue d'être utilisée et la `'3'` est la plus complète.

Pour spécifier la version, il suffit de faire:
```yml
version: '3.6'
```

Si notre fichier `docker-compose.yml` ne contient que le numéro de version, il ne se passe rien lorsqu'on l'utilise:
 - On met le numéro de version dans un fichier `docker-compose.yml`:
     ```yml
     version: '3.6'
     ```
 - On exécute la commande pour savoir si tout s'est bien lancé:
     ```bash
     > docker-compose up
     Attaching to
     >
     ```

`docker-compose up` permet de lancer les services décrits dans le fichier `docker-compose.yml`, mais on en parlera plus tard.

##### networks
Les networks permettent de mettre des container sur un même réseau virtuel (par défaut, les networks sont en mode bridge).

La spécification est assez simple, il faut le spécifier:
```yml
networks:
  network-name:
```

Les valeurs par défaut suffisent amplement pour toute utilisation normale de docker-compose. (La doc est disponible pour les gens hardcore qui veulent allez plus loin)

Pour essayer:
 - On peut partir du fichier `docker-compose.yml` du dernier exemple:
    ```yml
    version: '3.6'
    ```
 - Et lui ajouter un network:
    ```yml
    version: '3.6'

    networks:
        network-name:
    ```
 - On execute:
    ```bash
    > docker-compose up
    WARNING: Some networks were defined but are not used by any service: network-name
    Attaching to
    >
    ```
 - On réfléchit:
    Vu que nous n'avons pas encore mis de services (oui, ça arrive, ça arrive), docker-compose ne peut pas faire un lien entre les containers et les networks, c'est pour ça qu'il indique une erreur.

##### volumes
Pour conserver des fichiers entre deux runs de containers, on peut créer des volumes.

Il y a deux moyens de créer des volumes:
 - En utilisant des dossiers et des fichier locaux (a faire directement dans le service)
 - En utilisant un file system virtuel

Pour cela, on peut faire:
```yml
volumes:
  volume-name:
    external: false
```

Sachant que l'on peut utiliser des file system distants, mais c'est pas fou.

##### services
Voilà enfin la partie intéressante du fichier docker-compose, et là où on va passer le plus de temps.

Le service va donc transformer une image docker en container. Il va décrire à docker-compose (et donc docker) comment run le container.

Un fichier `docker-compose.yml` va donc se composer de plusieurs parties:

```yml
version: '3.6'

networks:
  network-toto:

volumes:
  volume-tata:

services:
  mon-service_1:
    ...
  mon-service_2:
    ...
```

###### image
C'est là qu'on spécifie le nom de l'image à utiliser pour le service.
Ce nom va servir à la fois si on veut pull l'image et si on veut la build localement.

Cet argument à la même forme que lorsqu'on a fait des `FROM` dans le Dockerfile: `[repo/][user/]<nom>[:tag]`.

On peut donc faire:
 - Partir du `docker-compose.yml` précédent:
    ```yml
    version: '3.6'

    networks:
      network-name:
    ```
 - Et lui ajouter un service avec un nom d'image:
    ```yml
    version: '3.6'

    networks:
      network-name:

    services:
      swagger:
        image: swaggerapi/swagger-ui
    ```
 - Et on exécute:
    ```bash
    > docker-compose up
    WARNING: Some networks were defined but are not used by any service: network-name
    Creating network "demo_default" with the default driver
    Pulling swagg (swaggerapi/swagger-ui:)...
    latest: Pulling from swaggerapi/swagger-ui
    188c0c94c7c5: Pull complete
    61c2c0635c35: Pull complete
    378d0a9d4d5f: Pull complete
    2fe865f77305: Pull complete
    b92535839843: Pull complete
    5a845b721bd5: Pull complete
    11a7ab608318: Pull complete
    2940893bd159: Pull complete
    6e4cbe1b9035: Pull complete
    9f938b7a4eb0: Pull complete
    d58a47b6172e: Pull complete
    Digest: sha256:17c0001f808d76c6833e6f33b965ceb1aa4955a277eb826e0eb3369eb54aae91
    Status: Downloaded newer image for swaggerapi/swagger-ui:latest
    Creating demo_swagg_1 ... done
    Attaching to demo_swagg_1
    ^CGracefully stopping... (press Ctrl+C again to force)
    Stopping demo_swagg_1 ... done
    ```

    En analysant, on peut remarquer que:
     - Le network n'est pas utilisé
     - L'image n'était pas présente sur ma machine, `docker-compose` va pull l'image directement.
     - Le container est attaché au tty actuel et si on sort du container (`Ctrl-d` ou `exit`) le container est stoppé.

###### build
La directive `build` va permettre à docker-compose de décrire à docker comment build son image, tous les champs de `docker build`.

```yml
build:
  context: ./dir
  dockerfile: Dockerfile-alternate
  args:
    buildno: 1
```

On peut donc spécifier:
 - Le dossier contexte dans lequel faire le build
 - Le nom du ficher `Dockerfile`
 - Les ARGS a donner au fichier

Essayons:
 - Si on reprend le `dockerfile` pour le hello world en rust (ne pas oublier le fichier `app.rs`):
    ```dockerfile
     FROM rust:1.31
     COPY app.rs .
     RUN rustc app.rs -o app
     ARG LANG
     RUN ./app
    ```
 - Et on peut faire un fichier `docker-compose.yml` pour build:
     ```yml
    version: '3.6'

    services:
      my_rusty_docker:
        image: tommoulard/my_rusty-container
        build:
          context: .
          dockerfile: Dockerfile
          args:
            LANG: EN
    ```
 - Et il n'y a plus qu'à lancer:
    ```bash
    > docker-compose build
    Building my_rusty_docker
    Step 1/5 : FROM rust:1.31
    1.31: Pulling from library/rust
    cd8eada9c7bb: Pull complete
    c2677faec825: Pull complete
    fcce419a96b1: Pull complete
    045b51e26e75: Pull complete
    3b969ad6f147: Pull complete
    2074c6bfed7d: Pull complete
    Digest: sha256:e2c4e3751290e30c3f130ef3513c7999aee87b5e7ac91e2fc9f3addcdf1f1387
    Status: Downloaded newer image for rust:1.31
     ---> 6f61eb35ad91
    Step 2/5 : COPY app.rs .
     ---> 938d9a7ff71d
    Step 3/5 : RUN rustc app.rs -o app
     ---> Running in f25b678abb66
    Removing intermediate container f25b678abb66
     ---> 9e6749471396
    Step 4/5 : ARG LANG=FR
     ---> Running in 1d1cb6dd7206
    Removing intermediate container 1d1cb6dd7206
     ---> 3434f48a83da
    Step 5/5 : RUN ./app
     ---> Running in 1a17201ae1d1
    Hello World
    Removing intermediate container 1a17201ae1d1
     ---> 61a07c21cc05

    Successfully built 61a07c21cc05
    Successfully tagged tommoulard/my_rusty-container:latest
    ```

###### restart
Cette option permet de définir la politique de redémarrage du container en cas d'arrêt (segfault, erreur, ...).

Elle est définit comme:
```yml
restart: "no" # do not restart (default value)
restart: always # alway restart
restart: on-failure # restart only when there's an error (exit code != 0)
restart: unless-stopped # always restart the container except when the container is stopped (manually or otherwise)
```

Tips: utiliser `restart: unless-stopped` est souvent une bonne idée.

Par exemple:
 - On prend un fichier `docker-compose.yml` vierge, et on y met:
    ```yml
    version: '3.6'

    services:
      ubuntrash:
        image: ubuntu:20.04
        command: ["/bin/sh", "-c", "echo 'failure'", "&&", "exit 1"]
        restart: always
    ```
 - Si on build l'image, on obtient:
    ```bash
    >  docker-compose up
    Creating network "demo_default" with the default driver
    Pulling ubuntrash (ubuntu:20.04)...
    20.04: Pulling from library/ubuntu
    6a5697faee43: Pull complete
    ba13d3bc422b: Pull complete
    a254829d9e55: Pull complete
    Digest: sha256:fff16eea1a8ae92867721d90c59a75652ea66d29c05294e6e2f898704bdb8cf1
    Status: Downloaded newer image for ubuntu:20.04
    Creating demo_ubuntrash_1 ... done
    Attaching to demo_ubuntrash_1
    ubuntrash_1  | failure
    demo_ubuntrash_1 exited with code 0
    demo_ubuntrash_1 exited with code 0
    ```
 - Si on inspecte le container, on peut voir qu'il restart sans cesse:
    ```bash
    > docker-compose ps
             Name                   Command             State      Ports
    --------------------------------------------------------------------
    demo_ubuntrash_1   echo failure   Restarting
    > docker-compose logs ubuntrash
    Attaching to demo_ubuntrash_1
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ubuntrash_1  | failure
    ```

Tips: Si vous voulez arrêter le redémarrage en boucle des services, vous pouvez faire `docker-compose down`

###### network
Pour faire un lien réseau entre plusieurs container, on peut utiliser `networks`.

Si on veut connecter une base de données et un backend d'application, on peut les mettre sur le même network et ainsi pouvoir scale la DB autant que possible:
```mermaid
graph LR
    subgraph application-network
        r[redis]
        lb[Load  Balancer]
        lb --> b
        lb --> r
        subgraph db-network
            b[backend]
            db1[(Database)]
            db2[(Database)]
            db3[(Database)]
            b --> db1
            b --> db2
            b --> db3
        end
    end
```

Et comme docker c'est super cool, depuis l'intérieur d'un container, pour accéder aux autres containers, leur hostname corresponds à leurs noms (docker is love).

Si on fait `docker-compose up` avec ce fichier:
```yml
version: '3.6'

networks:
  demo-net:

services:
  python:
    image: python:3.9-alpine
    command: ["/bin/ping", "-c", "5", "my-nginx"]
    networks:
      - 'demo-net'

  my-nginx:
    image: nginx:alpine
    networks:
      - 'demo-net'
```

On obtient:
```bash
> docker-compose up
Recreating demo_python_1 ... done
Starting demo_my-nginx_1 ... done
Attaching to demo_my-nginx_1, demo_python_1
python_1    | PING my-nginx (172.23.0.2): 56 data bytes
python_1    | 64 bytes from 172.23.0.2: seq=0 ttl=64 time=0.095 ms
my-nginx_1  | /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
my-nginx_1  | /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/
my-nginx_1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
my-nginx_1  | 10-listen-on-ipv6-by-default.sh: error: IPv6 listen already enabled
my-nginx_1  | /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh
my-nginx_1  | /docker-entrypoint.sh: Configuration complete; ready for start up
python_1    | 64 bytes from 172.23.0.2: seq=1 ttl=64 time=0.151 ms
python_1    | 64 bytes from 172.23.0.2: seq=2 ttl=64 time=0.149 ms
python_1    | 64 bytes from 172.23.0.2: seq=3 ttl=64 time=0.141 ms
python_1    | 64 bytes from 172.23.0.2: seq=4 ttl=64 time=0.151 ms
python_1    |
python_1    | --- my-nginx ping statistics ---
python_1    | 5 packets transmitted, 5 packets received, 0% packet loss
python_1    | round-trip min/avg/max = 0.095/0.137/0.151 ms
demo_python_1 exited with code 0
^CGracefully stopping... (press Ctrl+C again to force)
Stopping demo_my-nginx_1 ... done
```

Tips, pour faire des connections en "One to many", on peut utiliser `links`

###### environment
Une best practice de docker-compose est de donner la configuration des process qui tournent dans un container par des variables d'environnement.

On peut utiliser les variables d'environnement de deux manières différentes:
```yml
environment:
    MYSQL_ROOT_PASSWORD: example
    # or
    - 'MYSQL_ROOT_PASSWORD=example'
```

Par exemple, le hub de [mysql](https://hub.docker.com/_/mysql), nous propose de faire:
```yml
version: '3.1'

services:

  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
```

Et après avoir fait `docker-compose up` et être allé sur [localhost:8080](http://localhost:8080). On peut voir qu'`adminer` nous permet de nous connecter avec les credentials `root:example` sur mysql et donc que le mdp par défaut de la base de donnée a été changé.

###### volumes
Comme nous avons vu précédemment, on aimerait bien garder des données sur le disk même si on supprime le container, et pour ça on utilise des volumes.

Les volumes virtuels sont bien mais il y a toujours des frictions pour les utiliser.

On a donc deux manières d'utiliser des volumes:
```yml
volumes:
  - 'data:/data' # With docker virtual fs
  - '/srv/data:/data' # Using a file or a folder using the absolute path
  - './data:/data' # docker-compose allows to use a relative path too
```

On les reconnait grâce au premier caractère:
 - si on a un `/` ou un `.`, c'est un fs local
 - sinon, c'est un file system virtuel

Pour reprendre l'exemple de mysql, si on a envi d'utiliser des volumes, on peut faire:
```yml
version: '3.1'

services:

  db:
    image: mysql
    command: --default-authentication-plugin=mysql_native_password
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: example
    volumes:
      - './mysql/data:/var/lib/mysql'

  adminer:
    image: adminer
    restart: always
    ports:
      - 8080:8080
```

Tips, si on utilise un chemin du fs courant, et que le dossier n'existe pas, docker va en créer un par défaut.

###### ports
Vous l'avez sûrement deviné avec l'exemple précédent, l'argument `port` permet d'exporter des ports.

Ils ont deux modes d'expressions:
```yml
ports:
  # shorts
  - "3000"
  - "3000-3005"
  - "8000:8000"
  - "9090-9091:8080-8081"
  - "49100:22"
  - "127.0.0.1:8001:8001"
  - "127.0.0.1:5000-5010:5000-5010"
  - "6060:6060/udp"
  - "12400-12500:1240"

  # long
  - target: 80
    published: 8080
    protocol: tcp
    mode: host
```

###### depends_on
`depends_on` permet de dire à docker-compose d'attendre que le container démarre avant de lancer le suivant. On pourrait avoir envie d'attendre que la base de données soit initialisée avant de lancer le container qui va l'utiliser.

Attention, `depends_on` n'attend pas l'initialisation complète du container, il attend qu'il soit lancé. Si vous voulez attendre l'initialisation, je vous conseille d'aller jeter un coup d'œil [ici](https://docs.docker.com/compose/startup-order/).

Par exemple, pour déployer un site en django, on pourrait avoir le fichier `docker-compose.yml`:
```yml
version: '3.6'
services:
  db:
    image: postgres
  django:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - '.:/code'
    ports:
      - '8000:8000'
    depends_on:
      - 'db'
```

##### Fichiers `.env`
`docker-compose` va lire automatiquement les fichiers `.env` que vous aurez dans vos dossier pour avoir des configurations plus explicites.

##### Aller plus loin
 - secrets
 - labels
 - user

#### CLI
 - `ps`
 - `up` / `down`
 - `build`
##### Aller plus loin
 - `scale`
 - Ne pas avoir tous ses services dans un seul fichier `docker-compose.yml`

# Projet
Pour s'amuser un peu après autant de cours, on va faire une petite mise en pratique.

Le but va être de compiler un projet en Go et de le déployer pour l'utiliser.

Pour cela, vous trouverez sur le [repository github](https://github.com/tomMoulard/gochain) tous les fichiers nécessaires pour faire le devoir.

![Fork](https://github-images.s3.amazonaws.com/help/bootcamp/Bootcamp-Fork.png)

Le rendu du devoir se fera sur github directement. Pour cela, il va falloir fork le [projet](https://github.com/tomMoulard/gochain) et réaliser le maximum d'étapes. Nous regarderons les forks du projets à la fin du temps impartit (cf le haut du TP). N'oubliez pas de push ce que vous faites !

Toutes les optimisations seront regardées et prises en compte, donc n'hésitez pas à en faire !

## Docker
La première étape va être de créer une image avec le binaire go.

Points bonus: Celui qui fait l'image la plus petite gagne !

### Compiler du go
Pour compiler un binaire go, il suffit de faire:
```bash
go get -d -v \ # to get dependencies
    github.com/lib/pq \
    github.com/julienschmidt/httprouter
go build -o a.out # to compile to a binary a.out
```

Tips, On peut compiler du go statiquement (avec toutes les librairies) avec quelques variations:
```bash
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o a.out
```

Ne pas oublier d'exposer un port (je conseille d'utiliser le port 8000).

## docker-compose
Après avoir créé l'image Docker, il faut l'utiliser.

On va donc devoir lancer le container, exposer les ports et configurer l'app.

L'application Go utilise 4 arguments (via les variables d'environnement):
 - `IP`: L'IP à utiliser pour l'application
 - `PORT`: Le port que va bind l'application
 - `POSTGRES_URL`: L'adresse de la base de données
 - `POSTGRES_PASSWORD`: Le mot de passe de la base de données

### Postgresql
Il faudra aussi, pour que l'application fonctionne, une base de données : `postgres`.

Il faudra donc que l'application go soit capable d'accéder à la base de données.

Et il faudra faire en sorte que les données (situées dans `/var/lib/postgresql/data`) soient préservées.

Tips, il n'y a pas besoin de toucher aux user de Postgres, il faut set le password.

## Test
Pour tester, il n'y a rien de mieux que de faire `docker build` puis `docker-compose up`, n'hésitez pas à le faire régulièrement.

Il ne faut surtout pas hésiter à poser des questions aux assistants, quitte à se faire debugger. Ils sont là pour ça. Ne les laissez pas s'ennuyer.

Pour aller voir l'interface web, il faut aller sur [localhost:8000](http://localhost:8000).

Pour ajouter des block dans la chaine, il faut faire:
```bash=
curl 0.0.0.0:8000/add -d '{"data":"Mon block, Vive les TCOMs"}'
```

## Bonus
Si vous avez tout fait, vous trouverez un fichier yaml `openapi.yml` qui est une spécification de type OpenAPI qui fonctionne avec [swagger](http://swagger.io/).
Vous pouvez tenter de lancer le service en même temps que le reste de l'application.

# Clean des images
Si vous voulez clean tout ce qui a été fait sur votre machine depuis le début du TP, vous pouvez:
 - `docker system prune`: cela permet de supprimer toutes les données de docker qui ne sont plus utilisées.
 - Désinstaller docker.

# Aller plus loin
 - [jessfraz](https://blog.jessfraz.com/): [contenerize everything](https://www.youtube.com/watch?v=cYsVvV1aVss)
 - [Analyse statique de vulnérabilités pour des containers](https://github.com/quay/clair), [HN: security best practices](https://cloudberry.engineering/article/dockerfile-security-best-practices/)
 - [Avoir son serveur maison](https://github.com/tommoulard/make-my-server)
 - Podman - BSD Jail - chroot - Linux-VServer - LXC - OpenVZ
 - [hadolint: Dockerfile linter](https://github.com/hadolint/hadolint)
 - [Faire des images multi architectures](https://blog.moulard.org/multi-arch-golang-app-docker/)
 - One of many [docker cheat sheet](https://gwendal.orinel.net/2019/06/27/docker_tricks/)
 - k8s, k3s, micro k8s
 - `docker run -it traefik/jobs`

![HitCount](https://hits.cyprin.eu/assistants/tcom/docker.svg)