
Tras la introducción teórica, espero que el lector haya comprendido el enorme potencial que ha convertido a Docker en todo un estándar. Si no es así, lo descubrirá por sí mismo a medida que aprenda a sacarle partido en su día a día. Antes de introducirlo en nuestros proyectos, veamos cómo utilizarlo de forma muy básica. Y es que, a pesar de que este capítulo es algo más largo de lo habitual, Docker requiere de un curso específico y extenso para conocerlo en profundidad que queda muy lejos del propósito de este curso dedicado al desarrollo con Jakarta EE.
Imágenes y contenedores
Empecemos con los conceptos fundamentales que debemos conocer para trabajar con Docker, mostrados en la siguiente imagen.

Por capa, vamos a entender un conjunto de cambios en un sistema de ficheros. El apilamiento varias de ellas y cierta información adicional, con detalles de configuración, definen una imagen. A partir de ella, se instancian todos los contenedores que queramos. Dicho de otro modo, una imagen sirve de plantilla para la creación de contenedores. También se puede aplicar un símil muy cercano: la imagen es la definición de una clase, y los contenedores sus objetos.
La imagen no se verá modificada por los cambios realizados en los contenedores porque sus capas son inmutables y se comparten entre todos ellos. Este diseño tan peculiar contribuye a mejorar el rendimiento: la creación de un contenedor no requiere el clonado de la imagen, lo que hace al proceso más rápido y ahorra espacio en disco. Así pues, las imágenes son inalterables, pero no los contenedores porque dentro de ellos ejecutamos nuestras aplicaciones y servicios. Esto se consigue agregando a las capas de la imagen una propia de escritura, interna y exclusiva.
Si todo lo anterior resulta confuso, no deberíamos preocuparnos. La estructura de capas es «invisible» y lo que veremos dentro del contenedor si accedemos al mismo es un sistema de archivos. Volveremos sobre este concepto en breve, antes vamos a ejecutar nuestro primer contenedor con el siguiente comando.
docker run -p 8080:8080 jboss/wildfly:21.0.2.Final
En pocos segundos tenemos en ejecución un servidor WildFly, tal y como podemos comprobar en la dirección http://localhost:8080/. ¿Cómo es posible esta magia?
- docker run es el comando para crear y ejecutar un contenedor a partir de una imagen.
- -p es la opción para exponer (binding) los puertos del contenedor hacia el exterior (el equipo anfitrión). El formato es -p {puerto host}:{puerto contenedor}. Los puertos locales que asignemos no deben estar en uso.
Obsérvese que no se ha publicado el puerto 9990 por lo que la API de administración no está disponible. - Por último, con jboss/wildfly:21.0.2.Final hemos establecido el nombre de la imagen. Tras los dos puntos se indica su tag que sirve para identificar una versión de la misma. Si no se proporciona, se asume que su valor es latest (la más reciente).
Si la imagen no se encuentra instalada en el equipo, se descarga -en terminología Docker, se hace un pull– capa a capa y en paralelo, desde un registro de imágenes, usándose de forma predeterminada el oficial de Docker, llamado Docker Hub. En él podemos publicar imágenes de forma gratuita siempre y cuando sean públicas, con precios económicos si necesitamos almacenar imágenes privadas.

