Monitorando um servidor do Forge com Netdata
4 min de leitura
Eu queria olhar CPU, memória e disco do meu servidor de produção sem abrir o Netdata Cloud, sem expor a porta 19999 e sem instalar mais nada pesado. O resultado foi um Netdata escutando só em localhost, um proxy no Nginx do Forge protegido por token e uma página HTML estática.
O servidor é pequeno: um Linode com 1 vCPU e 2 GB de RAM, rodando Ubuntu 24.04 e gerenciado pelo Forge.
A ideia é experimental, quando tiver mais tempo irei polir melhor e quem sabe transformar em app.
A arquitetura
Ninguém fala direto com o Netdata. Tudo passa pelo Nginx, que só deixa passar /api/ com o token certo.
flowchart LR
B["Navegador"] -- "HTTPS + Bearer" --> CF["Cloudflare"]
CF --> NG["Nginx do Forge"]
NG -- "/api/* com token válido" --> ND["Netdata em 127.0.0.1:19999"]
NG -- "página inicial /" --> HTML["index.html estático"]
NG -- "qualquer outra rota" --> X["404"]
Três decisões sustentam o resto:
- O Netdata não escuta na internet. Só o Nginx alcança a porta 19999.
- O token é validado no Nginx, e o header é removido antes de chegar ao Netdata.
- Só o prefixo
/api/e a página/existem. O resto é 404, inclusive o dashboard nativo do Netdata.
1. Instalando o Netdata
Usei o script oficial com pacotes nativos. Assim o Netdata vem do repositório apt deles e atualiza junto com o resto do sistema.
wget -O /tmp/netdata-kickstart.sh https://get.netdata.cloud/kickstart.sh
sh /tmp/netdata-kickstart.sh --stable-channel --native-only --disable-telemetry
Depois, duas mudanças. A primeira prende o servidor web do Netdata em localhost, em /etc/netdata/netdata.conf:
[web]
bind to = 127.0.0.1:19999
A segunda desliga o Netdata Cloud, em /var/lib/netdata/cloud.d/cloud.conf:
[global]
enabled = no
Reinicie e confirme que ele responde só localmente:
sudo systemctl restart netdata
curl -s http://127.0.0.1:19999/api/v1/info | head -c 200
ss -ltnp | grep 19999
O ss precisa mostrar 127.0.0.1:19999, e não 0.0.0.0:19999. Se mostrar 0.0.0.0, a porta está aberta para o mundo.
2. O proxy no Forge
No Forge, adicionei o domínio netdata.seudominio.com.br ao site que já existia, emiti o certificado Let's Encrypt pelo próprio painel e editei a configuração Nginx desse domínio. Na Cloudflare, o registro DNS ficou com o proxy ligado.
O token é só um valor aleatório longo:
openssl rand -hex 32
O bloco que vai dentro do server da porta 443:
root /home/forge/netdata-dash;
location ^~ /api/ {
if ($http_authorization != "Bearer SEU_TOKEN") { return 401; }
proxy_pass http://127.0.0.1:19999;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header Authorization "";
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location = / {
try_files /index.html =404;
add_header Cache-Control "no-cache";
}
location / {
return 404;
}
O que cada parte faz:
^~ /api/pega tudo que começa com/api/e impede que outralocationcom regex tome a frente.- O
ifcompara o header inteiro com o token.ifcomreturné o uso seguro dele no Nginx. proxy_set_header Authorization ""tira o token antes de repassar. O Netdata nunca vê o segredo.proxy_http_version 1.1comConnection ""mantém conexões reaproveitadas com o Netdata, o que ajuda quando o painel faz várias chamadas por segundo.location = /serve a página.location /fecha todo o resto.
Uma requisição passa por este caminho:
sequenceDiagram
participant C as Navegador
participant N as Nginx
participant D as Netdata
C->>N: GET /api/v1/data com Authorization Bearer
alt token diferente ou ausente
N-->>C: 401 com página HTML do Nginx
else token certo
N->>D: GET /api/v1/data sem Authorization
D-->>N: JSON
N-->>C: 200 JSON
end
Um detalhe que custa tempo: o 401 volta em HTML, e não em JSON. Quem consome a API precisa checar o status antes de fazer o parse. Senão o erro vira um Unexpected token '<' que não diz nada.
Trocar o token
Gere um novo com openssl rand -hex 32, troque a linha do if no Forge e cole o token novo no painel. Como o valor está em um só lugar no servidor, a rotação leva um minuto.
3. O que consultar na API
Três endpoints resolvem quase tudo:
| Endpoint | Para quê |
|---|---|
/api/v1/info |
versão, sistema, núcleos, RAM total. Chame uma vez. |
/api/v1/data?chart=... |
série temporal de um chart. É o endpoint principal. |
/api/v1/alarms?active |
alertas em WARNING ou CRITICAL. |
O valor atual de um chart é a média dos últimos 5 segundos em um ponto:
/api/v1/data?chart=system.cpu&after=-5&points=1&group=average&format=json&options=abs
As contas que uso:
CPU usada = soma de todas as dimensões de system.cpu
RAM usada % = system.ram.used / soma de system.ram * 100
Disco % = used / (avail + used + reserved for root) * 100
Load % = system.load.load1 / núcleos * 100
As armadilhas que encontrei:
- Números como texto. No
/info,cores_totaleram_totalvêm como string. - Valores negativos. Em rede e disco, "enviado" e "escrita" vêm negativos.
options=absresolve. - Ordem invertida. O ponto mais novo vem primeiro. Use
options=flipou ordene no cliente. - Nomes que mudam entre versões. No Netdata v2,
system.iousa as dimensõesreadsewrites, e não maisineout. Meu gráfico de disco ficou vazio até eu perceber. - Uma dimensão chamada
time. O chart de retenção do banco devolve["time", "space", "time"]. Umlabels.indexOf("time")acha a coluna do horário, e não a dimensão. Procure a partir da coluna 1.
5. A página HTML
O painel é um único index.html, sem build e sem dependências além de uma fonte do Google Fonts. Ele pede o token uma vez, guarda no localStorage do navegador e faz as chamadas com fetch.
Algumas escolhas deixam ele leve:
- As chamadas de cada ciclo saem em paralelo, com
Promise.all. Cada/dataleva dezenas de milissegundos. - O polling pausa quando a aba fica oculta.
- Charts que não existem no servidor não derrubam a página. Eles aparecem como indisponíveis, e o resto continua.
- O topo responde a pergunta que importa ("está saudável", "sob pressão", "precisa de atenção") antes de qualquer gráfico.
O deploy é uma cópia de arquivo:
scp index.html servidor:/home/forge/netdata-dash/index.html
Guardar o token no localStorage é uma troca consciente. Para um painel pessoal, acessado só por mim, é suficiente. Para um time, eu colocaria o domínio atrás do Cloudflare Access e tiraria o token do navegador.
6. Quanto custa monitorar
O Netdata mede a si mesmo pelo plugin de apps, nos charts app.netdata_*. No meu servidor, com 1 vCPU e 2 GB:
| Recurso | Uso do Netdata |
|---|---|
| CPU | cerca de 1,2% a 1,5% do vCPU |
| Memória | cerca de 112 MiB de RSS |
| Escrita em disco | cerca de 2 KiB/s |
| API | cerca de 2,4 requisições por segundo, respondendo em 1,4 ms |
O histórico fica num banco próprio, em três camadas. Cada uma tem um limite de disco e de tempo, e vale o que vier primeiro:
| Camada | Resolução | Limite padrão |
|---|---|---|
| tier 0 | 1 ponto por segundo | 1 GiB ou 14 dias |
| tier 1 | 1 ponto por minuto | 1 GiB ou 3 meses |
| tier 2 | 1 ponto por hora | 1 GiB ou 2 anos |
Em dois dias, o tier 0 ocupou uns 225 MiB. Nesse ritmo, o limite de 1 GiB segura cerca de 10 dias de histórico por segundo. Quando enche, o Netdata apaga sozinho o mais antigo. O disco nunca lota por causa dele, e o histórico por minuto e por hora continua.
Resumo
- Netdata em
127.0.0.1, com Cloud e telemetria desligados. - Nginx do Forge como única porta de entrada, com token no header e o header removido antes do Netdata.
- Tudo fora de
/api/e/responde 404. - Certificado e Nginx só pelo painel do Forge.
- O painel checa o status antes do parse, tratam
nullcomo "sem valor" e não confiam em nomes de dimensão entre versões.
O custo total ficou em pouco mais de 1% de CPU e uns 100 MiB de memória. Em troca, uma página me diz em um segundo se o servidor está bem.