Containerizar una aplicación Spring Boot parece sencillo al principio, pero hacerlo bien —imagen ligera, arranque rápido, lista para Kubernetes— requiere varias decisiones técnicas que este artículo cubre de forma práctica.
Por qué Docker en proyectos Java
Java tiene fama de producir imágenes Docker grandes y lentas. La razón suele ser el mismo error: copiar el JAT completo sobre una imagen JDK de 600 MB sin más.
Con las técnicas que verás aquí, puedes pasar de 650 MB → menos de 100 MB y reducir el tiempo de arranque a menos de 3 segundos en producción.
Dockerfile con multi-stage build
La clave es separar la fase de compilación (necesitas el JDK completo) de la fase de ejecución (solo necesitas el JRE).
# ── Fase 1: compilación ──────────────────────────────────────
FROM eclipse-temurin:21-jdk-alpine AS build
WORKDIR /app
# Copia solo los archivos de dependencias primero (aprovecha la caché de Docker)
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon
# Ahora copia el código fuente y compila
COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test
# ── Fase 2: imagen de producción ─────────────────────────────
FROM eclipse-temurin:21-jre-alpine AS production
# Usuario sin privilegios — nunca uses root en producción
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
WORKDIR /app
# Copia solo el JAR final
COPY --from=build /app/build/libs/*.jar app.jar
# Health check para Docker Swarm / Kubernetes
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
EXPOSE 8080
# Flags JVM optimizadas para contenedores
ENTRYPOINT ["java", \
"-XX:+UseContainerSupport", \
"-XX:MaxRAMPercentage=75.0", \
"-XX:+ExitOnOutOfMemoryError", \
"-Djava.security.egd=file:/dev/./urandom", \
"-jar", "app.jar"]
El truco del .dockerignore
Sin este archivo, Docker copia build/, .git y node_modules al contexto, ralentizando cada build:
# .dockerignore
.git
.gradle
build/
out/
*.md
.env
.env.*
Docker Compose para desarrollo local
En local necesitas Spring Boot + PostgreSQL + Redis sin instalar nada más en tu máquina:
# docker-compose.yml
services:
app:
build: .
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/mydb
SPRING_DATASOURCE_USERNAME: dev
SPRING_DATASOURCE_PASSWORD: dev
SPRING_DATA_REDIS_HOST: redis
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
volumes:
# Hot reload en desarrollo (con Spring DevTools)
- ./build/libs:/app/libs
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: mydb
POSTGRES_USER: dev
POSTGRES_PASSWORD: dev
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev -d mydb"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7-alpine
ports:
- "6379:6379"
command: redis-server --maxmemory 256mb --maxmemory-policy allkeys-lru
volumes:
postgres_data:
Con esto, docker compose up levanta todo el stack en segundos.
Perfiles Spring Boot por entorno
Usa variables de entorno para distinguir configuración entre entornos sin cambiar el código:
# application.yml
spring:
config:
activate:
on-profile: "!test"
datasource:
url: ${SPRING_DATASOURCE_URL:jdbc:postgresql://localhost:5432/mydb}
username: ${SPRING_DATASOURCE_USERNAME:dev}
password: ${SPRING_DATASOURCE_PASSWORD:dev}
hikari:
maximum-pool-size: ${DB_POOL_SIZE:10}
minimum-idle: 2
management:
endpoints:
web:
exposure:
include: health,info,metrics
endpoint:
health:
show-details: when-authorized
Optimización del tiempo de arranque con Layered JARs
Spring Boot 2.3+ soporta JARs por capas, lo que acelera la reconstrucción de imágenes cuando solo cambias tu código (sin tocar dependencias):
FROM eclipse-temurin:21-jre-alpine AS production
WORKDIR /app
ARG JAR_FILE=build/libs/*.jar
COPY ${JAR_FILE} app.jar
# Extraer capas del JAR
RUN java -Djarmode=layertools -jar app.jar extract
# Orden: las capas que cambian menos van primero (mejor caché)
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=production /app/dependencies/ ./
COPY --from=production /app/spring-boot-loader/ ./
COPY --from=production /app/snapshot-dependencies/ ./
COPY --from=production /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Con esta estrategia, si solo cambias código de negocio, Docker solo reconstruye la última capa (application/) en segundos en lugar de minutos.
Preparando para Kubernetes
Cuando tu contenedor corre en Kubernetes, añade estas configuraciones en application.yml:
management:
endpoint:
health:
probes:
enabled: true # Activa /actuator/health/liveness y /actuator/health/readiness
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
Y en tu Deployment de Kubernetes:
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
Conclusión
Una imagen Docker bien construida para Spring Boot combina:
- Multi-stage build para imágenes pequeñas
- Layered JARs para builds rápidos en CI/CD
- Flags JVM correctas para entornos containerizados
- Health checks para orquestadores como Kubernetes
- Usuario sin privilegios por seguridad
Con estas prácticas, tu aplicación Spring Boot estará lista para cualquier entorno de producción moderno.