¿Cómo dockerizar el proyecto maven? y cuantas formas de lograrlo?

87

Soy nuevo en Docker y no sé cómo ejecutar un proyecto java con maven aunque he leído muchos documentos y he probado muchos métodos.

  1. ¿Debo construir la imagen usando Dockerfile?
  2. ¿Cómo son los comandos cuando se va a ejecutar el proyecto maven en el host Dockerfile?
Yashon Lin
fuente

Respuestas:

122

Ejemplo de trabajo.

Este no es un tutorial de arranque de primavera. Es la respuesta actualizada a una pregunta sobre cómo ejecutar una compilación de Maven dentro de un contenedor Docker.

Pregunta publicada originalmente hace 4 años.

1. Genere una aplicación

Utilice el inicializador de primavera para generar una aplicación de demostración

https://start.spring.io/

ingrese la descripción de la imagen aquí

Extraiga el archivo zip localmente

2. Crea un Dockerfile

#
# Build stage
#
FROM maven:3.6.0-jdk-11-slim AS build
COPY src /home/app/src
COPY pom.xml /home/app
RUN mvn -f /home/app/pom.xml clean package

#
# Package stage
#
FROM openjdk:11-jre-slim
COPY --from=build /home/app/target/demo-0.0.1-SNAPSHOT.jar /usr/local/lib/demo.jar
EXPOSE 8080
ENTRYPOINT ["java","-jar","/usr/local/lib/demo.jar"]

Nota

  • Este ejemplo utiliza una compilación de varias etapas . La primera etapa se usa para construir el código. La segunda etapa solo contiene el jar construido y un JRE para ejecutarlo (observe cómo se copia jar entre etapas).

3. Construye la imagen

docker build -t demo .

4. Ejecute la imagen

$ docker run --rm -it demo:latest

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v2.1.3.RELEASE)

2019-02-22 17:18:57.835  INFO 1 --- [           main] com.example.demo.DemoApplication         : Starting DemoApplication v0.0.1-SNAPSHOT on f4e67677c9a9 with PID 1 (/usr/local/bin/demo.jar started by root in /)
2019-02-22 17:18:57.837  INFO 1 --- [           main] com.example.demo.DemoApplication         : No active profile set, falling back to default profiles: default
2019-02-22 17:18:58.294  INFO 1 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 0.711 seconds (JVM running for 1.035)

Misc

Lea la documentación del centro de Docker sobre cómo se puede optimizar la compilación de Maven para usar un repositorio local para almacenar archivos en caché.

Actualización (2019-02-07)

Esta pregunta tiene ahora 4 años y en ese momento es justo decir que la creación de aplicaciones con Docker ha experimentado un cambio significativo.

Opción 1: construcción de varias etapas

Este nuevo estilo le permite crear imágenes más livianas que no encapsulan sus herramientas de compilación y código fuente.

El ejemplo aquí nuevamente usa la imagen base oficial de Maven para ejecutar la primera etapa de la compilación usando una versión deseada de Maven. La segunda parte del archivo define cómo se ensambla el jar construido en la imagen de salida final.

FROM maven:3.5-jdk-8 AS build  
COPY src /usr/src/app/src  
COPY pom.xml /usr/src/app  
RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java  
COPY --from=build /usr/src/app/target/helloworld-1.0.0-SNAPSHOT.jar /usr/app/helloworld-1.0.0-SNAPSHOT.jar  
EXPOSE 8080  
ENTRYPOINT ["java","-jar","/usr/app/helloworld-1.0.0-SNAPSHOT.jar"]  

Nota:

  • Estoy usando la imagen base sin distribución de Google , que se esfuerza por proporcionar el tiempo de ejecución suficiente para una aplicación Java.

Opción 2: foque

No he usado este enfoque, pero parece digno de investigación, ya que te permite crear imágenes sin tener que crear cosas desagradables como Dockerfiles :-)

https://github.com/GoogleContainerTools/jib

El proyecto tiene un complemento de Maven que integra el empaquetado de su código directamente en su flujo de trabajo de Maven.


Respuesta original (incluida para completar, pero escrita hace mucho tiempo)

Intenta usar las nuevas imágenes oficiales, hay una para Maven

https://registry.hub.docker.com/_/maven/

La imagen se puede usar para ejecutar Maven en el momento de la compilación para crear una aplicación compilada o, como en los siguientes ejemplos, para ejecutar una compilación de Maven dentro de un contenedor.

Ejemplo 1: Maven ejecutándose dentro de un contenedor

El siguiente comando ejecuta su compilación de Maven dentro de un contenedor:

