Cet article est le deuxième d’une série de trois articles.
Maintenant que l’hôte Docker est opérationnel, nous allons créer et exécuter un conteneur avec une application .NET Core.
Pour l’exemple, nous allons utiliser un projet d’exemple ASP.NET 5 (RC1-update1) déjà prêt, disponible sur Github
.NET Core mérite à lui seul un futur article. ASP.NET 5 étant encore en bêta, et afin de nous concentrer sur Docker plutôt que sur une API qui n’est pas encore dans son état final, nous allons récupérer le code source d’un des exemples du repository ASP.NET 5 (actuellement en Release Candidate 1, Update 1), et nous allons le déployer dans une image Docker.
L’exemple de base du site .NET Core est pour le moins limité!
Vous pouvez suivre le tutoriel du site https://dotnet.github.io/getting-started/. En résumé, celui-ci consiste à exécuter les commandes suivantes (toujours dans le terminal client Docker):
docker run -it microsoft/dotnet:latest dotnet new dotnet restore dotnet run
La première commande compile et exécute le conteneur .NET Core. Si l’image (la source du conteneur) n’existe pas encore en local, elle sera téléchargée depuis le hub Docker (un registry dans la terminologie Docker).
La deuxième commande crée un nouveau projet .NET Core dans le répertoire courant. Le projet par défaut contient un template avec un exemple console de type « Hello World ».
La troisième commande restaure les dépendances manquantes via Nuget.
Enfin, la dernière commande exécute l’application .NET Core directement depuis le conteneur.
Pour arrêter le conteneur .NET Core, tapez la commande exit
.
Il y a peu de chances pour que vous ayez jamais à suivre cette séquence: vous développerez sur votre poste, et vous voudrez déployer le projet dans une nouvelle image Docker.
Notez les deux paramètres -i
et -t
utilisés lors de la commande docker run
: ceux-ci permettent de garder ouvert le flux d’entrée de la console vers l’entrée du conteneur. Généralement, ces deux paramètres s’utilisent ensemble (ou pas du tout). Une fois cette première commande exécutée, vous avez donc changé de contexte et vous êtes à l’intérieur du conteneur. Encore une fois, vous aurez rarement besoin de ces deux paramètres, sauf si vous souhaitez vous lancer dans du diagnostic à l’intérieur de votre conteneur. En général, on lance un conteneur en mode « service », c’est-à-dire en tâche de fond.
Enfin, il est important de comprendre ce que fait réellement notre première commande docker run -it microsoft/dotnet:latest
.
Le nom microsoft/dotnet:latest
est constitué d’un nom de repository « microsoft/dotnet » et d’un tag « latest ». Le tag « latest » est spécial, il s’agit simplement de la dernière version d’image déposée dans le repository. La concaténation du repository et du tag identifie une image. Si le tag n’est pas spécifié, le tag « latest » est implicitement utilisé. Une image est comme le code source d’un conteneur. Docker doit d’abord compiler l’image (après l’avoir éventuellement téléchargé), et créer un conteneur, qu’il exécute. La commande `docker run` effectue donc systématiquement ces deux actions: création du conteneur et exécution (et optionnellement une troisième action préliminaire consistant à télécharger l’image). Si vous exécutez plusieurs fois cette commande, vous ne lancez pas à chaque fois le même conteneur mais vous créez un nouveau conteneur à partir de l’image source!
Un conteneur est en soi très léger. En exécuter plusieurs n’est pas spécialement plus lourds qu’exécuter plusieurs fois la même application sur une seule machine (sans Docker).
Par exemple, une fois dans le terminal client Docker, tapez les commandes suivantes:
docker run -it microsoft/dotnet:latest exit docker run -it microsoft/dotnet:latest exit docker ps -a
La commande docker ps -a
permet de lister les conteneurs existants. Vous pouvez voir que vous avez plusieurs copies de conteneurs créées à partir de l’image microsoft/dotnet:latest
.
Vous aurez peut-être remarqué un signe pendant vos test: le prompt est différent à chaque exécution de conteneur: le prompt de cette image affiche le nom d’hôte du conteneur. Comme c’est un conteneur différent à chaque fois, vous lisez un nom d’hôte différent à chaque fois. En général, vous voudrez nommer vos conteneurs grâce au paramètre --name
.
L’exemple a le mérite d’être simple, mais il ne correspond pas à grand chose.
Premier « vrai » exemple
Comme cet article se concentre sur Docker et pas sur le développement .NET Core, nous allons supposer que votre application .NET Core est celle-ci: https://github.com/aspnet/Home/tree/dev/samples/1.0.0-rc1-update1/HelloMvc.
Commencez par récupérer ce code source depuis GitHub (le plus simple est de télécharger le ZIP depuis la page d’accueil du projet).
Décompressez le fichier ZIP. Le répertoire contenant le code source de votre application est: /samples/1.0.0-rc1-update1/HelloMvc
.
Vous constaterez que ce répertoire contient déjà un fichier Dockerfile
. En temps normal, vous devrez créer vous-même ce fichier. En supposant que vous souhaitiez créer une image par application, ce qui est la pratique généralement recommandée, vous devrez créer un fichier Dockerfile
par application. Avec cet exemple, vous disposez du code source de l’application, et du fichier Dockerfile
à sa racine. Notez que vous n’avez pas les binaires: ceux-ci seront compilés lors de la construction de l’image, grâce au Dockerfile
.
Le fichier Dockerfile est le manifeste de votre image
Ce fichier sert à construire l’image. Nous allons donc le détailler:
# Image de base FROM microsoft/aspnet:1.0.0-rc1-update1 # Déploiement de l'application COPY . /app/ WORKDIR /app RUN ["dnu", "restore"] # Exposition du port 5004 (règle pour le pare-feu du conteneur) EXPOSE 5004 # Commande de base: dnx -p project.json web ENTRYPOINT ["dnx", "-p", "project.json", "web"]
L’instruction `FROM ` définit l’image de base.
Les images Docker sont typiquement des images imbriquées les unes dans les autres. Il est très rare qu’il soit nécessaire de créer une image à partir de rien. Par exemple, l’image de base « microsoft/aspnet » est elle-même basée sur l’image « debian » (une distribution Linux).
L’instruction `EXPOSE` déclare un port utilisé par le conteneur
Sans cette instruction, même si nous mettions en oeuvre un serveur sur ce port dans le conteneur, le serveur ne serait pas accessible depuis l’extérieur du conteneur. Cette instruction ne suffit pas à rendre le port accessible de l’extérieur, mais c’est un pré-requis. Le port devra ensuite être mappé par l’hôte Docker lorsque nous exécuterons le conteneur.
L’instruction `ENTRYPOINT` définit la commande de base
C’est le « point d’entrée » de notre conteneur exécutable. Une fois le conteneur créé, nous pourrons le lancer avec des arguments en ligne de commande qui seront ajoutés à la commande définie dans l’instruction ENTRYPOINT
.
Il ne peut y avoir qu’une seule instruction ENTRYPOINT
par conteneur (la dernière remplace la précédente).
Création de la nouvelle image
Les « sources » pour créer notre image sont prêtes. Il suffit de construire l’image:
cd /samples/1.0.0-rc1-update1/HelloMvc docker build -t="mon_app/v1" .
Vous devrez adapter la première commande pour vous positionner dans le répertoire contenant votre Dockerfile
.
Le plus simple est de taper la commande pwd
dans le client Docker pour vous localiser votre position courante et en déduire le chemin à utiliser. Par exemple, si votre répertoire source se trouve sur « E:\mes\sources », le chemin dans le client sera probablement « /e/mes/sources ».
La commande docker build
créera une image nommée « mon_app/v1 » (plus précisément une image avec le tag « v1 » dans le repository « mon_app »). Notez qu’il y a des conventions de nommage à respecter. Le nom du repository doit être essentiellement en minuscules, chiffres ou lettres.
Vous constaterez que c’est un long processus. Chaque instruction du fichier Dockerfile
crée une nouvelle image à partir de laquelle l’instruction suivante est exécutée. Et cela à partir de la première image de base. C’est un peu l’essence du fonctionnement de Docker: les images sont en fait des graphs de systèmes de fichiers. Le système de fichiers d’une image est toujours en lecture seule. Lorsqu’une image est exécutée, ce n’est pas l’image mais le conteneur qui l’est. Les modifications qui ont lieu pendant l’exécution sont sauvées dans le conteneur, mais pas dans l’image. Chaque instruction crée donc une image et un conteneur intermédiaire qui sert de base pour l’image suivante (l’instruction suivante). Les images intermédiaires seront supprimées à la fin du processus. Si une instruction échoue, vous n’obtiendrez pas l’image finale voulue, mais vous conserverez la dernière image créée, ce qui facilite le diagnostic.
A la fin, vous pouvez taper la commande `docker images` pour vérifier que votre nouvelle image apparait.
Création et exécution du conteneur
Enfin, pour lancer notre conteneur :
docker run --name mon_app_demo -t -d -p 80:5004 mon_app/v1
Cette fois, nous créons un conteneur nommé « mon_app_demo » et nous l’exécutons en mode « démon » grâce au paramètre -d
, c’est-à-dire en arrière plan. Nous n’avons donc pas quitté le contexte du client Docker. Le paramètre -t
est actuellement requis mais il ne devrait pas l’être à terme (prérequis pour ASP.NET 5 dans sa version actuelle).
Le paramètre -p 80:5004
crée une redirection du port 80 de l’hôte Docker (si vous êtes sous Windows c’est donc le port 80 de la VM Linux), vers le port 5004 du conteneur. Le serveur ASP.NET 5 contenu dans l’image écoute ce port. Il est également possible d’utiliser le paramètre -p 5004
qui crée une redirection sur un port dynamique de l’hôte Docker. Pour identifie le port dynamique sélectionné par l’hôte Docker, vous pourrez utiliser la commande docker port mon_app_demo
.
Pour constater que notre conteneur est lancé:
docker ps
La sortie devrait indiquer que le conteneur est en marche. Son identifiant nous est également fourni.
En supposant que son identifiant est « b0401d849118 », nous pouvons lire ses dernières traces grâce à la commande:
docker logs b0401d849118
Comme nous avons donné un nom au conteneur, nous pouvons aussi utiliser la commande suivante:
docker logs mon_app_demo
La sortie obtenue devrait être semblable à cela:
Hosting environment: Production Now listening on: http://*:5004 Application started. Press Ctrl+C to shut down.
Test de notre site web
Le conteneur tourne, on peut accéder à notre site web.
Comme nous avons redirigé le port 80 de l’hôte Docker vers le port réellement écouté par notre serveur web hébergé dans le conteneur, il nous suffit d’accéder à l’adresse IP de l’hôte Docker depuis un navigateur web pour accéder à l’application ASP.NET. Si vous êtes sous Windows, il s’agit de l’adresse IP de la VM Linux. L’adresse IP est affichée lorsque vous lancez le terminal client Docker. Vous pouvez la déduire à tout moment grâce à la commande suivante dans le terminal:
printenv | grep DOCKER_HOST
Dans mon cas, l’adresse est 192.168.99.100. Je peux donc accéder à la page http://192.168.99.100 pour accéder à l’application ASP.NET 5 déployée dans le conteneur.
Vous devriez obtenir la page de l’exemple ASP.NET 5.
Pour arrêter le conteneur (donc le serveur), utilisez la commande:
docker stop mon_app_demo
Si vous souhaitez relancer le conteneur sans en recréer un nouveau à partir de l’image, utilisez:
docker start mon_app_demo
Vous verrez que la page ASP.NET est de nouveau accessible.
En général cependant, la pratique privilégiée est de créer un nouveau conteneur à chaque fois, via la commande docker run
. Lors de l’arrêt d’un conteneur, il doit alors être nettoyé s’il n’est plus utilisé, avec la commande docker rm mon_app_demo
(ou l’identifiant du conteneur). Evidemment, le conteneur ne doit posséder aucune donnée applicative si on le détruit à chaque arrêt.
Récapitulatif
C’est la fin de ce deuxième article. Nous avons vu comment :
- Construire une nouvelle image Docker (avec une application ASP.NET 5):
Dockerfile
etdocker build
. - Compiler et lancer un conteneur en exposant un port réseau à l’extérieur de celui-ci:
docker run
. - Afficher la sortie console du conteneur:
docker logs
. - Arrêter et démarrer un conteneur:
docker stop
etdocker start
. - Supprimer un conteneur:
docker rm
.
Dans le dernier article de cette série, nous aborderons des notions plus avancées, pour travailler avec plusieurs conteneurs.