Docker

Docker ha dejado de ser una tecnología desconocida para convertirse (casi) en un estándar: La mayoría de empresas y desarrolladores lo utilizan a diario por su simplicidad para empaquetar y ejecutar aplicaciones (especialmente en sistemas distribuidos). Docker nos evita tener que instalar miles de dependencias, lo que hace que nuestros sistemas de producción sean mucho más fáciles actualizar y mantener.

Si (casi) todo el mundo está utilizando Docker para desplegar sus aplicaciones en contenedores... es hora de ponernos las pilas ;)

¿Qué es un contenedor (Linux)?

Un contenedor es una serie de procesos que sólo tienen acceso a cierta parte de los recursos de la máquina donde se están ejecutando. Para que esto funcione, se utilizan algunas utilidades del Kernel de Linux, como los `cgroups` y los `namespaces`. El Container Run Time más utilizado es Docker. Docker es una plataforma abierta para que desarrolladores y administradores de sistemas construyan, desplieguen y ejecuten aplicaciones en contenedores de software.

En otras palabras: un contenedor es una manera de empaquetar tu aplicación asegurándote de que se va a comportar de la misma manera independientemente de dónde se ejecute.

Ventajas

Los contenedores nos ayudan a empaquetar nuestras aplicaciones y todas sus dependencias, asegurándonos de que el contexto de ejecución sea siempre el mismo, así evitamos el clásico 'Pues en mi máquina funciona'.

Los sistemas de microservicios suelen ser políglotas (pueden estar implementados en lenguajes y frameworks distintos). Esto hace que las fases de construcción y despliegue puedan ser bastante complejas. Sin embargo, si ejecutamos las aplicaciones en contenedores, todas hablarán un lenguaje común en términos de ejecución y/o desarrollo. Gracias a los contenedores, los desarrolladores podemos tener un entorno estable de desarrollo y por otro lado facilitamos la labor al departamento de operaciones

Además, algunas plataformas de contenedores - como Docker - nos proporcionan sistemas distribuidos - como los repositorios de Docker - que podemos utilizar para almacenar y distribuir nuestras aplicaciones.

Por tanto, si usamos contenedores, nos ayudarán a empaquetar, distribuir y ejecutar nuestras aplicaciones.

Instalación

Si todavía no has instalado Docker CE, aquí tienes los enlaces por SO:

Windows

- Guía de instalación de Docker CE para windows aquí Windows installation guide

- Descarga directa de Docker CE descarga Docker CE

MacOS

- Guía de instalación de Docker CE para MacOS aquí guía de instalación MacOS

- Descarga directa de Docker CE descarga Docker CE

Linux

- Centos guía de instalación

- Debian guía de instalación

- Fedora guía de instalación

- Ubuntu guía de instalación

- Binaries guía de instalación

Agenda

En este artículo vamos a ver cómo utilizar contenedores (elegimos Docker como tencología) en una aplicación distribuida. Vamos a cubrir los siguientes temas:

  • Dockerizar un Back End, crear los ficheros Dockerfile y .dockerignore.

  • Construir una imagen de Docker a partir del Back End

  • Ejecutar el contenedor del Back End desde el build.

  • Dockerizar el Front End, utilizando un build multietapa. También incluiremos nginx como servidor del Front End.

  • Construir una imagen de un Front End.

  • Ejecutar el contenedor de dicho Front End desde su carpeta build.

  • Crear una infraestructura multi contenedor gestionada por Docker Compose.

Ejemplo

Como ejemplo, vamos a utilizar una aplicación de Chat. La aplicación está dividida en dos partes: cliente (Front End) y servidor (Back End). Vamos a utilizar Docker para dividirla en contenedores, que después ejecutaremos.

La aplicación de Chat que usaremos como ejemplo

La aplicación de Chat que usaremos como ejemplo

Aquí tenéis los dos repositorios que vamos a utilizar para crear la aplicación de chat:

- Front End

- Back End

