Aller au contenu
English

L'essentiel du Dockerfile

  • Leçon 4
  • 50 min
  • Module 2 : Construire l'image de l'application

Ce que vous saurez faire

Jusqu’ici, vous avez exécuté des images créées par d’autres personnes. Dans cette leçon, vous écrivez la recette de votre propre image : un Dockerfile. Ensuite, vous construisez une image de l’API de billets qui fonctionne de la même façon sur chaque ordinateur.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 50 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

Testé avec Docker Desktop 4.81.0 (Docker Engine 29.6.1) sur macOS, avec une puce Apple. Les étapes pour Windows et Linux ne sont pas testées.

Un Dockerfile est un fichier texte contenant les étapes pour créer une image. Chaque ligne commence par une instruction, comme FROM ou COPY. Docker exécute les lignes de haut en bas. Le résultat est une image.

Commencez par le plus petit exemple utile. Dans un nouveau dossier vide, ~/projects/hello-image, créez deux fichiers :

hello.py
import platform
print("Hello from", platform.system(), "with Python", platform.python_version())
Dockerfile
FROM python:3.14-slim
WORKDIR /app
COPY hello.py .
CMD ["python", "hello.py"]

Construisez l’image et nommez-la hello-image. Le . à la fin signifie « ce dossier » :

Terminal, in ~/projects/hello-image
docker build -t hello-image .

La sortie (abrégée) :

[+] Building 0.4s (8/8) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
...
=> [1/3] FROM docker.io/library/python:3.14-slim@sha256:f85c5697265c178c 0.0s
...
=> CACHED [2/3] WORKDIR /app 0.0s
=> [3/3] COPY hello.py . 0.1s
...
=> => naming to docker.io/library/hello-image:latest 0.0s

Ensuite, exécutez-la :

Terminal
docker run --rm hello-image

La sortie :

Hello from Linux with Python 3.14.8

Sur le Mac utilisé pour tester cette leçon, python3 hello.py affiche Hello from Darwin with Python 3.14.6 (bonjour depuis Darwin avec Python 3.14.6). Dans le conteneur, le même fichier s’exécute sous Linux, avec le Python de l’image. C’est le rôle d’une image : elle apporte ses propres fichiers de système d’exploitation et son propre Python.

Les trois étapes de construction sont les trois lignes après FROM. La sortie les numérote de [1/3] à [3/3]. CACHED (en cache) signifie que Docker a utilisé le résultat d’une construction précédente sur cet ordinateur, sans refaire l’étape. Sur votre ordinateur, la ligne peut afficher CACHED ou non. La prochaine leçon explique le cache.

Ces six instructions suffisent pour l’API de billets :

Instruction Ce qu’elle fait Exemple
FROM Part d’une image de base : une image qui contient déjà un système d’exploitation et, ici, Python. Chaque Dockerfile commence par FROM. FROM python:3.14-slim
WORKDIR Crée un dossier dans l’image et y entre, comme mkdir et cd. Les chemins suivants sont relatifs à ce dossier. WORKDIR /app
COPY Copie des fichiers du dossier de votre projet dans l’image. COPY hello.py .
RUN Exécute une commande pendant la construction de l’image, par exemple pour installer des paquets. Son résultat est conservé dans l’image. RUN pip install -r requirements-run.txt
EXPOSE Indique le port sur lequel l’application écoute. C’est seulement de la documentation : cette instruction n’ouvre pas le port. Le module 3 l’explique. EXPOSE 8000
CMD La commande exécutée au démarrage d’un conteneur. Elle est conservée dans l’image, mais ne s’exécute pas pendant la construction. CMD ["python", "hello.py"]

La différence entre RUN et CMD est la plus importante. RUN s’exécute une fois, sur votre ordinateur, pendant docker build. CMD s’exécute chaque fois qu’une personne démarre un conteneur à partir de l’image, sur n’importe quel ordinateur.