docker run -it --rm \
       -v "$(pwd)":/opt/maven \
       -w /opt/maven \
       maven:3.2-jdk-7 \
       mvn clean install

Notas:

  • Lo bueno de este enfoque es que todo el software se instala y se ejecuta dentro del contenedor. Solo necesita Docker en la máquina host.
  • Ver Dockerfile para esta versión

Ejemplo 2: usar Nexus para almacenar archivos en caché

Ejecuta el contenedor Nexus

docker run -d -p 8081:8081 --name nexus sonatype/nexus

Cree un archivo "settings.xml":

<settings>
  <mirrors>
    <mirror>
      <id>nexus</id>
      <mirrorOf>*</mirrorOf>
      <url>http://nexus:8081/content/groups/public/</url>
    </mirror>
  </mirrors>
</settings>

Ahora ejecute Maven vinculando al contenedor nexus, para que las dependencias se almacenen en caché

docker run -it --rm \
       -v "$(pwd)":/opt/maven \
       -w /opt/maven \
       --link nexus:nexus \
       maven:3.2-jdk-7 \
       mvn -s settings.xml clean install

Notas:

  • Una ventaja de ejecutar Nexus en segundo plano es que otros repositorios de terceros se pueden administrar a través de la URL del administrador de forma transparente para las compilaciones de Maven que se ejecutan en contenedores locales.