Para poder ejecutarla en local vamos añadir otra pieza más: un balanceador de carga. Éste se encargará de enrutar el tráfico al front o al back, según la url que haga la petición. El balanceador de carga va a estar en un contenedor independiente, que también vamos a ejecutar y desplegar con Docker.

Como vamos a trabajar con varios contenedores, utilizaremos docker-compose: una herramienta que nos permite trabajar con entornos multi-contenedor.

Para comprobar que las aplicaciones front y back funcionan

Clona ambos repositorios en la misma carpeta raíz:

.
├── container-chat-back-example
└── container-chat-front-example

Abrimos dos terminales y ejecutamos desde la carpeta raíz de ambas aplicaciones los siguientes comandos (desde la shell o bash)

npm install
npm start

En la otra terminal accedemos a container-chat-front-example, y arrancamos la aplicación de front:

npm start

Si accedemos a http://localhost:8080 desde un navegador web, podremos introducir un nick, elegir una sala y empezar a chatear.

Podemos abrir varias instancias del navegador (con nicks diferentes), y ver cómo los mensajes de la misma sala se muestran en tiempo real.

Qué son las imagenes Docker

Ya sabemos lo que son los contenedores, pero todavía nos falta un poco hasta que podamos levantar uno. Éstos se ejecutan a partir de una imagen de Docker, que es una plantilla de sólo lectura que contiene los comandos necesarios para crearlo. Podemos usar esta plantilla para lanzar tantos contenedores como necesitemos.

Cómo puedo construir una imagen de Docker?

Para crear una imagen de Docker necesitamos crear un fichero, normalmente llamado `Dockerfile`, donde escribiremos todos los pasos necesarios para construirla.

Construir una imagen de Docker es un proceso de composición, donde partimos de una imagen ya construida, y la modificamos, le añadimos pasos tales como: añadir código, compilar código... y le indicamos cómo ejecutar nuestra aplicación. Cada paso produce una imagen intermedia, llamada capa. Hay algunas palabras reservadas, como por ejemplo:

- FROM: Para partir de una imagen de Docker como base.

- COPY/ADD: Para copiar ficheros de nuestra máquina a la imagen.

- RUN: Para ejecutar comandos.

- CMD/ENTRYPOINT: Para indicar cómo ejecutar tu aplicación, y por tanto, levantar el contenedor.

El fichero `DockerFile` suele estar localizado en la carpeta raíz del proyecto, y puede tener un contenido como este:

FROM node
COPY src/ app/
RUN npm install
CMD ["npm", "start"]

Una vez tengas tu `Dockerfile` preparado, puedes construir la imagen de docker ejecutando el siguiente comando desde la carpeta de la aplicación (el simbolo $ es sólo el simbolo del terminal de linux, no lo tienes que copiar):

$ docker build -t mydockerimage.

Si la imagen de docker ha sido construida correctamente, ¡ya la puedes utilizar para crear contenedores!

dockerfile2.png

Back End

Desde un punto de vista funcional, el Back End de nuestra app es un servidor de chat. Es una aplicación express que utiliza socket.io para dar soporte a websockets.

Vamos a crear un contenedor para nuestro Back End utilizando Docker. El primer paso es crear un fichero Dockerfile en la carpeta raíz del proyecto, que va a contener los pasos necesarios para producir la imagen de Docker.

  1. Indicar la imagen base

  2. Establecer un directorio de trabajo

  3. Copiar el código

  4. Instalar dependencias

  5. "Exponer" el puerto

  6. Indicar cómo iniciar la aplicación

Para evitar que se añadan ficheros/carpetas que no necesitamos a nuestra imagen, podemos crear un fichero llamado .dockerignore en la raíz del proyecto e indicar los ficheros que no queremos que se incorporen (este fichero ademite caracteres de tipo comodín, más informacion en este enlace):

./container-chat-back-example/.dockerignore

node_modules/
.gitignore
Dockerfile
License
test/
e2e/

1. Indicar Imagen Base

Como ya sabemos, para construir una imagen de Docker necesitamos una imagen base. En nuestro caso, vamos a utilizar la imagen de node (un SO linux que trae nodejs instalado). En Docker Hub tenemos una gran cantidad de imágenes públicas que podemos utilizar.

