Colocar uma aplicação Java 25 no OpenShift não deveria depender de descobrir, depois do deploy, qual JDK veio em um ImageStream chamado apenas de java. O caminho mais previsível é escolher explicitamente a imagem, fixar sua versão, separar a etapa de preparação do artefato da imagem de runtime e declarar probes que respondam à pergunta certa.
A base usada aqui é OpenShift Container Platform 4.22, Spring Boot 4.1.1 e Java 25 LTS. A matriz do Spring Boot 4.1.1 exige Java 17 ou superior e declara compatibilidade até Java 26, portanto Java 25 está dentro do conjunto suportado. Para o sistema operacional e o JDK, usaremos as imagens GA ubi9/openjdk-25 e ubi9/openjdk-25-runtime do catálogo da Red Hat.
O resultado será um Containerfile com tag fixa, execução sem root e um Deployment com startupProbe, livenessProbe e readinessProbe adequado ao padrão restricted-v3 de instalações novas do OpenShift 4.20 em diante. Os comandos de verificação abaixo são para o ambiente de build e para o cluster que realmente receberá a imagem.
Não confunda imagem disponível com imagem selecionada
A imagem ubi8/openjdk-17 continua existindo como builder S2I. Isso não prova que ela seja “o Java padrão” do OpenShift 4.22, nem há base para afirmar que todo template Java de um cluster aponta para ela. O catálogo e os ImageStreams instalados podem variar, especialmente em clusters atualizados ao longo dos anos.
A correção é simples: não dependa de um nome genérico quando a versão do JDK faz parte do contrato da aplicação. Referencie registry.access.redhat.com/ubi9/openjdk-25 no Containerfile ou configure esse endereço de forma explícita no BuildConfig. A própria ubi9/openjdk-25 é uma imagem S2I, com scripts em /usr/local/s2i, diretório de deployments e usuário numérico 185.
Para observar o que está sendo usado, consulte o manifesto construído e o digest efetivamente implantado, em vez de inferir pelo nome amigável:
oc get deployment demo -o jsonpath='{.spec.template.spec.containers[0].image}{"\n"}'
oc get pod -l app=demo -o jsonpath='{range .items[*]}{.metadata.name}{" "}{.status.containerStatuses[0].imageID}{"\n"}{end}'
Tags flutuantes são convenientes para experimentar e péssimas para explicar por que dois builds feitos em dias diferentes não são iguais. O computador, com sua habitual falta de espírito colaborativo, apenas fez o que foi pedido.
O contrato das imagens UBI 9 com OpenJDK 25
A imagem builder ubi9/openjdk-25 traz o JDK e as ferramentas necessárias à preparação da aplicação. Nela, JAVA_HOME aponta para /usr/lib/jvm/java-25, JAVA_VERSION=25 já vem definido e o usuário padrão é o UID 185. A variável JAVA_VERSION é informativa: redefini-la não baixa, troca nem instala outro JDK.
A imagem ubi9/openjdk-25-runtime é menor e destinada à execução. Ela contém o runtime, não Maven nem o conjunto completo de ferramentas do JDK; seu JAVA_HOME aponta para o JRE fornecido pela imagem. Use a builder para extrair as camadas do jar e a runtime para iniciar a aplicação.
O catálogo publica latest, 1.24 e tags carimbadas da linha 1.24. Para produção, fixe ao menos 1.24; para reprodutibilidade mais rígida, prefira uma tag carimbada ou digest revisado no momento da promoção. Os exemplos abaixo usam 1.24 para permanecerem legíveis sem deixar a base completamente solta.
Containerfile em duas etapas, sem root
Primeiro gere o jar com o toolchain compatível com Spring Boot 4.1.1. O exemplo supõe que o build produza target/demo.jar:
./mvnw clean package
Depois use este Containerfile:
# syntax=docker/dockerfile:1
FROM registry.access.redhat.com/ubi9/openjdk-25:1.24 AS builder
USER 185
WORKDIR /home/default
COPY --chown=185:0 target/demo.jar /tmp/application.jar
RUN java -Djarmode=tools -jar /tmp/application.jar \
extract --layers --destination /tmp/extracted
FROM registry.access.redhat.com/ubi9/openjdk-25-runtime:1.24
USER 185
WORKDIR /deployments
COPY --from=builder --chown=185:0 /tmp/extracted/dependencies/ ./
COPY --from=builder --chown=185:0 /tmp/extracted/spring-boot-loader/ ./
COPY --from=builder --chown=185:0 /tmp/extracted/snapshot-dependencies/ ./
COPY --from=builder --chown=185:0 /tmp/extracted/application/ ./
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/deployments/application.jar"]
Não há USER root em nenhuma etapa. O UID 185 é numérico, e os arquivos copiados pertencem a 185:0. Isso respeita o contrato da imagem e evita o clássico conserto de permissão que concede privilégios maiores do que o problema pedia.
O OpenShift pode executar o contêiner com um UID arbitrário permitido pela SCC. Por isso, qualquer diretório em que a aplicação escreva precisa aceitar escrita pelo grupo 0, seguindo a orientação da Red Hat para criação de imagens. As imagens OpenJDK já preparam seus diretórios de trabalho para esse modelo. Se sua aplicação criar outro diretório, ajuste-o durante o build com grupo 0 e permissões equivalentes, sem recorrer a root em runtime.
Há ainda uma diferença importante nas opções da JVM fornecidas pelas imagens Red Hat: JAVA_OPTS substitui as opções geradas pelo script da imagem, enquanto JAVA_OPTS_APPEND acrescenta opções ao conjunto calculado. Se você optar pelo launcher padrão da imagem, prefira JAVA_OPTS_APPEND para ajustes adicionais. O ENTRYPOINT direto acima não passa por esse launcher; nesse caso, use uma opção padrão da JVM, como JAVA_TOOL_OPTIONS, ou declare os argumentos explicitamente. Misturar os dois modelos costuma produzir uma investigação interessante e pouco produtiva.
Verifique a imagem localmente antes de promovê-la
Rode os comandos no ambiente de build, com Podman. Troque podman por docker se esse for o engine disponível:
podman pull registry.access.redhat.com/ubi9/openjdk-25:1.24
podman run --rm --entrypoint java \
registry.access.redhat.com/ubi9/openjdk-25:1.24 -version
podman inspect --format '{{.Config.User}}' \
registry.access.redhat.com/ubi9/openjdk-25:1.24
podman build -t demo-java25:local -f Containerfile .
podman run --rm --entrypoint java demo-java25:local -version
podman inspect --format '{{.Config.User}}' demo-java25:local
A observação esperada é Java 25 e usuário configurado como 185 na imagem-base e na imagem final. Não copie uma string exata de patch do JDK para uma documentação permanente: confirme o valor da tag que será promovida. Se o pull em registry.redhat.io retornar 401, autentique-se com uma conta de serviço; as imagens UBI em registry.access.redhat.com são o caminho sem autenticação usado aqui.
Probes do Spring Boot que medem coisas diferentes
Com Actuator no classpath, o Spring Boot 4.1.1 disponibiliza os grupos Kubernetes em /actuator/health/liveness e /actuator/health/readiness. Liveness deve responder se o processo consegue continuar, não se banco, broker e todos os vizinhos estão felizes. Apontá-la para /actuator/health pode reiniciar uma JVM saudável durante uma indisponibilidade externa e transformar um problema em dois.
Se quiser aprofundar essa separação, o post sobre health, liveness e readiness em produção detalha o papel de cada sinal.
O manifesto abaixo inclui uma startupProbe para impedir que liveness e readiness atuem enquanto a JVM ainda inicializa:
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo
spec:
replicas: 1
selector:
matchLabels:
app: demo
template:
metadata:
labels:
app: demo
spec:
hostUsers: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: image-registry.example/demo:1.24
ports:
- name: http
containerPort: 8080
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
startupProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 10
failureThreshold: 30
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
periodSeconds: 10
timeoutSeconds: 2
failureThreshold: 3
A startupProbe concede até cinco minutos antes de falhar: 30 tentativas com intervalo de 10 segundos. Ajuste esse orçamento medindo a inicialização real, não aumentando o número até o alerta parar. Quando readiness falha, o pod sai dos endpoints do serviço; quando liveness falha, o kubelet reinicia o contêiner conforme a política do pod. A documentação de health checks do OpenShift 4.22 descreve esse comportamento.
Se Actuator estiver em uma porta de gerenciamento separada, a probe pode ficar verde enquanto a porta principal está travada. Uma correção é definir management.endpoint.health.probes.add-additional-paths=true e sondar /livez e /readyz na porta principal. Em produção, observe mudanças de readiness, reinícios e eventos do pod:
oc get pods -l app=demo -w
oc describe pod -l app=demo
oc get events --sort-by=.lastTimestamp
restricted-v3 não é a antiga restricted
Em instalações novas a partir do OpenShift 4.20, a SCC padrão mais restritiva para usuários autenticados é restricted-v3. Ela evolui restricted-v2 com isolamento por user namespace e exige hostUsers: false. A documentação do OpenShift 4.22 é explícita em dois pontos importantes: não modifique as SCCs padrão e verifique a SCC aplicada no cluster de destino.
O manifesto usa hostUsers: false, remove capabilities, bloqueia elevação de privilégios e exige execução sem root. A imagem já declara um USER numérico. Não fixe runAsUser: 185 no Deployment: permita que a SCC atribua o UID aceito pelo namespace. Também evite portas abaixo de 1024 e diretórios graváveis apenas pelo proprietário original.
Clusters atualizados de versões antigas podem conservar outra configuração, como restricted-v2. A observação correta vem do pod admitido no cluster alvo:
oc get scc
oc describe pod <nome-do-pod>
oc get pod <nome-do-pod> -o jsonpath='{.metadata.annotations.openshift\.io/scc}{"\n"}'
Se a admissão falhar, corrija a imagem ou o securityContext; não edite restricted-v3 para acomodar o manifesto. Uma SCC padrão afrouxada resolve o deploy de hoje e distribui a conta para todos os outros workloads do cluster.
Limites e ordem de promoção
OpenShift 4.22 é a plataforma do exemplo. O suporte a Java 25 vem do ciclo de vida do Red Hat build of OpenJDK 25, da matriz Java do Spring Boot e da imagem publicada no catálogo — não de uma célula de compatibilidade da plataforma. A 4.20 permanece uma versão GA documentada mais antiga, não a baseline do tutorial.
Antes de promover o manifesto, leia e verifique nesta ordem:
- As release notes do OpenShift 4.22 e o caminho suportado de atualização do seu cluster.
- O ciclo de suporte do OpenJDK 25 para a base RHEL usada pela imagem.
- A matriz do Spring Boot 4.1.1, que admite Java 17 a 26.
- A tag ou o digest da imagem
ubi9/openjdk-25no catálogo; não promovalatest. - A SCC efetivamente aplicada ao pod no cluster de destino.
A recomendação da DevDojo é adotar esse conjunto quando a aplicação já está validada no Spring Boot 4.1.1, o pipeline consegue fixar e inspecionar a imagem UBI 9 e o manifesto passa sob a SCC real sem privilégios extras. Recue da promoção se a matriz do framework, o ciclo do OpenJDK, a tag do catálogo ou a política do cluster não fecharem juntos.
O próximo passo é curto: fixe ubi9/openjdk-25:1.24, construa a imagem, confirme java -version localmente e adicione as três probes ao Deployment. Depois valide a SCC e os eventos no OpenShift que receberá o workload — porque o cluster de destino é o único que pode responder por sua própria configuração.