Crafteo - Docker Training

Bienvenue sur les exercices de la formation Docker ! Suivre les liens du menu pour y accéder aux exercices.

Plus d'info sur crafteo.io et blog.crafteo.io

Docker - basics

docker --help|-h
docker [COMMAND] --help
docker run --help
docker image -h

# Run a container
docker run [OPTIONS] IMAGE

# List existing containers
> docker ps [-a]

# Show container logs
> docker logs [OPTIONS] CONTAINER

# start, stop, restart containers
> docker start|stop|restart CONTNER

Exercices

Lancer un container utilisant l'image httpd:alpine (image officielle Apache HTTP server) en mode détaché (daemon) et le nommer myapache

  • docker run devra rendre la main et afficher un ID de container

Vérifier que myapache existe et est actif

  • Le container doit être au status Up

Afficher les 2 dernières lignes de logs de myapache et suivre les changements


Arrêter puis redémarrer le container myapache

  • Plusieurs solutions possible utilisant le même set de commandes

Supprimer le container myapache

  • Des actions préalables peuvent être requises

Docker - exposing container ports

# expose host port 8080 on container port 80
docker run -d --name myport -p 8080:80 httpd:alpine

# try it out!
curl localhost:8080

# help may always be useful
docker run -h

Exercices

Lancer 2 containers httpd:alpine exposant chacun leur port 80 sur des ports différents (tel que 8081 et 8082) et vérifier le fonctionnement

  • l'option -d peut être utile pour lancer le container en background
  • curl localhost:8081 et localhost:8082 permettront de tester avec l'un et l'autre
  • possible aussi de tester avec votre navigateur web

Lancer un container httpd:alpine utilisant le réseau hôte directement et tester

  • Apache se lancera directement sur le réseau de la machine hôte en se bindant au port 80
  • Si le port 80 de la machine hote est déjà utilisé par un autre processus il y aura un conflit de port
  • Pour voir les process utilisant le port 80 sur la machine hote:
    sudo netstat -lnap | grep -e ":80\s"
    

Docker - misc commands

Exercices

Puller l’image httpd taggée 2.4.41-alpine et lancer un nouveau container en mode détaché sans le nommer

  • docker pull -h, si jamais...
  • penser à récupérer l'identifiant unique de votre container donné en sortie de docker run

Renommer le container lancé précédemment en myalpine en utilisant son identifiant unique (pas son nom)

  • l'occasion de découvrir une nouvelle commande?

Afficher l’ensemble des processus du container myalpine

  • équivalent de Linux top ou ps pour un container Docker

Inspecter le container myalpine et trouver la commande lancée au démarrage

  • il s'agit du processus qui sera lancé au démarrage du container

Obtenir les statistiques d’usage ressources (CPU, RAM...) du container

  • pratique pour débugger dans certain cas

Mettre à jour la configuration du container myalpine pour le redémarrer automatiquement lors du boot de la machine

Docker - exec and co.

# Execute command inside a container
docker exec [OPTIONS] CONTAINER COMMAND [ARG...]

# delete /tmp/somefile in container
docker exec mycontainer rm /tmp/somefile

# need help?
docker exec -h 

Exercices

Lancer un container httpd:alpine exposant 8083:80 et y ouvrir une session shell interactive avec pseudo-TTY et explorer le système de fichier (i.e. filesystem ou FS)

  • le résultat est équivalent à ouvrir une session SSH sur une machine virtuelle... même si le fonctionnement n'a absolument rien à voir
  • le FS de votre container est-il le même que celui de votre machine?

Modifier le fichier htdocs/index.html dans le container et vérifier ce que renvoi localhost:8083

  • Utiliser une commande echo "Test content" > index.html ou vi
  • Que se passe-t-il si on lance un autre container utilisant la même image? Lancer un autre container httpd:alpine exposant 8084:80 et comparer!

Lister les process du container lancé précédemment depuis le container lui-même. Vérifier si ce même processus est visible depuis la machine hôte.

  • ps -ef permet d'afficher les processus machine et/ou du container
  • pstree -p permet d'afficher l'ensemble des process sous forme d'arbre parent/enfant

Docker - File System

Les fichiers de nos containers et images ne sortent pas de n'importe ou!

Exercices

Lancer un container httpd:alpine nommé filesystem et y créer un fichier. Trouver l'emplacement des fichiers du container sur la machine hôte.

  • docker exec -it filesystem bash avec touch somefile pour créer un fichier
  • docker exec filesystem sh -c 'touch somefile' pour les pros ;)
  • docker -h est toujous notre ami!

Trouver l'emplacement des fichiers de l'image httpd:alpine sur la machine hôte.

  • il est conseillé de ne toucher à ces fichiers qu'avec les yeux...

Docker - bind mount

# mount file (or folder) from ./some-data in container at /example/mydir
# short syntax: -v SOURCE:DEST
docker run -v ./some-data:/example/mydir -d httpd:alpine

# long syntax, equivalent of short syntax
# recommended by Docker but both work just as fine
docker run --mount type=bind,source=/tmp/myfile,target=/myfile -d httpd:alpine

# on oublie pas... 
docker run -h

Exercices

Créer un dossier apache/ avec un fichier apache/index.html contenant "Hello Docker!"

# Example
mkdir apache
echo 'Hello Docker!' > ./apache/index.html

Lancer un container httpd:alpine montant le fichier ./apache/index.html sur htdocs/index.html du container (et exposant 8085:80)

  • le chemin complet de destination sera peut-être nécéssaire
  • tester avec curl localhost:8085 ou votre navigateur
  • lancer une session shell dans le container et modifier le fichier index.html, re-tester localhost:8085 et constater les changements
    # rappel: lancer une session shell
    docker exec -it mycontainer sh
    
  • modifier le fichier apache/index.html directement depuis la machine locale, re-tester localhost:8085 et constater les changements

Même exercice mais monter le dossier complet ./apache/ dans le container afin de pouvoir écraser l'index.html du container et obtenir le même résultat que précédemment

