Pular para o conteúdo principal

Aula 15: Docker, Observabilidade e Deploy

Objetivos

  • Containerizar o CritiqueHub com Docker usando multi-stage builds
  • Orquestrar serviços (aplicação + banco de dados) com Docker Compose
  • Configurar health checks (liveness e readiness) para monitoramento de disponibilidade
  • Implementar métricas de aplicação com Micrometer para monitoramento de performance
  • Estruturar logging e perfis de configuração para múltiplos ambientes (dev, test, prod)

Contexto e Motivação

Até agora desenvolvemos o CritiqueHub localmente, num ambiente controlado com quarkus dev. Porém, uma aplicação profissional precisa ser empacotada, monitorada e distribuída de forma reproduzível. Se cada desenvolvedor configura seu ambiente de forma diferente, surgem os clássicos problemas de "funciona na minha máquina".

Docker resolve este problema criando ambientes idênticos e reproduzíveis. Combinado com health checks e métricas, temos uma aplicação production-ready — pronta para executar em qualquer infraestrutura, de um servidor local a um cluster Kubernetes.

O Caminho para Produção

Conteúdo

Docker — Fundamentos para Desenvolvedores Java

O Que é um Container?

Um container é um processo isolado que executa com seu próprio sistema de arquivos, rede e variáveis de ambiente. Diferente de uma máquina virtual, um container compartilha o kernel do sistema operacional host, sendo muito mais leve e rápido para iniciar.

AspectoVMContainer
IsolamentoCompleto (SO próprio)Processo isolado (compartilha kernel)
TamanhoGBsMBs
StartupMinutosSegundos (ou menos com Quarkus)
OverheadAltoMínimo
PortabilidadeLimitadaTotal

Dockerfile Multi-Stage para Quarkus

O multi-stage build separa a compilação da execução, resultando em imagens menores e mais seguras:

# ====== Estágio 1: Build ======
FROM maven:3.9-eclipse-temurin-21-alpine AS build
WORKDIR /app

# Cache de dependências — só rebuilda se pom.xml mudar
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Build da aplicação
COPY src ./src
RUN mvn package -DskipTests -B

# ====== Estágio 2: Runtime ======
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app

# Copia apenas o artefato final — imagem runtime não tem Maven nem código-fonte
COPY --from=build /app/target/quarkus-app/ ./quarkus-app/

# Usuário não-root por segurança
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 8080

# Health check integrado ao Docker
HEALTHCHECK --interval=30s --timeout=3s --start-period=15s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/q/health/ready || exit 1

ENTRYPOINT ["java", \
"-Djava.util.logging.manager=org.jboss.logmanager.LogManager", \
"-jar", "quarkus-app/quarkus-run.jar"]

Construção e execução:

# Build da imagem JVM
docker build -t critiquehub:latest .

# Executar o container
docker run -d --name critiquehub \
-p 8080:8080 \
-e QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://host.docker.internal:5432/critiquehub \
critiquehub:latest

# Verificar logs
docker logs -f critiquehub

Docker Compose — Orquestração Local

O Docker Compose define e executa múltiplos containers como um serviço unificado:

# docker-compose.yml
version: '3.9'

services:
# Banco de dados PostgreSQL
postgres:
image: postgres:16-alpine
container_name: critiquehub-db
environment:
POSTGRES_DB: critiquehub
POSTGRES_USER: critiquehub
POSTGRES_PASSWORD: critiquehub123
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U critiquehub"]
interval: 10s
timeout: 5s
retries: 5

# Aplicação CritiqueHub
app:
build:
context: .
dockerfile: Dockerfile
container_name: critiquehub-app
ports:
- "8080:8080"
environment:
QUARKUS_DATASOURCE_JDBC_URL: jdbc:postgresql://postgres:5432/critiquehub
QUARKUS_DATASOURCE_USERNAME: critiquehub
QUARKUS_DATASOURCE_PASSWORD: critiquehub123
QUARKUS_HIBERNATE_ORM_DATABASE_GENERATION: update
QUARKUS_PROFILE: prod
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/q/health/ready"]
interval: 30s
timeout: 5s
start_period: 30s
retries: 3

volumes:
postgres_data:
# Iniciar tudo
docker compose up -d

# Ver status dos serviços
docker compose ps

# Seguir logs da aplicação
docker compose logs -f app

# Parar tudo
docker compose down

# Parar e remover volumes (dados do banco)
docker compose down -v

Health Checks — Liveness e Readiness

Health checks permitem que orquestradores (Docker, Kubernetes) monitorem a saúde da aplicação e tomem ações automáticas (reiniciar, remover do load balancer).

Conceitos

TipoPerguntaAção se falhar
Liveness"A aplicação está viva?"Reiniciar o container
Readiness"A aplicação pode receber requisições?"Remover do load balancer
Startup"A aplicação já iniciou?"Aguardar antes de verificar liveness

Dependência

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>

Com essa dependência, o Quarkus automaticamente expõe:

  • /q/health — status geral
  • /q/health/live — liveness checks
  • /q/health/ready — readiness checks
  • /q/health/started — startup checks

Health Checks Customizados

@Liveness
@ApplicationScoped
public class CritiqueHubLivenessCheck implements HealthCheck {

@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("CritiqueHub Liveness")
.up()
.withData("versao", "1.0.0")
.withData("timestamp", Instant.now().toString())
.build();
}
}

@Readiness
@ApplicationScoped
public class BancoDeDadosReadinessCheck implements HealthCheck {

@Inject
EntityManager em;

@Override
public HealthCheckResponse call() {
try {
// Testa conexão com o banco
em.createNativeQuery("SELECT 1").getSingleResult();
return HealthCheckResponse.named("Banco de Dados")
.up()
.withData("tipo", "PostgreSQL")
.build();
} catch (Exception e) {
return HealthCheckResponse.named("Banco de Dados")
.down()
.withData("erro", e.getMessage())
.build();
}
}
}

Resposta do endpoint /q/health:

{
"status": "UP",
"checks": [
{
"name": "CritiqueHub Liveness",
"status": "UP",
"data": {
"versao": "1.0.0",
"timestamp": "2024-11-15T10:30:00Z"
}
},
{
"name": "Banco de Dados",
"status": "UP",
"data": {
"tipo": "PostgreSQL"
}
}
]
}

Métricas com Micrometer

Métricas permitem monitorar o comportamento da aplicação em produção: quantas requisições por segundo, tempo médio de resposta, uso de memória, estado do pool de conexões.

Dependência

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-micrometer-registry-prometheus</artifactId>
</dependency>

O endpoint /q/metrics é exposto automaticamente no formato Prometheus.

Métricas Automáticas

O Quarkus + Micrometer registra automaticamente:

  • HTTP: requests_total, request_duration_seconds, requests_active
  • JVM: jvm_memory_used_bytes, jvm_gc_pause_seconds, jvm_threads_live
  • Pool de Conexões: agroal_active_count, agroal_available_count
  • Hibernate: hibernate_query_executions_total, hibernate_entities_loads_total

Métricas Customizadas

@ApplicationScoped
public class AvaliacaoService {

@Inject
MeterRegistry registry;

@Inject
AvaliacaoRepository avaliacaoRepository;

// Contador — quantas avaliações foram criadas
private final Counter avaliacoesCriadas;

// Timer — quanto tempo leva a busca
private final Timer buscaTimer;

@Inject
public AvaliacaoService(MeterRegistry registry) {
this.registry = registry;
this.avaliacoesCriadas = Counter.builder("critiquehub.avaliacoes.criadas")
.description("Total de avaliações criadas")
.tag("aplicacao", "critiquehub")
.register(registry);
this.buscaTimer = Timer.builder("critiquehub.avaliacoes.busca")
.description("Tempo de busca de avaliações")
.register(registry);
}

@Transactional
public Avaliacao criarAvaliacao(AvaliacaoDTO dto) {
Avaliacao avaliacao = new Avaliacao();
// ... mapeamento
avaliacaoRepository.persist(avaliacao);

avaliacoesCriadas.increment(); // registra métrica
return avaliacao;
}

public List<Avaliacao> buscarPorFilme(Long filmeId) {
return buscaTimer.record(() -> // mede tempo de execução
avaliacaoRepository.find("filme.id", filmeId).list()
);
}
}

Anotação @Timed e @Counted

Para simplificar, use anotações:

@ApplicationScoped
public class FilmeService {

@Inject
FilmeRepository filmeRepository;

@Timed(value = "critiquehub.filmes.listagem", description = "Tempo de listagem de filmes")
public List<Filme> listarTodos() {
return filmeRepository.listAll();
}

@Counted(value = "critiquehub.filmes.buscas", description = "Total de buscas realizadas")
public List<Filme> buscar(String termo) {
return filmeRepository.buscarPorTermo(termo);
}
}

Perfis de Configuração

O Quarkus suporta perfis de configuração que ativam diferentes valores com base no ambiente:

# application.properties

# ====== Configuração padrão (todos os perfis) ======
quarkus.application.name=critiquehub
quarkus.application.version=1.0.0

# ====== Perfil DEV (quarkus dev) ======
%dev.quarkus.datasource.db-kind=h2
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:critiquehub
%dev.quarkus.hibernate-orm.database.generation=drop-and-create
%dev.quarkus.hibernate-orm.log.sql=true
%dev.quarkus.log.level=DEBUG
%dev.quarkus.log.category."br.edu.ifsp".level=DEBUG

