Compartir a través de


Introducción a los contenedores para aplicaciones Java

Los contenedores proporcionan un entorno coherente y portátil para sus aplicaciones Java en las etapas de desarrollo, prueba y producción. En este artículo se presentan los conceptos de contenedorización para aplicaciones Java y se guía a través de la creación, depuración, optimización e implementación de aplicaciones Java en contenedores en Azure Container Apps.

En este artículo, aprenderá conceptos esenciales de contenedorización para desarrolladores de Java y las siguientes habilidades:

  • Configuración del entorno de desarrollo para aplicaciones Java en contenedores.
  • Creación de Dockerfiles optimizados para cargas de trabajo de Java.
  • Configuración de flujos de trabajo de desarrollo local con contenedores.
  • Depuración de aplicaciones Java en contenedores.
  • Optimización de contenedores Java para producción.
  • Implementación de las aplicaciones Java en contenedores en Azure Container Apps.

Al incluir sus aplicaciones Java en contenedores, obtiene entornos coherentes, una implementación simplificada, una utilización eficiente de los recursos y una escalabilidad mejorada.

Contenedores para aplicaciones Java

Los contenedores empaquetan las aplicaciones con sus dependencias, lo que garantiza la coherencia en todos los entornos. Para los desarrolladores de Java, esto significa agrupar la aplicación, sus dependencias, Java Runtime Environment/Java Development Kit (JRE/JDK) y los archivos de configuración en una sola unidad portátil.

La contenedorización tiene ventajas clave sobre la virtualización que la hacen ideal para el desarrollo en la nube. A diferencia de una máquina virtual, un contenedor se ejecuta en el kernel del sistema operativo host de un servidor. Esto es beneficioso para las aplicaciones Java, que ya se ejecutan en la máquina virtual Java (JVM). La inclusión en contenedores de aplicaciones Java agrega una sobrecarga mínima y proporciona importantes beneficios de implementación.

El ecosistema de contenedores incluye los siguientes componentes clave:

  • Imágenes: los planos.
  • Contenedores: instancias en ejecución.
  • Registros: donde se almacenan las imágenes.
  • Orquestadores: sistemas que administran contenedores a escala.

Docker es la plataforma de contenedorización más popular y está bien respaldada en el ecosistema de Azure a través de Azure Container Apps.

Configure tu entorno de desarrollo

Esta sección le guía a través de la instalación de las herramientas necesarias y la configuración del entorno de desarrollo para crear, ejecutar y depurar aplicaciones Java en contenedores.

Instalar las herramientas necesarias

Para crear aplicaciones Java en contenedores, necesita tener instaladas las siguientes herramientas en el equipo de desarrollo:

Compruebe la instalación mediante los siguientes comandos:

docker --version
docker compose version

Configuración de Visual Studio Code para el desarrollo de contenedores

Para el desarrollo de Java en contenedores, configure Visual Studio Code instalando el paquete de extensión de Java y configurando el JDK. La extensión Dev Containers permite abrir cualquier carpeta dentro de un contenedor y usar el conjunto completo de características de Visual Studio Code dentro de ese contenedor.

Para permitir que Visual Studio Code compile y se conecte automáticamente a un contenedor de desarrollo, cree un archivo .devcontainer/devcontainer.json en el proyecto.

Por ejemplo, la siguiente configuración de ejemplo define una compilación de Java:

{
    "name": "Java Development",
    "image": "mcr.microsoft.com/devcontainers/java:21",
    "customizations": {
        "vscode": {
            "extensions": [
                "vscjava.vscode-java-pack",
                "ms-azuretools.vscode-docker"
            ]
        }
    },
    "forwardPorts": [8080, 5005],
    "remoteUser": "vscode"
}

Esta configuración usa la imagen de contenedor de desarrollo Java de Microsoft, agrega extensiones esenciales y reenvía tanto el puerto 8080de aplicación como el puerto de depuración, 5005.

Creación de un archivo Dockerfile

Un Dockerfile contiene instrucciones para crear una imagen de Docker. En el caso de las aplicaciones Java, el Dockerfile suele incluir los siguientes componentes:

  • Una imagen base con el JDK o JRE.
  • Instrucciones para copiar archivos de aplicación.
  • Comandos para establecer variables de entorno.
  • Configuraciones de puntos de entrada.

Seleccionar una imagen base