El contenedor se ejecuta en la terminal en la que hemos lanzado el comando y se puede detener cerrándola o matando el proceso (Ctrl+C). Es lo que vemos en el siguiente video. En el caso particular de la imagen de WildFly, se muestra su salida estándar (la bitácora o log).
Otra posibilidad es lanzar el contenedor en segundo plano (modo detached) con el parámetro -d o –detach para que funcione como un servicio típico y no se comporte como un proceso en ejecución dentro de la terminal desde la que se ha lanzado. En este caso, para mostrar el log usaremos el comando docker logs indicando el nombre del contenedor (no el de la imagen), y añadiendo la opción «f» si queremos que el proceso se mantenga abierto y muestre el log en tiempo real. El nombre del contenedor lo podemos proporcionar al ejecutar la imagen con –name.
docker run -d -p 8080:8080 --name hello_wildfly jboss/wildfly docker logs hello_wildfly -f
Si no damos nombre a un contenedor, Docker asignará uno de forma automática. Ese nombre, y el identificador numérico único, lo averiguamos con el comando docker ps el cual muestra un listado con los contenedores en ejecución. Si añadimos el parámetro -a se mostrarán todos los existentes en el sistema.
Detenemos el contenedor con docker stop {nombre contenedor}.
docker stop hello_wildfly
Para volver a arrancarlo, usaremos la orden start. El contenedor conserva la configuración de arranque asignada en el momento de su creación con docker run y no se puede cambiar. De hecho, start no admite el parámetro -p.
docker start hello_wildfly
El nombre de los contenedores es único. Si intentamos crear uno con un nombre que esté siendo utilizado, sea cual sea su imagen, se producirá el siguiente error.
docker: Error response from daemon: Conflict. The container name "/hello_wildfly" is already in use by container "165396e065ead3f082c972befee784dd44796fef76723159de23d710c5180e4e". You have to remove (or rename) that container to be able to reuse that name. See 'docker run --help'.
Los contenedores que no estén en ejecución y que no necesitemos se eliminan con docker rm {nombre contenedor}. Si al crear uno ya sabemos que no queremos conservarlo, lo indicamos con la opción –rm para que se elimine de forma automática tras su detención. Así, creamos contenedores desechables de usar y tirar.
docker run --rm -d -p 8080:8080 --name hello_wildfly jboss/wildfly
En la práctica, dentro del contenedor nos encontramos con un sistema Linux (*) en el que se pueden ejecutar comandos con docker exec. Es más, podemos lanzar el intérprete de comandos bash -o alguno de los instalados- en modo interactivo para trabajar en una terminal del contenedor, aunque lo normal es que tengamos muy pocos comandos y herramientas disponibles. Es lo que hace la siguiente instrucción.
docker exec -it hello_wildfly bash
(*) Microsoft distribuye imágenes basadas en Windows que solo funcionan en anfitriones con Windows.
Hasta ahora, nos hemos limitado a trabajar con imágenes de WildFly para ver el uso más habitual de Docker: la ejecución de servidores en modo detached y con el binding de algunos puertos. Pero la creación y ejecución de un contenedor va a depender de cada imagen y de lo que queramos hacer con ella. Así, en el caso de que el cometido del contenedor sea ofrecer una terminal para la introducción de datos, tenemos que arrancarlo en modo interactivo con «-it». De lo contrario, se detendrá enseguida. Es el caso de las imágenes «limpias» de Linux.

Algunas de las operaciones básicas que hemos visto pueden hacerse a golpe de ratón con la herramienta gráfica Dashboard incluida en Docker Desktop.
Lamentablemente no se encuentra disponible para Linux. El sustituto que utilizo es DockStation.
Prontuario de comandos
En este enlace hay un resumen bastante bueno de los comandos. Incluye una imagen muy útil.
Creación de imágenes
Es posible hacer el «camino inverso» y crear una nueva imagen a partir de un contenedor con la orden commit, consistente en las capas de la imagen original más la capa de escritura del contenedor.
$ docker commit {contenedor id o nombre} {nombre nueva imagen}
Esta funcionalidad permite evolucionar una imagen conservando todas las versiones previas que se deseen. También es útil para experimentar con ella. Pero la forma más habitual y práctica de definir una imagen consiste en crear un simple fichero de texto plano, llamado Dockerfile, en el que se especifica una secuencia de operaciones que se realizará en el momento de construir la imagen. Podemos tratarlo como si se fuera código fuente e incluirlo en el sistema de control de versiones del proyecto con el objetivo de facilitar su mantenimiento y distribución.

