Curso Jakarta EE 9 (9). Docker (2): imágenes y contenedores.

logo Jakarta EE

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.

>>>> ÍNDICE <<<<

Imágenes y contenedores

Empecemos con los conceptos fundamentales que debemos conocer para trabajar con Docker, mostrados en la siguiente imagen.

docker imagenes, contenedores y capas

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.

docker run download image wildlfy

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.

docker dashboard en Windows 10

Lamentablemente no se encuentra disponible para Linux. El sustituto que utilizo es DockStation.

dockstation en linux
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 -solo puede ser uno- 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.
    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.

Docker hello app

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, y 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.

Vimos que con CMD solo es posible ejecutar un comando. Ahora que conocemos COPY, podemos sortear esta restricción copiando un fichero de script y ejecutándolo con CMD.

COPY init.sh init.sh
CMD ./init.sh

Seguimos teniendo un solo comando, pero en init.sh están todas las operaciones que necesitamos.

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.

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.

docker volumen

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.

Docker WLS2 filesharing

En el caso de Hyper-V, hay que configurar la carpeta como un recurso con Docker Desktop.

Docker Desktop resources

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.

Docker Hyper-V filesharing

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.

docker content in ubuntu

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, y que los puertos expuestos por cada contenedor sean accesibles dentro de la red aunque no se hayan declarado con el atributo ports, el cual usaremos solo para definir los puertos que deben verse fuera de la red interna.

Sin Docker Compose, podemos conseguir lo mismo pero de forma más laboriosa mediante la creación manual de una red con la orden docker network create y asociando contenedores a la misma con el parámetro –network de docker run.

Por tanto, en nuestro ejemplo no sería necesario publicar el puerto 3306 de MySQL para que la base de datos sea alcanzable desde las aplicaciones que tenemos en WildFly, y en los datos de conexión usaremos 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. Asimismo, se evitan conflictos de puertos: podemos tener en el anfitrión servicios en ejecución que usen los mismos puertos que los contenedores, ya que los de estos últimos quedan dentro de su propia red.

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.

>>>> ÍNDICE <<<<

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios .