# Compilaciones multietapa





## Explicación

En una compilación tradicional, todas las instrucciones de compilación se ejecutan en secuencia y en un único contenedor de compilación: descarga de dependencias, compilación de código y empaquetado de la aplicación. Todas esas capas terminan en tu imagen final. Este enfoque funciona, pero genera imágenes voluminosas que contienen peso innecesario y aumentan los riesgos de seguridad. Aquí es donde entran las compilaciones multietapa.

Las compilaciones multietapa introducen múltiples etapas en tu Dockerfile, cada una con un propósito específico. Piensa en ello como la capacidad de ejecutar diferentes partes de una compilación en múltiples entornos diferentes, de forma concurrente. Al separar el entorno de compilación del entorno de ejecución final, puedes reducir significativamente el tamaño de la imagen y la superficie de ataque. Esto es especialmente beneficioso para aplicaciones con grandes dependencias de compilación.

Se recomiendan las compilaciones multietapa para todo tipo de aplicaciones.

- Para lenguajes interpretados, como JavaScript, Ruby o Python, puedes compilar y minificar tu código en una etapa y copiar los archivos listos para producción a una imagen de ejecución más pequeña. Esto optimiza tu imagen para el despliegue.
- Para lenguajes compilados, como C, Go o Rust, las compilaciones multietapa te permiten compilar en una etapa y copiar los binarios compilados en una imagen de ejecución final. No es necesario incluir todo el compilador en tu imagen final.

Aquí tienes un ejemplo simplificado de una estructura de compilación multietapa utilizando pseudocódigo. Ten en cuenta que hay múltiples instrucciones `FROM` y un nuevo `AS <stage-name>`. Además, la instrucción `COPY` en la segunda etapa está copiando `--from` la etapa anterior.

```dockerfile
# Etapa 1: Entorno de compilación
FROM builder-image AS build-stage
# Instalar herramientas de compilación (por ejemplo, Maven, Gradle)
# Copiar código fuente
# Comandos de compilación (por ejemplo, compilar, empaquetar)

# Etapa 2: Entorno de ejecución
FROM runtime-image AS final-stage
#  Copiar los artefactos de la aplicación desde la etapa de compilación (por ejemplo, archivo JAR)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# Definir la configuración del tiempo de ejecución (por ejemplo, CMD, ENTRYPOINT)
```

Este Dockerfile utiliza dos etapas:

- La etapa de compilación utiliza una imagen base que contiene las herramientas de compilación necesarias para compilar tu aplicación. Incluye comandos para instalar herramientas de compilación, copiar el código fuente y ejecutar comandos de compilación.
- La etapa final utiliza una imagen base más pequeña adecuada para ejecutar tu aplicación. Copia los artefactos compilados (un archivo JAR, por ejemplo) de la etapa de compilación. Finalmente, define la configuración del tiempo de ejecución (utilizando `CMD` o `ENTRYPOINT`) para iniciar tu aplicación.

## Pruébalo

En esta guía práctica, descubrirás el poder de las compilaciones multietapa para crear imágenes de Docker ligeras y eficientes para una aplicación Java de muestra. Utilizarás como ejemplo una aplicación sencilla de Spring Boot basada en "Hello World" y compilada con Maven.