Mark O'Connor
fuente
¿se puede usar para reemplazar el maven central para una compilación de gradle? como se indica en support.sonatype.com/entries/…mavenCentral() Reemplacé en mis dependencias gradle con maven {url "http://nexus:8081..."y ahora solo tengo problemas de resolución.
Mohamnag
@mohamnag Correcto, el archivo de "configuración" de Maven anterior hace exactamente eso, redirigiendo todas las solicitudes de Maven Central al repositorio nexus local. Necesita describir el tipo de problemas de resolución que tiene. Podría ser cualquier cosa ... Por ejemplo, ¿ha configurado el enlace de Docker para que el host "nexus" se resuelva correctamente?
Mark O'Connor
Resultó que todo estaba bien, pero Nexus necesitaba tiempo para construir el índice o algo más causó el problema. También intenté activar una actualización de índice, por lo que no estoy seguro de qué caso solucionó el problema. Gracias sin embargo.
Mohamnag
Nexus 3 ahora también está disponible como contenedor acoplable. Utilice "sonatype / nexus3"
Thorbjørn Ravn Andersen
1
@avandeursen Tiene razón en que el parámetro --link está en desuso (Respuesta hace más de 3 años). Sin embargo, la solución más correcta (en mi opinión) es crear una red acoplable y ejecutar ambos contenedores en ella. De esa manera, puede aprovechar la función DNS nativa en Docker y continuar refiriéndose al contenedor nexus por su nombre. Actualizará el ejemplo más tarde
Mark O'Connor
47

Puede haber muchas formas ... Pero implementé siguiendo dos formas

El ejemplo dado es del proyecto maven.

1. Usando Dockerfile en el proyecto maven

Utilice la siguiente estructura de archivos:

Demo
└── src
|    ├── main
|    │   ├── java
|    │       └── org
|    │           └── demo
|    │               └── Application.java
||    └── test
|
├──── Dockerfile
├──── pom.xml

Y actualice el Dockerfile como:

FROM java:8
EXPOSE 8080
ADD /target/demo.jar demo.jar
ENTRYPOINT ["java","-jar","demo.jar"]

Navegue a la carpeta del proyecto y escriba el siguiente comando, podrá crear una imagen y ejecutar esa imagen:

$ mvn clean
$ mvn install
$ docker build -f Dockerfile -t springdemo .
$ docker run -p 8080:8080 -t springdemo

Obtenga un video en Spring Boot con Docker

2. Uso de complementos de Maven

Agregue el complemento de maven dado en pom.xml

<plugin>
    <groupId>com.spotify</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.4.5</version>
        <configuration>
            <imageName>springdocker</imageName>
            <baseImage>java</baseImage>
            <entryPoint>["java", "-jar", "/${project.build.finalName}.jar"]</entryPoint>
            <resources>
                <resource>
                    <targetPath>/</targetPath>
                    <directory>${project.build.directory}</directory>
                    <include>${project.build.finalName}.jar</include>
                </resource>
            </resources>
        </configuration>
    </plugin>

Navegue a la carpeta del proyecto y escriba el siguiente comando, podrá crear una imagen y ejecutar esa imagen:

$ mvn clean package docker:build
$ docker images
$ docker run -p 8080:8080 -t <image name>

En el primer ejemplo, estamos creando Dockerfile y proporcionando una imagen base y agregando jar an so, después de hacer eso, ejecutaremos el comando docker para construir una imagen con un nombre específico y luego ejecutaremos esa imagen.

Mientras que en el segundo ejemplo estamos usando el complemento maven en el que proporcionamos baseImagey, imageNamepor lo tanto, no necesitamos crear Dockerfile aquí ... después de empaquetar el proyecto maven obtendremos la imagen de la ventana acoplable y solo necesitamos ejecutar esa imagen ...

Riddhi Gohil
fuente
En lugar de modificar el punto de entrada para especificar el nombre del artefacto, puede usar un enfoque como aquí: alooma.com/blog/building-dockers : utilice maven-dependency-plugin para usar un nombre común. No es necesario colocar un jar con versión en el contenedor de la ventana acoplable, ya que el contenedor en sí está versionado.
kboom
14

Como regla general, debe crear un JAR grueso usando Maven (un JAR que contiene tanto su código como todas las dependencias).

Luego, puede escribir un Dockerfile que coincida con sus requisitos (si puede construir un JAR pesado, solo necesitaría un sistema operativo base, como CentOS, y la JVM).

Esto es lo que uso para una aplicación Scala (que está basada en Java).

FROM centos:centos7

# Prerequisites.

RUN yum -y update
RUN yum -y install wget tar

# Oracle Java 7

WORKDIR /opt

RUN wget --no-cookies --no-check-certificate --header "Cookie: gpw_e24=http%3A%2F%2Fwww.oracle.com%2F; oraclelicense=accept-securebackup-cookie" http://download.oracle.com/otn-pub/java/jdk/7u71-b14/server-jre-7u71-linux-x64.tar.gz
RUN tar xzf server-jre-7u71-linux-x64.tar.gz
RUN rm -rf server-jre-7u71-linux-x64.tar.gz
RUN alternatives --install /usr/bin/java java /opt/jdk1.7.0_71/bin/java 1

# App

USER daemon

# This copies to local fat jar inside the image
ADD /local/path/to/packaged/app/appname.jar /app/appname.jar

# What to run when the container starts
ENTRYPOINT [ "java", "-jar", "/app/appname.jar" ]

# Ports used by the app
EXPOSE 5000

Esto crea una imagen basada en CentOS con Java7. Cuando se inicie, ejecutará su aplicación jar.

La mejor manera de implementarlo es a través del Docker Registry, es como un Github para imágenes de Docker.

Puedes construir una imagen como esta:

# current dir must contain the Dockerfile
docker build -t username/projectname:tagname .

Luego puede empujar una imagen de esta manera:

docker push username/projectname # this pushes all tags

Una vez que la imagen está en el Registro de Docker, puede extraerla de cualquier parte del mundo y ejecutarla.

Consulte la Guía del usuario de Docker para obtener más información.

Algo a tener en cuenta :

También puede extraer su repositorio dentro de una imagen y construir el jar como parte de la ejecución del contenedor, pero no es un buen enfoque, ya que el código podría cambiar y podría terminar usando una versión diferente de la aplicación sin previo aviso.

La construcción de un frasco grueso elimina este problema.

Matteo Pacini
fuente
Hola, utilicé su ejemplo en mi archivo de la ventana acoplable para copiar el tarro gordo en la imagen, pero la compilación falla por no poder encontrar el tarro gordo dada la ruta local. ¿Es algo como target / app.jar?
user_mda
Hola, ¿hay alguna forma de que pueda descargar el artefacto de nexus en tiempo de ejecución? ¿Para especificar el artefacto que se descargará usando propiedades y no un enlace real al jar en sí? En el dockerfile: RUN wget -O {project.build.finalname}.jar pero quiero descargar el jar anterior de nexus.
Pramod Setlur
1
Incluir un FAT JAR para codificar el repositorio es lento para mí. ¿Hay alguna forma de usar maven para construir la imagen?
WoLfPwNeR
1
Los frascos gordos también tienen algunos problemas, especialmente con los frascos firmados.
Thorbjørn Ravn Andersen
1
Como regla general, nunca use un frasco pesado, es probable que los paquetes que se basan en el manifiesto u otros archivos de datos dentro de sus frascos fallen, ya que no existe una forma segura de fusionar estos datos. Archivos de manifiesto con rutas coincidentes dentro de un conflicto de jar y la primera o la última victoria (no recuerdo cuál). La única vez que debería considerar el uso de un tarro gordo es si tiene un conjunto muy limitado de dependencias y sabe muy bien que sus tarros no tienen datos de manifiesto contradictorios. De lo contrario, utilice los frascos de forma segura, ya que han sido diseñados para ser utilizados (es decir, por separado).
PiersyP
3

Aquí está mi contribución.
No intentaré enumerar todas las herramientas / bibliotecas / complementos que existen para aprovechar Docker con Maven. Algunas respuestas ya lo han hecho.
en lugar de, me centraré en la tipología de aplicaciones y la forma de Dockerfile.
Dockerfilees realmente un concepto simple e importante de Docker (todas las imágenes conocidas / públicas se basan en eso) y creo que tratar de evitar comprender y usar Dockerfiles no es necesariamente la mejor manera de ingresar al mundo de Docker.

Dockerizar una aplicación depende de la aplicación en sí y del objetivo a alcanzar

1) Para las aplicaciones que queremos continuar, ejecutarlas en un servidor Java instalado / independiente (Tomcat, JBoss, etc.)

El camino es más difícil y ese no es el objetivo ideal porque agrega complejidad (tenemos que administrar / mantener el servidor) y es menos escalable y menos rápido que los servidores integrados en términos de compilación / implementación / anulación de implementación.
Pero para las aplicaciones heredadas, eso puede considerarse como un primer paso.
Generalmente, la idea aquí es definir una imagen de Docker para el servidor y definir una imagen por aplicación a implementar. Para aplicaciones enormes (millones de líneas de códigos) con una gran cantidad de elementos heredados, y tan difíciles de migrar a una solución integrada de arranque completo, esa es realmente una buena mejora.
Las imágenes de la ventana acoplable para las aplicaciones producen el WAR / EAR esperado, pero no se ejecutan como contenedor y la imagen de la aplicación del servidor implementa los componentes producidos por estas imágenes como aplicaciones implementadas.

No detallaré más ese enfoque, ya que es para casos de uso menores de Docker, pero quería exponer la idea general de ese enfoque porque creo que para los desarrolladores que enfrentan estos casos complejos, es bueno saber que algunas puertas están abiertas a integrar Docker.

2) Para aplicaciones que incrustan / arrancan el servidor en sí (Spring Boot con servidor incrustado: Tomcat, Netty, Jetty ...)

Ese es el objetivo ideal con Docker . Especifiqué Spring Boot porque es un marco realmente agradable para hacer eso y también tiene un nivel muy alto de capacidad de mantenimiento, pero en teoría podríamos usar cualquier otra forma de Java para lograrlo.
Generalmente, la idea aquí es definir una imagen de Docker por aplicación para implementar.
Las imágenes de la ventana acoplable para las aplicaciones producen un JAR o un conjunto de JAR / clases / archivos de configuración y estos inician una JVM con la aplicación (comando java) cuando creamos e iniciamos un contenedor a partir de estas imágenes.
En el caso de nuevas aplicaciones o aplicaciones que no sean demasiado complejas para migrar, esa forma debe ser preferida a los servidores independientes porque es la forma estándar y la forma más eficiente de utilizar contenedores.
Detallaré ese enfoque.

Dockerizar una aplicación maven

1) sin bota de resorte