Docker - Volumes

docker volume -h
# should be sufficient for this one! 

Exercices

Créer un volume Docker nommé httpd_vol

  • cette étape est optionnelle pour la suite, mais il serait intéréssant de comprendre pourquoi

Lancer un container httpd:alpine nommé containervol montant le volume Docker htdocs_vol sur /usr/local/apache2/htdocs. Vérifier le contenu du dossier htdocs dans le container une fois démarré.

  • que se passe-t-il si le Volume n'existe pas au préalable?
  • afficher les montages avec df -h ou mount donne des infos intéréssantes

Modifier le contenu de htodcs/index.html dans le container containervol puis stoppez et supprimer le container containervol. Relancer un nouveau container nommé containervol2 montant httpd_vol

  • plutôt pratique pour backuper et manipuler des données lorsqu'une base de donnée type Mongo ou MySQL tourne en containerisée

Lancer un container basé sur busybox montant httpd_vol en parallèle de containervol2 et faites des manipulations sur le fichier dans chacun des containers

Docker - build

# Build an image with a Dockerfile
docker build [OPTIONS] CONTEXT_PATH

# Use Dockerfile by default with context from current directory (.)
docker build .

# did you miss me?
docker build -h

# Example Dockerfile content to build a custom Apache image

# Image used as base to create our new image
FROM alpine:3.20

# Run some commands to install apache
RUN apk add apache2

# define commands which will be run on startup
CMD ["-DFOREGROUND"]
ENTRYPOINT ["httpd"]

Exercices

Créer un dossier mybuild et y créer un fichier Dockerfile avec le contenu de l'exemple ci-dessus. Builder une image Docker à partir du Dockerfile et la tagger myapache:1.0

  • votre image doit apparaitre via docker images

Lancer un container utilisant votre image myapache et exposer 8089:80, vérifier le fonctionnement

  • rappel: curl localhost:8089 ou navigateur
  • essayer de monter des volumes pour obtenir un fichier index.html customisé!

Créez un fichier index.html avec un contenu customisé, par exemple echo "Hello from file" > index.html.

Modifier votre Dockerfile pour y ajouter une instruction COPY permettant de copier index.html à l'emplacement /var/www/localhost/htdocs/ (emplacement par défaut du package Alpine apache2, différent de l'image officiel httpd).

Testez le fonctionnement de votre image en lançant un container.

Docker Compose - basics

Nous utiliserons l'application Example Voting App pour l'exercice. Elle se trouve dans le dossier ~/example-voting-app.

Le fichier docker-compose.yml décrit l'ensemble des services définissant notre stack Docker Compose:

  • Vote: permet de voter pour Chien ou Chat
  • Result: permet de voir les résultats des votes
  • Worker: Traite les votes pour les insérer en base de donnée
  • Redis: gère les votes en cours via un système de queue
  • DB (Postgres): stocke les résultats

Nous utiliserons cette application pour illustrer nos exemples et exercices.

Documentation de référence:

🔖 Conseil: Ajoutez des liens à fos favoris, ils seront utiles !

Note: la CLI docker-compose standalone a été dépréciée en faveur de docker compose


Exercices

Lancement d'une stack Docker Compose

  • Utiliser la CLI docker compose pour lancer la stack en mode détachée
    docker compose --help
    

Les services Vote et Result expose une interface web accessible depuis la machine locale une fois lancés. Trouver dans le fichier docker-compose.yml les bindings de ports utilisés pour y accéder via votre navigateur.


Configuration de services Docker Compose

Modifier le service Redis pour:

  • Utiliser l'image redis:7.2.1
  • Nommer le container my_redis au démarrage
  • Ajouter une variable d'environnement FOO=BAR

Fichier .env et variables d'environnement

Il est possible de référencer des variables d'environnements dans notre fichier docker-compose.yml et de spécifier un fichier .env contenant des variables d'environnements par défaut. Par exemple:

# .env example
DOTENV_POSTGRES_USER: "postgres"
DOTENV_POSTGRES_PASSWORD: "postgres"

Créer un fichier .env et modifier le service db pour utiliser les valeurs de ce fichier plutôt que des valeurs hardcodées.

Healthcheck & depends on

Faisons en sorte de démarrer le service Worker après nous êtes assuré que la base de donnée Postgres soit disponible:

Ajouter une instruction healthcheck au service db:

  • Faire un bind mount du script resources/healthchecks/postgres.sh dans le container
  • Configurer healthcheck pour lancer le script toutes les 5s afin de vérifier la "healthiness" du service

Build avec Docker Compose

docker compose build permet de lancer des builds d'images Docker en spécifiant le dossier de contexte, le Dockerfile, etc. de façon similaire à docker build.

Exercices

Les Dockerfile des services result et worker sont présents dans des sous-dossiers correspondants à leurs noms (i.e. result/Dockerfile) mais le build des images n'est pas managé par Docker Compose avec la configuration actuelle.

Modifier la configuration des services result et worker dans docker-compose.yml pour indiquer l'emplacement de build à Docker Compose.

Docker Compose CLI

La plupart des commandes docker ont leurs équivalents avec docker compose, mais leurs fonctionnalités sont adaptées à la gestion de stack multi-container et les fonctionnalités ne sont pas toujours équivalentes.

# Rappel
docker compose --help

Exercices

Utiliser une commande permettant de puller l'ensemble des images de la stack en parallèle.

  • Permet de gagner du temps lors du pull de nombreuses images

Lancer la stack Compose avec les options suivantes:

  • Mode détachée (Tout comme docker, docker compose lance les containers en mode interactif par défaut)
  • Forcer la récréation des containers déjà existants

Lancer la stack puis modifier le fichier docker-compose.yml pour changer le port exposé de result pour 5002. Appliquer les changements à la stack avec une commande docker compose.