La mejor manera de aprender a definir imágenes es viendo un ejemplo sencillo. Vamos a crear una basada en la que hemos usado (jboss/wildfly con Centos Linux) que despliegue en el servidor WildFly una aplicación compilada directamente desde sus fuentes en GitHub y consistente en una página html. Puesto que en la imagen es para Linux, buena parte del fichero, que he llamado hello-wildfly.Dockerfile, constará de comandos del sistema operativo del pingüino.
FROM jboss/wildfly:21.0.2.Final
LABEL maintainer=danielme_com@yahoo.com
USER root
RUN yum -y install git
USER jboss
RUN git clone -q https://github.com/danielme-com/hello-docker-war.git
&& cd hello-docker-war
&& ./mvnw package
&& mv target/hello-docker.war $JBOSS_HOME/standalone/deployments
&& cd ..
&& rm -rf hello-docker-war
EXPOSE 8080
CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0"]
- FROM. Siempre debemos empezar con esta instrucción para indicar la «imagen base» sobre la que construimos la nuestra. Un símil muy apropiado es el mecanismo de herencia de la orientación a objetos, pues estamos creando una jerarquía de imágenes en la que cada una hereda las capas de su predecesora.
En Docker Hub disponemos de miles de imágenes, incluyendo las versiones oficiales de numerosos servicios y aplicaciones (bases de datos, servidores web, marcos de trabajo…). También podemos utilizar una distribución de Linux «pura» diseñada para construir una imagen más personalizada si cabe. Una opción muy popular es Alpine por su ridículo tamaño (5 MB). Cuanto menor sea una imagen mejor, pues solo queremos lo mínimo imprescindible. - LABEL. Se recomienda que esta etiqueta opcional sea la segunda instrucción. Sirve para indicar información general sobre la imagen.
- USER. El usuario activo de la imagen base es jboss, pero es necesario convertirnos en root para realizar las acciones de las líneas siguientes.
- RUN. Ejecuta el comando indicado. git no está disponible en la imagen base pero sí tenemos el gestor de paquetes yum con el que podemos instalarlo. Cada instrucción RUN crea una nueva capa dentro de la imagen, por eso es habitual agrupar comandos que tengan un objetivo común.
- USER. Volvemos al usuario regular jboss.
- RUN. Procedemos a clonar el repositorio que contiene la aplicación. Tiene el wrapper de Maven, así que lo usamos para construir el artefacto. Por último, copiamos el .war al directorio de despliegue de WildFly, que en la imagen base está en /opt/jboss/wildfly. Como buena práctica, eliminamos los ficheros que no necesitemos para evitar engordar la imagen.
- EXPOSE. Listado de puertos del contenedor que están a la escucha dentro de la red interna de Docker a la que se asocie (lo veremos cuando hablemos de Docker Compose). Para tener acceso desde fuera del contenedor y su red sigue siendo necesario utilizar la opción -p al arrancar el contenedor para mapear los puertos.
- CMD. El Dockerfile suele terminar con esta instrucción que contiene el comando y sus parámetros a ejecutar cada vez que arranque un contenedor. Si la imagen base ya tiene una instrucción CMD, se sobrescribe (solo se puede ejecutar una). Normalmente será el comando que inicie el servicio\proceso para el que estamos creando la imagen. La alternativa a CMD, aunque no es exactamente lo mismo, es ENTRYPOINT. Aunque es posible ejecutar más de un comando, en estos casos la práxis habitual es usar un script. En breve explicaré cómo.
WildFly hay que arrancarlo de tal modo que acepte conexiones remotas (desde fuera del contenedor); esto ya lo expliqué en el capítulo dedicado a Arquillian.
Ahora vamos a crear la imagen en nuestro equipo.
$ docker image build -t hello-wildfly -f hello-wildfly.Dockerfile .
Con la opción -t le damos nombre, y podría incluir su tag o versión si así lo indicamos con dos puntos. El punto que aparece al final no es una errata: señala que la ruta al fichero Dockerfile comienza en el directorio en el que se está ejecutando el comando. En el ejemplo, la construcción de la imagen se lanza en la misma carpeta en la que se ubica el fichero.
Esta construcción desde cero puede demorarse más de un minuto si tenemos que descargar la imagen base. A esto hay que añadir la instalación de git y la obtención de las dependencias de Maven.
Tras generar nuestra flamante imagen, ya podemos crear contenedores de la forma habitual, por ejemplo de un solo uso con –rm.
docker run -d --rm -p 8080:8080 --name hello_wildfly hello-wildfly
Comprobamos que todo está en orden con un navegador.

