De notebook velho a home server: começando com um simples Pi-hole
Eu só queria reaproveitar um notebook velho.
Era um Samsung NP270E4E, com um Core i3 de terceira geração e 12 GB de RAM. Hardware que já não fazia muito sentido como computador principal, mas que ainda tinha recursos suficientes para ficar ligado em casa executando serviços leves.
O plano inicial parecia simples:
- instalar Linux;
- usar o notebook como home server;
- colocar alguns serviços em Docker;
- instalar Pi-hole;
- futuramente ter agentes autonomos e/ou equipe agêntica
O que começou como reaproveitamento de hardware acabou se transformando em um laboratório para experimentar conceitos que normalmente associamos a sistemas de produção: rollout gradual, dry-run, fail-closed, audit trail, reconciliação de estado, rollback e observabilidade com minimização de dados.
E boa parte disso começou porque a placa de rede do notebook não era confiável.
O primeiro problema não foi Docker. Foi a rede.
Antes de pensar em Pi-hole, precisei tornar o servidor minimamente confiável.
A Ethernet onboard apresentava quedas recorrentes de link. Não era um problema de aplicação, container ou DNS. Era um problema físico — exatamente o tipo de coisa que um homelab feito com hardware antigo eventualmente obriga você a enfrentar.
Um adendo a correção via substituição do hardware com um usb ethernet adapter, tive que usar comandos que não conhecia ou pouco usei
Até então eu só tinha usado Linux para escapar dos bloats do windows ou como uma especie de plataforma de desenvolvimento mais livre, mas nunca me preocupando como administrar um servidor sem interface gráfica, começou a me obrigar a entender melhor as ferramentas que o próprio sistema oferece para explicar o que está acontecendo.
Debugando o servidor
Comandos como:
systemctl status <service>
ajudavam a verificar se os componentes responsáveis pela rede estavam ativos, enquanto o journalctl permitia olhar para trás e descobrir o que havia acontecido antes da queda:
journalctl
ou filtrar eventos relacionados ao boot, kernel e serviços específicos.
Também comecei a usar ferramentas como:
ip addr
ip route
networkctl status
resolvectl status
para diferenciar problemas de endereço IP, rota, DNS e estado físico da interface.
Foi através dessa investigação que apareceram eventos recorrentes de perda e recuperação de link na Ethernet onboard.
Ou seja: a aplicação não estava caindo, o Docker não era o problema e o DNS sequer era o principal suspeito.
O próprio link de rede estava desaparecendo.
Esse momento foi importante porque mudou a forma como eu fazia troubleshooting.
Em vez de:
não funciona
↓
reiniciar
o processo começou a ficar mais parecido com:
sintoma
↓
estado atual
↓
logs históricos
↓
hipótese
↓
teste
↓
evidência
E o journalctl foi particularmente útil porque mostrou uma característica importante de administrar Linux: muitas vezes o problema já deixou pistas suficientes no sistema — você só precisa saber onde procurar.
A partir dali o homelab também virou uma desculpa prática para aprender ferramentas Unix que eu dificilmente estudaria apenas lendo documentação.
systemctl, journalctl, ip, networkctl, resolvectl, ss, dig e outras ferramentas deixaram de ser comandos isolados e passaram a fazer parte de um processo real de diagnóstico.
No fim, a decisão de substituir a Ethernet onboard por um adaptador USB não veio apenas de uma impressão de que “a rede estava ruim”.
Veio de evidência coletada no próprio sistema.
E essa talvez tenha sido uma das primeiras mudanças de mentalidade do projeto:
Antes de corrigir um problema, tente fazer o sistema explicar o que está acontecendo.
A solução prática foi introduzir um adaptador Ethernet USB, só assim tinha sessões ssh estaveis e possibilidade de seguir com demais projetos sem intermitencia de hardware
Mas eu não queria trocar uma interface por outra e torcer para funcionar.
A partir daí começou a surgir um padrão que, sem perceber, eu acabaria reutilizando no restante do projeto:
inspect
↓
plan
↓
test
↓
validate
↓
apply
↓
validate again
Antes de alterar a configuração de rede, foram criadas verificações somente leitura para entender interfaces, rotas e estado do servidor.
Quando chegou a hora de definir um endereço estável para a nova interface, a mudança passou primeiro por:
netplan generate
e depois:
netplan try
Somente depois de validar conectividade e acesso durante a janela de teste a configuração foi persistida.
O Wi-Fi permaneceu disponível como fallback.
Parece exagero para um notebook em casa.
Mas havia uma razão simples: eu administrava aquele servidor remotamente. Uma configuração errada de rede poderia significar levantar da cadeira, conectar teclado e monitor e descobrir o que eu tinha quebrado.
Foi provavelmente a primeira lição relevante do projeto:
Nunca faça uma mudança capaz de remover seu próprio acesso sem saber previamente como voltar.
Naquele momento eu ainda não chamava isso de control plane.
Mas a ideia já estava ali.
Então veio o Pi-hole
Com a rede suficientemente estável, o próximo passo foi transformar o notebook em servidor DNS da rede usando Pi-hole.
A aplicação foi preparada em Docker Compose, com estado persistente fora do repositório e configuração sensível mantida localmente.
O objetivo da primeira execução também foi deliberadamente limitado.
Nada de alterar o DNS do roteador.
Nada de migrar todos os dispositivos da casa.
Nada de transformar imediatamente o servidor no ponto central da rede.
Primeiro, o objetivo era apenas:
- subir o container;
- verificar saúde;
- verificar portas;
- testar DNS diretamente contra o Pi-hole;
- verificar a interface web;
- confirmar que o restante do servidor continuava intacto.
O container subiu.
O healthcheck ficou saudável.
As portas pareciam corretas.
E o DNS não funcionou.
healthy não significa que o serviço funciona
Esse foi um dos episódios mais úteis do projeto.
O Docker dizia que o container estava saudável, mas consultas DNS reais não recebiam resposta.
Investigando os logs, ficou claro que o Pi-hole estava ignorando as consultas porque elas chegavam através do caminho de rede do Docker e eram classificadas como não locais.
A solução não foi simplesmente usar:
network_mode: host
nem publicar DNS indiscriminadamente em todas as interfaces.
Mantivemos a exposição restrita ao endereço destinado ao serviço e alteramos explicitamente o modo de escuta do Pi-hole.
Depois de recriar o container, as consultas passaram:
dig @<PIHOLE_IP> pi-hole.net
dig @<PIHOLE_IP> google.com
A interface web também respondeu como esperado.
Foi uma pequena falha, mas consolidou outra regra:
Container saudável é evidência de que o container está saudável. Não é evidência suficiente de que o sistema funciona.
Healthcheck não substitui teste funcional.
O rollout começou com um cliente
Com o Pi-hole funcionando no servidor, seria tentador alterar o DNS do roteador e migrar toda a casa.
Não fizemos isso.
O próximo teste foi feito a partir de um único cliente.
A estratégia era simples: apontar explicitamente algumas consultas para o Pi-hole sem alterar ainda a configuração persistente do sistema operacional.
Consultas normais funcionaram.
Um domínio conhecido da blocklist retornou a resposta de bloqueio esperada.
Até aí, ótimo.
Mas outros testes não passaram.
A conectividade TCP para DNS apresentou problema.
A interface web não estava acessível pelo mesmo caminho do cliente.
A tentativa de alterar persistentemente o DNS do Windows também encontrou restrições de permissão.
Tecnicamente, parte do sistema funcionava.
A classificação do teste, entretanto, não foi:
SUCCESS
Foi:
PARTIAL / BLOCKED
E o rollout global permaneceu bloqueado.
Essa talvez tenha sido uma das decisões mais importantes do projeto.
Porque existe uma diferença enorme entre:
"Eu consegui resolver um domínio usando o Pi-hole."
e:
"Tenho confiança operacional suficiente para transformar esse serviço no DNS da minha rede."
O primeiro é um teste.
O segundo é uma decisão operacional.
Client-by-client em vez de big bang
Os problemas foram sendo investigados, novos critérios de validação apareceram e mais clientes foram adicionados gradualmente.
Nesse meio tempo também ficou claro que o acesso administrativo ao roteador não era tão simples quanto gostaríamos.
A arquitetura poderia ter sido forçada.
Em vez disso, o Pi-hole passou a operar inicialmente em modo client-by-client.
Cada dispositivo era incorporado de forma controlada.
Isso não era o desenho mais elegante possível.
Mas era o desenho cujo rollback eu conhecia.
Essa escolha resume bastante a filosofia que começou a aparecer no homelab:
O caminho operacionalmente previsível pode ser melhor que o caminho arquiteturalmente mais bonito.
Em algum momento, o Pi-hole deixou de ser apenas um Pi-hole
Até aí o projeto ainda poderia ser descrito como:
clients
│
▼
Pi-hole
│
▼
upstream DNS
Mas o homelab-ai tem um objetivo maior.
A ideia é que alguns serviços do homelab eventualmente possam ser operados através de uma camada de automação — e, no futuro, possivelmente através de ferramentas, agentes ou MCP.
Isso muda completamente a pergunta.
Deixa de ser apenas:
Como bloquear um domínio?
E passa a ser:
Como permitir que software altere infraestrutura doméstica sem transformar qualquer bug, prompt ruim ou erro de integração em uma mudança perigosa?
A API administrativa do Pi-hole tornou essa questão concreta.
Tecnicamente, seria possível construir uma abstração simples:
POST /block
recebendo:
{
"domain": "example.com"
}
e chamar diretamente a API.
Isso funciona.
Também seria uma excelente forma de construir uma automação perigosa.
O bloqueio virou um pequeno control plane
Antes de expor qualquer operação de escrita para componentes maiores do projeto, foi criada uma política específica para mutações no Pi-hole.
O primeiro caso implementado foi intencionalmente minúsculo:
adicionar uma única regra exact-domain deny.
Sem regex.
Sem wildcard.
Sem edição em massa.
Sem alteração de grupos.
Sem manipulação arbitrária de clientes.
Sem endpoint remoto.
Uma única operação.
Mas com várias salvaguardas.
O fluxo ficou aproximadamente assim:
request
│
▼
input validation
│
▼
read current state
│
▼
dry-run
│
▼
short-lived plan
│
├─ target state
├─ expected pre-state
├─ expiry
└─ digest
│
▼
explicit apply
│
▼
single mutation
│
▼
post-state verification
│
▼
audit
E existe um caminho separado para rollback.
Dry-run deixou de ser uma flag decorativa
Uma das decisões principais foi tornar o dry-run obrigatório.
O sistema não recebe uma solicitação e imediatamente modifica o Pi-hole.
Primeiro ele produz um plano.
Esse plano possui vida curta — dez minutos — e é associado a um digest.
Quando chega a hora do apply, o sistema verifica novamente o estado atual antes de realizar a alteração.
Isso reduz alguns riscos importantes.
Imagine:
T0: plano criado
T1: outro processo altera o estado
T2: apply executado
Sem reconciliação, o plano poderia operar sobre premissas que não existem mais.
Por isso o apply não assume que o mundo permaneceu congelado.
Ele observa novamente.
Uma mutação significa uma mutação
Outro detalhe proposital:
não existe retry automático para operações de escrita.
Retries são excelentes para muitas operações read-only.
Para mutações, podem ser perigosos.
Considere:
request sent
│
▼
timeout
O timeout não responde uma pergunta essencial:
a operação falhou
ou:
a operação aconteceu, mas a resposta se perdeu?
Se o sistema simplesmente tentar novamente, pode executar a mesma alteração duas vezes ou esconder um estado ambíguo.
No control plane do Pi-hole, uma operação de escrita tenta no máximo uma mutação.
Se o resultado não puder ser determinado com segurança, o estado é classificado como incerto e exige reconciliação.
Isso é bem menos conveniente que:
for attempt in range(3):
try_again()
E exatamente por isso é mais interessante.
O rollback também precisa provar que é dono da mudança
Remover uma entrada parece simples.
Mas existe uma pergunta importante:
Como sabemos que essa entrada ainda é a mesma entrada criada por nós?
O MVP mantém um marcador de ownership associado à alteração e registra eventos de auditoria.
O rollback só é autorizado quando o estado atual e o histórico esperado correspondem.
Isso evita que a ferramenta simplesmente delete algo porque encontrou um domínio igual.
Em outras palavras:
"essa entrada existe"
não é suficiente.
Precisamos conseguir afirmar:
"essa entrada existe,
foi criada por esta operação,
há evidência de auditoria correspondente,
e seu estado continua compatível com o rollback"
Só então a operação prossegue.
Locks, estado privado e fail-closed
O fluxo também possui lock exclusivo durante as janelas críticas de planejamento e aplicação.
Dois processos não devem poder preparar ou aplicar mudanças concorrentes assumindo o mesmo estado inicial.
O estado operacional sensível — credenciais, planos, auditoria e arquivos de controle — fica fora do Git.
Permissões incorretas, arquivos inesperados, symlinks ou estado local considerado inseguro fazem a operação falhar.
A lógica é deliberadamente fail-closed:
não consigo provar que é seguro
↓
parar
em vez de:
provavelmente está tudo certo
↓
continuar
Então testamos de verdade
Implementar salvaguardas sem exercitá-las seria pouco convincente.
O MVP recebeu uma suíte sintética cobrindo cenários como:
- entrada inválida;
- estado local inseguro;
- plano expirado;
- plano adulterado;
- conflitos;
- concorrência;
- confirmação interativa;
- resultado de mutação ambíguo;
- logout da sessão;
- rollback autorizado.
A suíte passou com 16 testes.
Depois veio o runtime dry-run.
E, finalmente, uma janela controlada de validação.
Nenhum domínio real foi usado.
O alvo era um domínio reservado .invalid, criado especificamente para o teste.
O fluxo executado foi:
plan
↓
apply
↓
verify post-state
↓
create rollback plan
↓
rollback
↓
verify final state
O bloqueio foi aplicado.
O novo estado foi confirmado.
O rollback foi planejado.
O rollback foi executado.
E a verificação final confirmou que a entrada fictícia não permaneceu configurada.
Esse foi o ponto em que, para mim, o projeto deixou definitivamente de ser apenas:
"Tenho Pi-hole rodando em casa."
Observabilidade também pode vazar informação
O passo seguinte trouxe outro problema interessante.
DNS é uma fonte extremamente sensível de dados.
Um sistema de observabilidade mal projetado poderia transformar algo criado para melhorar a operação do homelab em um registro detalhado da atividade da casa.
Por isso definimos uma separação explícita entre três superfícies:
Pi-hole native data
│
├─ queries
├─ domains
└─ clients
control-plane observability
│
├─ health
├─ operation status
└─ bounded categories
private mutation audit
│
└─ recovery information
Os dados não devem ser misturados.
A primeira versão de observabilidade não pode registrar:
- domínios;
- consultas DNS;
- clientes;
- endereços;
- credenciais;
- IDs de planos;
- IDs de auditoria;
- respostas HTTP cruas;
- stack traces;
- erros arbitrários.
Até métricas DNS agregadas foram adiadas.
Em uma infraestrutura enorme, um contador global pode parecer anônimo.
Em uma casa com poucos dispositivos, até um gráfico agregado de atividade pode revelar padrões sobre os moradores.
Às vezes privacy engineering não significa criptografar mais dados.
Significa simplesmente decidir:
Não precisamos coletar isso.
O hardware continua sendo ruim — e isso é ótimo
O servidor continua sendo um notebook antigo.
O processador continua sendo um Core i3 de terceira geração.
A Ethernet onboard não magicamente se tornou confiável.
O objetivo nunca foi competir com um servidor moderno.
Mas essa limitação acabou sendo parte importante do valor do projeto.
Quando hardware, rede e acesso administrativo não são perfeitos, você precisa pensar mais sobre:
- recovery;
- fallback;
- rollout;
- observabilidade;
- dependências;
- blast radius;
- estado persistente;
- operações irreversíveis.
Uma cloud abstrata muitas vezes esconde essas coisas.
Um notebook velho na sua casa não esconde.
Se você derrubar a rede, você percebe.
O que eu realmente construí?
Se eu resumisse o estado atual visualmente:
old laptop
│
▼
Linux home server
│
▼
network hardening
│
▼
Docker
│
▼
Pi-hole
│
▼
client-by-client DNS
│
▼
authenticated API
│
▼
controlled mutation layer
│
├─ dry-run
├─ expiring plans
├─ locking
├─ audit
├─ reconciliation
└─ rollback
Não criei uma alternativa ao Pi-hole.
Também não inventei nenhuma dessas técnicas.
O que fiz foi usar um serviço doméstico real como ambiente para praticar princípios de engenharia que normalmente aparecem em sistemas muito maiores.
E talvez essa seja a parte mais útil de um homelab.
Você não precisa simular completamente uma empresa.
Só precisa criar problemas reais o bastante para ser obrigado a tomar decisões de engenharia.
E onde entra IA nisso?
Essa é a próxima parte.
O projeto se chama homelab-ai, mas deliberadamente ainda não coloquei um agente controlando essas mutações.
Isso não é atraso.
É parte do design.
Antes de fornecer a uma LLM algo equivalente a:
block_domain(domain)
eu quero que o mecanismo abaixo dela já saiba responder perguntas como:
Essa operação é permitida?
Qual é o estado atual?
O plano ainda é válido?
O estado mudou desde o dry-run?
Quem criou essa alteração?
Existe rollback conhecido?
O resultado da mutação é realmente conhecido?
A LLM não deveria ser o componente responsável por garantir essas propriedades.
Essas garantias pertencem à infraestrutura.
Se no futuro um agente ou servidor MCP chamar esse control plane, a ideia é que ele receba menos poder do que parece, não mais.
O que um notebook velho acabou me ensinando
Quando comecei esse projeto, eu provavelmente descreveria o objetivo assim:
Reaproveitar um notebook velho como home server.
Hoje eu descreveria de outra forma:
Construir um ambiente pequeno o suficiente para eu controlar, mas real o suficiente para me obrigar a pensar sobre engenharia operacional.
O Pi-hole foi apenas o primeiro componente a tornar isso muito claro.
A jornada foi mais ou menos esta:
"vamos instalar Pi-hole"
↓
"por que a rede cai?"
↓
"preciso de fallback"
↓
"vamos testar antes de aplicar"
↓
"o container está healthy, mas o DNS não funciona"
↓
"vamos validar com um cliente"
↓
"funcionou parcialmente, então ainda não é rollout"
↓
"e se software começar a alterar o Pi-hole?"
↓
"precisamos de dry-run"
↓
"e de reconciliation"
↓
"e de audit"
↓
"e de rollback"
↓
"e talvez não devêssemos coletar metade desses dados"
Essa é, até agora, a parte mais interessante do homelab-ai.
E o próximo problema provavelmente será ainda melhor:
como conectar agentes e MCP a esse ambiente sem destruir exatamente as garantias que acabamos de construir?
Top comments (0)