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.
| Aspecto | VM | Container |
|---|---|---|
| Isolamento | Completo (SO próprio) | Processo isolado (compartilha kernel) |
| Tamanho | GBs | MBs |
| Startup | Minutos | Segundos (ou menos com Quarkus) |
| Overhead | Alto | Mínimo |
| Portabilidade | Limitada | Total |
Dockerfile Multi-Stage para Quarkus
O multi-stage build separa a compilação da execução, resultando em imagens menores e mais seguras:
- JVM Mode
- Native Mode (GraalVM)
# ====== 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"]
# ====== Estágio 1: Build Nativo ======
FROM quay.io/quarkus/ubi-quarkus-mandrel-builder-image:jdk-21 AS build
WORKDIR /app
COPY --chown=quarkus:quarkus pom.xml .
COPY --chown=quarkus:quarkus src ./src
# Build nativo — demora mais, mas gera executável standalone
RUN ./mvnw package -Dnative -DskipTests -B
# ====== Estágio 2: Runtime Mínimo ======
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /app
COPY --from=build /app/target/*-runner /app/application
RUN chmod +x /app/application
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --quiet --tries=1 --spider http://localhost:8080/q/health/ready || exit 1
ENTRYPOINT ["/app/application", "-Dquarkus.http.host=0.0.0.0"]
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
| Tipo | Pergunta | Açã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→ perfildev@QuarkusTest→ perfiltestjava -jar→ perfilprod- 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:
BancoDeDadosReadinessCheckque 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
- Crie um
docker-compose.prod.ymlque adicione um container Nginx como reverse proxy na frente da aplicação - Implemente um health check que verifica se o serviço de e-mail (SMTP) está alcançável
- Adicione um
Gaugeque monitore o número de sessões ativas (usuários logados) - Configure logging para arquivo rotacionado em produção (
quarkus.log.file) - Crie um perfil
stagingcom configurações intermediárias entre dev e prod - Implemente um endpoint
/q/infocustomizado que retorne versão da aplicação, uptime e contagem de registros das entidades principais