Destiné à ceux qui partent de 0 et qui veulent maitriser les bases de Docker
Supposons que quelqu'un te donne un script nécessitant Python 3.12 précisément. Peu probable que tu l'ai, et tu vas devoir l'installer juste pour cette fois :
python3 #Execute cette commande pour si tu as python3
python3.12 #Mais as tu cette version précise ?
Plusieurs problèmes :
Là, ça parait encore gérable.
Mais imaginez pour un gros projet, avec plusieurs langages, des dépendances Python, des paquets Linux pour gérer une base donnée par exemple... C'est pas tenable.
Cette fois, tu vas demander à Docker de créer un environnement Linux contenant exactement Python 3.12, sans rien installer localement.
Première étape : installer Docker ==> https://docs.docker.com/engine/install/ubuntu/
Suivre la partie "Install using the apt repository"
Puis tape :
docker run python:3.12-slim python -c 'x = 7 * 6; print("Résultat :", x)'
Tu viens d'exécuter un script python... Sans python 3.12 d'installé :
Si vous obtenez une erreur Permission denied, c’est simplement que vous n’avez pas les privilèges nécessaires.
Dans ce cas, exécutez les commandes en les précédant de sudo
docker run : Créer puis exécuter un conteneurpython:3.12 : Image contenant Python 3.12 + un Linux minimalpython : Commande exécuté au lancement du conteneur-c 'something' : On précise un script pour pythonLance :
docker pull hello-world
docker run hello-world
Comme python:3.12, hello-world est une autre image Docker. Elle affiche juste le fonctionnement de Docker.
Tu remarqueras que j'ai d'abord pull l'image, avant de la run. En effet, pour la lancer, il faut d'abord la télécharger. Bon... c'est vrai docker run télécharge automatiquement si l'image n'est pas présente. Mais comme ça vous connaissez la commande docker pull
Maintenant, essaye de relancer docker run hello-world, ça se lance instantanément, car tu as déjà téléchargé (pull) l'image.
D'ailleurs, cette image t'indique une nouvelle commande. Tu la vois ? Tapes là :
docker run -it ubuntu bash
Tu viens d'ouvrir... un monde parallèle
-it : ouvre un shell interactif, pour pouvoir taper des commandes dans le conteneurubuntu bash : lance une conteneur ubuntu, et la commande bashTu es à l'intérieur du conteneur. C'est un 2ème linux, isolé, avec son propre système de fichiers. Tu peux l'explorer avec les commandes classiques :
cd /
ls
whoami
ps aux #Affiche les processus linux
Sans fermer le conteneur, ouvre un deuxième terminal et tape :
docker ps
Tu devrais voir ton conteneur Ubuntu en cours d'exécution avec ses informations. Tu peux d'ailleurs utiliser son < Container-ID > ou son < Names > pour exécuter une commande depuis l'extérieur :
docker exec <Names> ls /
Tu peux ensuite arrêter le conteneur. Mais même éteint, il n'est pas supprimé de ta machine :
docker stop <Names>
docker ps # Le conteneur a disparu
docker ps -a # Mais Docker en garde la trace pour le relancer sans avoir à le retélécharger
docker rm <Names> # Essaye de vraiment l'effacer et regagner l'espace de stockage
docker ps -a # Tu le vois encore ?
Essaye de lancer un conteneur avec l'option -rm : docker run --rm -it ubuntu bash
Ensuite regarde s'il est présent dans docker ps -a. C'est pratique si tu veux tester vite fait un truc sans garder le conteneur en mémoire.
D'une manière générale, je te laisse explorer les options de Docker avec --help
De Docker HUB ! https://hub.docker.com/
Vous pouvez l'assimiler au "Github des conteneurs". C'est là dessus que l'on peut retrouver pleins d'images créées par la communauté, pour n'importe quel linux, application, langage de programmation,...
En tapant Image Docker <application> sur votre moteur de recherche, vous trouverez un dépôt docker sur Docker HUB.
Par exemple, essaye de retrouver le dépôt des images Python ! Et regarde combien de fois elles ont été téléchargées
Celle sur Ubuntu se trouve ici : https://hub.docker.com/_/ubuntu
On va créer un dossier tp-docker pour la suite du TP. Et dedans on va fabriquer notre premier conteneur Une appli python qui va enregistrer les personnes qui la visite.
mkdir ~/tp-docker
On créer un sous-dossier flask-app, et dedans un fichier app.py
En résumé : on crée un site web qui affiche quand un utilisateur se connecte dessus
# ~/tp-docker/flask-app/app.py
from flask import Flask, request
from datetime import datetime
app = Flask(__name__)
# Liste contenant chaque tentative de connexion séparément
CONNECTIONS = []
# Racine du site web "/"
@app.route("/")
def index():
ip = request.remote_addr or "?"
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# Ajouter une entrée pour chaque visite (même IP plusieurs fois)
CONNECTIONS.append({"ip": ip, "time": now})
rows = "".join(
f"<tr><td>{i+1}</td><td>{a['ip']}</td><td>{a['time']}</td></tr>"
for i, a in enumerate(CONNECTIONS)
)
# Générer le HTML de la page
return f"""
<!doctype html>
<html><head><meta charset="utf-8"><title>Connexions</title></head>
<body>
<h1>Demo</h1>
<p>Ton IP : {ip}</p>
<table border="1" cellpadding="4">
<thead><tr><th>#</th><th>IP</th><th>Horodatage</th></tr></thead>
<tbody>{rows}</tbody>
</table>
</body></html>
"""
if __name__ == "__main__":
app.run(debug=True)
On va aussi ajouter un petit fichier requirements.txt dans le même dossier. Pour ceux qui ne connaissent pas, c'est très pratique en python pour tenir une liste de toutes les bibliothèques à installer pour faire tourner un script
Pour l'instant on a juste flask qu'on écrit à l'intérieur.
Etape optionnel pour tester si l'application marche, on peut faire :
python -m venv ~/tp-docker/venv # On crée un environnement virtuel python, pour ne pas installer des packages directement sur ton linux source ~/tp-docker/venv/bin/activate # On charge l'environnement pour pouvoir l'utiliser pip install -r ./requirements.txt # On télécharge les bibliothèques pythonPuis on lance flask :
python3 app.py
Enfin tu devrais voir un truc du style dans ton terminal :http://127.0.0.1:5000
C'est l'adresse à taper dans ton navigateur web pour accéder à l'application
Ca marche en local ? Parfait ! Mais comme tu l'as constaté, il a fallu installé un environnement virtuel, avec python et des dépendances. Et si tu supprimais le dossier venv, et qu'à la place tu construisais un conteneur ?
Pour cela, dans le même dossier, tu vas créer un fichier Dockerfile
touch ~/tp-docker/flask-app/Dockerfile
Un peu de vocabulaire
- Le
Dockerfile: C'est le plan de construction de ton image Docker. Tu précises ses caractéristiques : télécharger des paquets, ajouter des fichiers, ouvrir des ports...- L'
image Docker: C'est le fichier que tu obtiens en buildant le Dockerfile. C'est ce qui est stocké sur ton pc et que tu récupères avecdocker pull- Le
Conteneur: Il tourne réellement sur ta machine. Tu peux l'arrêter, le redémarrer, en lancer plusieurs identiques à partir de l'image Docker correspondante
Et maintenant on va arrêter de juste faire du clic-clic, tu vas travailler un peu. C'est toi qui va créer le Dockerfile. Pour ça tu peux t'aider de cette page qui explique tout : https://docs.docker.com/reference/dockerfile/
Un peu d'aide quand même. Voici un exemple de Dockerfile :
Pour notre application, on va devoir le modifier. On veut valider les critères suivant :
- On a besoin d'une image python 3.12
- On veut que le fichier python soit dans le dossier
/app- Il faut installer les requirements
- Il faut exécuter le script python au démarrage.
- Très important : Il fallait se connecter sur le port :5000 de ta machine pour accéder au site. Là le site tourne DANS ton conteneur. Il faut donc trouver un moyen d'accéder au port :5000 du conteneur depuis ta machine
J'espère que tu t'en ai sorti. Une fois ton Dockerfile fini, il faut le build :
docker build -t flask-app:1.0 .
-t flask-app : le nom de ton image:1.0 : le tag de ton image. Pour une même image, tu peux lui donner différents tags, si tu veux en faire des variantes, ou des versions. : ce point n'est pas là pour faire joli. C'est le dossier où se trouve ton Dockerfile.docker run flask-app:1.0
ça marche ? Vous arrivez à vous connecter au site ?
Si oui, c'est que vous avez un coup d'avance (bien joué)
Pour les autres, le problème vient du fait que :
"Qu'est ce que tu racontes ? J'ai exposé le port 5000 dans mon Dockerfile... ça devrait marcher ?"
On va voir pourquoi c'est important d'avoir les bases en réseau quand on fait du Docker. Voici un exemple avec 2 conteneurs : une appli web et une base de donnée