# ====== Perfil TEST ======
%test.quarkus.datasource.db-kind=h2
%test.quarkus.datasource.jdbc.url=jdbc:h2:mem:critiquehub-test
%test.quarkus.hibernate-orm.database.generation=drop-and-create
%test.quarkus.log.level=WARN

# ====== Perfil PROD ======
%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.jdbc.url=${DATABASE_URL}
%prod.quarkus.datasource.username=${DATABASE_USER}
%prod.quarkus.datasource.password=${DATABASE_PASSWORD}
%prod.quarkus.hibernate-orm.database.generation=none
%prod.quarkus.log.level=INFO
%prod.quarkus.log.console.json=true

# ====== Health & Metrics apenas em prod ======
%prod.quarkus.smallrye-health.ui.always-include=false
%prod.quarkus.micrometer.export.prometheus.enabled=true

O perfil é selecionado automaticamente:

  • quarkus dev → perfil dev
  • @QuarkusTest → perfil test
  • java -jar → perfil prod
  • Manual: java -Dquarkus.profile=staging -jar app.jar

Logging Estruturado

Em produção, logs devem ser estruturados (JSON) para integração com ferramentas de análise (ELK, Grafana Loki):

# Logging JSON em produção
%prod.quarkus.log.console.json=true
%prod.quarkus.log.console.json.date-format=yyyy-MM-dd'T'HH:mm:ss.SSSZ
%prod.quarkus.log.console.json.additional-field.app.value=critiquehub
%prod.quarkus.log.console.json.additional-field.environment.value=${ENVIRONMENT:production}

Uso no código:

@ApplicationScoped
public class FilmeService {

// Log com io.quarkus.logging.Log (estático, zero-cost se desabilitado)
public Filme criar(FilmeDTO dto) {
Log.infof("Criando filme: titulo=%s, ano=%d", dto.titulo(), dto.anoLancamento());

Filme filme = new Filme();
// ... mapeamento
filme.persist();

Log.infof("Filme criado com sucesso: id=%d", filme.id);
return filme;
}
}

Saída JSON em produção:

{
"timestamp": "2024-11-15T10:30:00.123-0300",
"level": "INFO",
"loggerName": "b.e.i.c.service.FilmeService",
"message": "Filme criado com sucesso: id=42",
"app": "critiquehub",
"environment": "production"
}

Laboratório 10 — Containerizando o CritiqueHub

Objetivo

Containerizar o CritiqueHub completo com Docker, configurar Docker Compose com PostgreSQL, implementar health checks customizados e adicionar métricas de negócio.

Passo 1 — Dockerfile

Crie o Dockerfile multi-stage conforme os exemplos (modo JVM).

Passo 2 — Docker Compose

Crie o docker-compose.yml com serviços postgres e app conforme os exemplos.

Passo 3 — Health Checks

Adicione a dependência quarkus-smallrye-health e implemente:

  • BancoDeDadosReadinessCheck que valida a conexão
  • Um readiness check que verifica se existe pelo menos um gênero cadastrado (dados iniciais carregados)

Passo 4 — Métricas de Negócio

Adicione a dependência quarkus-micrometer-registry-prometheus e implemente:

  • Contador de avaliações criadas por nota (1-5 estrelas)
  • Timer nas operações de busca do FilmeRepository
  • Gauge do total de filmes cadastrados

Passo 5 — Perfis

Configure os três perfis (dev, test, prod) no application.properties conforme exemplos.

Passo 6 — Validação

# Subir os serviços
docker compose up -d

# Verificar saúde
curl http://localhost:8080/q/health | jq .

# Verificar métricas
curl http://localhost:8080/q/metrics | grep critiquehub

# Fazer algumas operações e verificar métricas novamente
curl -X POST http://localhost:8080/filmes -H "Content-Type: application/json" \
-d '{"titulo": "Matrix", "anoLancamento": 1999}'

curl http://localhost:8080/q/metrics | grep critiquehub

Exercícios

  1. Crie um docker-compose.prod.yml que adicione um container Nginx como reverse proxy na frente da aplicação
  2. Implemente um health check que verifica se o serviço de e-mail (SMTP) está alcançável
  3. Adicione um Gauge que monitore o número de sessões ativas (usuários logados)
  4. Configure logging para arquivo rotacionado em produção (quarkus.log.file)
  5. Crie um perfil staging com configurações intermediárias entre dev e prod
  6. Implemente um endpoint /q/info customizado que retorne versão da aplicação, uptime e contagem de registros das entidades principais

Referências