./container-chat-back-example/Dockerfile

FROM node

2. Establecer el Directorio de Trabajo

El siguiente paso es establecer un directorio de trabajo, donde estará localizado nuestro código. En nuestro caso, vamos a elegir el directorio `/opt/back`, a tener en cuenta: recuerda utilizar el sistema de ficheros de Linux.

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back

3. Copiar el Código

Copiar el código es muy fácil, usaremos el comando COPY, como previamente hemos creado el .dockerIgnore file no nos tenemos que preocupar de que copie ficheros innecesarios.

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back
COPY . .

4. Instalar Dependencias

Una vez tengamos nuestro código, tenemos que instalar las dependencias, usaremos el mismo comando que emplearíamos de manera local: npm install. Para ello tenemos que utilizar la palabra reservada RUN.

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back
COPY . .
RUN npm install

5. "Exponer" el Puerto

En realidad la palabra reservada EXPOSE no expone ningún puerto. Es una especie de 'mensaje' entre el desarrollador que crea el Dockerfile y el desarrollador que va a ejecutar el contenedor. El desarrollador que ejecutará el contenedor tendrá la información necesaria para mapear los puertos entre el contenedor y el anfitrión.

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back
COPY . .
RUN npm install
EXPOSE 3000

6. Indicar Cómo Iniciar la Aplicación

El último paso es indicarle a Docker cómo iniciar la aplicación, y por tanto, el contenedor. En este caso, vamos a utilizar la palabra reservada ENTRYPOINT, y el comando. El comando debe seguir el patrón ["ejecutable", "param"]

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back
COPY . .
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "start"]

El Dockerfile final que nos queda es el siguiente:

./container-chat-back-example/Dockerfile

FROM node
WORKDIR /opt/back
COPY . .
RUN npm install
EXPOSE 3000
ENTRYPOINT ["npm", "start"]

Resumen de los pasos de configuración que hemo creado:

  • FROM node especificamos la imagen base de node.

  • WORKDIR /opt/back establecemos el directorio de trabajo como /opt/back.

  • COPY . . copiamos el contenido al contenedor. El destino será el directorio de trabajo especificado anteriormente: /opt/back.

  • RUN npm install instalamos las dependencias.

  • EXPOSE 3000 informamos de cuál es el puerto que expondrá nuestra app.

  • ENTRYPOINT ["npm", "start"] comando para iniciar nuestro contenedor (arrancamos nuestro servidor de backend).


Si quieres aprender más sobre los comandos utilizados échale un vistazo a la sección final de recursos de este post.

La estructura de la carpeta que nos queda:

.
├── Dockerfile
├── .dockerignore
├── .git
├── .gitignore
├── e2e
├── LICENSE
├── package.json
├── README.md
├── src
└── test

Back End Docker Image

Una vez tengas listo tu `Dockerfile`, puedes construir la imagen de Docker, para ello ejecutamos el siguiente comando desde el terminal.

$ docker build -t back .

Tambien puedes ver que imagenes de docker tienes en tu máquina, en este caso queremos ver el detalle de la imagen que acabamos de crear, para ello ejecutamos el comando docker images.:

$ docker images back
REPOSITORY    TAG       IMAGE ID        CREATED          SIZE
back          latest    b8c7bb8dc6c4    2 minutes ago    918MB

Contenedor Docker del Back End

Una vez que tengas tu imagen de Docker para el Back End, puedes lanzar tu primer contenedor en local utilizando el comando docker run <options> <nombre de la imagen>. Cuando lo ejecutemos, tenemos que especificar un mapeo entre puertos. Como vamos a ejecutar la aplicación en el puerto 3000 dentro del contenedor, podemos mapearlo al mismo puerto en nuestro anfitrión usando la opción -p. También podemos asignarle un nombre al contenedor: mybackend.

$ docker run -p 3000:3000 --name mybackend back