L’image a besoin d’une liste des paquets nécessaires à l’API, avec leurs versions exactes. Vous avez déjà requirements-lock.txt du cours sur les API, mais ce n’est pas la bonne liste :

  • Vous l’avez créée sur votre ordinateur, avec votre système d’exploitation. L’image utilise Linux. Une liste créée sous Windows ne contient pas uvloop, que fastapi[standard] installe seulement sous Linux et macOS. Une liste créée sur un Mac peut aussi différer.
  • Elle contient les outils de test (pytest, httpx2). L’image exécute seulement l’API, donc elle n’en a pas besoin.

Créez donc la liste là où l’image s’exécute : dans un conteneur Linux, avec le même Python. Exécutez cette commande dans le dossier du projet. Elle prend environ 15 secondes :

Terminal, in ~/projects/ticket-api
docker run --rm python:3.14.8-slim-trixie sh -c 'pip install -q --root-user-action=ignore --disable-pip-version-check "fastapi[standard-no-fastapi-cloud-cli]==0.142.2" && pip freeze' > requirements-run.txt

Élément par élément :

Élément Signification
docker run --rm python:3.14.8-slim-trixie Démarre un conteneur à partir de l’image Python. --rm supprime le conteneur à son arrêt.
sh -c '...' Exécute le texte entre guillemets avec l’interpréteur de commandes du conteneur : deux commandes reliées par &&.
pip install -q ... "fastapi[standard-no-fastapi-cloud-cli]==0.142.2" Installe FastAPI avec ses composants supplémentaires standards, sans la commande FastAPI Cloud, que ce cours n’utilise pas. -q et les deux options rendent pip silencieux, pour que seule la liste sorte.
pip freeze Affiche chaque paquet installé avec sa version exacte.
> requirements-run.txt Votre propre interpréteur de commandes, sur votre ordinateur, écrit la sortie du conteneur dans le fichier.
Votre terminal démarre un conteneur Linux avec docker run. Le conteneur exécute pip install, puis pip freeze, qui affiche 50 lignes. Les lignes reviennent à votre terminal par la sortie standard. Le > de l'interpréteur de commandes les écrit dans requirements-run.txt sur votre ordinateur. --rm supprime le conteneur.
Les paquets sont installés dans le conteneur, mais votre propre interpréteur de commandes écrit le fichier sur votre ordinateur.
Description textuelle du diagramme

Le terminal envoie docker run --rm python:3.14.8-slim-trixie sh -c '...' à un nouveau conteneur Linux. Dans le conteneur, pip install installe FastAPI 0.142.2 avec ses composants supplémentaires, et pip freeze affiche 50 lignes. Les lignes reviennent au terminal par la sortie standard. Avec >, l’interpréteur de commandes du terminal les écrit dans requirements-run.txt, dans le dossier de votre projet. Grâce à --rm, Docker supprime le conteneur à son arrêt. Le conteneur ne voit aucun dossier de votre ordinateur. Le fichier vous appartient donc, plutôt qu’à l’utilisateur du conteneur.

Vérifiez le fichier. Sur macOS et Linux :

Terminal
wc -l requirements-run.txt
grep -E "^fastapi|^uvloop" requirements-run.txt

La sortie :

50 requirements-run.txt
fastapi==0.142.2
fastapi-cli==0.0.32
uvloop==0.23.0

Cinquante paquets, chacun avec une version exacte, et uvloop figure dans la liste.

Amira écrit le premier Dockerfile de ticket-api. Créez le fichier Dockerfile (sans extension) dans le dossier du projet :

Dockerfile
FROM python:3.14-slim
WORKDIR /app
COPY requirements-run.txt .
RUN pip install -r requirements-run.txt
COPY pyproject.toml .
COPY ticket_api ./ticket_api
EXPOSE 8000
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000"]
Ligne Pourquoi elle est là
FROM python:3.14-slim Python 3.14 sur un petit système Debian Linux. « slim » contient seulement les fichiers nécessaires à Python. La prochaine leçon fixe une version exacte.
WORKDIR /app Le projet se trouve dans /app dans l’image.
COPY requirements-run.txt . D’abord, seulement la liste des paquets, pour que pip install puisse s’exécuter.
RUN pip install -r requirements-run.txt Installe les 50 paquets aux versions exactes dans l’image.
COPY pyproject.toml . fastapi run lit le point d’entrée ticket_api.main:app dans ce fichier.
COPY ticket_api ./ticket_api Le code de l’API. Les tests, les fichiers Postman et .venv restent dehors : l’image exécute seulement l’API.
EXPOSE 8000 Documente le port.
CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000"] Démarre l’API en mode production. 0.0.0.0 et 8000 sont les valeurs par défaut de fastapi run. La ligne les indique explicitement, car le module 3 les modifie et les explique.

