ballena

Vamos a mostrar como dockerizar una aplicación Angular, la cual vamos a construir con la herramienta AngularCLI y para meter toda la aplicación dentro de un container (dockerizarla) vamos a usar Docker junto a Docker-Compose, seteando una configuración para el entorno de desarrollo y otra diferente para el entorno productivo (no soy desarrollador Angular, lo aclaro por si meto algun error de conceptos del framework).

Pre-requisitos

  • Instalar NodeJS en su última Versión
  • Actualizar NPM (el gestor de paquetes de node)
  • Instalar la última versión de Angular-CLI (la consola de Angular)
  • Tener nociones básicas de Docker, si estas comenzando con Docker te recomiendo el post Minicurso de Docker desde Cero.

 

Creamos el Proyecto

Vamos a levantar un proyecto Angular desde cero gracias a la magia de AngularCLI con el siguiente comando y luego vamos a posicionarnos en el directorio de la app que acabamos de crear:

ng new app-zeppelin
cd app-zeppelin

Aclaración: cuando le demos enter al primer comando, Angular CLI nos va a preguntar dos cosas, la primera es si deseamos utilizar Angular Routing y la segunda, que sistema de estilos queremos utilizar (css, sass etc) estas opciones dependerán del proyecto y del desarrollador obviamente, en mi caso utilizo el sistema de rutas del framework y utilizo sass para los estilos.

ngnew

 

Dockerfile (Entorno Dev)

Vamos a crear un Dockerfile para uso exclusivo en entorno de desarrollo (más adelante les cuento porque). Vamos a agregar un archivo llamado dev.Dockerfile al root directory de la aplicación Angular que acabamos de crear, con el siguiente contenido:

# base image
FROM node:11.9.0
MAINTAINER Manu Barrios<manuel.barrios@mymware.com>

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# add .bin to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install package.json (o sea las dependencies)
COPY package.json /usr/src/app/package.json
RUN npm install
RUN npm install -g @angular/cli@1.7.3 

# add app
COPY . /usr/src/app

# start app
CMD ng serve --host 0.0.0.0

Parafraseando: Parafraseando el dockerfile muuuuyyyy por arriba, nos basamos en la imagen node:11.9.0 (la última al momento que se escribió esta entrada), seteamos un directorio de trabajo, añadimos los binarios al path, copiamos el contenido del archivo package.json (que contiene todas las dependencias del proyecto) instalamos todo con npm, copiamos todos los archivos de nuestro proyecto dentro del workdir del contenedor y le damos un start al server nativo de Angular.

Agregamos también el archivo .dockerignore con el siguiente contenido:

node_modules
.git

básicamente lo único que hace el dockerignore es evitar que lo que especificamos en dicho archivo sea enviado al Docker Daemon (que los ignore) en este ejemplo añadí las dependencias locales y la info de git.

Buildeamos (un término bien bien latino) el dockerfile que acabamos de crear, también vamos a ponerle un tag y a pasarle un Dockerfile especifíco, con el siguiente comando:

docker build -f dev.Dockerfile -t app-zeppelin .

Cuando termine este proceso, ya tendremos creada nuestra imagen docker, para verla la podemos listar con el comando docker image list como se muestra en la siguiente imagen:

dockerimagelist

Ahora vamos a crear/levantar/correr el contenedor propiamente dicho, a partir de nuestra imagen, que creamos a partir de nuestro dockerfile (weeeeeeee) con el siguiente comando:

docker run -it \
  -v ${PWD}:/usr/src/app \
  -v /usr/src/app/node_modules \
  -p 4200:4200 \
  --rm \
  app-zeppelin

Parafraseando: corremos el contenedor en modo interactivo (vamos a ver una especie de mensaje de debug en la consola), si lo desean pueden quitar ese flag y correr el container en segundo plano con -d, bien, con el flag -v estamos asignando volúmenes (almacenamiento persistente) dentro del contenedor, luego le estamos diciendo que queremos exponer el puerto 4200 del lado del container y del lado del host también (es decir, como una especie de forward de puertos, si apuntamos al puerto 4200 de nuestra maquina, esta petición va a ir al puerto 4200 de nuestro contenedor), con rm le estamos diciendo a docker que una vez que paremos la ejecución del mismo el container se borre automáticamente, y por último le pasamos el nombre que le pusimos a nuestra imagen anteriormente, todo esto puede ir en una sola línea, pero utilizamos la barra invertida para ordenarlo y verlo un poco mejor (luego pueden matar el contenedor con ctrl+c).

En este punto ya tenemos nuestra aplicación corriendo dentro de un contenedor, vamos a nuestro browser y accedemos a http://localhost:4200/ veremos la aplicación que acabamos de crear

angularenbrowser

En este escenario si realizamos algún cambio por ejemplo en el template del AppComponent de Angular (src/app/app.component.html) vamos a notar que el cambio impacta automáticamente en nuestra aplicación, gracias al Hot-Reload de Angular, es decir, el cambio lo hacemos en dicho archivo, y automáticamente esto impacta en la aplicación y también en el contenedor en tiempo real y en caliente, sin necesidad de volver a correr el proyecto o el contenedor, esto resulta bastante cómodo para el desarrollador, es por este motivo que lo hemos armado de esta forma, tanto el Dockerfile, como el modo de correr el container , para trabajarlo en entornos de desarrollo

Docker Compose

Bien, ahora, veamos cómo lanzar nuestro contenedor utilizando Docker Compose, vamos a crear un archivo también en el root directory de nuestra aplicación, llamado docker-compose.yml con el siguiente contenido:

version: '3.5'

