Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose

Finalizamos la serie de artículos de Kotlin y SpringBoot con el presente artículo con título “Kotlin, SpringBoot, Docker y DockerCompose III: docker y docker-compose”. El objetivo de la entrada es dockerizar la aplicación para hacer funcionar la aplicación desarrollada en un contenedor Docker. Además, definiremos la aplicación para que funcione en una composición de contenedores con un contenedor con MySQL.

La serie está compuesta de tres entradas ordenadas de forma cronológica:

La estructura del artículo está compuesto por los siguientes puntos:

  1. Docker
    • Propuesta 1
    • Propuesta 2
    • Propuesta 3
  2. DockerCompose

1.- Docker

La aplicación la podemos dockerizar de tres formas posibles. Las diferencia reside en el tamaño de la imagen resultante.

Propuesta 1

La estrategia consiste en copiar el contenido de la aplicación en el interior de la imagen y posteriormente realizar la creación del artefacto. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
RUN mv /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app/app.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v1 .

Para visualizar las imágenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de la ejecución del comando muestra una imagen con un tamaño aproximado a 686MB.

Propuesta 2

La segunda estrategía consigue reducir el tamaño de la imagen. Para ello, se realiza la creación del artefacto fuera de la imagen y se copia al interior de la imagen. Es decir, respecto a la solución anterior, se elimina el paso de construcción del artefacto. Para realizar la creación del artefacto, es necesario definir una tarea de creación del fichero jar mediante una tarea en Gradle. La tarea Gradle es la siguiente:

tasks.register("buildDocker"){
  dependsOn("build")
  doLast{
    fileList("./build/libs").forEach { file ->
      file.copyTo(File("./ejem1kotlindocker.jar"))
    }
  }
}

Como se observa en el snippet anterior, la tarea “buildDocker” tiene una dependencia con la tarea build; una vez ejecutado build, su resultado, generado en la carpeta “./build/libs” del proyecto, se copia a la carpeta raíz del proyecto. Esta tarea debe de ser ejecutada previamente a la construcción de la imagen.

La definición de la imagen en el Dockerfile es la siguiente:

FROM openjdk:8-jdk-alpine
VOLUME /tmp
RUN mkdir /app
WORKDIR /app
EXPOSE 8080
ADD ./ejem1kotlindocker.jar /app/ejem1kotlindocker.jar
ENTRYPOINT ["java", "-jar", "/app/ejem1kotlindocker.jar"]

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v2 .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable.

Propuesta 3

La tercera estrategia consiste en fusionar los objetivos de las anteriores. Para ello, primeramente, realizamos la creación del artefacto en una primera capa; posteriormente,
con el artefacto creado, construimos la imagen. Todo ello en el paso de construcción de la imagen y en elmomento de crear la imagen Docker. El snippet con el contenido del Dockerfile es el siguiente:

FROM openjdk AS build
VOLUME /tmp
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN /app/gradlew build
FROM openjdk:8-jdk-alpine

ARG APP_VERSION=1.0.0-SNAPSHOT
ENV database__client=mysql
ENV database__connection__host=mysql
ENV database__connection__user=root
ENV database__connection__password=password
ENV database__connection__database=prueba
ENV SPRING_PROFILES_ACTIVE=dev
LABEL org.label-schema.version=$APP_VERSION
COPY --from=build /app/build/libs/*.jar /app/app.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app/app.jar" ]

Con esta opción, definimos las variables de entorno con la configuración por defecto a la base de datos cuyos valores pueden ser pasados por parámetro en la creación de la imagen. De la misma manera que los argumentos necesarios.

Para crear la imagen ejecutamos el siguiente comando docker:

docker image build -t alvaroms/ejem1kotlindocker:v3 --build-arg APP_VERSION=1.0.3-SNAPSHOT .

Para visualizar las imagenes creadas ejecutamos el siguiente comando:

docker image list

El resultado de esta forma implica la creación de una imagen con un tamaño aproximado a 149MB. En relación a la primera forma, la reducción del tamaño es notable;y, respecto a la segunda, no es necesario realizar ninguna operación intermedia. Así, de las tres definiciones de Dockerfile, esta tercera, es la solución más eficiente.

2.- DockerCompose

DockerCompose permite realizar la composición de contenedores interconectados. La definición de la composición se realiza en el fichero docker-compose.yml, ubicado en la raíz del proyecto. La composición se define en base a dos servicios: el primero, mysql, corresponde con un contenedor de una imagen de MySQL; el segundo, kotlindocker, corresponde con la aplicación desarrollada.

Cada contenedor está definida en dos redes: kotlinnetwork, para el contenedor de la aplicación; y, mysqlnetwork, para el contenedor de la base de datos MySQL. La conexión la realiza el servicio de kotlin al tener conexión a las dos redes. Para los volúmenes, se definen un volumen para cada servicio.

Otro detalle importante, reside en qué imagen de la aplicación seleccionamos, o bien, creamos la imagen a partir del código existente: opción build, creamos la imagen a partir del código; e, image, creamos el contenedor a partir de una imagen definida.

El contenido del fichero docker-compose es el siguiente:

version: '3'
services:
  mysql:
    container_name: mysql
    image: mysql:5.7
    environment:
      - MYSQL_DATABASE=prueba
      - MYSQL_ROOT_PASSWORD=password
    ports:
      - 3306:3306
    volumes:
      - mysqlvolume:/var/lib/mysql
    networks:
      - mysqlnetwork
  kotlindocker:
    depends_on:
      - mysql
    container_name: kotlindocker
      # image: alvaroms/ejem1kotlindocker:v4
      build: ./
    environment:
      - SPRING_PROFILES_ACTIVE=dev
    ports:
      - 8087:8080
    volumes:
      - kotlinvolume:/var/lib/kotlin
    networks:
      - kotlinnetwork
      - mysqlnetwork
    volumes:
      kotlinvolume:
      mysqlvolume:
    networks:
      kotlinnetwork:
        driver_opts:
          com.docker.network.bridge.name: kotlinNetwork
      mysqlnetwork:
        driver_opts:
          com.docker.network.bridge.name: MySQLNetwork

Para el lector interesado en el código del proyecto puede acceder en el siguiente enlace

Finalizamos con una reflexión sobre la serie sobre kotlin, Spring Boot y Docker: un punto a tener en cuenta es que el código Kotlin presentado es muy mejorable al no haber aplicado patrones funcionales como los que se pueden presentar con la librería Arrow; con ella, la mejora del código es notable, por ejemplo, utilizando contenedores binarios.