Quelques manipulations:

  • Arrêter, démarrer puis re-démarrer la stack
  • Lister les images utilisées par la stack
  • Lister les containers de la stack
  • Afficher les logs de l'ensemble des containers de la stack
  • Afficher les logs d'un container de la stack et suivre les changements
  • Executer une session shell interactive dans le container vote
  • Arrêter et supprimer la stack, puis ne lancer que le service vote
  • Arrêter et supprimer la stack

Ces commandes seraient possibles directement avec docker en y spécifiant les options requises. docker compose intéragi avec le Daemon Docker tout comme docker.


Plusieurs stacks peuvent coéxister en s'assurant qu'il n'y a pas de conflits de ports ou autre.

Copier le fichier docker-compose.yml et nommer cette copie docker-compose-bis.yml puis lancer 2 stacks en parallèle

  • Celle utilisant docker-compose.yml doit être nommé app
  • Celle utilisant docker-compose-bis.yml doit être nommée app-bis
  • Attention aux conflits de nom de container et ports: le nom des containers doivent être uniques ainsi que les ports exposées sur la machine

Docker Compose - ateliers avancés

Volumes

Configuration de Volume: configurer un volume persistant pour le service postgres. Il doit être conservé même en cas de destruction du container ou de la stack Compose.

  • Ajouter un volume db-data
  • Configurer le service db pour utiliser le volume db-data à l'emplacement /var/lib/postgresql/data

Réseaux

Configuration de Réseaux:

  • Ajouter un réseau app-network
  • Configurer l'ensemble des services pour que les containers soient associés au réseau app-network et relancer la stack

Variables d'environnement

Docker Compose permet de définir un fichier de variable d'environnement global qui seront affectées à chaque container.

  • Trouver dans la documentation la fonctionnalité correspondante
  • Remplacer les variables d'environnement du service db dans docker-compose.yml et par le fichier de variable d'environnement utilisé par Compose

Docker Compose permet de définir des variables qui seront substituées aux variables d'environnement.

  • Modifier docker-compose.yml pour définir la version du service redis pour qu'elle utilise la variable d'environnement REDIS_VERSION si possible, et alpine par défaut:
  • Définir la variable d'environnement REDIS_VERSION=5.0.7-alpine et relancer la stack. Vérifier que le container redis utilise l'image redis:5.0.7-alpine

Override docker-compose.yml

Docker Compose permet d'utiliser plusieurs fichiers de configuration .yml pour étendre une configuration de base.

  • Trouver dans la documentation la référence à ce méchanisme d'extention des configurations
  • Créer un fichier docker-compose.dev.yml étendant docker-compose.yml tel que:
    • le service vote monte le code source vote/ à l'emplacement /app
    • le service vote utilise la commande python -m flask run -h 0.0.0.0 -p 80
    • le service vote dispose des variables d'environnements:
      FLASK_APP=app.app
      FLASK_ENV=development
      
  • Relancer la stack en combinant docker-compose.yml et docker-compose.dev.yml
  • Renommer docker-compose.dev.yml de façon à pouvoir lancer la stack avec docker-compose.yml et le fichier d'override avec uniquement la commande docker compose up -d sans aucune autre option

Cette configuration permet de monter le code source de l'application Vote (Python) directement dans le container et de relancer l'application dynamiquement au sein du container sans avoir à re-builder ou redémarrer le container.

Cette méthode utilise les fonctionnalités de Flask (serveur web Python) mais la plupart des frameworks permettent un setup similaire pour les environnement de développement.

Il est ainsi possible de définir une configuration de base Docker Compose avec des overrides selon différent contextes (dev, CI, production, etc.)

Bridge networking

Quelques exercices sur Bridge networking avec Docker.

Exercices

Lancer la stack docker-compose.yml. Par défaut, un réseau est créé et attaché à chaque container.

  • Identifier le réseau bridge créé et utilisé par la stack par défaut
    • Utiliser la CLI Docker: docker network --help
  • Identifier l'ensemble des réseaux actuellement existant et le Driver utilisé par chacun
  • Inspecter le réseau utilisé par la stack Compose et:
    • Trouver la liste des containers associés au réseau
    • Identifier le subnet utilisé par le réseau

Quid de l'isolation des réseaux Bridge? Par défaut, les containers sur un même réseau sont joignables par leur nom (i.e. le container vote est joignable via le hostname vote). Docker effectue une résolution DNS interne.

Isolons les containers de notre stack:

  • Configurer les réseaux vote-net et result-net
  • Isoler vote, redis, worker dans le réseau vote-net
  • Isoler worker, db et result dans le réseau result-net
  • Note: le container worker sera dans 2 réseaux: vote-net et result-net

Seul worker pourra communiquer avec chaque container. vote / redis et db / result seront mutuellement isolés dans leurs réseaux respectifs.

Vérifier la non-connectivité entre le container vote et result

  • Lancer une session shell sur vote et essayer de joindre result
    docker exec -it vote sh
    
    # Need curl and/or ping ?
    # Use 'apk add curl iputils-ping' 
    # or 'apt update && apt install -y curl iputils-ping'
    
    # Try to connect
    $ curl result
    
    # Check DNS resolution
    $ getent hosts result
    $ getent hosts worker
    

Docker peut aussi connecter et déconnecter des containers d'un réseau à la volée (sans besoin de redémarrer ou recréer un container).

  • Connecter le container vote manuellement au réseau result-net
  • Vérifier à nouveau la connectivité
  • Déconnecter le container vote de result-net

Configuration réseau customisée avec Docker Compose

  • Ajouter un réseau bridge my-bridge dans docker-compose.yml et configurer chaque container pour utiliser ce réseau
  • Modifier la configuration de my-bridge pour:
    • Forcer l'utilisation du driver réseau bridge
    • Affecter un nom spécifique named-bridge
    • Utiliser le subnet 172.42.0.0/16
  • Appliquer les configurations et inspecter le réseau named-bridge pour vérifier les configurations

Il est aussi possible d'utiliser un réseau déjà existant par ailleurs:

  • Créer un réseau Bridge nommé my-external-network
    • docker network --help
  • Configurer les services pour utiliser ce réseau déjà existant et appliquer les configurations

Bridge networking - advanced

Recherchons comment Docker intéragi avec le système Linux pour manager les réseaux et interfaces:

  • Lancer la stack Compose
  • Trouver l'interface réseau Linux sous-jacente du réseau Docker utilisé par la stack
    • Les interfaces réseaux sont les devices Linux tels que eth0, lo (loop local), etc.
    • Les commandes nmcli device status ou ifconfig permettent d'afficher les interfaces réseaux
  • Trouver l'entrée de la table de routage permettant de rediriger les packets réseaux vers cette interface réseau
    • Utiliser ip route ou netstat -rn

Host networking

Configurer docker-compose.yml pour:

  • Utiliser le network host pour chacun des services
  • Lancer la stack et vérifier que chacun des services est Up
  • Se connecter au service Vote et Worker pour vérifier leur fonctionnement
  • Trouver l'adresse IP du container worker - si elle existe

  • Identifier le réseau Docker utilisant le driver host
  • Essayer de créer un autre réseau Docker utilisant le driver host

Bind Mount avancé

Exercices

Besoin: afin de conserver les données PostgreSQL sur la machine locale même si le container est supprimé et pour faciliter le processus de backup, vous cherchez une solution pour que les données de la BDD soient persistées. Un Bind Mount vous parait une bonne solution.

  • Configurer le service db pour monter le dossier local /home/docker/db-data à l'emplacement /var/lib/postgresql/data
  • Redémarrer le container de la stack pour prendre en compte les configurations
  • Observer le contenu du dossier /home/docker/db-data

Besoin: vous devez configurer plus finement la base de donnée via un fichier de configuration postgresql.conf fourni par votre administrateur système et qui doit être utilisé par la BDD:

# Full content of postgresql.conf to be mounted in container
listen_addresses = '*'
temp_file_limit = 1000
  • Trouver l'emplacement auquel le fichier doit être monté dans le container db basé sur l'image postgres
    • La documentation des images Docker disponible sur Docker Hub indique généralement ces use-cases typiques
    • Pour postgres il faudra passer un flag de configuration au binaire lancé au démarrage du container
  • Monter le fichier de configuration via un Bind Mount avec les contraintes:
    • le fichier de configuration doit être monté en read-only (lecture seule)
    • Définir l'option Bind Propagation à rprivate
  • Appliquer les modifications sans redémarrer le container, à la place envoyer un signal SIGHUP au container pour reloader la configuration

De nombreux applicatifs utilisent SIGHUP pour recharger une configuration sans avoir à faire un redémarrage complet, cette feature n'est pas spécifique à Docker mais néanmoins très pratique.


Besoin: vous souhaitez configurer une procedure de backup des données via un au container postgres qui fera un dump régulier de la BDD

Lancer un container utilisant permettant d'effectuer un backup:

  • utiliser la même image que le service db
  • monter un dossier spécifique permettant de persister les backups (par ex. un bind mount $PWD/backup:/backup)
  • supprimer automatiquement le container lorsqu'il s'arrêtera
  • lancer directement la commande de backup, par exemple:
    PGPASSWORD=postgres pg_dumpall -h db -c -U postgres -w > /backup/my-backup.sql
    
    • Attention: pour résoudre le nom d'hôte db le container de backup doit se trouver sur le même réseau.

Il est possible de monter le même dossier/fichier sur plusieurs containers, pratique dans divers situations comme le backup de données

Bonus: configurer une tâche cron qui lancera toutes les heures notre backup. Pour créer une tâche cron:

# run cron editor for user
crontab -e

# specify cronjob to run every hour
0 * * * *   COMMAND 

Docker Volumes

Exercices sur les volumes Docker. Quelques rappels:

# as usual
docker volume --help
docker volume create --help

# create a volume named myvolume
docker volume create myvolume

# run a container and attach a volume
docker run -v myvolume:/data some_image

Exercices

Besoin: vous souhaitez gérer les données Postgres via un volume plutôt qu'un bind mount afin de limiter l'accès aux données depuis la machine locale et faciliter les procédures de backup

Modifier docker-compose.yml pour:

  • configurer un volume psqsl-data et
  • monter ce volume à l'emplacement /var/lib/postgresql/data du service db
  • redémarrer le service db pour prendre en compte les modifications

Une fois effectué:

  • Lister les volumes existants et trouver le volume créé par Docker Compose
  • Trouver l'emplacement des données du volume sur le disque local

Cleanup: supprimer la stack Docker Compose et les volumes utilisés par Postgres

Montage tmpfs

Exercices sur le montage de volume de type tmpfs*

Exercices

Contexte: les données Redis sont dans notre cas considérées comme éphémères et ne doivent pas être persistées sur le disque ou dans le writable layer du container. De plus, pour optimiser les performances les données Redis doivent être stockées en mémoire directement.

Modifier docker-compose.yml pour:

  • configurer un volume de type tmpfs sur le service redis à l'emplacement /data
  • limiter la taille du volume à 500Mo
  • redémarrer le service pour prendre en compte les modifications

Lancer une session shell dans le container redis et:

  • créer un fichier test
    echo test > /data/test 
    
  • redémarrer le container
  • vérifier l'éxistence du fichier précédemment créé

Container Layer data

Quelques exercices de manipulation des données d'un container

Exercices

Lancer une session shell dans le container vote (docker exec -it vote sh) et:

  • Créer un fichier /test.txt avec le contenu test
    echo test > /test.txt
    
  • Editer le fichier app.py et remplacer les options A et B Cat et Dog par Windows et Mac
    • installer un editeur avec apt update && apt install nano si besoin (ctrl+O: sauvegarder et ctrl+X: exit)
  • Quitter et redémarrer le container
  • Se connecter à localhost:5000 et constater les changements