La idea es crear un frasco gordo con Maven (el complemento de ensamblaje de maven y el complemento de sombra de maven ayudan para eso) que contenga las clases compiladas de la aplicación y las dependencias de maven necesarias.
Entonces podemos identificar dos casos:

  • si la aplicación es una aplicación de escritorio o autónoma (que no necesita ser implementada en un servidor): podríamos especificar como CMD/ENTRYPOINTen la Dockerfileejecución java de la aplicación:java -cp .:/fooPath/* -jar myJar

  • si la aplicación es una aplicación de servidor, por ejemplo Tomcat, la idea es la misma: obtener un frasco grande de la aplicación y ejecutar una JVM en el CMD/ENTRYPOINT. Pero aquí con una diferencia importante: necesitamos incluir algunas bibliotecas lógicas y específicas ( org.apache.tomcat.embedbibliotecas y algunas otras) que inician el servidor integrado cuando se inicia la aplicación principal.
    Tenemos una guía completa en el sitio web de heroku .
    Para el primer caso (aplicación autónoma), esa es una forma directa y eficiente de usar Docker.
    Para el segundo caso (aplicación de servidor), que funciona pero que no es directo, puede ser propenso a errores y no es un modelo muy extensible porque no coloca su aplicación en el marco de un marco maduro como Spring Boot que hace muchos de estas cosas para usted y también proporciona un alto nivel de extensión.
    Pero eso tiene una ventaja: tiene un alto nivel de libertad porque usa directamente la API de Tomcat integrada.

2) Con bota de primavera

Por fin, aquí vamos.
Eso es simple, eficiente y muy bien documentado.
Realmente hay varios enfoques para hacer que una aplicación Maven / Spring Boot se ejecute en Docker.
Exponerlos a todos sería largo y quizás aburrido.
La mejor elección depende de sus necesidades.
Pero sea cual sea la forma, la estrategia de compilación en términos de capas de ventana acoplable parece la misma.
Queremos utilizar una compilación de varias etapas: una que se base en Maven para la resolución de dependencias y para la compilación y otra que se base en JDK o JRE para iniciar la aplicación.

Etapa de construcción (imagen de Maven):

  • pom copiar a la imagen
  • descargas de dependencias y complementos.
    Acerca de eso, mvn dependency:resolve-pluginsencadenado a mvn dependency:resolvepuede hacer el trabajo, pero no siempre.
    Por qué ? Debido a que estos complementos y la packageejecución para empaquetar el jar gordo pueden depender de diferentes artefactos / complementos e incluso para un mismo artefacto / complemento, estos aún pueden extraer una versión diferente. Por lo tanto, un enfoque más seguro, aunque potencialmente más lento, es resolver las dependencias ejecutando exactamente el mvncomando utilizado para empaquetar la aplicación (que extraerá exactamente las dependencias que necesita), pero omitiendo la compilación de origen y eliminando la carpeta de destino para acelerar el procesamiento y evitar cualquier detección de cambio de capa no deseado para ese paso.
  • copia del código fuente a la imagen
  • empaquetar la aplicación

Ejecutar etapa (imagen JDK o JRE):

  • copia el frasco de la etapa anterior

Aquí dos ejemplos.

a) Una forma sencilla sin caché para las dependencias de maven descargadas

Dockerfile:

########Maven build stage########
FROM maven:3.6-jdk-11 as maven_build
WORKDIR /app

#copy pom
COPY pom.xml .

#resolve maven dependencies
RUN mvn clean package -Dmaven.test.skip -Dmaven.main.skip -Dspring-boot.repackage.skip && rm -r target/

#copy source
COPY src ./src

# build the app (no dependency download here)
RUN mvn clean package  -Dmaven.test.skip

# split the built app into multiple layers to improve layer rebuild
RUN mkdir -p target/docker-packaging && cd target/docker-packaging && jar -xf ../my-app*.jar

########JRE run stage########
FROM openjdk:11.0-jre
WORKDIR /app

#copy built app layer by layer
ARG DOCKER_PACKAGING_DIR=/app/target/docker-packaging
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/lib /app/lib
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/classes /app/classes
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/META-INF /app/META-INF

#run the app
CMD java -cp .:classes:lib/* \
         -Djava.security.egd=file:/dev/./urandom \
         foo.bar.MySpringBootApplication

¿Inconveniente de esa solución? Cualquier cambio en pom.xml significa que vuelve a crear toda la capa que descarga y almacena las dependencias de maven. Eso generalmente no es aceptable para aplicaciones con muchas dependencias (y Spring Boot extrae muchas dependencias), en general si no usa un administrador de repositorio maven durante la construcción de la imagen.

b) Una forma más eficiente con caché para las dependencias de maven descargadas

El enfoque es aquí el mismo, pero las descargas de dependencias maven que se almacenan en caché en la caché del generador de Docker.
La operación de caché se basa en buildkit (api experimental de Docker).
Para habilitar buildkit, la variable env DOCKER_BUILDKIT = 1 debe configurarse (puede hacerlo donde quiera: .bashrc, línea de comando, archivo json del demonio docker ...).

Dockerfile:

# syntax=docker/dockerfile:experimental

########Maven build stage########
FROM maven:3.6-jdk-11 as maven_build
WORKDIR /app

#copy pom
COPY pom.xml .

#copy source
COPY src ./src

# build the app (no dependency download here)
RUN --mount=type=cache,target=/root/.m2  mvn clean package -Dmaven.test.skip

# split the built app into multiple layers to improve layer rebuild
RUN mkdir -p target/docker-packaging && cd target/docker-packaging && jar -xf ../my-app*.jar

########JRE run stage########
FROM openjdk:11.0-jre
WORKDIR /app

#copy built app layer by layer
ARG DOCKER_PACKAGING_DIR=/app/target/docker-packaging
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/lib /app/lib
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/BOOT-INF/classes /app/classes
COPY --from=maven_build ${DOCKER_PACKAGING_DIR}/META-INF /app/META-INF

#run the app
CMD java -cp .:classes:lib/* \
         -Djava.security.egd=file:/dev/./urandom \
         foo.bar.MySpringBootApplication
davidxxx
fuente