Crear Imagen y Contenedor Docker Nivel Jōnin

Has llegado a un buen nivel hacia la Dockerizacion de tus aplicaciones. Ahora, sigue avanzando con la construcción de imágenes Docker al automatizar la compilación de tus aplicaciones en el proceso; integra recursos, librerías que tu aplicación utilizará. Esta vez, explicaremos con un ejemplo la construcción de imagen Docker para un proyecto Web Net Core que usa React Js.

¿Cómo llegamos aquí?

Conseguiste superar el nivel Genin al crear un contenedor a partir de una imagen del repositorio público; después en el nivel Chūnin, te hiciste de tu contenedor al fabricar tu propia imagen Docker utilizando una imagen base y código.

Un Héroe con… muchas Capas

Así es, y nuestro héroe elemental es la Imagen Docker y sus capas. Durante su construcción con Dockerfile ejecutamos comandos, cada uno le va agregando una capa (layer) de cambios,  incluyendo el comando y argumentos. Los cambios entre capas se agregan de forma diferencial (deltas), es decir, una capa contiene sólo lo que ha cambiado respecto a la capa anterior, esto sirve para el versionado de imágenes y hace ligera a la imágen Docker final; contribuye a implementaciones rápidas (escalamiento horizontal) y reutilización (shared layers).

Ejemplo Web Net Core React Js

Para ejemplificar este post, utilizaremos la plantilla de proyecto React y Redux Js de Visual Studio. Así la ubicas, por si deseas practicar, aunque no te preocupes en tener este insumo para seguir el post:

Recomendamos agregar la opción compatibilidad con Docker, haciendo clic secundario en el proyecto -> Agregar -> Compatibilidad con Docker y seleccionamos el SO destino (Linux en nuestro caso)

Notita mental 🤯: Net Core es tecnología de Microsoft multiplataforma, por lo que puede ejecutarse en un SO Linux.

Las fases con Dockerfile

En un sólo archivo Dockerfile pueden haber varias fases y son bloques de tareas, cada uno con objetivos diferentes pero cuyos resultados (artefactos) sirven a otras fases subsecuentes o al objetivo que es: la construcción de una imagen Docker para un contenedor. El ejemplo de este post mostrará las siguientes fases y sus objetivos específicos:

  1. Descarga de imagen base: Para contar con el conjunto de archivos y el entorno que necesita tu aplicativo para ejecutarse
  2. Compilación de código: Para convertir tu código fuente en un formato ejecutable. Si tu código tiene errores, la compilación no puede ocurrir
  3. Publicar aplicación: Genera en una ruta el conjunto de archivos que utiliza tu aplicación, código compilado, dependencias, paquetes, etc.
  4. Copiar archivos de publicación y configurar punto de entrada: El punto de entrada sirve para indicar qué proceso se ejecutará al iniciar el contenedor.

Como analogía, un punto de entrada de una imagen Docker es similar al método Main existente en varios lenguajes de programación, que es a partir de ahí donde la ejecución de la aplicación o proceso comienza.
Cada aplicativo puede tener más o menos fases y cada una, esencialmente, generará Artefactos. Un artefacto es el resultado de un proceso, por ejemplo los archivos de un proceso de compilación, que se utiliza como pieza en la construcción de un Software final.

Creando fases con Dockerfile

Recordemos que el archivo Dockerfile tiene las instrucciones para poder construir nuestra imagen Docker, requisito previo a instanciarla en un contenedor; una tarea como esta puede llevar más de una fase (stage). Las fases, o stages, en un archivo Dockerfile comienzan con la instrucción FROM, su argumento principal es la imagen base de algún repositorio o alguna que provenga de otra fase; seguido de otras instrucciones como COPY, RUN, WORKDIR, etc.

Fase: Imagen Base

Similar a lo que vimos en el post nivel Chūnin, nuestro Dockerfile comienza con instrucción FROM para indicar la imagen base, en nuestro ejemplo es ASP Net Core, y necesitamos establecer el Directorio de Trabajo (WORKDIR), también indicaremos los puertos que deseamos exponer, 80 para HTTP y 443 para HTTPS. Así que nuestra primera fase queda así:

FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443

Con el comando EXPOSE se indicaron los puertos por donde escuchará peticiones el contenedor, aunque pueden cambiar. Cuando llegue el momento de ejecutar el comando “docker run” para crear el contenedor y exponer los puertos del mismo, la opción “-p” le servirá para cambiar dichos puertos así como mapearlos a los puertos de su máquina Host, en cambio, si utiliza la opción “-P” los puertos a exponer del contenedor serán los indicados en el Dockerfile con EXPOSE aunque los puertos de la máquina Host serán aleatorios. Para recordar: es diferente los puertos que un contenedor expone a los puertos de la máquina Host (máquina que ejecuta el motor de Docker).

