Docker y Java: contenerización avanzada de aplicaciones
La contenerización de aplicaciones Java con Docker se ha convertido en un estándar de la industria. Sin embargo, los Dockerfile básicos y los comandos simples docker run son solo la punta del iceberg. A nivel avanzado, los desarrolladores se enfrentan a tareas como optimizar el tamaño de las imágenes, ajustar finamente la JVM dentro del contenedor, organizar compilaciones de múltiples etapas y orquestar microservicios. En este artículo, analizaremos las técnicas clave que le ayudarán a llevar la contenerización de Java a un nivel profesional.
1. Compilaciones de múltiples etapas: imagen mínima para producción
Uno de los principales problemas al contenerizar Java es el tamaño de la imagen final. La imagen base openjdk:17-jdk pesa alrededor de 400 MB, lo cual es excesivo para el runtime. La solución es la compilación de múltiples etapas (multi-stage build).
La primera etapa (build) utiliza el JDK completo y las herramientas de compilación (Maven/Gradle). La segunda etapa (runtime) utiliza una imagen JRE ligera y copia solo el JAR/WAR compilado.
# Etapa 1: Compilación de la aplicaciónFROM maven:3.8.5-openjdk-17 AS builderWORKDIR /appCOPY pom.xml .COPY src ./srcRUN mvn clean package -DskipTests
# Etapa 2: Imagen runtime mínimaFROM openjdk:17-jre-slimWORKDIR /app# Copiamos solo el artefacto compiladoCOPY --from=builder /app/target/myapp-*.jar app.jarEXPOSE 8080CMD ["java", "-jar", "app.jar"]Ventajas:
- La imagen final pesa ~180 MB en lugar de 400+ MB
- Se reduce la superficie de ataque (sin compilador, sin utilidades innecesarias)
- Se acelera el tiempo de despliegue y descarga de imágenes
2. Configuración de la JVM para trabajar en contenedor
Durante mucho tiempo, la JVM se adaptó mal a las limitaciones de los contenedores (cgroups). A partir de Java 10, aparecieron flags para indicar explícitamente los recursos, y en Java 17+ el comportamiento se volvió más predecible. Sin embargo, a nivel avanzado es importante controlar la memoria y la CPU manualmente.
Parámetros clave de JVM para contenedor:
-XX:MaxRAMPercentage=75.0— asigna el 75% de la memoria del contenedor al heap (en lugar de -Xmx fijo)-XX:+UseContainerSupport— activa el conocimiento de las limitaciones del contenedor (activado por defecto desde Java 10+)-XX:ActiveProcessorCount=2— indica explícitamente el número de procesadores disponibles-XX:+ExitOnOutOfMemoryError— finaliza el proceso abruptamente ante OutOfMemoryError (para reinicio rápido del contenedor)
# Ejemplo de Dockerfile con flags JVM optimizadosFROM eclipse-temurin:17-jre-alpineWORKDIR /appCOPY target/app.jar app.jar
# Configuración de JVM para contenedorENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 \ -XX:+UseContainerSupport \ -XX:+ExitOnOutOfMemoryError \ -XX:+PrintGCDetails"
EXPOSE 8080CMD ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]Importante: Utilice eclipse-temurin o amazon-corretto — son imágenes oficiales, bien mantenidas y con una política de actualizaciones transparente.
3. Docker Compose para arquitectura de microservicios
En proyectos reales, una aplicación Java rara vez existe de forma aislada. Normalmente es una combinación de: backend Spring Boot + PostgreSQL + Redis + Kafka. Docker Compose permite describir toda la infraestructura en un único archivo YAML.
version: '3.8'
services: postgres: image: postgres:15-alpine environment: POSTGRES_DB: myapp POSTGRES_USER: user POSTGRES_PASSWORD: secret volumes: - pgdata:/var/lib/postgresql/data networks: - backend
redis: image: redis:7-alpine ports: - "6379:6379" networks: - backend
app: build: context: . dockerfile: Dockerfile ports: - "8080:8080" environment: SPRING_DATASOURCE_URL: jdbc:postgresql: