“Encantado de conoceros, voy a ser vuestro nuevo compañero en el equipo de desarrollo, ¿quién me ayuda a montar el entorno en mi máquina?”
La anterior frase se repite muy a menudo en todos los rincones de (¿cualquier?) compañía a día de hoy, pero la respuesta puede ser más o menos dolorosa según se hayan hecho las cosas previamente.
¿Qué queremos solucionar?
A través de cuatro publicaciones, siendo esta la primera, vamos a hacer una introducción a Docker y cómo nos puede ayudar a preparar entornos de desarrollo fácil de transportar de una máquina a otra. Para este caso he elegido un entorno basado en PHP con Nginx, al que más adelante le añadiremos Redis.
Es cierto que si no hay un trabajo previo bien hecho, construir el entorno de trabajo para un determinado proyecto puede ser un auténtico dolor, por mucho gestor de paquetes que tengas siempre hay algo extra que se necesita, y que acaba llenando los ficheros ocultos del sistema. Los que usamos MacOS conocemos de sobra Brew, y cómo nos ayuda instalar librerías y ejecutables de manera sencilla, pero lo que nadie te comenta es cómo hacer una buena gestión de todo lo instalado. En mi caso Docker ha estado “robándole” entradas a Brew: versiones de php, ejecutables, etc.
La posibilidad de igualar el software de entornos de staging, pre y prod en local es algo que Docker hace de maravilla. De esta manera podemos apuntalar mucho mejor el desarrollo a sabiendas de que dónde va a ejecutar finalmente nuestro trabajo no difiere mucho de dónde se está construyendo.
Y finalmente, el caso de uso de Docker más claro es el de poder decirle a tu compañero: “Bájate esta imagen de docker y ejecútala, tendrás corriendo el servicio que necesitas para desarrollar, no necesitas instalar nada más…”.
¿Cómo funciona y qué ofrece Docker?
Primero veamos un esquema sencillo de cómo funcionan las máquinas virtuales:
Las máquinas virtuales hacen uso del llamado Hypervisor, que actúa de “puente” entre los recursos del sistema padre y los invitados, emulando los recursos que necesitan los invitados. En este esquema tenemos 3 sistemas operativos invitados corriendo en nuestro sistema, y para cada uno de ellos tendrías que configurar de manera individual la aplicación necesaria y sus librerías.
Ahora veamos un esquema básico que sitúa a Docker en nuestro sistema:
Lo que tenemos es un demonio de docker que ha sustituido a la capa del Hypervisor. ¿Cuál es la diferencia? Este demonio no “emula” recursos, da acceso directo a los propios recursos del sistema padre, con una especie de guardián (seccomp) de por medio para no abusar, lo cual evita instalar otros sistemas operativos que ocupan más espacio y generan más gestión de recursos.
A grandes rasgos, Docker funciona instalando un demonio en la máquina que expone una API, y a esa API podemos acceder con un CLI o con una UI como Docker for Mac:
Lo más importante de cómo funciona Docker no es quizás eso, sino la capacidad de compartir las “librerías/binarios” entre aplicaciones/servicios. What? Sí, un poco loco, pero primero veamos 3 términos fundamentales del ecosistema docker:
- Dockefile: conjunto de instrucciones/plantilla/receta que serán usadas para construir la imagen requerida.
- Imagen: aplicación/servicio construido listo para funcionar.
- Contenedor: imagen en funcionamiento.
Algunas analogías que ayudan a entender los conceptos:
- Programación: la imagen equivale a la clase y el contenedor a una instancia de esa clase.
- Esquema SQL 1 – * : de una imagen se pueden lanzar infinitos contenedores.
Lo que viene a continuación es un Dockerfile
muy básico, con 7 instrucciones que lanza un pequeño server en nodejs
:
En lenguaje natural:
- Descárgate de DockerHub la imagen de
node
etiquetada como argon
. - Crea un directorio
/usr/src/app
. - “Muévete” a ese directorio (se establece como path por defecto para lo que venga a continuación).
- Copia el
package.json
(que está en mismo el path del Dockerfile
) a ese directorio. - Instala librerías
npm
. - Copia el contenido de la carpeta actual del host a la carpeta
/usr/src/app
de la imagen. - Expon puerto 8080 para que otros contenedores de la misma red de Docker se puedan conectar a él.
- Arranca servidor.
Y ahora es cuando explicamos por qué hemos dicho antes que Docker comparte recursos entre aplicaciones/servicios. Veamos la siguiente imagen:
Si leemos de abajo a arriba coincide con las instrucciones del Dockerfile
, y a cada instrucción le asigna un hash
. Si una instrucción determinada se repite a en la construcción de otras imágenes, Docker reutiliza esa instrucción para no tener que ocupar espacio extra en disco, y de esta manera no tener que ejecutarse de nuevo la siguiente vez que se construye la imagen. La imagen se construye con capas, como si de una cebolla se tratase, pudiendo mover las capas a otra imagen que las necesite, como si de una caché de instrucciones se tratase.
Una posible pregunta en este punto sería: “¿Qué pasa si cambio mi package.json
? ¿Me estás diciendo que esa instrucción de copiar el packgae.json
se cachea, por lo que la carpeta node_modules
generada del npm install
estaría cacheada y no actualizada si yo hago un cambio en el package.json?
“
La respuesta es que eso es incorrecto. Cuando el hash
de una “capa” cambia (por ejemplo: ha cambiado el contenido de los ficheros relacionados con esa capa), Docker
invalida todas las instrucciones que vengan después. Fijaos en la posición que tienen el npm install
, la copia del package.json
y la copia de todos los ficheros del directorio actual. Cuando el package.json
cambia, Docker invalida tanto el npm install
como la copia de ficheros, por lo que se realizaría de nuevo un npm install
, que es lo que queremos. Imaginad que la copia de ficheros la hacemos antes del npm install
, en ese caso cada vez que cambiase un fichero se realizaría un npm install
(que es lo que queremos evitar), de ahí que la colocación de instrucciones no sea arbitraria y lleve consigo una estrategia a la hora de construir imágenes.
Resumiendo
En estos momentos estamos en condiciones de enumerar los beneficios más importantes de Docker:
- Espacio ocupado: una VM necesita un OS completo instalado en el host para poder ejecutar aplicaciones en él, mientras que
Docker
funciona sobre el host directamente, además de compartir recursos con el resto de entidades creadas para correr en el sistema. - Compatiblidad: el
Dockerfile
es lo único que necesitas para mover una aplicación de un sistema a otro, que con que tenga instalado Docker
es suficiente, y a día de hoy Docker
es compatible con la mayoría de sistemas del mercado. - Rapidez: arrancar una aplicación cuya imagen ya está construida es cuestión de segundos o incluso milisegundos. El trabajo grande se realiza al crear la imagen, pero lanzar un contenedor de esa imagen es muy rápido.
- Experimentación: se acabó tener diferentes versiones de aplicaciones en el sistema host. Con el etiquetado de imágenes puedo estar lanzando dos contenedores a la vez, uno con PHP5.6 y otro con PHP7.3 sin tener que instalar nada en el SO host.
Hasta ahora os he contado por encima los detalles que a mi parecer son los más importantes a la hora de introducirse en el mundo de Docker, pero básicamente estamos rascando la superficie del ecosistema Docker.
Taller: ejercicio básico con Nginx
Asumiendo que tenéis Docker instalado en el sistema, ejecutad docker run hello-world
y, si os salen instrucciones de qué ha sucedido al ejecutar el comando, querrá decir que está instalado correctamente, por lo que podremos proceder con el ejercicio.
A continuación vamos a crear nuestro primer Dockerfile
para crear una imagen de Nginx con un HTML nuestro, y una vez hecho eso lanzaremos el contenedor para verlo funcionar. Tenéis el código en el repo de Github.
Propongo un Dockerfile
como el siguiente:
FROM nginx:latest
COPY default.conf /etc/nginx/conf.d/default.conf
RUN mkdir -p /www/myapp
COPY . /www/myapp
LABEL maintainer="Andrew <XXXXXXYYYZZZZ@gmail.com>" \ version="1.0"
CMD ["nginx", "-g", "daemon off;"]
Traduciendo:
- Descárgate la última versión de la imagen de
Nginx
de DockerHub, será la que usemos de base. - Copia mi fichero específico de configuración a la carpeta de configuración de
Nginx
. - Crea la carpeta
/www/myapp
. - Copia todo el directorio actual a esa carpeta.
- Añade unas etiquetas, visibles a posteriori en la información de la imagen.
- Comando con el que arrancará cualquier contenedor que se lance con esta imagen. Si el proceso mostrado termina, el contenedor también lo hace.
Ya tenemos nuestro fichero mágico, siguiente paso: construir una imagen. Para ello acudimos a nuestra maravillosa terminal, vamos al path
donde se encuentra el Dockerfile
y ejecutamos:
docker image build -t test-nginx .
Traduciendo:
Construye una imagen y llámala test-nginx
, busca un Dockerfile
en el path
actual (por defecto ese nombre, aunque se puede indicar otro), y el contexto sobre el cual se va a trabajar será el path
actual (.
). El contexto es el root path
desde donde se van a ejecutar instrucciones como COPY
.
Si ejecutáis docker image ls
veréis vuestra imagen en el listado.
Siguiente paso, arrancar un contenedor de esa imagen. Otra vez a la terminal:
docker container run --rm -it -p 8081:80 --name test-nginx test-nginx
Traduciendo:
Arranca un contenedor de la imagen test-nginx
, llámalo también test-nginx
para que cuando haga docker container ls
aparezca con ese nombre. Además quiero que se enlace el puerto 8081
de mi localhost, al 80
del contenedor (que es donde se va ejecutar Nginx
), y si el contenedor se para quiero que se borre automáticamente de los contenedores creados (para eso es --rm
, sino se quedará parado esperando a ser iniciado de nuevo). Y como parámetro extra añadimos las flags
-it
para mantener el STDIN
abierto y una TTY
para conectarme al contenedor.
En estos momentos deberíais tener [localhost:8081](<http://localhost:8081>)
habilitado con esta pequeña web:
“¡Ey Andrés! ¿y ese último comando que sale ahí?”
Ahora viene la auténtica gracia de todo esto. Si modificáis el html
no veréis ningún cambio porque ese html
se metió a la hora de construir la imagen. Pero podemos “mapear” carpetas de nuestro sistema de ficheros con el volumen del contenedor, de manera que cualquier cambio que sufra nuestra carpeta se vea reflejado en el contenedor. Para ello haremos uso de la opción -v $PWD:/www/myapp
para sincronizar nuestra carpeta con la del contenedor. Ahora si arrancáis de nuevo el contenedor, cambiáis algo del HTML y recargáis el navegador, veréis el cambio. ¡Tachán!
Hasta aquí ha llegado esta introducción. La siguiente vez que nos veamos será para afianzar estos conceptos y para añadir PHP
a la ecuación. ¡Hasta la próxima!
muy útil y sencillo. Enhorabuena.
Al añañdir el comando -v $PWD:/www/myapp, -> aparece en el cmd:
«docker: Error response from daemon: create $PWD: «$PWD» includes invalid characters for a local volume name, only «[a-zA-Z0-9][a-zA-Z0-9_.-]» are allowed. If you intended to pass a host directory, use absolute path.
See ‘docker run –help’.»
Un saludo
Hola Jose Antonio,
¿podría ser que estuvieses en Windows? En ese caso «$PWD» , que hace referencia al path absoluto actual en el que estás, no funcionará. Tendrás que usar «%CD%» si estás en Windows.
En cualquier caso tanto $PWD como %CD% no dejan de ser atajos para describir un path, si quieres puedes probar a meter directamente el path absoluto e el que se encuentre la app. Pruébalo y nos cuentas.