Elegir la imagen base correcta es crucial. Considere estas opciones:

Descripción Nombre Observaciones
Imagen de desarrollo de Microsoft Java mcr.microsoft.com/java/jdk:21-zulu-ubuntu JDK completo y optimizado para Azure
Imagen de producción de Java de Microsoft mcr.microsoft.com/java/jre:21-zulu-ubuntu Solo en tiempo de ejecución y optimizado para Azure
Imagen oficial de desarrollo de OpenJDK openjdk:21-jdk JDK completo
Imagen oficial de producción de OpenJDK openjdk:21-jre Solo tiempo de ejecución

En el caso de los entornos de desarrollo, utilice una imagen JDK completa. Para producción, use una imagen JRE o sin distribución para minimizar el tamaño y la superficie de ataque de la aplicación.

Las imágenes de Java de Microsoft vienen con optimizaciones específicas de Azure y se actualizan periódicamente con parches de seguridad, lo que las hace ideales para las aplicaciones destinadas a Azure Container Apps.

Ejemplos básicos de Dockerfile

En el ejemplo siguiente se muestra un Dockerfile simple para una aplicación Java:

FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/myapp.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

En el caso de las aplicaciones de Spring Boot, puede configurar el Dockerfile con la siguiente base:

FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-Dspring.profiles.active=docker", "-jar", "app.jar"]

En el caso de las implementaciones de producción, utilice la imagen JRE que se muestra en el ejemplo siguiente para reducir el tamaño y minimizar la superficie expuesta a ataques de la aplicación:

FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENV JAVA_OPTS="-Dserver.port=8080"
ENTRYPOINT ["java", ${JAVA_OPTS}, "-jar", "app.jar"]

Desarrollo local con contenedores

Los contenedores están pensados para ejecutarse en varios contextos. En esta sección, aprenderá un flujo de desarrollo local para su uso con contenedores.

Usa Docker Compose para aplicaciones de varios contenedores

La mayoría de las aplicaciones Java interactúan con bases de datos, cachés u otros servicios. Docker Compose te ayuda a definir y organizar aplicaciones de varios contenedores mediante un archivo de configuración YAML simple.

¿Qué es Docker Compose?

Docker Compose es una herramienta que te permite realizar las siguientes tareas:

  • Defina aplicaciones multicontenedor en un solo archivo.
  • Administre el ciclo de vida de la aplicación, incluido el inicio, la detención y la reconstrucción.
  • Mantener entornos aislados.
  • Crear redes para la comunicación de servicios.
  • Conserve los datos mediante volúmenes.

Ejemplo: Aplicación Java con base de datos

El siguiente archivo compose.yml configura una aplicación Java con una base de datos PostgreSQL:

version: '3.8'
services:
  app:
    build: .                              # Build from Dockerfile in current directory
    ports:
      - "8080:8080"                       # Map HTTP port
      - "5005:5005"                       # Map debug port
    environment:
      - SPRING_PROFILES_ACTIVE=dev
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/myapp
    volumes:
      - ./target:/app/target              # Mount target directory for hot reloads
    depends_on:
      - db                                # Ensure database starts first
  
  db:
    image: postgres:13
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=myapp
    ports:
      - "5432:5432"                       # Expose PostgreSQL port
    volumes:
      - postgres-data:/var/lib/postgresql/data  # Persist database data

volumes:
  postgres-data:                          # Named volume for database persistence

Este archivo tiene las siguientes características:

  • Los servicios pueden hacer referencia entre sí por su nombre, por ejemplo, db en la URL de JDBC.
  • Docker Compose crea automáticamente una red para los servicios.
  • La aplicación Java espera a que se inicie la base de datos, debido a .depends_on
  • Los datos de la base de datos se conservan en los reinicios mediante un volumen con nombre.

Comandos comunes de Docker Compose

Después de crear el archivo compose.yml , administre la aplicación mediante los siguientes comandos:

# Build images without starting containers
docker compose build

# Start all services defined in compose.yml
docker compose up

# Start in detached mode (run in background)
docker compose up -d

# View running containers managed by compose
docker compose ps

# View logs from all containers
docker compose logs

# View logs from a specific service
docker compose logs app

# Stop all services
docker compose down

# Stop and remove volumes (useful for database resets)
docker compose down -v

Flujo de trabajo de desarrollo