Para comprobar si la aplicación se está ejecutando, abre el navegador (o Postman) y accede a la siguiente url (este endpoint nos devuelve una lista de las salas de chat que están disponibles): http://localhost:3000/api/rooms.


Comprobando que el servidor de chat se ha levantado correctamente

Comprobando que el servidor de chat se ha levantado correctamente

Front End

Nuestro Front End es un cliente web del Back End. Es una aplicación que está hecha en React, y que utiliza Redux y redux-sagas para manejar la asicronía. El cliente websocket del front está creado con socket.io, y como lenguaje de base, está programado en TypeScript, en vez de en JavaScript.


Vamos a crear un contenedor para la aplicación Front End con Docker. Arrancamos de la misma manera que con el backend, creando el fichero .dockerignore en la raíz del proyecto (así evitamos que copien ficheros que no necesitamos a la imagen de Docker).

./container-chat-front-example/.dockerignore

node_modules/
.gitignore
Dockerfile
License

Vamos a crear el Dockerfile (en la carpeta raíz) con todos los pasos necesarios para construir una imagen de Docker a partir de la aplicación Front End. Los pasos son:

  1. Establecer la imagen base

  2. Establecer el directorio de trabajo

  3. Copiar el Código

  4. Instalar dependencias

  5. Hacer un buld del código

  6. Utilizar el patrón Constructor

  7. Configurar el Servidor Web

1. Imagen Base

El primer paso es utilizar `node` como imagen base:


./container-chat-front-example/Dockerfile

FROM node

2. Directorio de Trabajo

Establecemos /opt/front como directorio de trabajo:

./container-chat-front-example/Dockerfile

FROM node
WORKDIR /opt/front

3. Copiar el Código

Copiamos el código fuente:

./container-chat-front-example/Dockerfile

FROM node
WORKDIR /opt/front
COPY . .

4. Instalar dependencias

Instalamos las dependencias con npm:

./container-chat-front-example/Dockerfile

FROM node
WORKDIR /opt/front
COPY . .
RUN npm install

5. Hacer un build del código (build de producción)

Construimos el código de producción traspilando nuestro código fuente:

./container-chat-front-example/Dockerfile

FROM node
WORKDIR /opt/front
COPY . .
RUN npm install
RUN npm run build:prod

6. Patrón Constructor

Ahora mismo estamos creando la imagen de una sola vez. Al crearla en un solo paso, hemos incluido varios ficheros intermedios / temporales en la imagen, esto es mejorable: para ahorrar espacio en la imagen final, sólo deberíasmo de utilizar los ficheros de producción.

¿No sería mejor copiar el código de producción de la carpeta `dist` y desechar el resto de la imagen? Para hacer esto, podemos utilizar el patrón Constructor: un Dockerfile multi-etapa donde una etapa puede aprovechar los resultados producidos por otras etapas. Más información sobre el patrón Constructor en este enlace . Vamos a aprovechar este paso, para usar un servidor web estático más potente: NgInx, y nombrar las etapas del proceso de construcción ( esto nos va a ser de gran ayuda a la hora de copiar ficheros).

Pero, ¿Qué es Nginx? Es un software open source que se puede utilizar como servidor web, reverse proxy, balanceador de carga, caché... Puedes encontrar más información aquí

¿Y por qué usamos Nginx? Porque Nginx escala muy bien, es muy versátil, y ha sido ampliamente testeado en este tipo de escenarios. Otro punto a favor es que Nginx nos permite usar patrones de servidor complejos, como _reverse proxy_, utilizando ficheros de configuración. Más información en este enlace: ¿Por qué usar Nginx?

En este paso además de añadir lineas de configuración al final del fichero, vamos a actualizar la primera (FROM node as builder).

./container-chat-front-example/Dockerfile

FROM node AS builder
WORKDIR /opt/front
COPY . .
RUN npm install
RUN npm run build:prod

FROM nginx
WORKDIR /var/www/front
COPY --from=builder /opt/front/dist/ .

Veamos esto paso a paso:

