La idea es ir empezando a ver como sería un entorno real de desarrollo con Docker teniendo:

  • Nuestro entorno de desarrollo: Un entorno donde creamos las cosas.
  • Entorno de test: Tras el desarrollo, pasamos nuestro código al entorno de test donde, como su propio nombre indica, probamos que todo funciona.
  • Entorno de producción: Tras pasar los tests pertinentes automáticamente el proyecto pasaría a producción.

Este workflow es lo que comunmente se conoce como Integración Continua, Continuous Integration o CI. Para este proceso usaremos herramientas o servicios como Github o Travis CI y desarrollaremos con ReactJS (no importa que no sepamos usarlos veremos lo necesario para entender lo que estamos haciendo).

Entorno de desarrollo

Empecemos por el primero de los entornos, el entorno de desarrollo. Lo que buscamos en este entorno es que nuestros cambios mientras estamos desarrollando se sincronicen automáticamente con el contenedor de desarrollo (esto en varios casos es innecesario pero lo veremos igualmente con los fines didácticos que nos ocupan).
Pensando en el objetivo de este entorno, ya hemos visto que si realizamos cambios en nuestro código tenemos que volver a realizar un build y luego arrancar un contenedor nuevo con la imagen que nos crearía el build anterior. Esto realmente no es lo que estamos buscando, es poco eficiente y tendríamos demasiadas imágenes de contenedor.

En docker existe una forma de solucionar esto, vamos a introducir algo que no hemos visto todavia: Los Volúmenes.
En docker un volumen no deja de ser una referencia al filesystem del contenedor y puede ser una referencia, como un mapeo (al igual que los puertos) de una carpeta local a una del contenedor, o un 'no lo toques' (que básicamente es mapea todo menos esto). Lo vamos a probar directamente, primero de todo vamos preparar un proyecto para trabajar con el, empecemos por crearnos un proyecto de ReactJS.

Para trabajar con react usaremos un paquete de npm que nos instala un proyecto básico inicial, lo instalamos con

npm -g install create-react-app

Una vez instalado ya podríamos crearnos un proyecto

create-react-app frontWeb

Esto nos creará un proyecto de react dentro de una carpeta llamada frontWeb. Para probar si funciona solo tenemos que entrar en la carpeta y ejecutar con:

npm start

Capture

Bien ya tenemos nuestra web de react, vamos ahora con la parte del entorno de desarrollo de docker.
Para ejecutar nuestro entorno de desarrollo en un contenedor solo necesitamos que tenga node instalado, es decir, que es similar a lo que hemos creado anteriormente.

Antes de continuar comentar que aunque vamos a trabajar casi todo el rato con comandos de docker-compose todo lo que hagamos a partir de ahora se puede hacer con comandos docker run también, pero realmente son comandos muy largos y poco útiles a la larga. En caso de necesidad siempre podemos buscar cual es el flag del comando para ponerlo directamente sin usar un dockerfile.

Continuemos, volvamos al principio, hemos dicho que queríamos un entorno de desarrollo donde nuestro contenedor se actualice automáticamente según vayamos haciendo cambios en local, para ello vamos a hacer uso de lo que en docker se conoce como volumenes.
Lo primero nos crearemos dentro de nuestra carpeta de proyecto de react un fichero dockerfile pero esta vez pondremos:

Dockerfile.dev

Como es normal aunque no lo hayamos visto a docker se le puede indicar el fichero dockerfile a usar cuando hacemos un build, solo tenemos que usar el flag -f, por ejemplo (ojo al punto del final)

docker build -f ./Dockerfile.dev .

Lo mismo con los ficheros para docker-compose, por lo que realmente no tendremos ningún problema y podemos tener varios dockerfile distintos según nuestro entorno.
Sabiendo esto continuamos con nuestro fichero Dockerfile.dev

FROM node:alpine

WORKDIR '/app'

COPY package.json .
RUN npm install

COPY . .

CMD ["npm", "start"]

Misma teoria que anteriormente, copiamos el package.json primero por posibles cambios solo del resto y no tener que hacer otra vez el npm install todo el rato cada vez que hagamos un build.
Ya tenemos nuestro fichero dockerfile, ahora como la idea es usar docker-compose para todo necesitamos crear el fichero docker-compose.yml

version: '3'
services:
    web_react:
        build: 
            context: .
            dockerfile: Dockerfile.dev
        ports:
            - "3000:3000"

Antes de continuar, vemos que ahora donde tenemos puesto build ahora hemos añadido 2 propiedades:

  • context: Indicamos el contexto desde(path) desde el que trabajara el build del docker-compose.
  • dockerfile: Nombre del fichero dockerfile que queremos usar.

