¡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
Gestión de entornos
Es muy habitual querer ejecutar tus contenedores con configuraciones distintas según el entorno en el que se ejecuten. Por ejemplo en desarrollo deseamos habilitar más logs y usar una determinada cadena de conexión, mientras que en otros entornos esos valores serán distintos.
Para ello podríamos tener varios ficheros de Compose, uno por entorno. Así tendríamos un docker-compose.development.yml con el contenido del entorno de desarrollo:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample ports: - "9000:3000"
Y otro fichero docker-compose.production.yml con el contenido del entorno de producción:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample ports: - "80:80" environment: - PORT=80
Luego para levantar los contenedores para el entorno de desarrollo podríamos usar el modificador -f de Compose que nos permite indicar el fichero a utilizar:
docker compose -f docker-compose.development.yml up
Y lo mismo para lanzar el entorno de producción.
De todos modos esa solución no es la idónea. En nuestro caso no vamos a tener problemas, ya que estamos en un escenario muy sencillo. Sin embargo, nos encontramos con que hay una parte de los ficheros que está repetida y que siempre es igual en todos los entornos. ¿Ves cuál es?
Exacto, la definición de qué servicios hay y cuál es la imagen de cada servicio, es la misma en todos los entornos. Lo que cambia de un entorno a otro es la configuración y/o los mapeos de los puertos. Pero qué servicios componen la aplicación y cuál es la imagen Docker asociada a cada servicio es la misma para todos los entornos.
Tal y como lo tenemos ahora, si la imagen del servicio simple-api cambia, deberemos cambiarla en todos los ficheros Compose de todos los entornos.
Combinación de ficheros Compose
Por suerte Compose ha tenido en cuenta esa casuística y es que le podemos pasar a Compose más de un fichero yml y Compose los combinará para formar un “único fichero” que será el que ejecutará (esa combinación es en memoria, no se genera fichero alguno real).
Eso nos permite separar la definición de la aplicación (servicios e imágenes) de la configuración, porque la primera es común a todos los entornos y la segunda es variable por entorno.
Así podemos tener un fichero, llamémosle docker-compose.yml con solo la definición de la aplicación:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample
Y ahora podemos tener un fichero adicional por entorno con la configuración (entorno y mapeos de puertos). Así podríamos tener un docker-compose.development.yml:
version: '3' services: sample-api: ports: - "9000:3000"
Observa cómo este fichero especifica solo lo que es del entorno de desarrollo, pero no especifica la imagen de Docker asociada al servicio porque ya está especificada en el fichero común.
De igual modo podemos tener un docker-compose.prod.yml con solo la configuración de producción:
version: '3' services: sample-api: ports: - "80:80" environment: - PORT=80
Ahora si hay un cambio en las imágenes de Docker solo debemos modificar el fichero docker-compose.yml.
Por supuesto si intentamos ejecutar solo el fichero docker-compose.prod.yml mediante el comando docker compose -f docker-compose.prod.yml up recibiremos un error, ya que dicho fichero por sí solo no basta (falta la imagen de Docker asociada al servicio). Debemos usar dos veces el modificador -f para pasar los dos ficheros:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
Cuando pasamos más de un -f a Compose, este combina todos los ficheros pasados en uno solo y ejecuta la definición resultante. En muchos escenarios con dos ficheros es suficiente, pero hay escenarios más avanzados donde se pueden usar tres o más ficheros.
El fichero docker-compose.override.yml
Es tan habitual tener separada la definición de la aplicación de la configuración en dos ficheros YAML, que en caso de no usar ningún modificador -f al hacer docker compose up se buscará:
- El fichero
docker-compose.yml - El fichero
docker-compose.override.yml
Y se combinarán. Por eso en entornos de desarrollo es habitual usar esos nombres.
Ver el fichero de Compose combinado
Como se ha mencionado antes, cuando se usan varios ficheros y se combinan, esta combinación ocurre en memoria. Pero existe el comando config que permite ver y validar el resultado final de usar uno o más ficheros. Así, si tecleamos:
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
La salida obtenida es algo parecido a:
services: sample-api: environment: PORT: '80' image: dockercampusmvp/nodejs-sample ports: - 80:80/tcp version: '3.0'
Que como puedes observar es la combinación de los dos ficheros que se le pasan. Si dicha combinación fuese incorrecta, docker compose indicaría el error. De esa manera podemos verificar y comprobar cuál es el resultado de una combinación de ficheros YAML (y por lo tanto la configuración final que Compose ejecutará) y si esta es o no es correcta y cuáles son sus errores.
Perfiles en Compose
En la lección anterior hemos visto cómo combinar distintos ficheros de Compose y así lograr una configuración distinta según el entorno en el que queramos trabajar. Pero ese mecanismo adolece de una limitación: todos los entornos levantan siempre los mismos servicios (contenedores).
Para solventar ese problema, Compose nos ofrece la posibilidad de usar “perfiles”. Usando esta característica es posible levantar un grupo de servicios/contenedores u otro en función del valor del parámetro --profile que se le pase a docker compose up.
Esta opción apareció en Docker Compose 1.28.0 así que asegúrate de tener una igual o superior.
Vamos a ver un ejemplo:
version: '3' services: frontend: image: frontendphp ports: - 8080:8080 profiles: ["front"] adminer: image: adminer ports: - 8081:8080 profiles: ["debug"] db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: test
Este fichero compose declara tres servicios (frontend, adminer y db) que ejecutan respectivamente un contenedor de una aplicación FrontEnd en PHP que estamos desarrollando, Adminer (un gestor de MySQL hecho en PHP) y un contenedor con un servidor de bases de datos MySql.
La novedad está en que dos de esos servicios están asociados a un perfil. Observa el uso del nodo profiles que contiene un array con los valores de todos los perfiles a los que está asociado un servicio. Por ejemplo, queremos que Adminer solo se levante cuando estamos depurando, por lo que lo asociamos a un perfil que hemos llamado debug (pero podría llamarse como quisiésemos).
Ahora, al usar docker compose up podemos especificar el perfil que queremos levantar. Un servicio se pondrá en marcha únicamente si el perfil que indicamos está definido en el array de perfiles de dicho servicio, o bien si el servicio no declara perfil alguno.
Así en este ejemplo:
docker compose --profile front uplevantará los serviciosfrontendydb(la base de datos no tiene perfiles declarados y, por lo tanto, se levantará siempre)docker compose --profile debug uplevantará los serviciosadminerydbdocker compose uplevantará tan solo el serviciodbpuesto que es el único que no tiene asociado perfil alguno y por lo tanto se levantará en todas las ocasiones.
Puedes levantar más de un perfil a la vez pasando el parámetro --profile varias veces.
Antes de disponer de la característica de perfiles ya era posible levantar solo algunos de los servicios definidos en el fichero Compose: pasando los nombres de los servicios después de docker compose up (p. ej. docker compose up adminer db), pero el uso de perfiles nos simplifica esa gestión y nos da mayor flexibilidad.
Construir imágenes usando Docker Compose
Puedes usar Docker Compose para construir las imágenes de Docker necesarias. En escenarios con un solo contenedor (como los que estamos viendo ahora) usar Compose para construir las imágenes no ofrece ninguna ventaja significativa frente a usar docker build.
Pero, en escenarios con más de un contenedor, el uso de Compose nos permite construir todas las imágenes necesarias con un solo comando.
Para que Compose pueda construir una imagen necesita, básicamente, que se le especifique la localización del fichero Dockerfile a usar. Para ello se usa la sección build:
version: '3' services: sample-api: image: dockercampusmvp/nodejs-sample:dev build: context: ./nodejs-sample dockerfile: Dockerfile
Observa cómo hemos agregado la sección build dentro del servicio simple-api. Dicha sección contiene dos opciones principales:
context: el contexto de construcción de la imagen. Generalmente es el directorio donde está el ficheroDockerfile(más adelante hablaremos de qué es exactamente el contexto de construcción de una imagen).dockerfile: el nombre del ficheroDockerfile(es opcional y por defecto se tomaDockerfile). Es decir, solo necesitas usar esa entrada si tu ficheroDockerfiletiene otro nombre distinto deDockerfile, o bien sicontextes otro directorio distinto del directorio que contiene elDockerfile(en este caso debes indicar también en esta entrada la ruta, relativa acontext, del ficheroDockerfile). De todos modos, se suele usar siempre por claridad.
Para construir las imágenes se usa el comando build de Compose (docker compose build), aunque el comando up también las construye si no existen.
Al igual que con up, al comando build se le puede pasar modificadores -f con los ficheros YAML.
