Nota: Este post ha sido importado de mi blog de geeks.ms. Es posible que algo no se vea del todo "correctamente". En cualquier caso puedes acceder a la versión original aquí
Seguimos con esta sobre el uso de Docker desde el punto de vista de un desarrollador asp.net (core). En este caso vamos a construir nuestra primera imagen Docker.
Nota: Visual Studio 2017 incorpora de serie las Docker Tools que automatizan todo lo que veremos en estos artículos. Tiempo tendremos, más adelante en esta serie, de hablar de las Docker Tools. La razón de hacerlo primero todo “manual” es porque el objetivo de esta serie es ayudarte a que entiendas Docker, no presentarte el “botón mágico” que se encarga de todo. Yo, es que soy de la vieja escuela: me gusta entender las cosas (al menos hasta donde puedo).
Creando el proyecto
Doy por supuesto que tienes instalado Docker for Windows, así que podemos ir directamente al grano. En este artículo vamos a usar netcore 1.0 con project.json o lo que es lo mismo las tools en preview2. Ya tendremos tiempo más adelante de pasar a las tools en preview3 (es decir las que vienen con VS2017 y usan csproj).
Para ello vamos a crear un directorio vacío y dentro de él vamos a crear un global.json. El objetivo de este fichero es fijar el SDK en su versión de preview2 y así usar project.json. Si no se usa global.json se usa por defecto la última versión de las herramientas y si te has instalado Visual Studio 2017 tendrás la preview3 que usa csproj. De esta manera podemos forzar a seguir usando project.json, que es lo que queremos por ahora.
Coloca el siguiente contenido en dicho fichero:
{
"sdk": {
"version": "1.0.0-preview2-003121"
}
}
Genial, ¡ahora ejecutamos “dotnet new” desde la herramienta de línea de comandos y ya tenemos nuestro proyecto! Podemos probar que funciona ejecutando “dotnet restore” primero y “dotnet run” después y deberíamos ver “Hello world!”
Creando la imagen
Bueno, el siguiente paso es crear la imagen. Para ello debemos crear un dockerfile, que es el fichero que indica a Docker como debe crear una imagen. Este fichero debe llamarse Dockerfile (sin extensión ni nada).
Las imágenes en Docker tienen una relación de herencia. Piensa que la imagen describe los contenedores creados a partir de ella. En estos momentos la palabra “contenedor” es ideal: piensa en los contenedores como eso… contenedores, en los cuales debemos meter “cosas” (vamos, ficheros, librerías y demás). Si queremos crear una imagen para contenedores que ejecuten netcore deberemos colocar el runtime de netcore en el contenedor… y todas las dependencias del runtime y las dependencias de las dependencias y las dependencias de las dependencias de las dependencias y… ¿lo pillas no?
Nosotros no queremos lidiar con todas esas dependencias, así que lo habitual es que nuestra imagen herede de otra. Es decir, que nuestra imagen parta de otra imagen y agregue ficheros a los ficheros especificada por la otra imagen. Podríamos partir de una imagen que tuviese un Ubuntu base y entonces deberíamos agregarle el runtime de netcore y todas sus dependencias (tal y como instalaríamos dotnet core en un Ubuntu salido de fábrica) o incluso mejor partir de una imagen que ya tenga esas dependencias instaladas y solo meterle dotnetcore… o mejor incluso partir de una que tenga dotnet core instalado y así ya solo meterle nuestro código.
Como todo en informática el truco está en esperar un tiempo prudencial para que otro se coma el marrón de generar esas imágenes. En nuestro caso ese tiempo ya ha pasado y Microsoft ya ha generado imágenes base con el runtime de netcore instalado. Así podemos empezar a editar nuestro Dockerfile colocando el siguiente código en él:
FROM microsoft/dotnet:1.0.1-runtime
¡Bienvenido a mi tutorial rápido de Dockerfile! Con FROM XXX indicamos que nuestra imagen hereda de la imagen XXX (calenturientos de mente apartaos XD). Los nombres de imágen tienen un formato usuario/imagen:tag. En este caso el usuario es “microsoft”, la imagen es “dotnet” y el tag es “1.0.1-runtime”.
Vale, esa imagen por si sola no hace nada, sirve solo de base. Ahora debemos meter nuestro código en él. Para ello usamos el comando COPY del Dockerfile. Ese comando copia ficheros del filesystem local al filesystem del contenedor. Dejemos nuestro Dockerfile tal y como sigue:
FROM microsoft/dotnet:1.0.1-runtime
COPY *.cs /app/
COPY project.json /app/
CMD ls app
Hemos añadido dos líneas para copiar nuestros ficheros. La sintaxis de COPY es muy sencilla: COPY origen destino, donde origen está en el filesystem local y destino en el del contenedor (fíjate en las barras a lo Unix, que los contenedores son Linux).
Finalmente la última línea necesita una explicación. ¿Recuerdas que un contenedor ejecuta un solo proceso? Por defecto este proceso es “/bin/sh –c”. Eso lo que hace es abrir un shell y ejecutar lo que se pase despues de –c. Pues bien, CMD nos permite especificar lo que se pasa a –c. Esta imagen todavía no ejecuta nuestro código, pero ya llegaremos a ello. De momento veamos como ejecutarla. ¡Ha llegado el momento de ejecutar la CLI de Docker!
El primer paso es construir la imágen. Para ello lo más sencillo es ejecutar el comando docker build . (observa el punto final). Este comando construye la imagen indicada por el Dockerfile que esté en el directorio indicado (le pasamos el punto que es el directorio actual):
Con ese comando Docker se baja si necesita la imagen base (si no la tienes ya en local) y luego construye nuestra imagen. Nos interesa el ID de esta imagen que es lo que nos da con el mensaje “Succesfully built xxxxxxxx” (lo he marcado en rojo). Por supuesto tu ID será distinto, los genera Docker cada vez.
Ya tenemos una imagen… ahora ya podemos crear y ejecutar contenedores basados en ella. Para ello basta con ejecutar docker run
Podemos ver que la salida es el resultado de listar los ficheros en el directorio /app, es decir el resultado de ejecutar la sentencia especificada en el CMD del Dockerfile. Una vez hecho esto el contenedor termina.
Vamos a borrar la imagen, para ello ejecuta docker rmi
Vale, vamos a modificar nuestro Dockerfile para lanzar nuestro programa. Para ello una suposición sería tener un Dockerfile como el siguiente:
FROM microsoft/dotnet:1.0.1-runtime
COPY *.cs /app/
COPY project.json /app/
CMD dotnet restore && dotnet run
Pero… ¡pataplaf! Ese Dockerfile no funciona. Si construyes la imagen y la ejecutas verás un error como el siguiente:
Este error se da porque la imagen base tiene el runtime de netcore, pero NO el SDK instalado. Podríamos instalarlo nosotros, pero por suerte Microsoft lo ha hecho por nosotros. Hay otra imagen que viene con el SDK instalado: microsoft/dotnet:1.0-sdk-projectjson.
De todos modos, tema de la imagen base aparte este Dockerfile no es correcto. No tiene sentido ejecutar un dotnet restore como parte del comando del contenedor. ¿Por que no? Pues porque entonces cada vez que levantemos un contenedor (con docker run) se ejecutará el restore. ¿Realmente queremos eso? No, porque todos los contenedores tienen los mismos ficheros. Basta con ejecutar el restore una vez y luego “guardarnos” el resultado del restore y cuando levantemos un contenedor ejecutar solo el dotnet run.
Recuerda que una imagen es una descripción a partir de la cual creamos contenedores. La imagen contiene los ficheros que va a tener el contenedor. El resultado de ejecutar “dotnet restore” añade ficheros (como mínimo el project.lock.json). Lo que queremos es que estos ficheros añadidos como parte de “dotnet restore” formen parte de la imagen.
Si estás pensando en ejecutar “dotnet restore” de forma manual y luego copiar el resultado a la imagen… ¡eso no es forma de hacerlo! (entre otras razones porque no tienes la garantía de que “dotnet restore” en tu máquina local genere lo mismo que “dotnet restore” en la máquina que ejecuta los contenedores (recuerda una VM de Linux)). No, la solución es ejecutar este comando y generar una nueva imágen a partir de él.
Y el Dockerfile tiene un comando específico para ello. El comando RUN. Dicho comando tiene la sintaxis RUN
Así nuestro Dockerfile queda como sigue:
FROM microsoft/dotnet:1.0-sdk-projectjson
COPY *.cs /app/
COPY project.json /app/
WORKDIR /app
RUN dotnet restore
CMD dotnet run
Descrito línea por línea:
- Heredamos de la imagen base que contiene el SDK de netcore
- Copiamos los ficheros .cs en /app
- Copiamos el project.json en /app
- Establecemos /app como el nuevo directorio de trabajo
-
Se ejecuta dotnet restore. Esto añade ficheros en la imagen.
- Definimos que el comando a ejecutar al iniciar el contenedor es dotnet run (que se ejecutará en /app debido a que es el directorio de trabajo).
-
- Establecemos /app como el nuevo directorio de trabajo
- Copiamos el project.json en /app
- Copiamos los ficheros .cs en /app
Si ahora ejecutas “docker build .” verás como el proceso tarda un poco más. Eso es porque el “dotnet restore” se ejecuta durante el proceso de construcción de la imagen:
Observa (marcado en verde) la salida del dotnet restore como parte del docker build. Ten presente que a pesar de que veas eso en la consola de windows, la construcción de la imagen la ejecuta la máquina virtual que tiene Docker for Windows.
Ahora ya podemos ejecutar la imagen y probar que realmente funciona:
¡Funciona! Podemos ver el resultado del dotnet run en nuestra consola. Una vez se ha ejecutado el contenedor termina, siempre podemos volver a lanzar otro contenedor ejecutando docker run de nuevo.
Bueno… dejémoslo aquí por hoy. Hemos visto el proceso de creación de una imagen y como crear un Dockerfile sencillo para ejecutar una app netcore. También hemos explorado un poco la CLI de docker.
Espero que os haya gustado… en el siguiente artículo, un poco más 😉