Aller au contenu
English

Ports et accès au service

  • Leçon 7
  • 40 min
  • Module 3 : Réseau, configuration et persistance

Ce que vous saurez faire

Accédez à l’API de billets de soutien dans son conteneur depuis votre ordinateur, et uniquement depuis celui-ci. Apprenez pourquoi un conteneur peut fonctionner sans répondre. Permettez à deux conteneurs de communiquer par leur nom.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 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 Windows et Linux n’ont pas été testées.

L’application fonctionne, mais vous ne pouvez pas y accéder

Section intitulée « L’application fonctionne, mais vous ne pouvez pas y accéder »

Commencez par un petit test. Amira lance l’image sans options supplémentaires, sauf la clé d’API :

Terminal
docker run -d --name ticket-api -e API_KEY=local-dev-key ticket-api:1.0.0
docker ps

La sortie commence par l’identifiant du nouveau conteneur :

7afe3d39fe5b1f08c2e9474dafa7aef9acfa3902a2abf0e137a52da4dc7b4903
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
7afe3d39fe5b ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 8000/tcp ticket-api

Le conteneur fonctionne, et l’API écoute sur le port 8000. Elle l’appelle depuis son ordinateur :

Terminal
curl -sS http://127.0.0.1:8000/health

La sortie :

curl: (7) Failed to connect to 127.0.0.1 port 8000 after 0 ms: Couldn't connect to server

Elle fait ensuite la même requête depuis l’intérieur du conteneur. L’image allégée ne contient pas curl, alors elle utilise urllib de Python :

Terminal
docker exec ticket-api python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8000/health').read().decode())"

La sortie :

{"status":"ok"}

L’application fonctionne. Il manque seulement un accès depuis l’extérieur. Un conteneur possède son propre réseau : ses adresses réseau et ses ports sont distincts de ceux de votre ordinateur. Le port 8000 du conteneur n’est pas le port 8000 de votre ordinateur. PORTS 8000/tcp (ports : 8000/tcp) dans docker ps provient de la ligne EXPOSE 8000 du Dockerfile. EXPOSE documente seulement le port. Cette instruction n’ouvre rien.

Terminal
docker rm -f ticket-api

Pour permettre l’accès depuis votre ordinateur, publiez un port au démarrage du conteneur avec -p. Docker redirige les connexions reçues sur ce port de votre ordinateur vers un port du conteneur :

-p 127.0.0.1:8000:8000
└ host address └ host port └ container port
  • Le port du conteneur est celui où l’application écoute : 8000, selon la commande de démarrage du Dockerfile.
  • Le port de l’hôte est le port que vous utilisez sur votre ordinateur. Il peut être différent, par exemple 8001.
  • L’adresse de l’hôte indique quelle adresse réseau de votre ordinateur accepte la connexion.

Amira relance l’API, cette fois avec le port publié :

Terminal
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0
docker ps
docker port ticket-api
curl -s http://127.0.0.1:8000/health

La sortie :

721de8ac187e07bc74f72ee4b8a94b213c2dd56e73b349a88cf04778d23fd51b
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
721de8ac187e ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 127.0.0.1:8000->8000/tcp ticket-api
8000/tcp -> 127.0.0.1:8000
{"status":"ok"}

127.0.0.1:8000->8000/tcp signifie : « Le port 8000 de l’adresse 127.0.0.1 de cet ordinateur mène au port 8000 du conteneur. » http://localhost:8000/health donne la même réponse, car localhost est le nom de 127.0.0.1.

Le journal de l’application montre l’origine de la requête :

Terminal
docker logs --tail 2 ticket-api

La sortie :

INFO ticket_api: GET /health 200 5ms id=338f1241cca9
INFO: 172.17.0.1:58980 - "GET /health HTTP/1.1" 200 OK

Le client est 172.17.0.1, et non 127.0.0.1. Pour l’application, la requête vient du réseau de Docker, et non de l’adresse de votre ordinateur. Sur un autre ordinateur, les nombres peuvent différer : vous voyez une adresse comme 172.17.0.1.

L’adresse de l’hôte détermine qui peut accéder au port publié. Amira teste l’accès depuis un autre appareil. Elle trouve d’abord l’adresse de son ordinateur sur le réseau local :

Fenêtre de terminal
ipconfig getifaddr en0

en0 correspond généralement au Wi-Fi. Si la commande n’affiche rien, essayez en1.