1. [Descarga e instala](https://www.docker.com/products/docker-desktop/) Docker Desktop.

2. Abre este [proyecto preinicializado](https://start.spring.io/#!type=maven-project&language=java&platformVersion=4.0.1&packaging=jar&configurationFileFormat=properties&jvmVersion=21&groupId=com.example&artifactId=spring-boot-docker&name=spring-boot-docker&description=Demo%20project%20for%20Spring%20Boot&packageName=com.example.spring-boot-docker&dependencies=web) para generar un archivo ZIP. Así es como se ve:

   ![Una captura de pantalla de la herramienta Spring Initializr seleccionada con Java 21, Spring Web y Spring Boot 3.4.0](/get-started/docker-concepts/building-images/multi-stage-builds/images/multi-stage-builds-spring-initializer.webp?border=true)

   [Spring Initializr](https://start.spring.io/) es un generador de inicio rápido para proyectos de Spring. Proporciona una API extensible para generar proyectos basados en JVM con implementaciones para varios conceptos comunes, como la generación básica de lenguaje para Java, Kotlin, Groovy y Maven.

   Selecciona **Generate** para crear y descargar el archivo zip de este proyecto.

   Para esta demostración, has emparejado la automatización de la compilación de Maven con Java, una dependencia de Spring Web y Java 21 para tus metadatos.

3. Explora el directorio del proyecto. Una vez descomprimas el archivo, verás la siguiente estructura de directorio del proyecto:

   ```plaintext
   spring-boot-docker
   ├── HELP.md
   ├── mvnw
   ├── mvnw.cmd
   ├── pom.xml
   └── src
       ├── main
       │   ├── java
       │   │   └── com
       │   │       └── example
       │   │           └── spring_boot_docker
       │   │               └── SpringBootDockerApplication.java
       │   └── resources
       │       ├── application.properties
       │       ├── static
       │       └── templates
       └── test
           └── java
               └── com
                   └── example
                       └── spring_boot_docker
                           └── SpringBootDockerApplicationTests.java

   15 directories, 7 files
   ```

   El directorio `src/main/java` contiene el código fuente de tu proyecto, el directorio `src/test/java` contiene el código fuente de las pruebas y el archivo `pom.xml` es el Modelo de Objetos del Proyecto (POM) de tu proyecto.

   El archivo `pom.xml` es el núcleo de la configuración de un proyecto Maven. Es un único archivo de configuración que contiene la mayor parte de la información necesaria para compilar un proyecto personalizado. El POM es enorme y puede parecer desalentador. Afortunadamente, aún no necesitas comprender todos sus detalles para usarlo de manera efectiva.

4. Crea un servicio web RESTful que muestre "Hello World!".

   Bajo el directorio `src/main/java/com/example/spring_boot_docker/`, puedes modificar tu archivo `SpringBootDockerApplication.java` con el siguiente contenido:

   ```java
   package com.example.spring_boot_docker;

   import org.springframework.boot.SpringApplication;
   import org.springframework.boot.autoconfigure.SpringBootApplication;
   import org.springframework.web.bind.annotation.RequestMapping;
   import org.springframework.web.bind.annotation.RestController;


   @RestController
   @SpringBootApplication
   public class SpringBootDockerApplication {

       @RequestMapping("/")
           public String home() {
           return "Hello World";
       }

   	public static void main(String[] args) {
   		SpringApplication.run(SpringBootDockerApplication.class, args);
   	}

   }
   ```

   El archivo `SpringBootDockerApplication.java` comienza declarando tu paquete `com.example.spring_boot_docker` e importando los frameworks de Spring necesarios. Este archivo Java crea una aplicación web Spring Boot simple que responde con "Hello World" cuando un usuario visita su página de inicio.

### Crear el Dockerfile

Ahora que tienes el proyecto, estás listo para crear el `Dockerfile`.

1.  Crea un archivo llamado `Dockerfile` en la misma carpeta que contiene todas las demás carpetas y archivos (como src, pom.xml, etc.).

2.  En el `Dockerfile`, define tu imagen base añadiendo la siguiente línea:

    ```dockerfile
    FROM eclipse-temurin:21.0.8_9-jdk-jammy
    ```

3.  Ahora, define el directorio de trabajo utilizando la instrucción `WORKDIR`. Esto especificará dónde se ejecutarán los comandos futuros y la ruta donde se copiarán los archivos dentro de la imagen del contenedor.

    ```dockerfile
    WORKDIR /app
    ```

4.  Copia tanto el script wrapper de Maven como el archivo `pom.xml` de tu proyecto en el directorio de trabajo actual `/app` dentro del contenedor Docker.

    ```dockerfile
    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
    ```

5.  Ejecuta un comando dentro del contenedor. Ejecuta el comando `./mvnw dependency:go-offline`, el cual utiliza el wrapper de Maven (`./mvnw`) para descargar todas las dependencias de tu proyecto sin compilar el archivo JAR final (útil para acelerar las compilaciones).

    ```dockerfile
    RUN ./mvnw dependency:go-offline
    ```

6.  Copia el directorio `src` de tu proyecto en la máquina host al directorio `/app` dentro del contenedor.

    ```dockerfile
    COPY src ./src
    ```

7.  Establece el comando predeterminado que se ejecutará cuando se inicie el contenedor. Este comando indica al contenedor que ejecute el wrapper de Maven (`./mvnw`) con el objetivo `spring-boot:run`, el cual compilará y ejecutará tu aplicación Spring Boot.

    ```dockerfile
    CMD ["./mvnw", "spring-boot:run"]
    ```

    Y con eso, deberías tener el siguiente Dockerfile:

    ```dockerfile
    FROM eclipse-temurin:21.0.8_9-jdk-jammy
    WORKDIR /app
    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
    RUN ./mvnw dependency:go-offline
    COPY src ./src
    CMD ["./mvnw", "spring-boot:run"]
    ```

### Compilar la imagen del contenedor

1.  Ejecuta el siguiente comando para compilar la imagen de Docker:

    ```console
    $ docker build -t spring-helloworld .
    ```

2.  Verifica el tamaño de la imagen de Docker utilizando el comando `docker images`:

    ```console
    $ docker images
    ```

    Al hacerlo, se producirá una salida como la siguiente:

    ```console
    REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
    spring-helloworld   latest    ff708d5ee194   3 minutes ago    880MB
    ```

    Esta salida muestra que tu imagen tiene un tamaño de 880 MB. Contiene el JDK completo, la cadena de herramientas de Maven y más. En producción, no necesitas eso en tu imagen final.

### Ejecutar la aplicación Spring Boot

1. Ahora que tienes una imagen compilada, es hora de ejecutar el contenedor.

   ```console
   $ docker run -p 8080:8080 spring-helloworld
   ```

   A continuación, verás una salida similar a la siguiente en el registro del contenedor:

   ```plaintext
   [INFO] --- spring-boot:3.3.4:run (default-cli) @ spring-boot-docker ---
   [INFO] Attaching agents: []

        .   ____          _            __ _ _
       /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
      ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
       \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
        '  |____| .__|_| |_|_| |_\__, | / / / /
       =========|_|==============|___/=/_/_/_/

       :: Spring Boot ::                (v3.3.4)

   2024-09-29T23:54:07.157Z  INFO 159 --- [spring-boot-docker] [           main]
   c.e.s.SpringBootDockerApplication        : Starting SpringBootDockerApplication using Java
   21.0.2 with PID 159 (/app/target/classes started by root in /app)
    ….
   ```

2. Accede a tu página "Hello World" a través de tu navegador web en [http://localhost:8080](http://localhost:8080), o mediante este comando curl:

   ```console
   $ curl localhost:8080
   Hello World
   ```

### Utilizar compilaciones multietapa

1. Considera el siguiente Dockerfile:

   ```dockerfile
   FROM eclipse-temurin:21.0.8_9-jdk-jammy AS builder
   WORKDIR /opt/app
   COPY .mvn/ .mvn
   COPY mvnw pom.xml ./
   RUN ./mvnw dependency:go-offline
   COPY ./src ./src
   RUN ./mvnw clean install

   FROM eclipse-temurin:21.0.8_9-jre-jammy AS final
   WORKDIR /opt/app
   EXPOSE 8080
   COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
   ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
   ```

   Ten en cuenta que este Dockerfile se ha dividido en dos etapas.
   - La primera etapa sigue siendo la misma que en el Dockerfile anterior, proporcionando un entorno Java Development Kit (JDK) para compilar la aplicación. A esta etapa se le da el nombre de `builder`.

   - La segunda etapa es una nueva etapa llamada `final`. Utiliza una imagen `eclipse-temurin:21.0.8_9-jre-jammy` más ligera, que contiene solo el Java Runtime Environment (JRE) necesario para ejecutar la aplicación. Esta imagen proporciona un Java Runtime Environment (JRE), que es suficiente para ejecutar la aplicación compilada (archivo JAR).

   > Para uso en producción, se recomienda encarecidamente que produzcas un entorno de ejecución personalizado tipo JRE utilizando jlink. Las imágenes JRE están disponibles para todas las versiones de Eclipse Temurin, pero `jlink` te permite crear un entorno de ejecución mínimo que contiene solo los módulos de Java necesarios para tu aplicación. Esto puede reducir significativamente el tamaño y mejorar la seguridad de tu imagen final. [Consulta esta página](https://hub.docker.com/_/eclipse-temurin) para obtener más información.

   Con las compilaciones multietapa, una compilación de Docker utiliza una imagen base para la compilación, el empaquetado y las pruebas unitarias, y luego una imagen separada para el tiempo de ejecución de la aplicación. Como resultado, la imagen final es más pequeña ya que no contiene herramientas de desarrollo o depuración. Al separar el entorno de compilación del entorno de ejecución final, puedes reducir significativamente el tamaño de la imagen y aumentar la seguridad de tus imágenes finales.

2. Ahora, vuelve a compilar tu imagen y ejecuta tu compilación de producción lista para usar.

   ```console
   $ docker build -t spring-helloworld-builder .
   ```

   Este comando compila una imagen de Docker llamada `spring-helloworld-builder` utilizando la etapa final de tu archivo `Dockerfile` ubicado en el directorio actual.

   > [!NOTE]
   >
   > En tu Dockerfile multietapa, la etapa final (`final`) es el objetivo predeterminado para la compilación. Esto significa que si no especificas explícitamente una etapa objetivo utilizando la bandera `--target` en el comando `docker build`, Docker compilará automáticamente la última etapa de forma predeterminada. Podrías usar `docker build -t spring-helloworld-builder --target builder .` para compilar solo la etapa `builder` con el entorno JDK.

3. Observa la diferencia en el tamaño de la imagen utilizando el comando `docker images`:

   ```console
   $ docker images
   ```

   Obtendrás una salida similar a la siguiente:

   ```console
   spring-helloworld-builder latest    c5c76cb815c0   24 minutes ago      428MB
   spring-helloworld         latest    ff708d5ee194   About an hour ago   880MB
   ```

   Tu imagen final es de solo 428 MB, en comparación con el tamaño de compilación original de 880 MB.

   Al optimizar cada etapa e incluir solo lo necesario, pudiste reducir significativamente el tamaño total de la imagen sin perder funcionalidad. Esto no solo mejora el rendimiento, sino que también hace que tus imágenes de Docker sean más ligeras, más seguras y más fáciles de gestionar.

## Recursos adicionales

- [Compilaciones multietapa](/build/building/multi-stage/)
- [Mejores prácticas de Dockerfile](/develop/develop-images/dockerfile_best-practices/)
- [Imágenes base](/build/building/base-images/)
- [Spring Boot Docker](https://spring.io/guides/topicals/spring-boot-docker)

