Docker 101: ¿Qué es Docker y para que se usa?

29/09/2020

Antes de definir qué es Docker, quiero contarles un problema que tuvimos en un proyecto en el que estuve trabajando.

Con el equipo empezamos configurando nuestro proyecto, definiendo la arquitectura, luego pasamos a la etapa de desarrollo de componentes, test, etc etc. Cuando llegó el momento de hacer un deploy en los ambientes del cliente, nos enteramos que todo ese proceso lo hacían a mano. Entonces, con la idea de que quién vaya a realizar esos deploy lo haga de la manera más fácil posible generamos una documentación de cómo clonar el proyecto, cómo instalar las dependencias y como ejecutarlo.

Una vez realizado el deploy, nos encontramos con la sorpresa de que nuestra app no corría como debía correr. Luego de debuggear un rato detectamos que el servidor donde estaba corriendo la app tenía instalado la v.14 de Node.js, y nuestra app tenía un lockdown a la v.12 lo que generaba algunos problemas de compatibilidad.

¿Por qué les cuento esto? Porque esto es uno de los típicos problemas que Docker viene a prevenir que sucedan.

Ahora si...

¿Qué es Docker?

Docker puede definirse como una forma de empaquetar software para que pueda ejecutarse en cualquier hardware.

Siguiendo el caso anterior, Docker nos permite generar un paquete que contiene todo lo necesario, código fuente, configuraciones y las dependencias, para que nuestra aplicación funcione de la manera adecuada.

Sus principales partes

Docker tiene un montón de partes muy interesantes y útiles, como los volumens, pero en este artículo nos enfocaremos solo en las principales, que son:

  • Dockerfile: el Dockerfile es un archivo que sirve para indicarle a Docker cómo se debe construir la imagen del proyecto. En otras palabras, es la receta que Docker usa para generar un paquete con nuestra aplicación.
  • Image: es un template para ejecutar el proyecto en cualquier contenedor. Una imagen de Docker es nuestro paquete.
  • Container: un container es básicamente, una imagen en ejecución.

¿Cómo dockerizar?

Lo primero que tenemos que hacer es generar nuestro proyecto. Vamos a generar un simple archivo html y un server en express para servir este archivo.

Para esto, dentro del directorio donde vamos a trabajar, iniciamos un nuevo proyecto con npm init y dejamos todas las configuraciones por defecto, de tal manera que nos quede un package.json así:

{
  "name": "docker101",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "Luciano Peñafiel",
  "license": "ISC"
}

Lo siguiente que vamos a hacer es crear un archivo index.html con un simple encabezado y lo colocaremos en un directorio que lo vamos a llamar build.

<!DOCTYPE html>
<html lang="es">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Docker 101</title>
</head>
<body>
    <h1>Hola mundo!</h1>
</body>
</html>

A continuación instalamos express ejecutando npm i express y creamos un archivo server.js que va a ser el encargado de servir nuestro html y agregamos un script en nuestro package.json para levantar este server.

const express = require("express");
const app = express();

app.use(express.static("build"));

app.get("/", (req, res) => {
  res.send("ok");
});

app.listen(3000, () =>
  console.log(`App listening on port http://localhost:3000`)
);
{
  "name": "docker101",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
        "start": "node server.js",
    "test": "echo "Error: no test specified" && exit 1"
  },
  "author": "Luciano Peñafiel",
  "license": "ISC"
}

Listo, ya tenemos nuestra app. Ahora solo ejecutamos el comando npm start y vamos a tener ver nuestra app corriendo en el puerto 3000.

¿Y Docker?

Ahora llegó el momento de trabajar con Docker, y lo primero que tenemos que hacer es instalarlo desde la web oficial de Docker.

Una vez instalado y corriendo, ejecutamos en nuestra terminal el comando docker ps para ver todas las imágenes que están corriendo actualmente. De momento, no deberíamos tener ninguna, y obtener una respuesta así:

Lo siguiente es construir nuestro Dockerfile y para esto creamos en la raíz de nuestro repositorio un archivo con ese nombre (y sin extensión) con el siguiente contenido:

FROM node:12-alpine

WORKDIR /usr/src/app

COPY package*.json ./

RUN npm ci

COPY . ./

CMD ["npm", "start"]

Entendamos un poco este archivo. Primero que nada es muy importante entender que cada instrucción funciona como una capa, y Docker intentará mantener en caché cada una de ellas si nada se ha modificado, por lo tanto es recomendable poner primero aquellas capas que menos van a cambiar a lo largo del proyecto.

Todo Dockerfile empieza con el comando FROM que sirve para indicarle a Docker cuál va a ser la imagen base de nuestra imagen. Sí, cada imagen parte de otra imagen.

Lo siguiente que tenemos hacer es agregar el código fuente de nuestra app a la imagen. Para eso primero definimos la raíz del directorio usando la instrucción WORKDIR. Todo lo siguiente que hagamos va a comenzar desde esa ruta.

Luego vamos a instalar todas nuestras dependencias usando el comando COPY que recibe dos argumentos, primero lo que queremos copiar y al último la ubicación donde queremos copiar esos archivos.

Luego vamos a instalar todo lo que tenemos en nuestro package.json con el comando RUN npm ci.

Una vez instaladas todas nuestra dependencias, copiamos el código fuente de nuestra app. En este punto se presenta un problema, y es que en los pasos anteriores instalamos nuestras dependencias, lo que generó dentro de la imagen un directorio node_modules y al copiar todo lo que tenemos en nuestro código fuente, vamos a sobre escribir esos módulos y no es algo que queremos que suceda. Para esto creamos un archivo .dockerignore (si, igual que un .gitignore) y colocamos dentro node_modules.

Y por último le indicamos a Docker como debe inicializar nuestra app con la instrucción CMD, que es la instrucción por defecto de Docker para iniciar una imagen.

Una vez creado nuestro Dockerfile, ahora sí vamos a generar una imagen del proyecto, y esto lo hacemos con el comando docker build -t docker101:v1 .

¿Qué hace esto? Bien, docker build va a generar nuestra imagen, y con el flag -t docker101:v1le asignamos una etiqueta a nuestra imagen y le indicamos que está en la versión 1.

Ahora si corremos el comando docker images vamos a ver todas las imágenes que tenemos en nuestro Docker con sus respectivos IDs.

Y por fin llegó la hora de ejecutar nuestra app dentro de un contenedor, esto lo hacemos con el comando docker run -p 3000:3000 docker101:v1 y voila, tenemos nuestra app corriendo en el puerto 3000.

¿Qué sigue?

La idea, siguiendo el ejemplo del principio, es entregarle a nuestro cliente todo empaquetado. Y ¿cómo hacemos esto? Bueno, hay muchas maneras de hacerlo, pero la más usada es subiendo nuestra imagen a un registry de imágenes como los es Docker Hub o GitHub Packages para que luego nuestro cliente pueda descargar la imagen.

Dudas? Consultas?

Para comunicarte conmigo mandame un email o buscame en mis redes.

Eso es todo por ahora, nos vemos pronto 👋🏻.

¡wow! ¡tu monitor es muy grande!