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étrica | Dockerfile básico | Com 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 root | Sim | Não |
| JVM respeitando limites | Não | Sim |
Dockerfile de Spring Boot não é boilerplate. É parte da engenharia.