Docker para Desarrolladores Spring Boot: Guía Completa

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.