Podemos asignar a cada fase un nombre, en este caso fue «base», de tal forma que en fases subsecuentes podemos hacer referencia a ella y usar sus artefactos generados.

Fase: Compilación de Código

Necesitamos contar con código ejecutable, por lo que necesitamos transformar nuestro código fuente mediante la Compilación; como Humano 😀 utilizamos nuestro IDE favorito y una opción que dice “Build” o tal cual “Compilar” 🙃, para conseguirlo

Pero ¿Y si está compilación se hiciera como parte de un proceso automático? 🤯 ¡Y que la hiciera un robot!😱🤖

Justamente esa tarea se puede automatizar en esta fase como se muestra en las siguientes líneas del Dockerfile:

FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build
WORKDIR /src
COPY ["React2Docker/React2Docker.csproj", "React2Docker/"]
RUN dotnet restore "React2Docker/React2Docker.csproj"
COPY . .
WORKDIR "/src/React2Docker"
RUN dotnet build "React2Docker.csproj" -c Release -o /app/build

Esto es lo que ocurre:

  • Cada fase comienza con comando FROM. Para el objetivo de esta fase no necesitamos el entorno donde la aplicación se va a ejecutar, necesitamos herramientas para compilar código, por esa razón la imagen a usar es el SDK DotNet. También le damos nombre de “build” a la fase
  • Se establece el directorio de trabajo con WORKDIR.
  • COPY ayuda a copiar archivos de una ruta a otra
  • Con RUN ejecutamos dotnet
  • Observe que se utilizan comandos dotnet propios del SDK: restore (restaurar paquetes) y build (compilar)
  • Además, dotnet build recibe parámetros para una versión Release así como la ruta donde queremos que nos entregue los archivos compilados (artefactos)

¿Y si falla el proceso de compilación? Entonces la fase falla y la construcción de la imagen ya no ocurrirá.

Fase: Publicación

Después de tener compilada nuestra aplicación, también en los IDE de desarrollo se incluye una herramienta para generar y colocar los archivos de publicación, algún botón que dice Publish o Publicar, seguido de métodos para especificar el destino de nuestro empaquetado final, como una ruta de algún directorio, FTP, destino en la nube, por mencionar algunos.

El DotNet SDK también tiene lo necesario para realizar el proceso de publicación, por lo que nuestra fase de Dockerfile queda así:

FROM build AS publish
RUN dotnet publish "React2Docker.csproj" -c Release -o /app/publish

Esto ocurre:

  • También comenzamos con instrucción FROM y hay algo interesante: en vez del nombre de imagen base de DotNet SDK, utilizamos la fase «build» 😱 que previamente generamos, ya que dicha fase ya cuenta con el SDK necesario 👍
  • Debido a que la fase “build” generó los artefactos, que son el conjunto de archivos para nuestra aplicación, ahora los utilizaremos en el proceso de publicación que hace dotnet publish
  • Esta fase, ¡también genera sus propios artefactos!; comprueba que la bautizamos como  «publish»

La Fase Final… ¡De veras!

Para conseguir la imagen final que llevará nuestro próximo contenedor, esta fase de construcción juntará dos piezas: los artefactos y el entorno para ejecutar la aplicación, para dar lugar a las siguientes instrucciones del Dockerfile

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "React2Docker.dll"]

Esto ocurre:

  • En el comando FROM utilizamos los artefactos de nuestra primer fase que llamamos “base”
  • Básicamente los artefactos de la fase “base” son los archivos descargados de la imagen de ASP Net y el directorio creado con WORKDIR “/app”
  • Como necesitamos nuestros archivos de publicación, recurrimos a COPY para copiar los archivos creados en la fase “publish” y que dejamos en ruta “/app/publish”; observe que la instrucción termina con un punto, ahí es donde indicamos la ruta destino, un punto en el comando COPY hace referencia al directorio de trabajo actual, el cual previamente establecimos con comando WORKDIR “/app”
  • No pierda de vista el hecho de que hemos copiado archivos de una fase previa a la fase actual de construcción, y el hecho de usar una fase creada varias fases antes (la fase “base”)

Dado que ya no hay más fases, el estado de la última fase es la imagen Docker final, con todos los artefactos que se hayan dejado en ella. 
Esto último es muy importante: el uso de fases (multi stages)  es para poder entregar imágenes Docker lo más ligeras posibles, con el software exacto con el que se debe contar. Cada fase puede dejar artefactos que no necesitará nuestro aplicativo, pero conseguimos descartarlos al hacer uso de comando FROM y COPY para traer entre las distintas fases los artefactos que sí vamos a usar.