El comando history muestra los detalles de la construcción de cualquier imagen. El resultado incluye la información sobre la imagen base y el tamaño de las capas.
$ docker history hello-wildfly IMAGE CREATED CREATED BY SIZE COMMENT c823c1756e42 3 minutes ago /bin/sh -c #(nop) CMD ["/opt/jboss/wildfly/… 0B f340431cc6c4 3 minutes ago /bin/sh -c #(nop) COPY file:e2eef5632ddd2e31… 30.1kB 45867c308435 3 minutes ago /bin/sh -c #(nop) EXPOSE 8080 0B bfdbdf1a37ed 3 minutes ago /bin/sh -c git clone -q https://github.com/d… 31.6MB 99f141daeb00 3 minutes ago /bin/sh -c #(nop) USER jboss 0B e373bcf54d6c 3 minutes ago /bin/sh -c yum -y install git 147MB e0566a98b346 3 minutes ago /bin/sh -c #(nop) USER root 0B 6fe83d5212ed 3 minutes ago /bin/sh -c #(nop) LABEL maintainer=danielme… 0B b7ead193d962 2 weeks ago /bin/sh -c #(nop) CMD ["/opt/jboss/wildfly/… 0B <missing> 2 weeks ago /bin/sh -c #(nop) EXPOSE 8080 0B <missing> 2 weeks ago /bin/sh -c #(nop) USER jboss 0B <missing> 2 weeks ago /bin/sh -c #(nop) ENV LAUNCH_JBOSS_IN_BACKG… 0B <missing> 2 weeks ago /bin/sh -c cd $HOME && curl -O https://d… 240MB <missing> 2 weeks ago /bin/sh -c #(nop) USER root 0B <missing> 2 weeks ago /bin/sh -c #(nop) ENV JBOSS_HOME=/opt/jboss… 0B <missing> 2 weeks ago /bin/sh -c #(nop) ENV WILDFLY_SHA1=fd8c3cfc… 0B <missing> 2 weeks ago /bin/sh -c #(nop) ENV WILDFLY_VERSION=21.0.… 0B <missing> 4 months ago /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B <missing> 4 months ago /bin/sh -c #(nop) USER jboss 0B <missing> 4 months ago /bin/sh -c yum -y install java-11-openjdk-de… 235MB
Otra instrucción muy útil es COPY: copia un fichero en la imagen. Vamos a añadirla al final del Dockerfile, justo antes de CMD, para sobrescribir el fichero de configuración de WildFly. standalone.xml está situado en la misma carpeta que el Dockerfile.
EXPOSE 8080 COPY standalone.xml $JBOSS_HOME/standalone/configuration CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0"]
Volvamos a generar la imagen.
$ docker image build -t hello-wildfly -f hello-wildfly.Dockerfile . Sending build context to Docker daemon 389.6kB Step 1/9 : FROM jboss/wildfly:21.0.2.Final ---> b7ead193d962 Step 2/9 : LABEL maintainer=danielme_com@yahoo.com ---> Using cache ---> 891577509cf8 Step 3/9 : USER root ---> Using cache ---> e3ce99a56976 Step 4/9 : RUN yum -y install git ---> Using cache ---> 18d3ba47ef20 Step 5/9 : USER jboss ---> Using cache ---> f3b526d2ab32 Step 6/9 : RUN git clone -q https://github.com/danielme-com/hello-docker-war.git && cd hello-docker-war && ./mvnw package && mv target/hello-docker.war $JBOSS_HOME/standalone/deployments && cd .. && rm -rf hello-docker-war ---> Using cache ---> e748141ebff9 Step 7/9 : EXPOSE 8080 ---> Using cache ---> 3afb73fe07ab Step 8/9 : COPY standalone.xml $JBOSS_HOME/standalone/configuration ---> 1e5674245de4 Step 9/9 : CMD ["/opt/jboss/wildfly/bin/standalone.sh", "-b", "0.0.0.0"] ---> Running in bfd1d5ea1513 Removing intermediate container bfd1d5ea1513 ---> adc4d22b3f72 Successfully built adc4d22b3f72 Successfully tagged hello-wildfly:latest
¡Esta vez el proceso es instantáneo! Docker conoce la secuencia de instrucciones con las que se construyó la última versión de la imagen y que son anteriores a la primera instrucción distinta que aparece en el Dockerfile actual. Así que recupera las capas adecuadas para no volver a generarlas. Es lo que nos dice el mensaje «Using cache».
La misma estrategia se aplica cuando hacemos un docker pull de una imagen completa, por ejemplo desde Docker Hub, ya que solo se descargan las capas que no tenemos en nuestro equipo. Si por algún motivo no quisiéramos este comportamiento, usaremos el parámetro –no-cache para construir toda la imagen desde cero.
La orden COPY es un poco especial en lo que respecta a la caché. Si el fichero standalone.xml cambia -Docker lo sabe porque calcula y guarda su hash-, COPY se ejecutará cuando se genere la imagen de nuevo aunque no haya cambiado el Dockerfile.
En el caso de que tengamos que ejecutar varios comandos al arrancar un contenedor, lo más práctico y flexible es declarar esas acciones en un fichero de script para ejecutarlo con CMD. Incluimos ese fichero en la imagen, cómo no, con COPY.
COPY init.sh init.sh CMD ./init.sh
Seguimos teniendo un solo comando, pero en init.sh están todas las operaciones que necesitamos. No obstante, si lo que se quiere es arrancar más de un servicio, mejor no hacerlo porque la filosofía de Docker es encapsular en un contenedor un único servicio. En caso de seguir adelante con esta idea, recomiendo recurrir a Supervisor.
Por último, la orden ADD es equivalente a COPY con la capacidad adicional de obtener un fichero desde una url y la descompresión automática de los empaquetados de tipo TAR. Sin embargo, se recomienda usar siempre COPY.
Volúmenes
Hemos visto que uno de los puntos fuerte de Docker, compartido con las máquinas virtuales, es que cada contendor funciona de forma aislada en el equipo anfitrión. Esto implica que el sistema de archivos del contenedor solo es accesible desde el mismo y cuando está en ejecución. Además, el contenido se pierde si el contenedor se borra.
Pero esta virtud supone una limitación si queremos que un contenedor interactúe con un sistema de archivos externo para compartir ficheros con otros contenedores, realizar copias de seguridad de su contenido o hacer que los datos estén accesibles aunque el contenedor esté detenido o incluso se elimine. El ejemplo más evidente es el de un contenedor con una base de datos: si creamos uno nuevo porque se ha actualizado la imagen de nuestra base de datos, en ese nuevo contenedor no tendremos los datos que ya teníamos en el contenedor antiguo salvo, claro está, que realicemos algún proceso de migración o similar. Otro caso típico es que queramos que los logs del servicio que corre dentro del contenedor se escriban en el equipo anfitrión para poder consultarlos con facilidad y/o conservarlos.
La solución más flexible que aporta Docker es el concepto de volumen: una carpeta ubicada en el anfitrión utilizable en el contenedor de forma transparente.