L’adresse de son ordinateur était 10.0.0.109. Depuis un deuxième appareil sur le même réseau, elle ouvre http://10.0.0.109:8000/health. Voici le résultat avec curl depuis cette adresse :

curl: (7) Failed to connect to 10.0.0.109 port 8000 after 8 ms: Couldn't connect to server

Elle démarre ensuite un deuxième conteneur, sans adresse d’hôte dans -p, sur le port 8001 de l’hôte :

Terminal
docker run -d --name ticket-api-open -p 8001:8000 -e API_KEY=local-dev-key ticket-api:1.0.0
docker ps --filter name=ticket-api-open
curl -s http://10.0.0.109:8001/health

La sortie :

bd5e48b1b1def0fc2b29ed84076bd91fa2304121002a480e8c16d09c28d290a9
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
bd5e48b1b1de ticket-api:1.0.0 "fastapi run --host …" 3 seconds ago Up 3 seconds 0.0.0.0:8001->8000/tcp, [::]:8001->8000/tcp ticket-api-open
{"status":"ok"}

0.0.0.0 (et [::], l’équivalent pour IPv6) signifie toutes les adresses réseau de cet ordinateur. Tout appareil qui peut accéder à votre ordinateur peut maintenant appeler l’API. Sur le Wi-Fi d’un café, cela inclut tout le monde dans le café.

Option -p Affichage de docker ps Qui peut se connecter
-p 127.0.0.1:8000:8000 127.0.0.1:8000->8000/tcp Seulement les programmes sur cet ordinateur
-p 8001:8000 0.0.0.0:8001->8000/tcp, [::]:8001->8000/tcp Tout appareil qui peut accéder à cet ordinateur
Sans -p 8000/tcp Personne à l’extérieur du réseau du conteneur

Ce cours utilise toujours -p 127.0.0.1:.... Omettez l’adresse seulement si d’autres appareils doivent se connecter, et seulement sur un réseau auquel vous faites confiance.

Amira supprime le conteneur ouvert aux autres appareils :

Terminal
docker rm -f ticket-api-open

L’adresse de l’hôte représente la moitié du trajet. L’autre moitié est l’adresse où l’application écoute dans le conteneur. Le Dockerfile démarre l’application avec fastapi run --host 0.0.0.0 --port 8000. Et s’il utilisait --host 127.0.0.1, comme le font fastapi dev et uvicorn par défaut?

Faites une prédiction : le conteneur fonctionne, et le port est publié. Qu’affiche curl?

Amira supprime le conteneur fonctionnel avec docker rm -f ticket-api. Elle démarre ensuite l’image avec une commande différente à la fin de docker run. Cette commande remplace le CMD de l’image :

Terminal
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0 fastapi run --host 127.0.0.1 --port 8000
docker ps
curl -sS http://localhost:8000/health

La sortie :

af205d8ff12517ec100dfb4adebfed5b3bc3aaff4d44aca93c88de17933fdac7
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af205d8ff125 ticket-api:1.0.0 "fastapi run --host …" 4 seconds ago Up 3 seconds 127.0.0.1:8000->8000/tcp ticket-api
curl: (52) Empty reply from server

Tout semble correct dans docker ps, mais la réponse est vide. Le journal montre la cause :

Terminal
docker logs ticket-api

La sortie (la dernière ligne) :

INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)

Dans le conteneur, 127.0.0.1 désigne le conteneur lui-même, et non votre ordinateur. L’application accepte seulement les connexions qui commencent dans le conteneur. La connexion redirigée par Docker arrive à l’adresse réseau du conteneur, par exemple 172.17.0.2, où aucun programme n’écoute. Un test depuis l’intérieur du conteneur fonctionne toujours. Donc, docker exec seul ne détecte pas ce problème.

Deux boîtes. Votre ordinateur : curl sur cet ordinateur appelle 127.0.0.1 au port 8000, le port publié. L'adresse Wi-Fi 10.0.0.109 n'a pas de port 8000. Docker redirige le port publié vers l'adresse réseau du conteneur, 172.17.0.2, au port 8000. Dans le conteneur, fastapi run --host 0.0.0.0 --port 8000 écoute sur l'adresse du conteneur et sur son adresse 127.0.0.1. Seul le conteneur peut utiliser cette dernière.
Votre ordinateur et le conteneur ont chacun leur propre adresse 127.0.0.1. Docker redirige vers l'adresse réseau du conteneur, donc l'application doit écouter sur 0.0.0.0.
Description textuelle du diagramme