services:

  app-zeppelin:
    container_name: app-zeppelin
    build:
      context: .
      dockerfile: dev.Dockerfile
    volumes:
      - '.:/usr/src/app'
      - '/usr/src/app/node_modules'
    ports:
      - '4200:4200'

Ahora, lanzar nuestro contenedor será mucho más sencillo, y solo nos bastará con la instrucción UP de docker compose y un par de parámetros

docker-compose up -d --build

Ahora queda checkear si se levantó nuestra aplicación, luego, para detener el container lo hacemos con el siguiente comando:

docker-compose stop

 

Salgamos a Producción !!!

Vamos a construir un Dockerfile exclusivo para usarlo en producción, usando un recurso muy bueno que tiene Docker llamado Multi-Stage Builds nuevamente creamos un archivo en el root directory de nuestra aplicación Angular, esta vez, la llamaremos prod.Dockerfile y tendrá el siguiente contenido:

########################################
##  _           _  _     _            ##
## | |__  _  _ (_)| | __| | ___  _ _  ##
## | '_ \| || || || |/ _` |/ -_)| '_| ##
## |_.__/ \_,_||_||_|\__,_|\___||_|   ##
########################################

# base image
FROM node:11.9.0 as builder
MAINTAINER Manu Barrios<manuel.barrios@mymware.com>

# set working directory
RUN mkdir /usr/src/app
WORKDIR /usr/src/app

# add `/usr/src/app/node_modules/.bin` to $PATH
ENV PATH /usr/src/app/node_modules/.bin:$PATH

# install and cache app dependencies
COPY package.json /usr/src/app/package.json
RUN npm install
RUN npm install -g @angular/cli@1.7.3 

# add app
COPY . /usr/src/app

#generate build
RUN npm run build 

############################
##                     _  ##
##  _ __  _ _  ___  __| | ##
## | '_ \| '_|/ _ \/ _` | ##
## | .__/|_|  \___/\__,_| ##
## |_|                    ##
############################
# base image
FROM nginx:1.15.9-alpine

# copy artifact build from the 'build environment'
COPY --from=builder  /usr/src/app/dist /usr/share/nginx/html
COPY default.conf /etc/nginx/conf.d/default.conf

# expose port 80
EXPOSE 80

# run nginx
CMD ["nginx", "-g", "daemon off;"]

Lo importante y lo mágico de este archivo, es que estamos aprovechando la función de multistage builds de Docker para crear en una primer instancia del archivo (el stage builder) una imagen temporal con la cual vamos a crear un artefacto que vamos a usar en el siguiente stage (stage prod); la imagen temporal de desecha junto con las dependencias, archivos y directorios originales de la imagen. El resultado de todo esto es una especie de compilación de lo más importante del proyecto, listo para ser publicado a producción. Como podemos ver en el siguiente stage, en la sentencia COPY from builder, estamos justamente utilizando el artefacto generado en el stage anterior para simplemente publicarlo en el public de nginx, también copiamos el archivo de configuración de nginx (default.conf, que lo podemos configurar como más nos guste, esto ya queda por cada admin setearlo), y por supuesto, exponer el puerto 80/443 y lanzar el webserver Una cosa más, los banners tipo ASCII no son para nada necesarios, los puse porque estan muy copados ( XD ) y tambien sirve bastante para diferenciar los dos stages, pero pueden quitarlos sin que esto afecte su dockerfile

  Buildeamos el dockerfile productivo con:

docker build -f prod.Dockerfile -t app-zeppelin-prod .

Y por supuesto…. lo corremos con:

docker run -it -p 80:80 --rm app-zeppelin-prod

Ahora lo verificamos en el browser consultando simplemente por http://localhost y veremos nuestra aplicación corriendo en modo productivo.

ATENCIÓN: quizás una de las cosas más importantes de este step o por lo menos lo que a mi me parece muy bueno, y vale la pena mencionarlo, es el gran ahorro de espacio en disco entre la imagen generada por el dockerfile de desarrollo Vs la imagen generada por el dockerfile productivo, a verlo:

  • Imagen generada por dev.Dockerfile:

dockerimagelistpesado

  • Imagen generada por prod.Dockerfile

dockerimagelistprod

El porqué de esto, no es ningún secreto, pensémoslo un momento… en el primer caso estamos frente a una imagen de docker basada en node (algo bastante pesadito) y en el otro caso estamos frente a un webserver super liviano como lo es nginx y a su vez este basado en una imagen alpine (una distro de GNU/Linux muy ligera), si bien usamos la misma imagen de node en el primer stage del dockerfile productivo, recuerden que solamente lo estamos usando como builder y que la imagen final va a ser la usada en el segundo stage.

Existe una cosa más, que es diferente en el ambiente productivo que acabamos de lanzar, recuerdan lo del Hot-Reload? bien, si prueban hacer alguna modificación a alguno de los archivos del proyecto, esto ya no va a impactar más en lo que tenemos publicado, ya que lo que publicamos es una versión compilada/empaquetada del código en el momento del deploy.

Que Sigue… producción con Docker Compose: Creamos el archivo docker-compose-prod.yml con el siguiente contenido:

version: '3.5'

services:

  app-zeppelin-prod:
    container_name: app-zeppelin-prod
    build:
      context: .
      dockerfile: prod.Dockerfile
    ports:
      - '80:80'

Lo corremos con:

docker-compose -f docker-compose-prod.yml up -d --build

Lo verificamos en el browser en http://localhost y veremos a nuestra querida aplicación Angular dockerizada en producción:

angularenbrowserprod

Nice Work!!! Bien, hasta acá llegó el post, espero que te haya servido.

Hasta acá la parte sencilla, ahora a programar…

Invitame un CaféInvitame un Café