El siguiente comando crea e inicia un contenedor mapeando una carpeta de un anfitrión Linux (/opt/docker) al directorio /var/host dentro del contenedor. Si este último ya existe, su contenido se perderá.
docker run -v /opt/docker:/var/host --rm -it alpine
Veamos el siguiente ejemplo para Windows.
docker run -v c:\Users\dani\shared:/var/host --rm -it alpine
Si usamos Docker con WSL2 no habrá ningún problema, aunque recibiremos esta notificación.

En el caso de Hyper-V, hay que configurar la carpeta como un recurso con Docker Desktop.
No obstante, la primera vez que se intente mapear una carpeta, la siguiente notificación nos permite hacer lo anterior en el acto simplemente pulsando un botón.

Esta estrategia es sencilla y práctica, pero poco portable entre equipos porque requiere de una carpeta específica en el anfitrión. Si queremos un espacio físico genérico en el anfitrión que pueda ser compartido con los contenedores, hay una opción mejor. Con el siguiente comando creamos un volumen dentro de Docker identificado por un nombre.
docker volume create host_folder
Ahora tenemos en Docker un volumen llamado host_folder que puede ser utilizado por cualquier contenedor.
docker run -v host_folder:/var/host --rm -it alpine
La ubicación del volumen en el sistema de archivos del anfitrión nos da un poco igual y de ello se encarga Docker en todo momento. Es más, el volumen es para que lo usen los contenedores y no nosotros. Podemos averiguar la ubicación exacta visualizando los detalles del volumen.
$ docker volume inspect host_folder [ { "CreatedAt": "2021-02-10T19:26:26+01:00", "Driver": "local", "Labels": {}, "Mountpoint": "/var/lib/docker/volumes/host_folder/_data", "Name": "host_folder", "Options": {}, "Scope": "local" } ]
El acceso a la carpeta /var/lib/docker sólo es posible como root.

