Stack ELK self-hosted na VPS: Elasticsearch + Kibana

A stack ELK (Elasticsearch + Logstash + Kibana) é o padrão de mercado para análise de logs em larga escala, full-text search, agregações em tempo real, dashboards poderosos. Pesada para VPS modestas (mínimo 8 GB RAM), mas insuperável para análise de logs estruturados e buscas complexas.

Instalar Elasticsearch e Kibana com Docker

Stack mínima: Elasticsearch + Kibana (sem Logstash):

yaml
# docker-compose.yml — ELK stack mínima:
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
    restart: always
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=true
      - ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
      - ES_JAVA_OPTS=-Xms2g -Xmx2g   # 2 GB de heap
    ports:
      - "127.0.0.1:9200:9200"
    volumes:
      - es_data:/usr/share/elasticsearch/data
    ulimits:
      memlock:
        soft: -1
        hard: -1

  kibana:
    image: docker.elastic.co/kibana/kibana:8.13.0
    restart: always
    depends_on:
      - elasticsearch
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
      - ELASTICSEARCH_USERNAME=kibana_system
      - ELASTICSEARCH_PASSWORD=${KIBANA_PASSWORD}
    ports:
      - "127.0.0.1:5601:5601"

volumes:
  es_data:

# .env:
# ELASTIC_PASSWORD=senha-forte
# KIBANA_PASSWORD=outra-senha

# Configurar senha do kibana_system após subir:
docker exec -it elasticsearch \
  elasticsearch-reset-password -u kibana_system

Enviar logs do Node.js para Elasticsearch

Indexar logs estruturados diretamente (sem Logstash):

typescript
// npm install @elastic/elasticsearch pino pino-elasticsearch

import { Client } from '@elastic/elasticsearch'
import pino from 'pino'

const elastic = new Client({
  node: 'http://localhost:9200',
  auth: { username: 'elastic', password: process.env.ELASTIC_PASSWORD! },
})

// Transport do Pino para Elasticsearch:
const logger = pino({
  transport: {
    targets: [
      // Console:
      { target: 'pino-pretty', level: 'debug', options: {} },
      // Elasticsearch:
      {
        target: 'pino-elasticsearch',
        level: 'info',
        options: {
          node: 'http://localhost:9200',
          auth: { username: 'elastic', password: process.env.ELASTIC_PASSWORD },
          index: 'minha-api-logs',  // índice por aplicação
          // Ou usar data stream com prefixo de data:
          // index: 'logs-minha-api-%{DATE}',
          flushBytes: 1000,         // flush a cada 1 KB
          flushInterval: 5000,      // ou a cada 5s
        },
      },
    ],
  },
})

// Usar normalmente — o pino-elasticsearch envia em batch:
logger.info({ userId: '123', rota: '/pedidos', duracao: 45 }, 'Pedido criado')
logger.error({ err: error, pedidoId: '456' }, 'Falha no pagamento')

Usar Filebeat em vez de envio direto

Coletar logs de arquivos e containers com Filebeat:

yaml
# docker-compose.yml — adicionar Filebeat:
  filebeat:
    image: docker.elastic.co/beats/filebeat:8.13.0
    restart: always
    user: root
    volumes:
      - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - /var/log:/var/log:ro