Les paquets viennent avant le code, volontairement. La prochaine leçon montre combien de temps cet ordre fait gagner.

Chaque ligne du Dockerfile et ce qu'elle crée dans l'image ticket-api:1.0.0. FROM python:3.14-slim fournit 4 couches de l'image de base, avec Debian et Python. WORKDIR /app crée une couche de 8,19 ko. COPY requirements-run.txt crée une couche de 12,3 ko. RUN pip install crée la plus grande couche : 50 paquets, 117 Mo. COPY pyproject.toml crée une couche de 12,3 ko. COPY ticket_api crée une couche de 102 ko. EXPOSE 8000 et CMD créent seulement des paramètres de 0 octet.
Les instructions qui modifient des fichiers créent une couche. EXPOSE et CMD écrivent seulement des paramètres. Les tailles proviennent de docker history.
Description textuelle du diagramme

La colonne de gauche contient les huit lignes du Dockerfile. Celle de droite indique ce que chaque ligne crée dans l’image ticket-api:1.0.0. FROM python:3.14-slim apporte les 4 couches de l’image de base : Debian et Python. WORKDIR /app crée une couche avec le dossier (8,19 ko). COPY requirements-run.txt . crée une couche avec un fichier (12,3 ko). RUN pip install -r requirements-run.txt crée la plus grande couche : 50 paquets, 117 Mo. COPY pyproject.toml . crée une couche avec un fichier (12,3 ko). COPY ticket_api ./ticket_api crée une couche avec le code (102 ko). EXPOSE 8000 et CMD ne créent aucun fichier. Ils écrivent seulement des paramètres dans l’image, avec une taille de 0 octet.

Le nom d’une image prend la forme name:tag (nom:étiquette). L’étiquette, après le deux-points, choisit une version (module 1). Construisez l’image nommée ticket-api:1.0.0, où 1.0.0 est la version dans pyproject.toml :

Terminal, in ~/projects/ticket-api
docker build -t ticket-api:1.0.0 .

La sortie, sur un ordinateur qui avait déjà l’image Python :

[+] Building 13.4s (11/11) FINISHED docker:desktop-linux
=> [internal] load build definition from Dockerfile 0.0s
=> => transferring dockerfile: 267B 0.0s
=> [internal] load metadata for docker.io/library/python:3.14-slim 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [1/6] FROM docker.io/library/python:3.14-slim@sha256:f85c5697265c178c 0.0s
=> => resolve docker.io/library/python:3.14-slim@sha256:f85c5697265c178c 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 47.05kB 0.0s
=> CACHED [2/6] WORKDIR /app 0.0s
=> [3/6] COPY requirements-run.txt . 0.0s
=> [4/6] RUN pip install -r requirements-run.txt 10.4s
=> [5/6] COPY pyproject.toml . 0.0s
=> [6/6] COPY ticket_api ./ticket_api 0.0s
=> exporting to image 2.7s
...
=> => naming to docker.io/library/ticket-api:1.0.0 0.0s

Lisez-la à partir du haut :

  • [1/6] à [6/6] sont les six étapes de construction. EXPOSE et CMD n’ont pas d’étape : ils écrivent seulement des paramètres.
  • pip install prend la majeure partie des 13,4 secondes. Si Docker doit d’abord télécharger l’image Python, la construction prend plus de temps.
  • transferring context: 47.05kB (transfert du contexte : 47,05 ko) : les fichiers que docker build a envoyés à Docker. La prochaine leçon explique ce contexte de construction.
  • naming to docker.io/library/ticket-api:1.0.0 (nom attribué : docker.io/library/ticket-api:1.0.0) : le nom complet de l’étiquette. docker.io/library/ est l’emplacement par défaut, et vous pouvez l’omettre.