El ejemplo anterior se ha realizado en Linux, pero si lo repetimos en Windows 10 la ruta es la misma, esto es, un path de Linux. Este hecho no debería sorprendernos, pues comenté en el capítulo anterior que los contenedores Linux solo puede ejecutarse en Linux, y en Windows ese Linux es simulado mediante tecnologías de virtualización. La ubicación del sistema de archivos del Linux de Docker en Windows depende de esa tecnología. En Hyper-V encontraremos un disco duro virtual en la ruta «C:\Users\Public\Documents\Hyper-V\Virtual hard disks» de acceso restringido.
Cuando ya no necesitemos un volumen creado de esta forma, podemos eliminarlo para liberar el espacio.
docker volume rm host_folder
Docker Desktop cuenta con una sección para la administración de volúmenes.

Docker Compose
Volvamos a la casuística que encontraremos en «Personal Budget», la aplicación de gestión de gastos que vamos a desarrollar en el curso. Nuestro proyecto va a requerir un servidor WildFly y una base de datos MySQL que definiremos más adelante en sus respectivas imágenes. Para que el sistema funcione, es imprescindible que los dos contenedores con estos servicios estén en ejecución y hayan sido creados con la configuración de puertos, volúmenes, etc., adecuada. Podemos hacer algo como lo que sigue.
$ docker run -d -p 8080:8080 --name personal_budget-wildfly personal_budget/wildfly $ docker run -d -p 3306:3306 --name personal_budget-mysql -v /opt/mysql/data:/var/lib/mysql personal_budget/mysql
Lanzando los comandos anteriores creamos y ejecutamos los contenedores correspondientes a las imágenes de nombre, por ejemplo, personal_budget/wildfly y personal_budget/mysql. Posteriormente, tendremos que iniciar cada contenedor para levantar de nuevo todo el sistema.
$ docker start personal_budget-mysql $ docker start personal_budget-wildfly
Todo un engorro. Es fácil ver que terminaremos creando un script que automatice estas tareas. Pero Docker incluye una herramienta de gestión de contenedores, denominada Docker Compose, que permite unificar tanto la configuración como la ejecución de un grupo de contenedores para trabajar con ellos como si se tratase de un todo. Este conjunto integrado de contenedores se configura en uno o varios ficheros en formato YAML, denominado por omisión docker-compose.yml, y se utiliza con el comando docker-compose. Si el lector no está familiarizado con este formato, no debe preocuparse pues resulta muy intuitivo.