Hemos creado un contenedor temporal para el proceso de construcción. Fijaos en que utilizamos un alias, **builder** (primera línea del fichero):

FROM node AS builder
WORKDIR /opt/front
COPY . .
RUN npm install
RUN npm run build:prod

Utilizamos el resultado del proceso de construcción para crear nuestra imagen final _nginx image_:

FROM nginx
WORKDIR /var/www/front
COPY --from=builder /opt/front/dist/ .

7. Configuración del Servidor Web

Si queremos utilizar Nginx, tenemos que crear un fichero de configuración para indicarle dónde están localizados los ficheros estáticos (`/var/www/front`) que se copiarán a la imagen. Este fichero se llama _nginx.conf_:

./container-chat-front-example/nginx.conf

worker_processes 2;
user www-data;

events {
  use epoll;
  worker_connections 128;
}

http {
  include mime.types;
  charset utf-8;
  server {
    listen 80;
    location / {
      root /var/www/front;
    }
  }
}

¿ Qué estamos configurando en este fichero? Veamos las entradas principales

  • **worker_processes** el número de procesos.

  • **user** define el usuario que los worker roles usarán.

  • **events** conjunto de directivas para manejo de conexiones.

    • **epoll** mecanismo de notificaciones del kernel de linux (escalable, se introdujo en la version 2.5.44 de Linux kernel mainline).

    • **worker_connections** nº máximo de conexiones simultáneas que puede abrir un worker role.

  • **http** define las directivas del servidor HTTP.

    • **server** en contexto **http** define un servidor virtual.

    • **listen** el puerto que escuchará el servidor virtual.

    • **location** la ruta de los ficheros que serán servidos. Fijaos en que **/var/www/front** es el sitio donde copiamos los ficheros del build de nuestra aplicacíon.

El servidor web funcionará en el puerto 80, que es el puerto http por defecto.


El Dockerfile final que nos quedaí:

./container-chat-front-example/Dockerfile

FROM node AS builder
WORKDIR /opt/front
COPY . .
RUN npm install
RUN npm run build:prod

FROM nginx
WORKDIR /var/www/front
COPY --from=builder /opt/front/dist/ .
COPY nginx.conf /etc/nginx/

Si quieres aprender más sobre los comandos que hemos utilizado utilizado en esta parte, échale un vistazo a la sección final de recursos de este post.

La estructura de la carpeta que nos quedaí:

.
├── container-chat-back-example
└── container-chat-front-example
      ├── src
      ├── .babelrc
      ├── .dockerignore
      ├── .gitignore
      ├── Dockerfile
      ├── License
      ├── nginx.conf
      ├── tsconfig.json
      ├── tslint.json
      ├── webpack.common.js
      ├── webpack.dev.js
      └── webpack.prod.js

Imagen de Docker del Front-End

Ya estamos listos para crear la imagen del Front End.

Establecemos el directorio raíz del código del front como directorio actual:

$ cd ./container-chat-front-example

Y creamos la imagen

$ docker build -t front .

Podemos listar la imagen para ver que se ha creado correctamente::

$ docker images front
REPOSITORY    TAG       IMAGE ID        CREATED          SIZE
front          latest    a3cjbbbdg6cw    1 minute ago    102MB

Contenedor de Docker del Front-End

Sólo nos queda ejecutar nuestra aplicación cliente. Teniendo en cuenta que el servidor web estará ejecutándose en el puerto 80 y que se llamará myfrontend, el comando que tenemos que utilizar es:

$ docker run -p 80:80 --name myfrontend front

Fijaos en que estamos utilizando el puerto 80, que es el puerto por defecto del protocolo HTTP.


Para probar la aplicaión, abre tu navegador web y teclea la siguiente url: http://localhost:80/ (el 80 es el puerto por defecto no te haría falta teclearlo)


Diagrama de Alto Nivel

Un diagrama que muestra lo que hemos construido:

ngInx proxy

ngInx proxy

Mejoras

La aproximación de usar localhost sólo es valida para desarrollo en local , no podemos usarlo en un entorno de producción.