Un flujo de trabajo de desarrollo de Java típico con Docker Compose contiene los siguientes pasos:

  1. Cree el archivo compose.yml y el Dockerfile.
  2. Ejecutar docker compose up para iniciar todos los servicios.
  3. Realice cambios en el código Java.
  4. Vuelva a compilar la aplicación. En función de la configuración, es posible que tenga que reiniciar los contenedores.
  5. Pruebe los cambios en el entorno en contenedores.
  6. Cuando haya terminado, ejecute docker compose down.

Ejecución de contenedores individuales con Docker

En escenarios más sencillos en los que no necesite varios servicios interconectados, puede usar el docker run comando para iniciar contenedores individuales.

Los siguientes comandos de Docker son típicos de las aplicaciones Java:

# Run a Java application JAR directly
docker run -p 8080:8080 myapp:latest

# Run with environment variables
docker run -p 8080:8080 -e "SPRING_PROFILES_ACTIVE=prod" myapp:latest

# Run in detached mode (background)
docker run -d -p 8080:8080 myapp:latest

# Run with a name for easy reference
docker run -d -p 8080:8080 --name my-java-app myapp:latest

# Run with volume mount for persistent data
docker run -p 8080:8080 -v ./data:/app/data myapp:latest

Depuración de aplicaciones en contenedores

La depuración de aplicaciones Java en contenedores a veces es un desafío porque el código se ejecuta en un entorno aislado dentro del contenedor.

Los enfoques de depuración estándar no siempre se aplican directamente, pero con la configuración adecuada, puede establecer una conexión de depuración remota con la aplicación. En esta sección se muestra cómo configurar los contenedores para la depuración, conectar las herramientas de desarrollo a los contenedores en ejecución y solucionar problemas comunes relacionados con los contenedores.

Configurar la depuración remota

La depuración de aplicaciones Java en contenedores requiere exponer un puerto de depuración y configurar el IDE para conectarse a él. Puede realizar estas tareas mediante los siguientes pasos:

  1. Para habilitar la depuración, modifique el Dockerfile para que contenga el siguiente contenido:

    Nota:

    En su lugar, puede modificar el comando de inicio del contenedor.

    FROM mcr.microsoft.com/java/jdk:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    EXPOSE 8080 5005
    ENTRYPOINT ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"]
    
  2. Configure el archivo launch.json de Visual Studio Code para conectarse al puerto de depuración, como se muestra en el ejemplo siguiente:

    {
      "version": "0.2.0",
      "configurations": [
        {
          "type": "java",
          "name": "Debug in Container",
          "request": "attach",
          "hostName": "localhost",
          "port": 5005
        }
      ]
    }
    
  3. Inicie el contenedor con el puerto 5005 asignado al host y, a continuación, inicie el depurador en Visual Studio Code.

Solución de problemas de contenedores

Cuando los contenedores no se comportan como se esperaba, puedes inspeccionar los registros de tu app para investigar el problema.

Utilice los siguientes comandos para solucionar problemas de la aplicación. Antes de ejecutar estos comandos, asegúrese de reemplazar los marcadores de posición (<...>) por sus propios valores.

# View logs
docker logs <CONTAINER_ID>

# Follow logs in real-time
docker logs -f <CONTAINER_ID>

# Inspect container details
docker inspect <CONTAINER_ID>

# Get a shell in the container
docker exec -it <CONTAINER_ID> bash

Para problemas específicos de Java, habilite los indicadores de JVM para un mejor diagnóstico, como se muestra en el siguiente ejemplo:

ENTRYPOINT ["java", "-XX:+PrintFlagsFinal", "-XX:+PrintGCDetails", "-jar", "app.jar"]

En la siguiente tabla se enumeran los problemas comunes y las soluciones correspondientes:

Error Posible solución
No hay memoria suficiente Aumentar los límites de memoria del contenedor
Tiempos de espera de conexión Compruebe si hay errores en la configuración de la red. Verifique los puertos y las reglas de enrutamiento.
Problemas de permisos Verifique los permisos del sistema de archivos.
Problemas de Classpath Compruebe la estructura y las dependencias de JAR.

Optimización de contenedores Java

Las aplicaciones Java en contenedores requieren una consideración especial para obtener un rendimiento óptimo. La JVM se diseñó antes de que los contenedores fueran comunes. El uso de contenedores puede provocar problemas de asignación de recursos si no se configuran correctamente.

