¡Esta es una revisión vieja del documento!
Tabla de Contenidos
Docker Compose
Notas del curso Docker a fondo e Introducción a Kubernetes: aplicaciones basadas en contenedores
Hasta ahora hemos visto cómo gestionar nuestras imágenes, así como crear imágenes nuevas y contenedores. Pero, por ahora hemos estado en escenarios con un solo contenedor. Tampoco hemos hablado sobre la manera de configurar contenedores más allá de usar la sentencia ENV en el Dockerfile.
En este módulo aprenderemos:
- A configurar contenedores: distintas configuraciones a distintos contenedores de la misma imagen
- El comando
docker compose(conocido generalmente como “Compose” a secas) - Cómo usar
composeen escenarios de un solo contenedor
En este módulo se asume ya fluidez en todos los aspectos explicados hasta ahora, así que no ¡dudes en repasar los módulos anteriores en cualquier momento!
Configuración de contenedores
Ficheros de configuración
Tradicionalmente para parametrizar el funcionamiento de las aplicaciones se han usado los ficheros de configuración. Esa técnica es posible aplicarla en Docker. Lo único que debemos tener presente es que debemos evitar que el fichero de configuración forme parte de la imagen. Es decir, debemos evitar que se incluya en la imagen usando las sentencias COPY o ADD del Dockerfile.
¿Por qué es mala idea que el fichero de configuración forme parte de la imagen?
Pues porque entonces todos los contenedores creados a partir de dicha imagen tendrían la misma configuración. Supón que la configuración es una cadena de conexión a una base de datos: dicha cadena de conexión será distinta según el entorno en el que estés ejecutando el contenedor (desarrollo, pruebas, preproducción, …). Si la configuración la tienes en la imagen vas a necesitar imágenes distintas por entorno y eso no es muy buena idea.
La solución pasa por usar un bind mount: tener los ficheros de configuración en el host y usar un bind mount para mapear la carpeta con los ficheros de configuración en un directorio determinado del contenedor. De esta manera, un mismo host puede contener ficheros de configuración de distintos entornos (en distintos directorios) y cuando se ejecutan los contenedores utilizar el bind mount para montar un directorio u otro en el contenedor, según las necesidades. Eso permite ejecutar contenedores con distintas configuraciones en el mismo host.
La idea es que la configuración no está en la imagen, sino que sea proporcionada por el entorno. La configuración forma parte del entorno y no de la imagen. Usar un bind mount es la manera de que el entorno (el host) proporcione ficheros de configuración al contenedor.
En según qué escenarios es posible usar un volumen externo (por ejemplo, si los ficheros de configuración no están en el host) pero la idea es la misma.
Variables de entorno
Dado que la configuración debe ser proporcionada por el entorno, lo ideal es que dicha configuración forme parte del propio entorno. Y para ello lo mejor es usar “variables de entorno”. Las variables de entorno son la forma preferida de configurar contenedores.
No todos los frameworks permiten usar variables de entorno con facilidad. Por ejemplo, en ASP.NET para crear aplicaciones web, existe el fichero web.config, que debe llamarse así y estar localizado en el directorio raíz de la aplicación web. En esos casos, por supuesto, es mejor la opción de usar un volumen o un bind mount.
Vamos a ver cómo pasar una variable de entorno a un contenedor…
Con la sentencia ENV del archivo Dockerfile creábamos una variable de entorno a nivel de imagen (para todos los contenedores creados a partir de esta imagen). Pero eso no es lo que queremos ahora. Ahora necesitamos crear una variable de entorno cuyo valor pueda ser distinto para cada contenedor que ejecutemos.
De nuevo es el comando docker run el que nos permite hacer eso. Mediante el modificador -e podemos establecer una variable de entorno:
docker run -e "variable=valor" <nombre_imagen>
El contenedor se inicia con la variable de entorno variable establecida al valor valor.
Podemos establecer tantas variables de entorno como necesitemos utilizando varias veces el modificador -e.
Podemos usar el modificador -e y especificar solo el nombre de una variable (docker run -e variable <nombre_imagen>). En este caso el contenedor tendrá la variable definida y su valor será el mismo que tenía dicha variable en el host al hacer el run. Existen muchas variables de entorno en los sistemas y de este modo no tendremos que definirlas explícitamente, pudiendo gestionarlas a nivel de sistema operativo para todos los contenedores.
Docker Compose
Docker Compose es un comando cuyo principal propósito es definir y ejecutar aplicaciones multicontenedor.
El uso de Compose en escenarios multicontenedor lo veremos en el siguiente módulo, pero Compose es de aplicación también en escenarios con un solo contenedor, que es lo que estudiaremos ahora.
Instalación de Compose
Docker compose forma parte de la herramienta de Docker. Es un comando más, tal y como lo son docker ps o docker run, por poner dos ejemplos. Pero eso no siempre fue así. Antiguamente era una aplicación que se instalaba y se descargaba aparte. Era por lo tanto un ejecutable distinto cuyo nombre era docker-compose (con un guion en medio).
Si tienes Docker for Windows o Docker for Mac ya tienes Docker Compose instalado en tu sistema. No es así en el caso de que uses una distro de Linux. Por suerte la instalación de Compose es trivial, ya que basta tan solo con descargarte el ejecutable. Para más información sigue los pasos indicados en su página web.
Docker compose tiene su propia versión (con un número distinto de la versión de Docker, por temas históricos), que puedes verificar con el comando:
docker compose --version
Por razones históricas existen dos grandes versiones de Compose. La antigua 1.x y la posterior 2.x. Actualmente Docker Desktop solo soporta la 2.x y, realmente, no hay casi ningún motivo para usar la antigua. De todos modos, si te encontrases con algunos de los raros casos en los que la versión 2.x no soportase algún escenario “heredado” que sí estaba soportada con la versión antigua, puedes usar la versión 1.x instalándola a mano (es el ejecutable docker-compose) y asegurándote de que la opción “Enable Docker Compose V1/V2 compatibility mode” está desactivada:
El fichero de Compose
El fichero Compose es el fichero que define una aplicación. Recuerda que, si Docker se encarga de la creación, ejecución y gestión de contenedores, Compose se encarga de la definición de aplicaciones. Por “aplicación” en este contexto entendemos uno o varios contenedores trabajando conjuntamente: eso significa que deben levantarse, pararse y configurarse de forma coordinada.
Este fichero es un fichero en formato YAML que describe:
- Qué contenedores forman parte de la aplicación
- Qué configuración tiene cada contenedor
- Qué puertos exponen y cómo se mapean
- Cómo se pueden comunicar entre ellos
Por defecto dicho fichero toma el nombre de docker-compose.yml, aunque se pueden usar otros nombres si se desea.
En el archivo docker-compose.yml no puedes usar tabulaciones para “sangrar” las diferentes líneas, ya que no están permitidas. La indentación que verás en los archivos es obligatoria y debe ser realizada mediante el uso de espacios. Asegúrate de configurar tu editor de texto para que convierta automáticamente tabuladores a espacios (casi todos permiten esta configuración. Por ejemplo: Visual Studio, Visual Studio Code, Notepad++).
Veamos un ejemplo de un fichero Compose que nos defina una aplicación que se compone de un solo contenedor con la imagen dockercampusmvp/nodejs-sample:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample ports: - "9000:3000"
Crea un directorio en tu máquina y copia el contenido anterior en un fichero llamado docker-compose.yml. Luego desde este mismo directorio usa el comando:
docker compose up
Recuerda la diferencia entre “docker compose” (sin guion) y “docker-compose” (con guion). En el primer caso estamos usando la CLI de Docker, así que este comando lo tendrás siempre disponible. El segundo caso (con guion) es usando la herramienta externa, pero hoy en día ya no hay necesidad de tenerla descargada, ya que el comando interno funciona exactamente igual.
Esto debería:
- Descargarte la imagen
dockercampusmvp/nodejs-sample(si no la tienes ya) - Poner en marcha un contenedor de dicha imagen
Al final deberías ver una salida parecida a esta (y la línea de comandos estará esperando):
Recreating testcompose_sample-api_1 ... Recreating testcompose_sample-api_1 ... done Attaching to testcompose_sample-api-api_1 sample-api_1 | Ejemplo del curso corriendo en puerto 3000
Y si ahora navegas a http://localhost:9000/ping deberías recibir el “pong” de respuesta, verificando que el contenedor está en marcha. ¡Felicidades! ¡Has puesto en marcha tu primera aplicación usando Compose!
También puedes, por supuesto, usar docker ps para ver el contenedor creado.
Para parar el contenedor pulsa ^C (Control + Ctrl). Compose intentará parar el contenedor correctamente antes de matarlo si es necesario.
Versiones del fichero Compose
Al principio del fichero Compose se define una versión (por ejemplo, version: '3'). No confundas la versión de fichero Compose con la versión de docker-compose. Por supuesto que hay una correlación, en tanto que es necesaria una versión mínima de Docker Compose para poder ejecutar ficheros Compose de una determinada versión.
Hay 3 grandes grupos de versiones de ficheros Compose. La 1.x, totalmente obsoleta y que no se debe usar, la 2.x que salió posteriormente y la última la 3.x que se diseñó para compatibilizar Compose con Swarm (el orquestador de contenedores de la propia Docker). Sobre qué version usar, la verdad es que entre la 2.x y la 3.x hay muy pocas diferencias, todo lo que vas a ver funciona en ambas versiones y en aquellos (pocos) casos en que no es así, te lo indicaré.
Mapeo de puertos con Compose
El contenedor que se ha creado con el comando docker compose up tiene su puerto 3000 mapeado al puerto 9000 del host. Eso es porque así lo hemos especificado en el fichero docker-compose.yml:
ports: - "9000:3000"
La sección ports permite especificar una lista de mapeos de puertos (en el formato clásico <puerto-host>:<puerto-contenedor>). Si el contenedor abre más de un puerto que deseas mapear, simplemente agrega más entradas a la lista.
Usar Compose para configuración
Aunque podemos usar el modificador -e de docker run para pasar configuración a nuestros contenedores, esto se vuelve muy pesado, en especial cuando queremos tener configuraciones distintas para entornos distintos. Por supuesto, siempre nos podemos crear nuestros ficheros de shell (.sh o .cmd) para lanzar los contenedores, pero para estos casos Compose ofrece una solución mejor.
Y es que mediante Compose podemos pasar la configuración (variables de entorno) que deseemos a nuestros contenedores.
Vamos a partir del fichero de Compose que teníamos en la lección anterior:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample ports: - "9000:3000"
Este fichero define una aplicación que tiene un servicio (sample-api) y nos especifica la imagen del contenedor y en este caso el mapeo de puertos (el 3000 del contenedor al 9000 del host).
Del mismo modo que hay una sección ports, existe una sección llamada environment cuyo objetivo es contener la configuración en variables de entorno del contenedor. Dicha sección contiene una lista en formato variable=valor:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample ports: - "9000:80" environment: - PORT=80
En este caso estamos estableciendo la variable de entorno PORT con el valor de 80. Así, el contenedor escuchará por su puerto 80, en lugar del 3000 que usa por defecto. Observa cómo cambiamos también el mapeo de puertos.
Por supuesto, puedes agregar tantas entradas a environment como necesites, para especificar todas las variables de entorno que sean necesarias.
Si ahora ejecutas el comando docker compose up la salida será parecida a:
Recreating untitledproject_sample-api_1 ... Recreating untitledproject_sample-api_1 ... done Attaching to untitledproject_sample-api_1 sample-api_1 | Ejemplo del curso corriendo en puerto 80
Observa como el contenedor indica que está corriendo en el puerto 80. Y si navegas a http://localhost:9000/ping deberías recibir el pong de respuesta.
Usar bind mounts y volúmenes en Compose
En el fichero Compose puedes definir volúmenes a usar, usando la sección volumes. Por ejemplo, para definir un bind mount en un determinado contenedor usaríamos la siguiente sintaxis:
services: my-service: image: my-image-name volumes: - /home/data:/var/lib/data
En este caso se crea un bind mount donde el directorio del host /home/data se monta en el directorio /var/lib/data del sistema de ficheros del contenedor.
Para usar volúmenes se emplea esta otra sintaxis:
services: my-service: image: my-image-name volumes: - data-volume:/var/lib/data volumes: data-volume:
Observa como hay dos secciones volumes: una global donde definimos el volumen (data-volume) y otra a nivel de servicio donde asignamos el volumen a un directorio del sistema de ficheros del contenedor.
Compose creará el volumen si no existe. Para evitar que Compose cree el volumen y dé error si este no existe, se puedes indicar que es un volumen externo:
volumes: data-volume: external: true