Continuemos, ahora vamos a hablar del concepto de Volume (por fin ;) ), primero añadamoslo al fichero

version: '3'
services:
    web_react:
        build: ./Dockerfile.dev
        ports:
            - "3000:3000"
        volumes:
            - /app/node_modules
            - .:/app

Si nos fijamos en lo que hemos puesto, tenemos realmente dos conceptos distintos dentro de volumes:

  • .:/app: Hablemos primero del segundo, este es similar al concepto de mapear puertos, básicamente le estamos indicando que mapee todo el contenido de la ruta actual de mi equipo local, al path /app del contenedor, lo que funcionaría similar a un acceso directo a los ficheros de la carpeta local de nuestro equipo.
  • /app/node_modules: Si nos fijamos en esta línea no tenemos ':', eso es porque aquí le estamos indicando que haga como un marcador de la carpeta node_modules del contenedor, es decir, usa la del contenedor, no la toques y dejala donde está (esto hace que la carpeta se mantenga aunque hagamos el paso anterior).

Ahora que ya sabemos lo que hemos puesto, viene la pregunta del ¿por qué?....bien, si pensamos en el proceso que hemos añadido en el Dockerfile.dev, tenemos una parte donde instalamos los paquetes que están indicados en el package.json, es decir, queremos que los descargues e instales de nuevo cuando hagamos un build de la imagen del contenedor, y no pasamos la carpeta local node_modules (que es donde instala las dependencias), que de hecho la vamos a eliminar para que veamos como funciona, si, eliminarla.

Ya que hablamos del Dockerfile.dev, alguno se puede preguntar si hacemos la refencia o linkado de nuestra carpeta local, ¿para qué hacemos el COPY?....bueno, esto es para prevenir creaciones para producción usando los mismos Dockerfiles, realmente en nuestro caso actual no lo necesitamos pero para producción siempre es mejor para evitar errores, ya que un contenedor en producción NO DEBE hacer referencias a carpetas locales de ningún sitio solo tiene que tener sus propios ficheros.

Ya tenemos todo ahora nos situamos en la ruta donde tenemos el fichero docker-compose.yml y ejecutamos:

docker-compose up

Si todo va bien deberíamos ver algo como esto en la consola
Captura-de-pantalla-2018-11-14-a-las-9.35.40

Y si accedemos en el navegador a:

localhost:3000

Deberíamos ver

Captura-de-pantalla-2018-11-14-a-las-9.37.02

Bien como tal ya tenemos todo funcionando, pero realmente lo que queremos es poder desarrollar en local y que se actualice el contenedor , ¿no?... pues vamos a probarlo.
Nos vamos a nuestra web de react, y dentro de la carpeta src modificamos el fichero App.js y ponemos lo que queramos:

 <div className="App">
        <header className="App-header">
          <img src={logo} className="App-logo" alt="logo" />
          <p>
            Hola ninjaaaaasssss
          </p> //<-- Esta es la linea que cambiamos
          <a
            className="App-link"
            href="https://reactjs.org"
            target="_blank"
            rel="noopener noreferrer"
          >
            Learn React
          </a>
        </header>
      </div>

Y una vez que guardemos el documento automáticamente se debería actualizar el navegador y mostrar

Captura-de-pantalla-2018-11-14-a-las-9.40.32

¡¡¡PERFECTO!!!... ya tenemos nuestro entorno de desarrollo funcionando.

Empecemos con el entorno de TEST

TEST

Por defecto react viene ya configurado con 1 test para poder probar, solo tenemos que hacer

npm run test

o

npm test

y automáticamente nos pasaría los tests

Capture-1

Bien, pues ahora queremos esto pero en un contenedor, como opción rápida podemos ejecutar una imagen creada con el mismo dockerfile pero cambiando el comando de arranque (esto es solo para que veais que funciona).

Por si no la tenemos creamos el build de la imagen, yo la voy a taggear para poder identificarla

docker build -f ./Dockerfile.dev . -t test_react

Esto nos devuelve una imagen con ese nombre

Capture-2

Ya la tenemos, ahora solo vamos a realizar un run habitual cambiando el comando de arranque

docker run -it test_react npm test

Le he añadido -it para poder trabajar con la consola de test y como algo nuevo si nos fijamos hemos puesto cosas tras el nombre de la imagen, básicamente todo lo que ponemos a continuación del nombre de la imagen a ejecutar lo toma como comando de arranque para el contenedor. Si lo ejecutamos nos devuelve
Capture-3