De hecho nuestra aplicación de Front espera encontrarse un servidor funcionando de manera local en el puerto 3000. En un entorno de producción, esto no es viable, ya que el navegador nunca va a estar localizado en la misma ubicación que el servidor. Hay varias formas de solucionar esto:

Lo primero en que solemos pensar es en "cambiar" la url que tenemos 'hard-codedada' en nuestra aplicacíon fornt a la url donde el servidor estará ejecutándose. Este enfoque nos soluciona el problema de no tener el cliente en la misma ubicación que el servidor, pero añade algunois problemas adicionales: tener que incluir en el build la url del servidor, tener que manejarnos con CORS…

Un enfoque más sencillo, sería acceder al servidor a través de la misma ubicación donde se sirven los ficheros estáticos (cliente). Dependiendo de la url que se visite, un balanceador de carga reenviará la petición al servidor de Front End, para servir los ficheros estáticos, o al servidor, para obtener datos de la api o abrir un websocket.

Es decir la foto que se nos queda es la siguiente:


Usando ngInx como balanceador de carga de nginx

Usando ngInx como balanceador de carga de nginx

En cuanto a los colores del diagrama:

  • Las flechas en azul son peticines para servir paginas HTML o fichero JS.

  • Las flechas en verde son las peticiones a la api (se diferencia porque la ruta en empieza por /api).

  • Las flechas en naranja representa la interacción con los websockets (lo mismo que con la api por fragmento de ruta lo identificamos)

Vamos a realizar esta configuración:

Primero vamos a arreglar nuestro código de front y eliminar todas las entradas a “localhost” que están harcodeadas: escribimos la url del back-end en el código del front-end, sustituyendo todos los const baseUrl = 'http://localhost:3000'; por const baseUrl = '';, que funcionará como la ruta relativa de la url.

Modifica ./src/pods/chat/sagas.business.ts de la siguiente manera:

_./src/pods/chat/sagas.business.ts_

import { createSocket, SocketDescriptor } from './api';