Sur votre ordinateur, curl appelle 127.0.0.1 au port 8000. C’est le port publié. L’adresse Wi-Fi 10.0.0.109 n’a pas de port 8000, car -p a choisi 127.0.0.1. Docker redirige la connexion vers l’adresse réseau du conteneur, 172.17.0.2, au port 8000. Le conteneur possède aussi sa propre adresse 127.0.0.1, que seuls les programmes du conteneur peuvent utiliser. fastapi run --host 0.0.0.0 écoute sur les deux adresses du conteneur, donc la connexion redirigée arrive à l’application. Avec --host 127.0.0.1, l’application écoute seulement sur l’adresse 127.0.0.1 du conteneur. La connexion redirigée ne trouve aucun programme à cette adresse.

Une séquence avec trois participants : curl sur votre ordinateur, Docker avec le port 127.0.0.1:8000, et fastapi run dans le conteneur. curl envoie GET /health à Docker, qui le redirige vers 172.17.0.2 au port 8000. Si l'application écoute sur 0.0.0.0, elle répond 200 avec le statut ok, et Docker transmet la réponse à curl. Si l'application écoute sur 127.0.0.1, aucun programme n'écoute sur 172.17.0.2. Docker ferme la connexion, et curl affiche : (52) Empty reply from server (réponse vide du serveur).
La même requête avec les deux adresses d'écoute. Seule 0.0.0.0 permet d'obtenir une réponse.
Description textuelle du diagramme

Sur votre ordinateur, curl envoie GET /health au port publié. Docker la redirige vers l’adresse du conteneur, 172.17.0.2, au port 8000. Si l’application écoute sur 0.0.0.0, elle répond 200 {"status":"ok"}, et Docker transmet la réponse. Si l’application écoute sur 127.0.0.1, aucun programme n’écoute sur 172.17.0.2. Docker ferme la connexion sans réponse, et curl affiche curl: (52) Empty reply from server (réponse vide du serveur).

Amira supprime le conteneur qui ne fonctionne pas :

Terminal
docker rm -f ticket-api

La même règle s’applique dans l’autre sens. Un programme dans un conteneur qui appelle localhost appelle son propre conteneur. Amira redémarre l’API avec la commande normale, puis l’appelle depuis un deuxième conteneur :

Terminal
docker run -d --name ticket-api -p 127.0.0.1:8000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0
docker run --rm python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://localhost:8000/health').read().decode())"

La sortie (la dernière ligne) :

urllib.error.URLError: <urlopen error [Errno 111] Connection refused>

Aucune application n’écoute sur le port 8000 du deuxième conteneur, donc la connexion est refusée.

Pour appeler un programme de votre ordinateur depuis un conteneur, Docker Desktop fournit le nom spécial host.docker.internal. Avec http://host.docker.internal:8000/health, la même commande a affiché {"status":"ok"} ici. Sur Linux, Docker Engine exige une option supplémentaire pour ce nom (--add-host=host.docker.internal:host-gateway); ce cours ne l’a pas testée.

D’un conteneur à l’autre : un réseau défini par l’utilisateur

Section intitulée « D’un conteneur à l’autre : un réseau défini par l’utilisateur »

Deux conteneurs qui travaillent ensemble, comme l’API et une base de données, ont besoin d’un accès direct, sans passer par votre ordinateur. Placez-les sur le même réseau défini par l’utilisateur. C’est un réseau que vous créez et nommez. Docker y donne à chaque conteneur un nom que les autres peuvent utiliser : son nom de conteneur.

Commençons par le réseau par défaut. Chaque conteneur démarré sans --network le rejoint. Amira appelle l’API par son nom :

Terminal
docker run --rm python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"

La sortie (la dernière ligne) :

urllib.error.URLError: <urlopen error [Errno -2] Name or service not known>

Sur le réseau par défaut, les noms ne fonctionnent pas. Amira supprime le conteneur avec docker rm -f ticket-api. Elle crée ensuite un réseau et y démarre l’API :

Terminal
docker network create ticket-net
docker run -d --name ticket-api --network ticket-net -e API_KEY=local-dev-key ticket-api:1.0.0
docker run --rm --network ticket-net python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"
docker logs --tail 1 ticket-api

La sortie :