Vale, ¿Pero funciona?

Recuerda, Dockerfile nos ayuda a construir la imagen Docker deseada, es un archivo con comandos para lograrlo, como si de una receta se tratase. En la práctica, se utiliza en flujos de trabajo (pipeline) que lo llaman de forma automática, también se puede invocar de forma manual. Se necesita abrir una ventana de terminal, posicionarse en una ruta sobre la que deseamos trabajar (o donde está nuestro proyecto) y ejecutar docker build de la siguiente manera:

C:\>cd C:\WSGit\React2Docker
C:\WSGit\React2Docker>docker build . -f React2Docker\Dockerfile -t myreact2docker:1.0.0

Para considerar: En mi local 😅, la estructura de carpetas donde está el código fuente y el Dockerfile es la siguiente:

El comando build inicia el proceso de construcción utilizando el archivo Dockerfile que se encuentre en la ruta establecida como directorio de trabajo actual, si no es el caso, use el parámetro -f para indicar su ubicación, también debe usarse si el archivo Dockerfile tiene otro nombre; se permite utilizar rutas relativas para acortar. Note que también bautizamos la imagen con nombre “myreact2docker” y una etiqueta a modo de versionado “1.0.0”.

Este es el resultado de ejecutar el comando “docker build”:

Ocurrió un error 😱, observe donde dice “npm: not found”, el error hace referencia a un gestor de paquetes de Node js, necesario al trabajar con React Js y compilar componentes del mismo. Basta con agregar al Dockerfile la descarga de dichas dependencias, en la fase “base” y “build”, teniendo un archivo final así

Dockerfile completo

FROM mcr.microsoft.com/dotnet/aspnet:3.1 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
RUN curl -sL https://deb.nodesource.com/setup_12.x |  bash -
RUN apt-get install -y nodejs

FROM mcr.microsoft.com/dotnet/sdk:3.1 AS build
RUN curl -sL https://deb.nodesource.com/setup_12.x |  bash -
RUN apt-get install -y nodejs
WORKDIR /src
COPY ["React2Docker/React2Docker.csproj", "React2Docker/"]
RUN dotnet restore "React2Docker/React2Docker.csproj"
COPY . .
WORKDIR "/src/React2Docker"
RUN dotnet build "React2Docker.csproj" -c Release -o /app/build

FROM build AS publish
RUN dotnet publish "React2Docker.csproj" -c Release -o /app/publish

FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "React2Docker.dll"]

De vuelta a ejecutar “docker build” ahora obtenemos la siguiente salida:

Verificamos con “docker images” la lista de imágenes para corroborar que contamos con la recién creada:

Si tienes Docker Desktop también puedes comprobar ahí:

Crear e Iniciar Contenedor

Ahora resta ejecutar la imagen en un contenedor con el comando “docker run”

docker run –name myreact-container -p 80:80 -p 443:443 -d myreact2docker:1.0.0

Después corroborar con docker ps cómo quedaron mapeados los puertos

Observe que utilizamos el parámetro -d, para ejecución del contenedor en segundo plano e imprimir el ID del contenedor; de otra forma, la terminal desde donde lanzamos el comando quedará “bloqueada” por el proceso del contenedor hasta que lo detengamos.
¡Ahora, a probarlo en el navegador! Por ser un entorno local, la ruta a probar sería http://localhost:80

En este ejemplo, al probar con https://localhost:443, lo más probable es que obtengamos un error, eso se debe a que faltan archivos y configuraciones para entrar por protocolo Https, y este tema lo dejaremos para futuras publicaciones.

El camino a la Dockerizacion no termina aquí

Una solución de software está conformada por varios aplicativos, servicios, microservicios, en diferentes tecnologías; algunos o todos sus componentes pueden estar implementados con Docker, y en la práctica implica considerar flujos (pipeline) para automatizar la creación y despliegue en contenedores. Justamente para la creación y despliegue de varios contenedores a la vez, es conveniente contar con alguna especie de “script” o archivo donde se describen parámetros y configuraciones, de tal forma que no tengamos que introducirlos manualmente por cada release para cada contenedor que utilice nuestra solución. En una futura publicación hablaremos de Docker Compose, una herramienta que utiliza archivos YAML para el despliegue de varios contenedores (multi-container), y tu camino hacia la Dockerización irá agilizando más. ¡De veras!

Deja un comentario

Tu dirección de correo electrónico no será publicada.