Il est fréquent d'avoir à éditer des fichiers au sein d'un container dans des environnements de dev ou test, et d'y installer des utilitaires "on the fly" - c'est cependant déconseillé en production: les changements seraient perdus en cas de re-création du container et pourrait modifier le comportement du container.


Inspecter le container vote et trouver l'emplacement des données du layer container sur le disque local.

Il est possible de modifier directement les données depuis le disque... mais c'est très fortement déconseillé et risque la corruption de l'installation Docker!


Supprimer le container vote et le récréer puis:

  • Lancer une session shell dans le container et vérifier si l'état des fichiers /test.txt et app.py
  • Vérifier l'existence des données du container layer sur le disque

Copy

CLI Docker: images

# list images
docker images # mind the plural!
docker image ls

# S.O.S
docker image -h

# pull an image
docker images pull redis:alpine
docker pull redis:alpine

Exercices

Inspecter l'image vote de votre stack Example Voting App et trouver l'emplacement sur le disque des fichiers de l'image.


Tagger l'image vote de votre stack en vote:newtag et configurer votre stack pour l'utiliser


Afficher et comparer l'historique de build de l'image vote de base et vote:newtag


Import/export d'image

Une image Docker n'est rien d'autre qu'une archive contenant des fichiers. Exportons notre image sous forme d'archive avant de la ré-importer comme image Docker.

  • Exporter l'image vote:newtag comme archive
    • Trouver la commande adapée via docker image --help
    • Cette action peut prendre quelques secondes...
  • Supprimer l'image Vote de votre système local
    • L'image ne doit plus apparaitre avec docker images
    • Si besoin, il sera possible de la re-builder from scratch
  • Re-importer l'image Vote depuis l'archive créée précédemment
    • docker images doit affiche l'image

Lancer un container basé sur vote:newtag et ouvrez une session bash dans le container (docker exec ...) pour modifier le contenu du fichier /app/app.py afin de modifier les options Cat/Dog pour Windows/Mac (les variables options_a|b) Redémarer le container pour constater les changements.

Il est possible de créer une image Docker directement à partir d'un container (sans passer par docker build). Ce principe est équivalent à créer une image de VM via un snapshot de VM existante afin de lancer des clones de la VM d'origine.

Créer une nouvelle image à partir du container modifié précédemment sans passer par docker build, tagger cette image voting-app:from-container puis démarrer un container à partir de celle-ci et tester.

Voting App: Build Python image

L'appplication Voting App dispose de plusieurs services:

  • Worker: récupère les votes et les stockes en base de donnée
  • Vote: application web permettant de voter
  • Result : permet d'afficher les résultats

Objectif de l'exercice: écrire un Dockerfile pour l'application Vote.

Lien utile: Dockerfile reference - l'ensemble des instructions utilisables dans un Dockerfile

Exercice - Dockerfile pour le service Vote

Le code du service Vote se trouve dans vote/:

  • app.py est le fichier applicatif permettant de lancer l'application
  • requirements.txt contiens les dépendences de l'application

Pour l'instant le service utilise une image Docker déjà buildée:

services:
  vote:
    image: crafteo/example-voting-app-vote

Nous allons faire en sorte de builder notre propre service Vote selon les contraintes suivantes:

  • Utiliser Python 3.9 ou plus récente

    • Chercher sur Docker Hub une image Python correspondante
  • L'ensemble du code source du service (fichiers dans /vote) doit être copiée dans l'image Docker

  • L'image Docker doit contenir l'ensemble du service. Pour installer les dépendences, utiliser la commande

    pip install -r requirements.txt
    
  • La commande qui doit être lancée au démarrage du container est:

    gunicorn app:app -b 0.0.0.0:80
    

    et doit être éxecutée depuis le dossier contenant l'ensemble du code source du service (le service exposera le port 80 par défaut une fois lancé)

Exercices:

  • Ecrire un fichier vote/Dockerfile permettant de builder l'image du service Vote
  • Builder votre image Vote et la tagger vote:local (mettre à jour la docker-compose.yml en conséquence)
  • Lancer la stack avec votre nouvelle image vote

Optimization de build

Problèmes typiques de build:

  • temps de build (notamment download des dépendances)
  • taille finale de l'image (temps de push/pull)
  • contenu de l'image: ne pas y copier des fichiers par accident (secret, token, etc.)

Documentation de référence:

Exercices

  • Ajouter un fichier .dockerignore permettant de filtrer Dockerfile du contexte
  • Changer les instructions de build pour n'invalider le cache de pip install qu'en cas de modification de requirements.txt
  • Utiliser une image alpine pour réduire la taille finale de l'image

ENTRYPOINT vs CMD

Tableau de correspondance ENTRYPOINT vs. CMD: Voir la documentation Docker


Exercice: définir ENTRYPOINT et CMD

Définir ENTRYPOINT et CMD afin d'avoir une commande lancée par Docker dans le container répondant à divers contraintes.

Exemple

# Le container sera lancé avec la commande
#   gunicorn app:app -b 0.0.0.0:80
CMD [ "gunicorn", "app:app", "-b", "0.0.0.0:80" ]

Pour tester:

# Update vote Dockerfile and build
docker compose build vote

# Run vote container
# COMMAND and ARG will override default COMMAND (CMD)
docker run -d --name test_entrypoint --rm vote:local [COMMAND] [ARG...]
# or
docker compose run vote [COMMAND] [ARG...]


# Check running processus
docker top test_entrypoint -o pid,command
# or
docker compose top vote -o pid,command
# Output like
#   PID   COMMAND
#   7767  /usr/local/bin/python /usr/local/bin/gunicorn app:app -b 0.0.0.0:80

# stop and remove container
docker stop test_entrypoint
# or
docker compose down vote

Cas 1

La commande lancée au démarrage par défaut doit être:

gunicorn app:app -b 0.0.0.0:80

Il doit être possible d'overrider les options de gunicorn définies par défaut (i.e. overrider l'usage de -b 0.0.0.0:80)