6b3a99b0f573fcdcc2eada873a3281c43a8e7f3f499e0fd889a3f98e590fbed7
859b97b8df79a958829450eea324a06347cda25073a2759a6d9b230d6d79c030
{"status":"ok"}
INFO: 172.21.0.3:54638 - "GET /health HTTP/1.1" 200 OK

Les deux premières lignes sont les identifiants du nouveau réseau et du nouveau conteneur. Le nom ticket-api a fonctionné. La requête provenait de l’adresse du deuxième conteneur sur ticket-net, soit 172.21.0.3. L’API n’avait pas besoin de -p : les conteneurs du même réseau accèdent directement aux ports des conteneurs. Un port publié sert seulement à votre ordinateur.

Dans les prochaines leçons, Docker Compose crée ce réseau pour vous. L’API trouve alors sa base de données avec le nom db.

Amira fait le nettoyage :

Terminal
docker rm -f ticket-api
docker network rm ticket-net
  1. Ouvrez un terminal dans ~/projects/ticket-api.

  2. Démarrez l’API avec le port publié sur 127.0.0.1 :

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

    Si Docker affiche Bind for 127.0.0.1:8000 failed: port is already allocated (liaison impossible : port déjà attribué), un autre programme utilise le port 8000. Lancez docker ps pour trouver un conteneur qui l’utilise, puis supprimez-le. Supprimez ensuite le conteneur dont le démarrage a échoué avec docker rm ticket-api, puis réessayez.

  3. Lancez docker ps. Vérifiez que PORTS (ports) affiche 127.0.0.1:8000->8000/tcp.

  4. Appelez l’API : curl -s http://127.0.0.1:8000/health. Vous devriez obtenir {"status":"ok"}.

  5. Trouvez l’adresse réseau de votre ordinateur, comme dans Choisir qui peut se connecter. Appelez http://<that address>:8000/health depuis un téléphone ou un autre ordinateur sur le même Wi-Fi. La connexion devrait échouer.

  6. Lancez docker logs --tail 2 ticket-api. Trouvez l’adresse du client pour votre requête de l’étape 4.

  7. Supprimez le conteneur : docker rm -f ticket-api.

Afficher les résultats attendus

Étape 3 : 127.0.0.1:8000->8000/tcp dans la colonne PORTS. Étape 4 : {"status":"ok"}. Étape 5 : le navigateur du téléphone indique qu’il ne peut pas se connecter, car le port écoute seulement sur 127.0.0.1. Étape 6 : une ligne comme INFO: 172.17.0.1:58980 - "GET /health HTTP/1.1" 200 OK. L’adresse est celle de Docker, et non 127.0.0.1. Les nombres peuvent différer sur votre ordinateur. Étape 7 : Docker affiche ticket-api.

Changez une condition : publiez l’API sur le port 9000 de l’hôte et placez-la sur un réseau personnalisé nommé ticket-net. Avant d’exécuter quoi que ce soit, notez deux URL : celle qui fonctionne depuis votre ordinateur et celle qui fonctionne depuis un deuxième conteneur sur ticket-net. Testez ensuite les deux, puis nettoyez.

Afficher la réponse
Fenêtre de terminal
docker network create ticket-net
docker run -d --name ticket-api --network ticket-net -p 127.0.0.1:9000:8000 -e API_KEY=local-dev-key ticket-api:1.0.0
curl -s http://127.0.0.1:9000/health
docker run --rm --network ticket-net python:3.14.8-slim-trixie python -c "import urllib.request; print(urllib.request.urlopen('http://ticket-api:8000/health').read().decode())"
docker rm -f ticket-api
docker network rm ticket-net

Depuis votre ordinateur : http://127.0.0.1:9000/health, le port de l’hôte. Depuis le deuxième conteneur : http://ticket-api:8000/health, le port du conteneur. Le port de l’hôte existe seulement sur votre ordinateur. Un appel du deuxième conteneur à http://ticket-api:9000/health donne Connection refused (connexion refusée). Rien dans le conteneur de l’API n’écoute sur le port 9000.

Erreur courante

Le conteneur fonctionne, mais curl indique : Empty reply from server (réponse vide du serveur)

Symptôme : docker ps affiche le conteneur comme Up (en cours d’exécution), avec le port publié. Un appel avec curl au port publié affiche curl: (52) Empty reply from server (réponse vide du serveur).