Regardez la nouvelle image :

Terminal
docker image ls ticket-api

La sortie :

IMAGE ID DISK USAGE CONTENT SIZE EXTRA
ticket-api:1.0.0 bbee51dd65d9 363MB 86.8MB

La prochaine leçon rend cette image plus petite.

Exécutez un conteneur à partir de l’image. -p 127.0.0.1:8000:8000 relie le port 8000 de votre ordinateur au port 8000 du conteneur. -e API_KEY=local-dev-key donne sa clé à l’application au démarrage. La clé n’est pas dans l’image.

Terminal
docker run --rm -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0

La sortie :

⚡️ Starting FastAPI in production mode
🐍 Using import string: ticket_api.main:app
🌐 Server started at http://0.0.0.0:8000
Documentation at http://0.0.0.0:8000/docs
Logs:
INFO: Started server process [1]
INFO: Waiting for application startup.
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Started server process [1] (processus serveur démarré [1]) : dans le conteneur, l’API est le processus 1, le premier processus et le principal. Gardez ce terminal ouvert. Dans un deuxième terminal, appelez l’API :

Fenêtre de terminal
curl http://127.0.0.1:8000/health
curl -X POST http://127.0.0.1:8000/v1/classify -H "Content-Type: application/json" -H "X-API-Key: local-dev-key" -d @requests/billing.json

La sortie :