Hagamos un breve repaso de la creación de un entorno de contenedores mediante la disección del siguiente fichero.
version: "3" services: personal_budget-mysql: image: mysql:8.0.23 ports: - 3307:3306 volumes: - /opt/mysql/data:/var/lib/mysql environment: MYSQL_DATABASE: onBudget MYSQL_USER: user MYSQL_PASSWORD: password MYSQL_ROOT_PASSWORD: root personal_budget-wildfly: build: dockerfile: hello-wildfly.Dockerfile context: . depends_on: - personal_budget-mysql ports: - 8080:8080 - 9990:9990 volumes: - personal_budget-logs:/opt/jboss/wildfly/standalone/logs volumes: personal_budget-logs:
- version: es la versión del formato de fichero. Consultar la compatibilidad con la versión de Docker aquí.
- services: indica la sección en la que se define cada servicio (contenedor).
- personal_budget-mysql: nombre del servicio.
- image: nombre de la imagen, tag con la versión incluido.
- ports: la apertura de puertos del contenedor al anfitrión, equivalente al que se hace con la opción -p del docker run.
- volumes: otra opción disponible en docker run y que antes usamos de forma abreviada con -v
- environment: definición de variables de entornos esperadas por la imagen. En un fichero Dockerfile, leeremos sus valores con $MYSQL_USER, $MYSQL_DATABASE, etc.
- personal_budget-wildfly: este servicio utiliza tres nuevas opciones con respecto al anterior:
- build: la imagen a utilizar debe crearse a partir de un Dockerfile. Indicamos su nombre y la ruta relativa con respecto a la ubicación actual (el fichero docker-compose) en la que se encuentra. Como en el ejemplo está en el directorio actual, se utiliza un punto.
- depends_on: servicios que se deben lanzarse antes que el que estamos configurando. Permite establecer un orden predeterminado de ejecución. En nuestro caso, queremos que la base de datos esté disponible cuando arranque WildFly.
- volumes: ahora, en lugar de mapear una carpeta del anfitrión se utiliza el nombre de un volumen gestionado por Docker. Lo usaremos para acceder a los ficheros con la bitácora del servidor.
Despleguemos la infraestructura de servicios del fichero.
$ docker-compose up -d Creating network "hello-docker_default" with the default driver Creating volume "hello-docker_pesonal_budget-logs" with default driver Creating hello-docker_pesonal_budget-mysql_1 ... done Creating hello-docker_pesonal_budget-wildfly_1 ... done
Vemos que en primer lugar se crea una red interna de la que van a formar parte todos los contenedores del conjunto. Esto permite que puedan verse entre sí utilizando a modo de dirección dns el nombre del servicio. Con respecto a los puertos, tengamos en cuenta estas consideraciones.
- Los publicados con port son accesibles desde fuera de la red y, por tanto, también por los contenedores dentro de ella.
- Con la opción expose, los puertos se publican solo dentro de la red del contenedor. No podemos leerlos desde fuera de ella.
En nuestro compose no hemos usado esta última opción. Si, por ejemplo, quisieramos que el puerto 3306 del contenedor con el servicio de MySQL solo esté publicado dentro de la red que comparte con el servicio WildFly, haríamos lo siguiente.
services: personal_budget-mysql: image: mysql:8.0.23 expose: - 3306
Sin Docker Compose, podemos conseguir lo mismo, pero resulta más laborioso. Definiremos una red con la orden docker network create para asociar a ella contenedores con el parámetro –network de docker run. Un contenedor puede formar parte de varias redes al mismo tiempo.
Por todo lo anterior, en nuestro ejemplo WildFly puede usar persona_budget-mysql:3306 como url para interactuar con MySQL. De este modo, vemos que la gestión de redes de Docker Compose simplifica la integración de los servicios y el aislamiento y empaquetado de los entornos.
Los contenedores que forman parte de Docker Compose aparecen en el listado mostrado con el comando docker ps, pero también contamos con el comando específico docker-compose ps.
$ docker-compose ps Name Command State Ports -------------------------------------------------------------------------------- hello-docker_personal_budget- docker-entrypoint.sh Up 0.0.0.0:3307->3306/tcp mysql_1 mysqld , 33060/tcp hello-docker_personal_budget- /opt/jboss/wildfly/bin Up 0.0.0.0:8080->8080/tcp wildfly_1 /sta ... , 0.0.0.0:9990->9990/tcp
Todo el sistema se detiene con la orden
$ docker-compose stop
Los mismos contenedores se levantarán la próxima vez que usemos la orden up, salvo que se realicen cambios en el fichero docker-compose.yml que obligue a Docker a recrear alguno de ellos.
$ docker-compose up -d Starting hello-docker_onbudget-mysql_1 ... done Creating hello-docker_onbudget-wildfly_1 ... done
Si se hace referencia a algún Dockerfile y lo hemos cambiado, forzaremos la creación de las nuevas imágenes con la opción –build.
$ docker-compose up -d --build
Con la orden down, los contenedores, además de detenerse, se eliminan. La red también se borra, pero los volúmenes, tal y como cabe esperar, se conservan. En nuestro ejemplo, eso significa que los datos almacenados por MySQL no se perderán.
$ docker-compose down Stopping hello-docker_personal_budget-wildfly_1 ... done Stopping hello-docker_personal_budget-mysql_1 ... done Removing hello-docker_personal_budget-wildfly_1 ... done Removing hello-docker_personal_budget-mysql_1 ... done Removing network hello-docker_default
Hasta ahora, siempre hemos hablado de un conjunto de contenedores para presentar las bondades de Docker Compose, pero nada impide definir en el fichero docker-compose un único servicio\contenedor . Esto tiene sentido porque es posible que simplemente queramos definir la configuración de un contenedor para ejecutarlo con mayor comodidad en lugar de tener que crear un script de bash o similar.
Código de ejemplo
El código de ejemplo de este capítulo se encuentra en GitHub. Para más información sobre cómo utilizar GitHub, consultar este artículo.