Cause : Docker a accepté la connexion sur votre ordinateur, mais rien n’a répondu à l’autre bout, dans le conteneur. Trois causes produisent ce même symptôme :

  1. L’application écoute sur 127.0.0.1 dans le conteneur, et non sur 0.0.0.0.
  2. Le port du conteneur dans -p est incorrect. Par exemple, -p 127.0.0.1:8000:80 redirige vers le port 80, mais l’application écoute sur le port 8000. Cela a aussi donné (52) ici.
  3. L’application est encore en train de démarrer. Une requête dans la première seconde après le démarrage peut recevoir (52). Attendez, puis réessayez.

Solution :

  1. Lisez la ligne de démarrage dans docker logs <container> : Uvicorn running on http://0.0.0.0:8000 (Uvicorn fonctionne sur http://0.0.0.0:8000) est correct. http://127.0.0.1:8000 indique un problème d’adresse d’écoute. Modifiez la commande de démarrage pour utiliser --host 0.0.0.0.
  2. Comparez le port de cette ligne avec le deuxième nombre dans docker port <container>. Ils doivent être identiques.
  3. Si les deux sont corrects, attendez quelques secondes et faites un nouvel appel.

Pour un vrai service d’IA : les serveurs de modèles et les cadres d’API écoutent souvent sur 127.0.0.1 par défaut, par sécurité sur un portable. Dans un conteneur sur une plateforme infonuagique, ce même réglage produit un service qui fonctionne, mais ne répond jamais. La vérification de l’état du service échoue, et la plateforme redémarre le conteneur sans arrêt (module 5).

Vérifiez votre compréhension

Quelle est la différence entre -p 127.0.0.1:8000:8000 et -p 8000:8000?

Afficher la réponse

Les deux redirigent le port 8000 de votre ordinateur vers le port 8000 du conteneur. Avec 127.0.0.1, seuls les programmes sur votre ordinateur peuvent se connecter. Sans adresse, Docker écoute sur toutes les adresses réseau de votre ordinateur (0.0.0.0 et [::]). Tout appareil pouvant joindre votre ordinateur peut alors appeler l’API, par exemple sur le même réseau Wi-Fi. Sous Linux, un tel port peut aussi contourner le pare-feu ufw.

Vérifiez votre compréhension

Un conteneur démarre l'application avec --host 127.0.0.1 et son port est publié avec -p 127.0.0.1:8000:8000. docker ps l'affiche comme Up (en cours d'exécution). Prédisez ce que curl http://localhost:8000/health affiche et expliquez pourquoi.

Afficher la réponse

curl: (52) Empty reply from server. Dans le conteneur, 127.0.0.1 désigne le conteneur lui-même. L’application accepte donc uniquement les connexions provenant de l’intérieur du conteneur. Docker redirige la requête vers l’adresse réseau du conteneur (par exemple 172.17.0.2), où rien n’écoute. La solution est d’utiliser --host 0.0.0.0 dans la commande de démarrage du conteneur.

Vérifiez votre compréhension

Le conteneur B doit appeler l'API du conteneur A. Pourquoi http://localhost:8000 ne fonctionne-t-il pas dans le conteneur B, et quelle solution fonctionne?

Afficher la réponse

Dans le conteneur B, localhost désigne le conteneur B lui-même, qui n’a aucune API sur le port 8000. Placez les deux conteneurs sur le même réseau personnalisé (docker network create, puis --network pour les deux). Appelez le conteneur A par son nom et son port de conteneur, par exemple http://ticket-api:8000. Aucun port publié n’est nécessaire pour cela.

Récapitulatif

  • Un conteneur a son propre réseau. EXPOSE sert seulement à documenter un port. -p host-address:host-port:container-port le publie.
  • -p 127.0.0.1:... autorise uniquement les connexions de votre ordinateur. Sans l’adresse, tout appareil pouvant joindre votre ordinateur peut se connecter.
  • L’application dans le conteneur doit écouter sur 0.0.0.0. Avec 127.0.0.1, le conteneur fonctionne, mais curl reçoit (52) Empty reply from server (réponse vide du serveur).
  • Dans un conteneur, localhost désigne ce conteneur. Les conteneurs sur un réseau personnalisé se trouvent par leur nom, sur le port du conteneur.

Ensuite, vous fournissez au conteneur ses paramètres et ses secrets au démarrage. L’API s’arrête avec un message clair quand un paramètre obligatoire manque.

Leçon suivante : Configuration à l'exécution