Qu'observe t-on ?
ip addrbridge : c'est littéralement un routeur virtuel, auquel tous les conteneurs sont "branchés". ça veut dire que TOUS les conteneurs peuvent se parler entre eux.eth0 avec l'ip 192.168.1.2. Donc TOUS les conteneurs peuvent parler avec l'extérieurMais y'a un twist : le bridge bloque le trafic qui n'est pas EXPLICITEMENT autorisé sur un port.
Et il y a 2 instructions pour autoriser un port :
EXPOSE: autorise un conteneur à dialoguer avec les autres conteneurs sur un portPORT: mappe un port de la machine hôte avec le conteneur, l'autorisant à dialoguer avec l'extérieur
Est ce que j'ai volontairement fais une erreur pour faire une ENORME parenthèse sur le réseau Docker ? Peut être... Mais ça rend le truc beaucoup moins mystérieux du coup
Bon tu l'auras deviner, il faut à la fois ajouter ça dans ton Dockerfile : EXPOSE 5000
Et il faut mapper le port 5000 sur un port de ta machine hôte (tant qu'à faire on prend le même port 5000)
En ce qui concerne l'adresse IP du conteneur, il faut utiliser cette commande plutôt que
python3 app.pyCMD ["flask", "run", "--host=0.0.0.0"] # 0.0.0.0 indique qu'on lance sur n'importe quel IP dispo sur ta machine
127.0.0.1est l'ip local de votre pc. Mais là on lance l'appli sur un conteneur. Il faudrait être DANS le conteneur pour accéder au siteNous ce que l'on veut, c'est lancer l'application sur l'ip Docker de notre conteneur (souvent 172.16.x.x)
docker run -p 5000:5000 flask-app:1.0
http://127.0.0.1:5000 # ça devrait fonctionner
Tu as constaté que l'application flask enregistre toutes les connexions au site.
Sauf si tu l'éteins...
docker run -p 5000:5000 flask-app:1.0 # Après avoir fais Ctrl + C sur ton précédent conteneur
# Ton tableau de connexion est réinitialisé
Normal tu vas me dire ! On stocke les infos dans une variable Python. Si on écrivait dans un fichier à la place ça irait mieux.
Faisons ça ! Remplace la variable "CONNECTION" par ces fonctions.
Je te laisse adapter le reste du script pour que ça marche.
# ~/tp-docker/flask-app/app.py
import json
from pathlib import Path
# On crée un fichier nommé "connections.log" au niveau du scriptpour stocker les connexions
STORAGE = str(Path(__file__).with_name("connections.log"))
# On ouvre et on ajoute une entrée au fichier de stockage
def append_connection(ip, time):
with open(STORAGE, "a") as f:
f.write(json.dumps({"ip": ip, "time": time}) + "\n")
# On lit toutes les entrées du fichier de stockage
def read_connections():
out = []
with open(STORAGE, "r") as f:
for line in f:
out.append(json.loads(line))
return out
Donc là, on stocke les infos dans un fichier. Même si on éteint le script, le fichier reste.
Tu peux tester en local, lancer, éteindre et relancer le script. Est ce que tu vois des choses dans le fichier connections.log ?
Maintenant essayons de reconstruire notre conteneur avec notre nouveau script :
docker build -t flask-app:1.1 . # 1.1 : On crée une nouvelle version
docker run -p 5000:5000 flask-app:1.1
docker run -p 5000:5000 flask-app:1.1
Et là, si on lance un container et qu'on le relance... ça ne marche pas ! Et vous pouvez essayer de juste stopper et relancer, ça ne change rien. Pourtant ça fonctionnait en local !
Enfait, un conteneur Docker n'enregistre rien. A chaque fois que tu l'éteins, il reprend ça forme initial.
C'est une force car tu repars toujours du même environnement : pas d'artefact restant qui s'accumulent
Par contre, impossible de stocker vite fait des données dans un fichier
Bon, vous vous doutez que ça serait assez problématique s'il n'y avait AUCUN moyen de stocker des données pour un conteneur.
La solution : en Docker, on peut créer des fichiers/dossiers PARTAGES entre le conteneur et l'hôte. Et c'est assez simple, ça s'appelle un bind-mount !
C'est exactement ce que vous pensez :
--> Si le conteneur écrit dans ce dossier, ça apparait sur l'hôte. Et si l'hôte écrit dessus, ça apparait aussi dans le conteneur
Du coup, crée un fichier connections.log, et monte le sur ton conteneur :
touch ~/tp-docker/flask-app/connections.log
docker run -p 5000:5000 -v "~/tp-docker/flask-app/connections.log:/app/connections.log" flask-app:1.1
Tu vois ! L'application tourne sur le conteneur. Mais si tu l'éteins, le fichier connections.log reste !
C'est une fonctionnalité extrêmement pratique de Docker :
Tu as construits une image avec un script à l'intérieur. Mais tu te rends compte que le script ne fait pas exactement ce que tu veux. 2 possibilités :
mountpath qui remplace ton script par un autre : rapide, adaptable au besoin de chacunEssayons ! Fais une modif dans le script python.
Par exemple, change la couleur de l'arrière plan pour le passer en mode nuit 🌙
<body style="background:#202020;color:white;">
Maintenant, monte le nouveau script par dessus l'ancien :
docker run -p 5000:5000 -v "~/tp-docker/flask-app/connections.log:/app/connections.log" -v "~/tp-docker/flask-app/app.py:/app/app.py" flask-app:1.1
Et voilà ! Tu devrais avoir ton arrière plan mode nuit. Sans reconstruire une image
Du stockage dans un fichier, c'est rigolo 5 minutes pour tester. Mais pour de vraies applications, on préfère une base de donnée. Par exemple MySQL.
Bien sûr, on ne va pas installer une base de donnée à la main. L'intérêt de Docker, c'est de ne pas faire de tâches redondantes. On peut prendre une base de donnée toute prête qui fonctionne sur Docker.
Effectivement c'est pas pratique de lancer nos 2 conteneurs en même temps via le terminal. Et quand on utilise couramment Docker, c'est tellement rapide qu'on fini par ajouter :
Bonne chance pour gérer tout ça en ligne de commande...
Heureusement, une solution existe : Docker Compose
La solution pour lancer 42 conteneurs en exécutant UN SEUL fichier
Pour résumer la philosophie, tu vois la commande docker run de tout à l'heure ? Qui prend potentiellement beaucoup d'arguments, et devient trèèèès longue ?
Tu peux créer un fichier nommé docker-compose.yaml. Et dedans tu notes l'équivalent de ta commande linux, mais au format YAML. Et tu peux même en mettre plusieurs.
Les avantages :
docker compose up/downTu dois créer un fichier :
~/tp-docker/flask-app/docker-compose.yaml
Puis tu vas "convertir" ta commande Docker en instructiondocker composequi fait exactement la même chose :
- Les 2 bind-mounts
- l'ouverture de port
- l'emplacement du
Dockerfile(comme ça l'image est reconstruite s'il y a du changement)
Je vous mets une page avec pleins d'exemples pour vous aider, y'a tout ce qu'il faut pour y arriver : https://blog.stephane-robert.info/docs/conteneurs/orchestrateurs/docker-compose/#structure-dun-fichier-docker-compose
Si vous avez réussi l'étape précédent, celle-ci n'est pas plus compliqué. Il va juste falloir expliquer 2 petits concepts en plus. Sinon n'hésitez pas à demander de l'aide.
Comme on l'a dit, on va utiliser une image toute faite. Parce que c'est la puissance de Docker.
Donc la première étape, c'est de trouver l'image sur internet.
Du coup, trouve la page Docker avec les images MySQL.
Souvent sur cette page, tu trouveras aussi des commandes toutes prêtes, et des infos en plus sur comment bien l'utiliser.
Si t'as bien fait le travail, tu as peut être même déjà un exemple de docker-compose fonctionnel. Je rajoute quelques contraintes :
- Utilises précisément la dernière version dispo en ajoutant un tag
- Place le conteneur en
restart: always(en cas de problème, il redémarre automatiquement)- Flask ne doit pas démarrer si Mysql ne fonctionne pas (hint :
depends_on, et c'est flask qu'il faut modifier)- Ajoute un volume nommé
dbdatapour le stockage de la base de donnée dans/var/lib/mysql- Ajoute aussi les variables d'environnements suivantes :
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: flaskdb
MYSQL_USER: flaskuser
MYSQL_PASSWORD: flaskpass
C'est quoi un volume ? Question intéressante. C'est relié au fait que les conteneurs ne sauvegardent pas de données quand ils sont éteints.
Bah oui c'est vrai, on a vu que les conteneurs ne gardent aucune données quand on les éteint. Comment on peut mettre une base de donnée dessus ?
En utilisant la même astuce que pour le stockage de notre appli Python : on mappe un dossier entre le container et la machine hôte. Quand le conteneur s'éteint, les données restent sur l'hôte.
Mais là on le fait un peu differemment, en utilisant un volume... c'est quoi la différence avec un bind-mount ?
A quoi servent les variables d'environnemment ?
A configurer de manière plus précise notre MySQL. Le conteneur donne une configuration par défaut, il est possible d'override les valeurs par défaut si ça nous arrange.
Si tu te rends sur la page des images MySQL, tu y verras toutes les variables d'environnement disponibles pour customiser ta base de données : https://hub.docker.com/_/mysql
On va commancer par ajouter la librairie MySQL à notre requirements.txt
flask
mysql-connector-python
On remplace la lecture et l'écriture dans un fichier par la connexion avec MySQL. Il faut configurer les paramètres de connexion :
import mysql.connector
db_config = {
"host": "mysql-db", # nom de votre "service" dans docker-compose
"user": "flaskuser",
"password": "flaskpass",
"database": "flaskdb"
}
def append_connection(ip, time):
# On se connecte et on ajoute une entrée dans la base de données
conn = mysql.connector.connect(
**db_config
)
cur = conn.cursor()
cur.execute("INSERT INTO visits (ip, time) VALUES (%s, %s)", (ip, time))
conn.commit()
cur.close()
conn.close()
def read_connections():
# On lit toutes les entrées de la base de données
conn = mysql.connector.connect(
**db_config
)
cur = conn.cursor()
cur.execute("SELECT id, ip, time FROM visits ORDER BY id ASC")
rows = cur.fetchall()
cur.close()
conn.close()
return rows
rows = "".join(
f"<tr><td>{r[0]}</td><td>{r[1]}</td><td>{r[2]}</td></tr>"
for r in read_connections()
)
On va enfin créer un dossier+fichier ~/tp-docker/mysql-init/init.sql pour initialiser la base de donnée (C'est du sql qui est lu au démarrage du container)
CREATE DATABASE IF NOT EXISTS flaskdb;
USE flaskdb;
CREATE TABLE IF NOT EXISTS visits (
id INT AUTO_INCREMENT PRIMARY KEY,
ip VARCHAR(100),
time DATETIME
);
Et on va bind-mount le dossier sur MySQL dans le docker-compose.yaml
volumes:
- dbdata:/var/lib/mysql
- ../mysql-init:/docker-entrypoint-initdb.d:ro # On place la config en mode ReadOnly
Et Vous pouvez désormais lancer votre docker-compose et vous connecter sur votre appli :
docker compose up
http://localhost:5000
A noter : si vous faites une erreur, le fichier
init.sqlne s'exécute que si aucun volume docker n'existe.
Pour le relancer, il faut d'abord supprimé le conteneur et son volume :
docker container prune -f # L'option prune permet de supprimer ce qui n'est plus utilisé
docker volume ls
docker volume prune -a -f # Pareil pour les volumes
Docker ça prend beaucoup de place. C'est bien de nettoyer après utilisation.
Je vous renvoie à cette page web qui détaille tout sur le sujet : https://www.datacamp.com/tutorial/docker-prune
services:
web:
build: .
container_name: flask-web
ports:
- "5000:5000"
depends_on:
- db
volumes:
- ./app.py:/app/app.py
db:
image: mysql:9.5.0
container_name: mysql-db
restart: always
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: flaskdb
MYSQL_USER: flaskuser
MYSQL_PASSWORD: flaskpass
volumes:
- dbdata:/var/lib/mysql
- ../mysql-init/init.sql:/docker-entrypoint-initdb.d:ro # On place la config en mode ReadOnly
volumes: #Il faut déclarer le volume à Docker pour qu'il s'en occupe
dbdata:
from flask import Flask, request
from datetime import datetime
import mysql.connector
app = Flask(__name__)
db_config = {
"host": "mysql-db",
"user": "flaskuser",
"password": "flaskpass",
"database": "flaskdb"
}
def append_connection(ip, time):
conn = mysql.connector.connect(**db_config)
cur = conn.cursor()
cur.execute("INSERT INTO visits (ip, time) VALUES (%s, %s)", (ip, time))
conn.commit()
cur.close()
conn.close()
def read_connections():
conn = mysql.connector.connect(**db_config)
cur = conn.cursor()
cur.execute("SELECT id, ip, time FROM visits ORDER BY id ASC")
rows = cur.fetchall()
cur.close()
conn.close()
return rows
@app.route("/")
def index():
ip = request.remote_addr or "?"
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
append_connection(ip, now)
rows_data = read_connections()
rows_html = "".join(
f"<tr><td>{r[0]}</td><td>{r[1]}</td><td>{r[2]}</td></tr>"
for r in rows_data
)
return f"""
<!doctype html>
<html><head><meta charset="utf-8"><title>Connexions</title></head>
<body style="background:#202020;color:white;">
<h1>Demo</h1>
<p>Ton IP : {ip}</p>
<table border="1" cellpadding="4">
<thead><tr><th>#</th><th>IP</th><th>Horodatage</th></tr></thead>
<tbody>{rows_html}</tbody>
</table>
</body></html>
"""
if __name__ == "__main__":
app.run(port=5000, debug=True)
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["flask", "run", "--host=0.0.0.0"]