export const establishRoomSocketConnection = (nickname: string, room: string) => {
// Elimina la linea comentada, y añade la de const baseUrl= '';
// const baseUrl = 'http://localhost:3000';
const baseUrl = '';

- Modifica ./src/pods/lobby/api/routes.ts de la siguiente manera:

./src/pods/lobby/api/routes.ts

// Elimina la idea comentada y añade la de const baseUrl='';
// const baseUrl = 'http://localhost:3000';
const baseUrl = '';

Diagrama de Alto Nivel

Un diagrama que muestra lo que vamos a construir:

nginx proxy

nginx proxy

Hemos utilizado Nginx como servidor web en el Front End, pero es una herramienta muy flexible que también puede funcionar como reverse-proxy y por tanto, asumir el rol de balanceador de carga. ¿ Cómo configuramos las rutas para realizar las redirecciones necesarias? En el fichero de configuración de ngInx, indicamos cómo redirigir el tráfico de las url visitadas al contenedor donde se ejecuta la aplicación. El balanceador de carga también va a estar en un contenedor.

El balanceador de carga se va a ejecutar en el puerto 80 y tenemos que definir tres _ubicaciones_:

  • `/`: se va a redirigir a la app del "front" que está ejecutándose en el puerto 80 para servir el servidor estático.

  • `/api`: se va a redirigir a la app del "back" que está ejecutándose en el puerto 3000 para servir datos de la api.

  • `/socket.io`: se va a redirigir a la app del "back" que está ejecutándose en el puerto 3000 para acceder a los websockets.

Empezamos creando una carpeta nueva, que llamaremos container-chat-lb-example, esta carpeta va a contener la nueva infraestructura. La estructura de carpetas que nos queda es la siguiente:

.
├── container-chat-back-example
├── container-chat-front-example
└── container-chat-lb-example

Vamos a creare un nuevo fichero de configuración, ./container-chat-lb-example/nginx.conf en el que definiremos como va a funcionar el balanceador de carga:


./container-chat-lb-example/nginx.conf

worker_processes 2;

events {
    worker_connections 1024;
    use epoll;
}

http {

    upstream front {
        server front:80;
    }

    upstream back {
        server back:3000;
    }

    server {

        listen 80;

        location / {
            proxy_pass http://front;
        }

        location /api {
            proxy_pass http://back;
        }

        location /socket.io {
            proxy_pass http://back;
        }
    }
}

Veamos los elementos principales de este fichero de configuración:

  • upstream, define un grupo de servidores.

  • proxy_pass, establece el protocolo y la dirección de los servidores proxied. En nuestro caso, los definidos en upstreams.

Tenemos que crear una configuración nueva en el Dockerfile para montar el load balancer. Sólo tenemos que copiar la configuración en una imagen de Docker cuya base sea _nginx_. Vamos a crear el fichero ./container-chat-lb-example/nginx.conf*

./container-chat-lb-example/nginx.conf

FROM nginx
COPY nginx.conf /etc/nginx/

Tras estos pasos la estructura de carpetas que nos queda es la siguiente:

.
├── container-chat-back-example
├── container-chat-front-example
└── container-chat-lb-example
    ├── Dockerfile
    └── nginx.conf

Para que esto funcione, las tres aplicaciones deben estar ejecutándose en el mismo Docker User-defined Network. Para que sea más sencillo, en el próximo paso vamos a trabajar con Docker Compose.

Multi Contenedor

En nuestro ejemplo estamos trabajando con tres contenedores, este escenario puede ser manejable. En una aplicacíon real, el número de contenedores que usemos puede ser mayor, y gestionar un buen número de contenedores se puede convertir en una tarea tediosa y propensa a que cometamos errores (aún estando en un entorno local), a esto le tenemos que añadir la problemática de que los tres contenedores tienen que ser capaces de comunicarse entre ellos, por lo que tienen que estar en la misma red. Para gestionar estos desafíos, utilizaremos Docker Compose.

Puedes instalar Docker Compose en este enlace.

Docker Compose es una herramienta que nos ayuda a trabajar en un entorno multi-contenedor. Docker Compose necesita un fichero .yml, que por defecto se llama docker-compose.yml, donde tenemos que especificar los contenedores, agrupados por servicios, y sus características. Los pasos para crear nuestro docker-compose.yml son:

  1. Definir Servicios

  2. Construir Imágenes

  3. Definir Dependencias

  4. Exponer Puertos

Antes de empezar

Vamos a crear un nuevo fichero docker-compose.yml y en el mismo nivel crear las carpetas container-chat-back-example, container-chat-front-example y container-chat-lb-example


La estructura de carpetas que nos queda es la siguienteí:

.
├── docker-compose.yml
├── container-chat-back-example
├── container-chat-front-example
└── container-chat-lb-example

1. Definir Servicios

Para acceder a un servicio, tenemos que utilizar su nombre. Si nos acordamos del fichero _nginx.conf_, especificamos que el servidor de front-end se llamaría "front" y que el Back End se llamaría "back". Los servicios que definamos en el Docker Compose se tienen que llamar igual que los del _nginx.conf_. Edita ./docker-compose.yml de la siguiente manera:

./docker-compose.yml

version: "3.7"
 services:
   front:
   back:
   lb:

2. Construir Imágenes

Como nuestros proyectos están almacenados en local, podemos indicarle a Docker, que en el caso de que no existan, donde tiene que construir las imágenes (esta operación se realiza antes de lanzar los contenedores).

./docker-compose.yml

version: "3.7"
services:
  front:
   build: ./container-chat-front-example
  back:
   build: ./container-chat-back-example
  lb:
   build: ./container-chat-lb-example

3. Dependencias

Antes de arrancar el El balanceador de carga, es importante que los servidores de Front y Back estén ya construidos y levantados, docker compose nos permite controlar esto con el tag depends_on (mira debajo de la etiqueta lb, ahí le estamos indicando que no levanta el balanceador de carga hasta que Back y Front estén preparados).

_./docker-compose.yml_

version: "3.7"
services:
  front:
    build: ./container-chat-front-example
  back:
    build: ./container-chat-back-example
  lb:
    build: ./container-chat-lb-example
   depends_on:
   - front
   - back

4. Exponer Puertos

La aplicación de chat sólo debería ser accesible a través del balanceador de carga. Ni la aplicación del front ni la del back deben exponer ningún puerto. En su lugar, el balanceador de carga debe exponer el puerto 80, puerto por defecto de http, y nosotros mapearemos el puerto 80 del balanceador de carga con el puerto 80 del anfitrión (ver la última línea del yml).

./docker-compose.yml

version: "3.7"
services:
  front:
    build: ./container-chat-front-example
  back:
    build: ./container-chat-back-example
  lb:
    build: ./container-chat-lb-example
    depends_on:
    - front
    - back
   ports:
   - "80:80"


El _docker-compose.yml_que nos queda es el siguiente:

version: "3.7"
services:
  front:
    build: ./container-chat-front-example
  back:
    build: ./container-chat-back-example
  lb:
    build: ./container-chat-lb-example
    depends_on:
      - front
      - back
    ports:
      - "80:80"

Un resumen con las principales entradas de configuración:

  • **version: "3.7"** el formato que será aplicado al _docker-compose_. Dependiendo del **Docker Engine** usaremos una versión específica. para saber más puedes pinchar en este enlace

  • **services** son los _servicios_ que constituyen nuestra aplicación, los definimos aquí, para que puedan ejecutarse juntos en un entorno aislado.

    • **front**, nuestro _servicio_ dedicado para el Front End.

      • **build: ./container-chat-front-example** especificamos la ruta relativa al **Dockerfile** para construir este servicio.

    • **back**, nuestro _servicio_ dedicado para el Back End.

      • **build: ./container-chat-back-example** especificamos la ruta relativa al **Dockerfile** para construir este servicio.

    • **lb**, nuestro _servicio_ dedicado al balanceador de carga.

      • **build: ./container-chat-lb-example** especificamos la ruta relativa al **Dockerfile** para construir este servicio.

      • **depends_on:** es la lista de servicios de la que depende este _servicio_ específico. Declaramos las dependencias del _servicio_, para que estas sean construidas antes que el _servicio_.

      • **ports:** los puertos que son expuestos al **anfitrión**.

Ejecutando el Sistema Multi-Contenedor

Para ejecutar el sistema entero, sólo tenemos que lanzar el siguiente comando desde el terminal:

$ docker-compose up

Si queremos reconstruir las imágenes cuando se haya algún cambio en las mismas:

$ docker-compose up --build


Ya sólo tienes que abrir el navegador web y teclear la url http://localhost para poder empezar a chatear.

Trouble shooting

Windows

Al empezar desde cero en una máquina con windows, nos puede aparecer el siguiente error:

Error starting userland proxy: mkdir /port/tcp:0.0.0.0:3000:tcp:172.17.0.2:3000: input/output error.

La manera más sencilla de evitarlo: si estás utilizando Docker desktop, deshabilita las características experimentales y reinicia Docker.

Conclusiones

Utilizando la tecnología de contenedores de Docker, nos permite simplifcar y automatizar los procesos de empaquetados y despliegue de nuestras aplicaciones.

Configurar un balanceador de carga ha sido tarea sencilla gracias a NgInx. Este balanceador es el único que queda expuesto al exterior, reduciendo así el area de ataque de nuestra infraestructura, ya que el resto están aisladas dentro del Docker user-defined Network, otro beneficio que obtenemos de esta aproximación es que los usuarios sólo podrán acceder a la aplicación a traves del balanceador, esto es clave para poder escalar gracias a la funcionalidad reverse-proxy de Nginx


Cómo guinda al pastel, docker-compose nos ayuda a gestionar conjuntos de contenedores, permitiendonos tratarlos como si fuera un único entorno.

¿Te gusta el mundo Devops?

En Lemoncode impartimos un Bootcamp Devops Online, si quieres saber más puedes pinchar aquí para más información.