80% dos Dockerfiles de Spring Boot em produção estão errados

80% dos Dockerfiles de Spring Boot em produção estão errados

80% dos Dockerfiles de Spring Boot que vejo em produção estão errados.

E o resultado é sempre o mesmo: imagem de 900MB, build de 10 minutos e deploy que dá medo de rodar.

O problema começa quando o time copia um Dockerfile básico da internet e nunca mais questiona. Funciona. Mas funciona mal.

O que aplico em todo projeto Java com Spring Boot:


1. Multi-stage build

Stage 1: JDK completo pra compilar e gerar o .jar com Maven ou Gradle. Stage 2: só o JRE (ou Distroless) pra rodar.

# Stage 1: build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests -B

# Stage 2: runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar

A imagem final não precisa do compilador, do Maven wrapper, dos sources, dos testes. Nada disso vai pra produção.


2. eclipse-temurin:21-jre-alpine como base final

Não o JDK. O JRE. Diferença de ~300MB numa imagem.

Se quiser ir mais longe: gcr.io/distroless/java21. Sem shell, sem package manager, sem superfície de ataque.

# Opção mais enxuta e segura
FROM gcr.io/distroless/java21
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

3. Camadas de cache com inteligência

No Maven, copia o pom.xml primeiro, roda mvn dependency:go-offline, depois copia o código. As dependências ficam em cache.

Muda só o código? O build ignora a resolução de deps inteira. Build de 8 minutos vira 90 segundos.

WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B   # <-- camada cacheada

COPY src ./src
RUN mvn package -DskipTests -B     # <-- só reexecuta se o src mudar

O Docker invalida o cache camada por camada, de cima pra baixo. A ordem do COPY importa.


4. Usuário non-root

RUN addgroup --system spring \
 && adduser --system spring --ingroup spring
USER spring

Container comprometido, atacante sem root. Detalhe simples, impacto enorme em qualquer análise de compliance ou pentest.


5. .dockerignore configurado

Sem isso o build context manda a pasta target inteira pro daemon antes de começar — às vezes centenas de MB que nunca serão usados.

target/
.git
.mvn
*.md
coverage/
.env
**/*.log

6. JVM otimizada pro container

A JVM não sabe que está dentro de um container por padrão. -XX:+UseContainerSupport já vem habilitado no Java 11+, mas vale checar se está ativo.

O mais importante: troque -Xmx fixo por percentual do limite do container.

ENV JAVA_OPTS="-XX:+UseContainerSupport \
               -XX:MaxRAMPercentage=75.0 \
               -XX:InitialRAMPercentage=50.0"

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

Com MaxRAMPercentage, a JVM respeita o --memory do container em vez de tentar usar toda a RAM do host. Isso evita OOMKill silencioso em ambientes de orquestração.


7. Health check

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

O orchestrator (Kubernetes, ECS, Swarm) sabe se a app está viva de verdade, não só se o processo subiu. Sem isso, tráfego pode ser roteado pra um container que subiu mas ainda não terminou de inicializar.

O --start-period=60s é importante pro Spring Boot — a JVM e o contexto do Spring levam tempo. Sem esse parâmetro o health check falha nos primeiros segundos e o container é reiniciado em loop.


Dockerfile completo

# Stage 1: build
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /app

COPY pom.xml .
RUN mvn dependency:go-offline -B

COPY src ./src
RUN mvn package -DskipTests -B

# Stage 2: runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

RUN addgroup --system spring \
 && adduser --system spring --ingroup spring

COPY --from=builder /app/target/*.jar app.jar

USER spring

ENV JAVA_OPTS="-XX:+UseContainerSupport \
               -XX:MaxRAMPercentage=75.0 \
               -XX:InitialRAMPercentage=50.0"

HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
  CMD curl -f http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]

O resultado

MétricaDockerfile básicoCom as otimizações
Tamanho da imagem~800–900MB~150–200MB
Build sem cache~10 min~4–5 min
Build com cache (só código mudou)~8 min~90 seg
Usuário rootSimNão
JVM respeitando limitesNãoSim

Dockerfile de Spring Boot não é boilerplate. É parte da engenharia.