Résultat attendu:

#
# Sans argument supplémentaire
#
docker compose run vote
#
# gunicorn app:app -b 0.0.0.0:80
#

#
# Arguments: --log-level DEBUG
#
docker compose run vote --log-level DEBUG
#
# gunicorn app:app --log-level DEBUG
#

Cette configuration peut-être utilisée pour fournir une image lançant notre serveur Vote en permettant à l'utilisateur final de passer à gunicorn des options différentes (comme un port différent ou un niveau de debug plus élévé)

Cas 2

La commande lancée au démarrage par défaut doit être:

gunicorn app:app -b 0.0.0.0:80

Il doit être possible de passer des options supplémentaires à gunicorn tout en conservant l'usage de -b 0.0.0.0:80 par défaut

Résultat attendu:

#
# Sans argument supplémentaire
#
docker compose run vote
#
# gunicorn app:app -b 0.0.0.0:80
#

#
# Arguments: --log-level DEBUG
#
docker compose run vote --log-level DEBUG
#
# gunicorn app:app -b 0.0.0.0:80 --log-level DEBUG
#

Cette configuration peut-être utilisée pour fournir une image lançant notre serveur Vote en permettant à l'utilisateur final de passer à gunicorn des options différentes (comme niveau de debug plus élévé) tout en semi-forçant l'utilisation de certaines options (dans notre cas, l'utilisation du port 80)

Cas 3

La commande lancée au démarrage par défaut doit être:

gunicorn app:app -b 0.0.0.0:80

Passer un argument au run du container doit overrider intégralement la commande lancée par le container par celle en paramètre.

Résultat attendu:

#
# Sans argument supplémentaire
#
docker compose run vote
#
# gunicorn app:app -b 0.0.0.0:80
#

#
# Arguments: --log-level DEBUG
#
docker compose run vote sh
#
# Lance une session shell interactive
#

Cette configuration peut-être utilisée pour fournir une image lançant notre serveur Vote en permettant à l'utilisateur final de lancer un binaire ou une commande différent au lancement du container.

Instruction de build Dockerfile avancées

De nombreuses instructions de build existent pour Dockerfile.

La documentation officielle Dockerfile reference référence l'ensemble des instructions disponibles

Rappel: commandes de build

# builder l'image vote
docker build -t vote:builder vote

# lancer l'image vote
docker run -d --name builder1 vote:builder

# lancer une session shell dans le container
# sera utile pour tester vos manipulations!
docker exec -it builder1 bash

# Supprimer un container et une image
docker stop builder1
docker rm builder1
docker rmi vote:builder

Exercice


Configurer l'image pour que l'utilisateur crafteo soit utilisé pour lancer le processus principal

  • Pour créer un utilisateur crafteo sous Linux Alpine:
    
    adduser -S crafteo
    

Il sera nécéssaire d'ajouter le chemin /home/crafteo/.local/bin au PATH du user crafteo pour éxecuter les binaires installés par Python.

  • Ajouter une instruction au Dockerfile permettant de définir la variable PATH tel que:
    PATH=$PATH:/home/crafteo/.local/bin
    

Attention: il n'est pas possible de binder un port <1024 avec un utilisateur non-root avec Linux, il sera nécéssaire d'utiliser un port plus élevé. Modifier votre Dockerfile pour lancer l'application en utilisant le port 8080.


Ajouter un healthcheck permettant de vérifier le fonctionnement de l'image avec curl localhost:80. Ce healthcheck permettra de vérifier que l'image est bien active si une réponse est renvoyée lors d'un appel à localhost:80

  • Il peut-être nécéssaire d'installer curl ou d'utiliser une image de base ou il l'est déjà
  • Pour installer curl sous Linux Alpine:
    apk add curl
    

Quel est le comportement d'un container Docker en cas de failure du healthcheck?


Ajouter un argument utilisable au build de l'image Vote permettant de spécifier la version de l'image Python de base à utiliser. Utiliser la version 3.7-alpine par défaut.

Par exemple, il devra être possible de lancer les commandes:

# utiliser Python 3.8
docker build -t vote:builder --build-arg PYTHON_VERSION=3.8-alpine vote

# utiliser Python 3.7 par défaut
docker build -t vote:builder vote

Harbor

Harbor est un outil de registry Docker. Nous allons créer un compte sur https://demo.goharbor.io pour expérimenter les fonctionnalités.

Exercices

Après le build locale nos images Example Voting App, pushons les sur une Registry pour les partager !

Aller sur demo.goharbor.io et créez-vous un compte (gratuit).

  • Créer un projet example-voting-app
  • Pusher les images Example Voting App dans la registry demo.goharbor.io. Vous devrez:
    • Vous authentifier avec docker login
    • Modifier docker-compose.yml pour spécifier l'URL de Harbor demo pour chaque image
    • Rebuilder les images avec le tag pointant vers Harbor demo
    • Pusher vos images avec docker compose push ou docker push

Docker Hub - Registry Docker officielle

Cette série d'exercice démontrera l'usage de Docker Hub, la registry officielle. Nous allons créer un compte, s'authentifier avec la CLI docker et pusher nos imagees buildées localement directement sur la registry.

Exercices

Après le build locale nos images Example Voting App, poussons les sur la Registry!

Aller sur hub.docker.com et créez vous un compte (gratuit).

  • Avec votre compte, créer un repository sur Docker Hub nommé voting-app-vote qui sera utilisé pour héberger l'image vote
  • Pour pouvoir pusher sur la registry, vous aurez besoin de vous authentifier avec docker login après avoir généré un token
    • Sur votre profil > Account settings > Security générer un token
    • Utiliser docker login avec votre ID et token pour vous authentifier

Nous avons à présent accès au Docker Hub depuis notre machine

  • Modifier docker-compose.yml pour que l'image vote, worker et result buildée soit nommée selon votre registry
  • Utiliser docker compose pour pusher l'image vote dans votre domaine Docker Hub
  • Utiliser docker compose pour pusher l'image worker dans votre domaine Docker Hub - pour lequel vous n'avez pas créé de repository pour l'instant
  • Utiliser la CLI docker pour pusher l'image result
  • Modifier docker-compose.yml pour tagger les images 1.0 et pusher toutes les images en une seule commande

Les images pushées sur la registry peuvent maintenant être utilisée publiquement. Il est aussi possible de les rendre privées, auquel cas il sera obligatoire de s'authentifier sur la registry avant de pouvoir les puller.

  • Essayer de puller les images construires et pushées depuis la registry de votre voisin
  • Essayer de pusher une image construite par vous-même sur la registry de votre voisin

Self-hosted Docker Registry

Cette série d'exercice nous permettra de déployer notre propre registry Docker.

Le Registry Docker se déploie sous forme d'un container Docker, par exemple pour déployer une registry sur votre machine écoutant sur le port 8080:

docker run -d -p 5010:5000 --name registry registry:2

Taggons une image pour la pusher sur notre registry locale:

docker pull ubuntu:latest
docker tag ubuntu localhost:5010/ubuntu
docker push localhost:5010/ubuntu

Pour plus de détails, voir la documentation officielle Docker

Dockerizer une application depuis le code source

Considérons le contexte suivant:

Vous êtes un opérateur travaillant avec une équipe de développeur. L'équipe de dev viens de vous livrer le code source d'une application web que vous devez déployer sous Docker.

L'application est codé en NodeJS et a pour but d'afficher un message à partir d'un fichier de configuration. Le code source de l'application se trouve à l'emplacement suivant: NodeJS app

Les développeurs vous donnent les instructions suivantes:

  • L'application peut se lancer avec NodeJS 15+
  • Pour fonctionner, elle va charger au démarrage le fichier de configuration ./config.yaml qui doit lui être mis à disposition. Les devs vous fournissent le fichier de config testé en développement:
    # Message to print when app is accessed via a web browser
    message: "Test message from dev"
    
    # Host and port on which to bind server
    hostname: 127.0.0.1
    port: 8080
    
  • Instructions d'utilisation de l'application:
    # Install Node dependencies
    # Require package.json and package-lock.json
    npm install
    
    # Run node app
    node app.js
    
    # App should not respond to web request
    # For example, localhost:8080
    

Vous devez dockeriser l'application:

  • Ecrire un Dockerfile permettant de builder l'application
  • Ecrire un docker-compose.yml permettant de lancer l'application:
    • Assurez-vous de monter un fichier config.yaml dans le container
  • Lancer l'application et vérifier qu'elle soit joignable

Note: ⚠️ attention avec l'image node: lancer npm install à la racine du système de fichier (dossier /) provoque un bug du type "idealTree" already exists. Cf. ce post Stack Overflow. Penser à utiliser un WORKDIR au préalable.

HTTPS & Reverse Proxying avec Traefik

Ajoutons des configurations TLS (HTTPS) et un reverse proxy (Traefik).

  • Explorer le contenu de resources/traefik.yml
  • Créer un fichier .env (remplacer <you> par votre nom):
    VOTE_URL=vote.<you>.training.crafteo.io
    RESULT_URL=result.<you>.training.crafteo.io
    
  • Lancer la stack avec les overrides Traefik:
    # make traefik
    docker compose -f docker-compose.yml -f resources/traefik.yml up -d
    

vote et result sont maintenant exposés via:

  • https://vote.<you>.training.crafteo.io
  • https://result.<you>.training.crafteo.io

Note: le certificat TLS ne sera pas reconnu par votre navigateur, ils sont fournis par (STAGING) Let's Encrypt, une version de test des certificats Let's Encrypt

Docker: quelques bonnes pratiques!

Quelques exercices sur les bonnes pratiques avec Docker: limitation de ressources, healthcheck, logging...

Les exercices utiliserons le docker-compose.yml suivant:

version: "3.7"

services:
  db:
    container_name: db
    image: postgres:9.4
    environment:
      POSTGRES_USER: "postgres"
      POSTGRES_PASSWORD: "postgres"

  redis:
    container_name: redis
    image: redis:alpine

  result:
    container_name: result
    image: crafteo/example-voting-app-result
    ports:
      - "5001:80"
      - "5858:5858"

  vote:
    container_name: vote
    image: crafteo/example-voting-app-vote
    ports:
      - "5000:80"

  worker:
    container_name: worker
    image: crafteo/example-voting-app-worker

Exercices

Se documenter sur les méthodes de limitation de ressources pour les containers Docker puis modifier le docker-compose.yml pour limiter la consommation de chaque service:

  • Maximum de 0.2 CPU et 256Mo RAM par service
  • Réservation de 0.1 CPU 128Mo de RAM par service

Note: Docker Compose v3 ignore par défaut les paramètres deploy.resources qui sont réservés à Docker Swarm. Pour les prendre en compte, utiliser --compatibility ou un fichier compose version: "2"


Docker permet de configurer des drivers de Logging. Faire quelques recherches sur les configuration de logging possible avec Docker puis:

  • Lancer un container fluent/fluentd écoutant montant le volume /tmp/docker-logs:/fluentd/log
    docker run -d --rm --name fluentd -v /tmp/docker-logs:/fluentd/log fluent/fluentd
    
  • Configurer le service db pour utiliser le driver fluentd afin d'envoyer les logs vers le container fluentd
    • Utiliser docker inspect pour obtenir l'IP du container fluentd
    • Ne pas utiliser le nom du container directement qui ne sera pas reconnu
  • Lancer la stack ainsi configurée

Vérifier l'existence des logs dans /tmp/docker-logs

Logging with Docker example: ELK Stack

Déployons une stack ELK (Logstash, Elasticearch, Kibana) que nous pourrons utiliser pour obtenir les logs de nos containers:

resources/elk-stack.yml contiens les configurations d'une stack ELK. La déployer avec:

# make elk
docker compose -f resources/elk-stack.yml up -d
  • L'interface Kibana est accessible via http://<host>:8082
  • Explorer le contenu de resources/elk-stack.yml pour y trouver les configurations des différents services

Une fois déployée, lancer une stack de containers avec le logging driver Docker adapté. Utiliser le fichier resources/logging.yml:

# make logging
docker compose -f docker-compose.yml -f resources/logging.yml up -d

Aller sur Kibana (<host>:8082) et:

  • Cliquer sur Discover (bouton avec la boussole)
  • Vous serez redirigé sur la page Create index pattern. Entrez logstash-* et cliquer sur Next step
  • Choisissez @timestamp comme Time filter field et confirmer avec Create index pattern
  • Cliquer à nouveau sur Discover pour accéder aux logs

Monitoring example: Prometheus + Grafana stack

Déployons une stack Prometheus + Grafana que nous pourrons utiliser pour obtenir des metrics sur nos containers (CPU, RAM, etc.) et configurer des alertes.

Cloner le repository Git Prometheus:

git clone https://github.com/PierreBeucher/prometheus.git

Lancer la stack Docker Compose:

cd prometheus
docker compose up -d
  • L'interface Grafana est accessible via http://<host>:3000 (login: admin, password: Prometheus2023)
  • Explorer le contenu de docker-compose.yml pour y trouver les configurations des différents services

Configuration avancées du Docker Daemon

Suite à l'installation de Docker sur Linux:

  • Docker est installé comme service Linux (systemd)
  • La CLI dockerd est utilisé pour lancer le Docker daemon
  • Il est possible d'overrider les configurations du daemon via des options CLI au lancement du service ou via un fichier /etc/docker/daemon.json lu par dockerd par défaut

Quelques infos et rappels:

  • Le Docker daemon est aussi appelé Docker host ou Docker server
  • Le Docker client (CLI docker) communique avec le client via API REST en utilisant la socket /var/run/docker.sock par défaut. Le client peut aussi contacter directement une adresse tel que tcp://127.0.0.1:2375 ou tcp://198.265.78.1:2376 en fonction des configurations du Daemon
  • Memo de commandes systemctl utile pour manipuler les services Linux:
    # restart, stop, get status of docker
    sudo systemctl [restart|stop|status|...] docker
    
    # edit systemd file override
    # or edit file directly at /lib/systemd/system/docker.service
    sudo systemctl edit docker 
    
    # reload after edit
    sudo systemctl daemon-reload
    
    # see newest service logs
    sudo journalctl -u docker -r
    

Exercices

Créer un fichier etc/docker/daemon.json et y configuer les options suivantes pour le Docker daemon:

  • Activer le mode debug
  • Configurer le daemon Docker pour écouter sur 127.0.0.1:2375
  • Configurer le port binding pour écouter sur 127.0.0.1 par défaut
    • sinon, un port binding avec docker run -p 80:80 ... écoutera sur 0.0.0.0:80 par défaut, ce qui peut représenter un risque de sécurité
  • Ajouter le DNS 8.8.8.8

Votre Docker daemon écoute maintenant sur 127.0.0.1:2375 et n'utilisera plus la socket /var/run/docker.sock. La CLI Docker utilisant cette socket par défaut, des configurations supplémentaires seront requises en utilisant la CLI docker.

Redémarrer le service Docker et tester:

  • Lancer un container exposant un port et vérifier que l'adresse d'écoute est bien 127.0.0.1
  • Au sein du container, vérifier que le DNS 8.8.8.8 est bien utilisé
  • Afficher les logs du Daemon

Certains déploiements mettent à disposition un daemon Docker accessible à distance. (i.e. le daemon Docker est installé sur une machine différente du client Docker)

Cela peut-être utile dans certaines situations, par exemple pour partager un daemon avec une équipe de développeur ou un système de CI et accéler le build d'image: le cache de build pourra être réutilisé facilement et ainsi réduire le temps de build (pratique dans certains cas ou un build sans cache dure 20+ min et avec cache seulement quelques secondes!)

Un tel déploiement demande de sécuriser les communications entre le client Docker et le daemon. Utilisant HTTP par défaut, il faut y configurer TLS pour passer en HTTPS.

Configurer le daemon Docker pour:

  • écouter sur 127.0.0.1:2376
  • Configurer l'authentification du serveur en TLS - un client pourra ainsi authentifier le serveur lors de son utilisation (similaire à la connection à un site web en HTTPS)
  • (Optionnel) Configurer l'authentification du client en TLS - le client devra s'authentifier auprès du serveur avec un certificate pour pouvoir utiliser le daemon

Ce setup s'appelle une double-authentification TLS. Pour ce besoin il faut un certificat et une clé pour être utilisé par le Daemon (idem pour le client):

  • Le client va "truster" un CA (certificate Authority) et le daemon fournira un certificat signé par ce CA pour s'authentifier (c'est exactement le même processus en se connectant à un site web en HTTPS)
  • Le Daemon va "truster" un CA (généralement le même), et le client devra fournir un certificat signé par ce CA pour être autorisé par le daemon

Dans le cadre de l'exercice nous pourrons générer nos propre CA et certificats. Pour générer une clé et un certificat autosigné:

# generate CA cert and key
openssl genrsa -out ca.key 4096
openssl req  -nodes -new -x509  -keyout ca.key -out ca.cert

# Generate key for daemon
openssl genrsa -out daemon.key 4096

# Generate CSR for daemon
openssl req -new -sha256 -key daemon.key -out daemon.csr

# Generate cert for daemon signed by CA
openssl x509 -req -in daemon.csr -CA ca.cert -CAkey ca.key -CAcreateserial -out daemon.cert -days 500

# same for client: key, csr and cert

Points d'attention:

  • Par convention, le port d'écoute de Docker est 2375 sans TLS et 2376 avec TLS.
  • Le client aura besoin de configuration supplémentaire pour activer TLS et vérifier l'authenticité du serveur