Puede mejorar significativamente el rendimiento y la eficiencia de las aplicaciones Java en contenedores ajustando la configuración de la memoria, optimizando el tamaño de la imagen y configurando la recolección de elementos no utilizados. En esta sección se tratan las optimizaciones esenciales para los contenedores Java, centrándose en la gestión de la memoria, el tiempo de inicio y la utilización de recursos.

Configuración de memoria JVM en contenedores

La JVM no detecta automáticamente los límites de memoria del contenedor en Java 8. Para Java 9+, el reconocimiento de contenedores está habilitado de forma predeterminada.

Configure la JVM para que respete los límites del contenedor, como se muestra en el ejemplo siguiente:

FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
WORKDIR /app
COPY target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]

Los siguientes indicadores de JVM son importantes para las aplicaciones en contenedores:

  • -XX:MaxRAMPercentage=75.0. Establece el montón máximo como un porcentaje de la memoria disponible.
  • -XX:InitialRAMPercentage=50.0. Establece el tamaño inicial del montón.
  • -Xmx y -Xms. Estas marcas también están disponibles, pero requieren valores fijos.

Preparación para la implementación en producción

Trasladar las aplicaciones Java en contenedores a producción requiere consideraciones que van más allá de la funcionalidad básica.

Los entornos de producción exigen una seguridad sólida, una supervisión fiable, una asignación de recursos adecuada y flexibilidad de configuración.

En esta sección se tratan las prácticas y configuraciones esenciales necesarias para preparar los contenedores Java para su uso en producción. La sección se centra en la seguridad, las comprobaciones de estado y la administración de la configuración, para garantizar que las aplicaciones se ejecuten de forma fiable en producción.

Procedimientos recomendados de seguridad

Proteja sus aplicaciones Java en contenedores mediante las siguientes prácticas:

  • Contexto de seguridad predeterminado. Ejecute las aplicaciones como un usuario no root, como se muestra en el siguiente ejemplo:

    FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    RUN addgroup --system javauser && adduser --system --ingroup javauser javauser
    USER javauser
    ENTRYPOINT ["java", "-jar", "app.jar"]
    
  • Busque problemas de forma proactiva. Analice periódicamente las imágenes de contenedor en busca de vulnerabilidades mediante el siguiente comando:

    docker scan myapp:latest
    
  • Frescura de la imagen base. Mantén tus imágenes base actualizadas.

  • Administración de secretos. Implementar una gestión adecuada de secretos. Por ejemplo, no codifique datos confidenciales en la aplicación y use un almacén de claves siempre que sea posible.

  • Contextos de seguridad restringidos. Aplique el principio de privilegios mínimos a todos los contextos de seguridad.

  • Acceso al sistema de archivos. Utilice sistemas de archivos de solo lectura siempre que sea posible.

Comprobaciones de estado y supervisión

Compruebe el estado de la aplicación con sondeos para asegurarse de que la aplicación se ejecuta correctamente.

En el caso de las aplicaciones de Spring Boot, incluya la dependencia Actuator para puntos de conexión de estado completos, como se muestra en el ejemplo siguiente:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

Configure la aplicación para generar registros en un formato adecuado para entornos de contenedores, como JSON.

Desplegar en Azure Container Apps

En esta sección se explica cómo preparar los contenedores de Java para la implementación de Azure Container Apps y se destacan las consideraciones clave sobre la configuración.

Preparación del contenedor para Azure

  • Configuración de puertos. Asegúrese de que el contenedor escucha en el puerto proporcionado por Azure, como se muestra en el ejemplo siguiente:

    FROM mcr.microsoft.com/java/jre:21-zulu-ubuntu
    WORKDIR /app
    COPY target/*.jar app.jar
    ENV PORT=8080
    EXPOSE ${PORT}
    CMD java -jar app.jar --server.port=${PORT}
    
  • Sonda de salud. Implemente sondeos de estado para las comprobaciones de actividad y preparación de Azure.

  • Configuración de registros. Configure el registro para que se genere en stdout/stderr.

  • Planifica para lo inesperado. Configure el manejo adecuado del apagado correcto con la configuración de tiempo de espera. Para más información, consulte Administración del ciclo de vida de las aplicaciones en Azure Container Apps.