Otra opción es añadirlo a nuestro docker-compose añadiendo otro servicio pero con la misma teoría, compartiendo o mapeando ficheros entre el equipo local y el contenedor, teniendo 2 contenedores funcionando uno para las pruebas en desarrollo y otro probando los test. El problema de este acercamiento es que no tenemos control sobre la consola de test por lo que no podemos hacer mucho más que ver como pasan los tests cada vez que hacemos un cambio en los ficheros.
Vamos a probarlo, cambiamos nuestro fichero docker-compose

version: '3'
services:
    web_react:
        build:
            context: .
            dockerfile: Dockerfile.dev
        ports:
            - "3000:3000"
        volumes:
            - /app/node_modules
            - .:/app
    test_react:
        build:
            context: .
            dockerfile: Dockerfile.dev
        volumes:
            - /app/node_modules
            - .:/app
        command: ["npm","test"]

Como véis hemos añadido otro servicio (contenedor ya sabéis), en este caso se llama test_react, que usa el mismo dockerfile y mapea de la misma forma los volumenes. Despues de eso si que tiene cambios, hemos quitado el mapeo del puerto porque ya no lo necesitamos y como extra nuevo hemos añadido la propiedad command que básicamente lo que hace es cambiar el comando de inicio del contenedor.
A continuación si ejecutamos nuestro docker-compose up, nos crea dos contenedores y como podremos ver el log ambos funcionan correctamente, y si cambiamos algo en los ficheros se actualizan ambos, tanto el de desarrollo como el que pasa los tests.

Capture-4

Ninguno de los casos es muy ideal pero son funcionales y puede que en algún caso nos pueda servir para algo, más adelante veremos un entorno de test más 'real' por el momento esto es más que suficiente, a continuación empezaremos a hablar un poco de PRODUCCIÓN

PRODUCCIÓN

Pasemos ahora a producción, la intención es crear un contenedor que nos devuelva nuestra aplicación ya preparada para producción, para el que no lo sepa, una app de react la preparamos para producción ejecutando el comando:

npm run build

Y este comando nos deja unos ficheros típicos de web (html, js y css), es decir, ficheros estáticos. Estos ficheros los deja en una carpeta llamada build dentro de nuestro proyecto.

Ahora necesitamos para producción un servidor web, los más utilizados son Apache o Nginx, aunque podríamos hacerlo con NodeJS, Go, Ruby, etc..... con casi todos los lenguajes tenemos alguna opción para hacerlo. En nuestro caso usaremos un contenedor con Nginx.

Si miramos la documentación del contenedor oficial podemos ver que los ficheros los sirve desde el path /usr/share/nginx/html. Sabiendo esto entonces básicamente lo que tendriamos que hacer sería copiar nuestros ficheros de producción en esa ruta del contenedor de nginx... pero claro se supone que no tenemos en local los archivos, veamos como podemos hacerlo con el entorno que tenemos ahora.

Nos vamos a crear un nuevo dockerfile con esto

FROM node:alpine as builder
WORKDIR '/app'
COPY package.json .
RUN npm install
COPY . .
RUN npm run build

FROM nginx
COPY --from=builder /app/build /usr/share/nginx/html

Empecemos por la primera linea:

FROM node:alpine as builder

Introducimos algo nuevo en este punto, básicamente lo que estamos haciendo es indicarle al proceso que añada como una referencia al resultado del build de ese contenedor con nombre builder, pero puede ser cualquier otro. Esta referencia solo está disponible en el contexto de la ejecución de docker build.
El resto del primer contenedor es algo que ya hemos visto, vayamos con el segundo

FROM nginx
COPY --from=builder /app/build /usr/share/nginx/html

Aquí empezamos con el segundo contenedor. Vemos como la instrucción COPY tiene algo nuevo

--from=builder

Como os podéis imaginar tiene que ver con la instrucción as builder del primer contenedor, aquí le estamos diciendo que del primer contenedor se copie la ruta /app/build y la pegue en /usr/share/nginx/html
Bien pues vamos a ejecutar nuestro nuevo build

docker build .

Una vez terminado

Capture-5

Tenemos ya construido una imagen con supuestamente nginx y nuestra app en producción. Por último nos faltaría crear un contenedor con esa imagen, pues vamos a ello

docker run -p 3500:80 --name webpro idImagen

Como extra he añadido --name que lo que hace es taggearnos el contenedor con un nombre que podamos gestionar de manera más comoda que un ID numérico

Capture-6

Se puede ver al principio el comando y a continuación un log (el de nginx) una vez que intentamos acceder a la página

Capture2

Y como podemos ver ya tenemos nuestro entorno para producción que básicamente es el build del dockerfile una vez que hemos terminado de desarrollar.

En próximos posts veremos una forma más profesional de hacer todo esto con Integración Continua gracias a TravisCI y GitHub.