docker
et docker-compose
Explications ici :)docker run hello-world
(pas de sudo
(c'est relou mais sécurisé))docker-compose --version
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.
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 ».
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.
A noter que docker n'est pas le seul outil de ce type, on peut aussi citer LXC
et Podman
par exemple.
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.
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.
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:
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.
Pour build une image Docker, il faut un fichier dockerfile
et build:
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/:
docker build -t my_awesome_image:version .
Explorons un peu les différentes instructions qui sont utiles. Vous pourrez trouver la spec 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).
Et ça ressemble à ça:
FROM <image>[:<tag>] [AS <name>]
Essayons:
Dockerfile
ceci:
FROM ubuntu:19.04
docker build .
On obtient ça:
> 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.
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":
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":
RUN ["executable", "param1", "param2", ...]
Tips: On peut utiliser des backslash \
pour faire des RUN
sur plusieurs lignes.
Essayons:
Dockerfile
de la dernière fois:
FROM ubuntu:19.04
FROM ubuntu:19.04
RUN cat /etc/os-release
docker build .
On obtient ça:
> 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:
> 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.
# 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:
echo "print('hello world')" > app.py
python
pour le Dockerfile
:
FROM python:3.8
COPY app.py .
RUN python app.py
docker build .
On obtient ça:
> 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
:
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 :
app.rs
:
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),
};
}
rust
pour le Dockerfile
:
FROM rust:1.31
COPY app.rs .
RUN rustc app.rs -o app
ENV LANG=EN
RUN ./app
docker build .
On obtient ça:
> 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:
> 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:
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
utilise ENTRYPOINT
pour démarrer le binaire et CMD
pour lui donner des paramètres.
Essayons de faire un hello world en rust :
ubuntu
pour le Dockerfile
:
FROM ubuntu:19.04
ENTRYPOINT ["bash"]
CMD ["--posix"]
docker build .
On obtient ça:
> 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.
# WORKDIR
WORKDIR /path/to/workdir
Un exemple de Dockerfile :
FROM ubuntu:19.04
WORKDIR a
WORKDIR b
WORKDIR c
RUN pwd
WORKDIR /d/e/f
RUN pwd
Et sa sortie :
> 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 :
# EXPOSE
EXPOSE <port> [<port>/<protocol>...]
#En pratique
EXPOSE 80/tcp
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:
> 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
HEALTHCHECK
USER
.dockerignore
# 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 .
# ....
Après la création de Dockerfile
, utilisons les vraiment !
build
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 dockerPour en savoir plus sur les options de build : Spec
Essayons avec l'image que nous avons fait
tag
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, 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 à :
haskell:8.8.4
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
# 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
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.
# 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:
docker login [OPTIONS] [SERVER]
run
docker run [OPTIONS] IMAGE[:TAG|@DIGEST] [COMMAND] [ARG...]
Docker run permet de lancer des conteneurs à partir d'une image.
# 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 plusUn autre exemple d'utilisation du run pour avoir un container Ubuntu pour faire des test:
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
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 :
# 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
docker ps [OPTIONS]
Docker ps permet de lister les conteneurs déployés
# 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
Tip: Pour arrêter tous les container qui sont en train de tourner, on peut faire : docker stop $(docker ps -q)
stop
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.
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 networkconnect
permet de connecter un container à un networkdisconnect
permet de déconnecter un container à un networkinspect
permet d'inspecter un network pour avoir des informations dessusls
permet de lister les networksrm
permet de supprimer un/des network(s).Woaw, vous avez atteint la moité de la partie cours du TP \o/
Vous savez maintenant utiliser docker (théoriquement) avec plein d'options.
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 :
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 :
docker-compose up
Ca vous parait magique ? Alors apprenons tous ensemble comment ça marche !
Encore une fois, la spec est disponible en ligne.
Comme pour utiliser docker, docker-compose utilise des fichiers docker-compose.yml
avec un formal yaml (wiki).
Il sont composés de plusieurs parties qui décrivent des requirements à match:
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:
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:
docker-compose.yml
:
version: '3.6'
> 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.
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:
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:
docker-compose.yml
du dernier exemple:
version: '3.6'
version: '3.6'
networks:
network-name:
> docker-compose up
WARNING: Some networks were defined but are not used by any service: network-name
Attaching to
>
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:
Pour cela, on peut faire:
volumes:
volume-name:
external: false
Sachant que l'on peut utiliser des file system distants, mais c'est pas fou.
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:
version: '3.6'
networks:
network-toto:
volumes:
volume-tata:
services:
mon-service_1:
...
mon-service_2:
...
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:
version: '3.6'
networks:
network-name:
Et lui ajouter un service avec un nom d'image:
version: '3.6'
networks:
network-name:
services:
swagger:
image: swaggerapi/swagger-ui
Et on exécute:
> 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:
docker-compose
va pull l'image directement.Ctrl-d
ou exit
) le container est stoppé.La directive build
va permettre à docker-compose de décrire à docker comment build son image, tous les champs de docker build
.
build:
context: ./dir
dockerfile: Dockerfile-alternate
args:
buildno: 1
On peut donc spécifier:
Dockerfile
Essayons:
dockerfile
pour le hello world en rust (ne pas oublier le fichier app.rs
):
FROM rust:1.31
COPY app.rs .
RUN rustc app.rs -o app
ARG LANG
RUN ./app
docker-compose.yml
pour build:
version: '3.6'
services:
my_rusty_docker:
image: tommoulard/my_rusty-container
build:
context: .
dockerfile: Dockerfile
args:
LANG: EN
> 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
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:
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:
docker-compose.yml
vierge, et on y met:
version: '3.6'
services:
ubuntrash:
image: ubuntu:20.04
command: ["/bin/sh", "-c", "echo 'failure'", "&&", "exit 1"]
restart: always
> 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
> 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
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:
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:
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:
> 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
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:
environment:
MYSQL_ROOT_PASSWORD: example
# or
- 'MYSQL_ROOT_PASSWORD=example'
Par exemple, le hub de mysql, nous propose de faire:
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. 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é.
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:
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:
/
ou un .
, c'est un fs localPour reprendre l'exemple de mysql, si on a envi d'utiliser des volumes, on peut faire:
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.
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:
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
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.
Par exemple, pour déployer un site en django, on pourrait avoir le fichier docker-compose.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'
.env
docker-compose
va lire automatiquement les fichiers .env
que vous aurez dans vos dossier pour avoir des configurations plus explicites.
ps
up
/ down
build
scale
docker-compose.yml
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 tous les fichiers nécessaires pour faire le devoir.
Le rendu du devoir se fera sur github directement. Pour cela, il va falloir fork le projet 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 !
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 !
Pour compiler un binaire go, il suffit de faire:
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:
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).
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'applicationPORT
: Le port que va bind l'applicationPOSTGRES_URL
: L'adresse de la base de donnéesPOSTGRES_PASSWORD
: Le mot de passe de la base de donnéesIl 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.
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.
Pour ajouter des block dans la chaine, il faut faire:
curl 0.0.0.0:8000/add -d '{"data":"Mon block, Vive les TCOMs"}'
Si vous avez tout fait, vous trouverez un fichier yaml openapi.yml
qui est une spécification de type OpenAPI qui fonctionne avec swagger.
Vous pouvez tenter de lancer le service en même temps que le reste de l'application.
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.docker run -it traefik/jobs