{"status":"ok"}
{"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}

Les mêmes réponses que dans le cours sur les API, mais depuis Linux, avec les paquets de l’image. Le premier terminal affiche une ligne de journal pour chaque requête, par exemple :

INFO ticket_api: POST /v1/classify 200 2ms id=ab6cfe124415
INFO: 172.17.0.1:60676 - "POST /v1/classify HTTP/1.1" 200 OK

L’adresse du client ressemble à 172.17.0.1, et non à 127.0.0.1 : la requête entre dans le conteneur par le réseau de Docker. Le module 3 l’explique. Pour arrêter le conteneur, appuyez sur Ctrl+C dans le premier terminal. --rm le supprime.

CMD a deux formes. La forme exec est une liste entre crochets : CMD ["fastapi", "run", ...]. Docker démarre fastapi directement, donc c’est le processus 1. La forme shell est du texte simple : CMD fastapi run --host 0.0.0.0 --port 8000. Docker démarre alors un interpréteur de commandes, /bin/sh -c, comme processus 1. Cet interpréteur démarre ensuite fastapi.

La différence apparaît quand vous arrêtez le conteneur. docker stop envoie un signal d’arrêt au processus 1, attend jusqu’à 10 secondes, puis force l’arrêt du conteneur. Amira a essayé les deux formes avec docker stop sur un conteneur en cours d’exécution :

Forme Processus 1 (selon docker top) Durée de docker stop
Forme exec /usr/local/bin/python3.14 /usr/local/bin/fastapi run --host 0.0.0.0 --port 8000 0,5 seconde
Forme shell /bin/sh -c fastapi run --host 0.0.0.0 --port 8000 10,2 secondes

Avec la forme shell, l’interpréteur reçoit le signal, mais ne le transmet pas à fastapi. Après 10 secondes, Docker force l’arrêt des deux processus. L’application n’a pas le temps de terminer ses requêtes. La construction vous avertit aussi :

1 warning found (use docker --debug to expand):
- JSONArgsRecommended: JSON arguments recommended for CMD to prevent unintended behavior related to OS signals (line 8)

Utilisez toujours la forme exec pour CMD.

Les étiquettes et pourquoi latest n’est pas une version

Section intitulée « Les étiquettes et pourquoi latest n’est pas une version »

Le nom d’une image comporte deux parties : le nom du dépôt (ticket-api) et l’étiquette après le deux-points (1.0.0). Si vous omettez l’étiquette, Docker utilise latest (la plus récente) :

Terminal
docker build -t ticket-api .
docker image ls ticket-api

La sortie :

IMAGE ID DISK USAGE CONTENT SIZE EXTRA
ticket-api:1.0.0 bbee51dd65d9 363MB 86.8MB
ticket-api:latest aa20ce395345 363MB 86.8MB

latest est seulement le nom par défaut. Il ne signifie pas « la plus récente », et Docker ne le met pas à jour pour vous. Il désigne ce que vous avez construit en dernier sans version. Ainsi, ticket-api:latest sur votre ordinateur et sur celui de Sam peuvent être deux images différentes. Une étiquette de version, comme 1.0.0, indique à tout le monde exactement quel code se trouve dans l’image. Utilisez des étiquettes de version. Supprimez maintenant l’étiquette latest :

Terminal
docker image rm ticket-api:latest

La sortie :

Untagged: ticket-api:latest
  1. Ouvrez un terminal dans ~/projects/ticket-api.

  2. Créez requirements-run.txt avec la commande docker run de Créer requirements-run.txt dans un conteneur.

  3. Vérifiez le fichier : il contient 50 lignes, dont uvloop==0.23.0.

  4. Créez le Dockerfile comme dans Écrire le Dockerfile.

  5. Construisez l’image avec docker build -t ticket-api:1.0.0 .. La sortie se termine par naming to docker.io/library/ticket-api:1.0.0.

  6. Exécutez docker image ls ticket-api. L’étiquette ticket-api:1.0.0 figure dans la liste.

  7. Démarrez un conteneur avec docker run --rm -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0. Le journal indique Uvicorn running on http://0.0.0.0:8000 (Uvicorn en cours d’exécution à http://0.0.0.0:8000).

  8. Dans un deuxième terminal, appelez /health et /v1/classify avec curl. Les réponses sont {"status":"ok"} et la classification de facturation.

  9. Appelez /v1/classify sans l’en-tête X-API-Key. Le code d’état est 401.

  10. Appuyez sur Ctrl+C dans le premier terminal pour arrêter le conteneur.

  11. Dans README.md, remplacez la section ## Deployed practice service (service d’exercice déployé) par cette section, pour que la prochaine personne sache exécuter l’image :

    README.md
    ## Run it in a container
    You need Docker. The API listens on port 8000 in the container.
    ```sh
    docker build -t ticket-api:1.0.0 .
    docker run --rm -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0
    ```
    Check it: `curl http://127.0.0.1:8000/health` gives `{"status":"ok"}`.
Afficher le résultat attendu des étapes 8 et 9
{"status":"ok"}
{"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}

Étape 9, avec curl -s -o /dev/null -w "%{http_code}\n" devant l’URL pour afficher seulement le code d’état : 401. Le journal du conteneur affiche INFO ticket_api: POST /v1/classify 401 1ms id=…. L’image offre la même protection que l’API dans le cours sur les API.

À vous de jouer : la même image, d’autres paramètres

Section intitulée « À vous de jouer : la même image, d’autres paramètres »

Démarrez l’image de nouveau, mais ajoutez -e SHOW_DOCS=false. Avant d’ouvrir http://127.0.0.1:8000/docs, faites une prédiction : devez-vous reconstruire l’image? Quel code d’état renvoie /docs?

Afficher la réponse

Vous ne reconstruisez pas l’image. L’application lit SHOW_DOCS au démarrage. La même image fonctionne donc avec les deux valeurs :

Fenêtre de terminal
docker run --rm -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key -e SHOW_DOCS=false ticket-api:1.0.0

/docs renvoie 404. Sans -e SHOW_DOCS=false, il renvoie 200. /health renvoie {"status":"ok"} dans les deux cas. Les modules 3 à 5 utilisent cette idée : une seule image, avec différents paramètres au démarrage. Elle fonctionne sur votre ordinateur et dans le nuage.

Erreur courante

Le conteneur s'arrête immédiatement : Could not find a default file to run (aucun fichier par défaut à exécuter trouvé)

Symptôme : la construction de l’image réussit, mais le conteneur s’arrête peu après son démarrage :

⚡️ Starting FastAPI in production mode
Could not find a default file to run, please provide an explicit path

Le code de sortie est 1.

Cause : pyproject.toml ne se trouve pas dans l’image. fastapi run y lit le point d’entrée ticket_api.main:app. Sans ce fichier, il cherche un fichier comme main.py dans /app, mais n’en trouve aucun. Ici, la ligne COPY pyproject.toml . manquait dans le Dockerfile.

Diagnostic : listez les fichiers dans l’image. Une commande placée après le nom de l’image remplace CMD :

Fenêtre de terminal
docker run --rm ticket-api:1.0.0 ls /app
requirements-run.txt
ticket_api

pyproject.toml n’y est pas.

Correction : ajoutez COPY pyproject.toml . avant COPY ticket_api ./ticket_api, puis reconstruisez l’image. ls /app affiche alors pyproject.toml, requirements-run.txt et ticket_api.

Pour un vrai service d’IA : une image contient seulement ce que le Dockerfile y copie. Si l’application lit un fichier de modèle, un fichier de paramètres ou un modèle de requête au démarrage, le Dockerfile doit aussi le copier. Sinon, l’application fonctionne sur votre ordinateur, mais échoue dans l’image. Quand un conteneur s’arrête immédiatement, exécutez ls dans l’image avant de modifier le code.

Vérifiez votre compréhension

Quelle est la différence entre RUN et CMD? Donnez un exemple de chacun tiré du Dockerfile de ticket-api.

Afficher la réponse

RUN exécute une commande une seule fois, pendant docker build, et enregistre son résultat dans l’image. Ainsi, RUN pip install -r requirements-run.txt installe les paquets dans l’image. CMD est enregistré dans l’image et s’exécute chaque fois qu’un conteneur démarre. Ainsi, CMD ["fastapi", "run", "--host", "0.0.0.0", "--port", "8000"] démarre l’API. RUN modifie l’image; CMD ne s’exécute pas pendant la construction.

Vérifiez votre compréhension

Pourquoi créer requirements-run.txt dans un conteneur Linux plutôt qu'avec pip freeze dans votre propre environnement virtuel?

Afficher la réponse

L’image utilise Linux avec Python 3.14.8. Les paquets que pip installe peuvent dépendre du système d’exploitation. Sur Windows, fastapi[standard] n’installe pas uvloop. Une liste créée sur Windows ne le contient donc pas. Une liste provenant de votre environnement virtuel contient aussi les outils de test. Une liste créée dans un conteneur utilisant la même image Python contient exactement les 50 paquets nécessaires à l’image. Leurs versions y fonctionnent.

Vérifiez votre compréhension

Sam télécharge ticket-api:latest le lundi, puis le vendredi. Est-ce la même image les deux fois?

Afficher la réponse

Ce n’est pas certain. latest est seulement l’étiquette par défaut. Elle pointe vers la dernière image construite ou envoyée sans version. Elle peut donc changer entre lundi et vendredi sans avertissement. Une étiquette de version comme ticket-api:1.0.0 désigne une version publiée précise. Pour garantir le contenu exact, utilisez l’empreinte numérique, que la prochaine leçon explique.

Récapitulatif

  • Un Dockerfile est la recette d’une image. FROM, WORKDIR, COPY et RUN créent les fichiers de l’image. EXPOSE et CMD enregistrent des paramètres.
  • requirements-run.txt liste les 50 paquets exacts. Cette liste est créée dans un conteneur Linux pour correspondre à l’image.
  • docker build -t ticket-api:1.0.0 . construit l’image. docker run --rm -p 127.0.0.1:8000:8000 -e API_KEY=... ticket-api:1.0.0 l’exécute, avec la clé fournie au démarrage.
  • Utilisez la forme exec de CMD pour que l’application soit le processus 1 et s’arrête proprement.
  • Utilisez des étiquettes de version. latest est un nom par défaut, pas une version.

Ensuite, vous accélérerez les constructions, réduirez la taille de l’image et fixerez chaque version.

Leçon suivante : Construire efficacement