# filebeat.yml:
filebeat.inputs:
  # Logs de containers Docker:
  - type: container
    paths:
      - /var/lib/docker/containers/*/*.log
    processors:
      - add_docker_metadata:
          host: unix:///var/run/docker.sock

  # Logs do Nginx:
  - type: log
    paths:
      - /var/log/nginx/access.log
    fields:
      service: nginx
      type: access-log
    json.keys_under_root: false

output.elasticsearch:
  hosts: ["http://elasticsearch:9200"]
  username: "elastic"
  password: "${ELASTIC_PASSWORD}"
  index: "filebeat-%{[agent.version]}-%{+yyyy.MM.dd}"

Criar dashboards no Kibana

Configurar index patterns, queries KQL e visualizações:

bash
# No Kibana — primeiros passos:

# 1. Criar Data View (index pattern):
#    Stack Management → Data Views → Create
#    Index pattern: minha-api-logs* ou filebeat-*
#    Timestamp field: @timestamp

# 2. Discover — explorar logs:
#    Analytics → Discover → selecionar data view
#    Query KQL: level: "error" AND service: "api"
#    Filtros de tempo: Last 24 hours

# Queries KQL comuns:
# Erros HTTP:
# response: [500 TO 599]

# Logs de um usuário específico:
# userId: "123"

# Latência alta:
# duration > 1000

# Texto livre (full-text search):
# "pagamento falhou" OR "timeout"

# 3. Criar visualização:
#    Analytics → Visualize Library → Create
#    Lens → arrastar campos → configurar agregações

# 4. Dashboard:
#    Analytics → Dashboard → Create
#    Adicionar painéis: gráfico de erros por hora,
#    top rotas por latência, mapa de erros

# 5. Alertas:
#    Stack Management → Rules → Create rule
#    Type: Elasticsearch query
#    Condição: count > 10 erros em 5 min
#    Ação: enviar email ou webhook

Gerenciar índices e retenção de dados

Configurar ILM (Index Lifecycle Management) para controlar custos:

json
# Index Lifecycle Management — via API do Elasticsearch:

# Criar política de ciclo de vida:
PUT _ilm/policy/logs-policy
{
  "policy": {
    "phases": {
      "hot": {
        "min_age": "0ms",
        "actions": {
          "rollover": {
            "max_size": "5GB",
            "max_age": "1d"     # rotacionar diariamente
          }
        }
      },
      "warm": {
        "min_age": "7d",
        "actions": {
          "shrink": { "number_of_shards": 1 },
          "forcemerge": { "max_num_segments": 1 }
        }
      },
      "delete": {
        "min_age": "30d",
        "actions": { "delete": {} }   # deletar após 30 dias
      }
    }
  }
}

# Aplicar à template de índice existente:
PUT _index_template/logs-template
{
  "index_patterns": ["minha-api-logs-*"],
  "template": {
    "settings": {
      "index.lifecycle.name": "logs-policy"
    }
  }
}

# Via Kibana: Stack Management → Index Lifecycle Policies → Create

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Elasticsearch requer no mínimo 4 GB de heap (8 GB de RAM no servidor para sobrar ao SO e outros processos). Na prática: VPS de 16 GB RAM para conforto. Com 8 GB: configure ES_JAVA_OPTS=-Xms2g -Xmx2g e terá operação marginal. Para logging em produção real: 16-32 GB. Para testes e desenvolvimento: 8 GB aceitável.

Para a maioria dos casos: não precisa do Logstash. Filebeat coleta e envia direto ao Elasticsearch (leve, ~50 MB RAM). pino-elasticsearch ou winston-elasticsearch enviam da aplicação diretamente. Logstash (pesado, 512+ MB RAM) só faz sentido para transformações complexas: parsear formatos proprietários, enriquecer com dados externos, múltiplas fontes com transformações diferentes.

Loki: 10× mais leve em RAM e disco, queries por labels, sem full-text search no conteúdo. ELK: full-text search poderoso, agregações complexas, dashboards ricos no Kibana, mas consome muito mais recursos. Para VPS de 4-8 GB com foco em "ver logs e filtrar por serviço": Loki. Para VPS de 16+ GB com foco em "análise, buscas complexas, compliance": ELK.

Elasticsearch tem snapshot API nativo. Configure um repositório S3/MinIO: PUT _snapshot/backup {"type":"s3","settings":{"bucket":"meu-bucket",...}}. Depois: PUT _snapshot/backup/snapshot-hoje {"indices":"*"}. Ou: snapshot do volume Docker diariamente com scripts. Para o Kibana: export dashboards em Management → Saved Objects → Export.

Sim, OpenSearch é o fork open-source do Elasticsearch criado pela Amazon depois da mudança de licença. API 100% compatível com o Elasticsearch 7.x, mesma linguagem de queries, mesmo Kibana (OpenSearch Dashboards). Imagem Docker: opensearchproject/opensearch. Vantagem: licença Apache 2.0 sem restrições. Desvantagem: algumas features novas do Elasticsearch 8.x não existem no OpenSearch.