O Maestro, Parte 3: O que o runbook não conta — Da primeira linha de comando à primeira resposta
Resumo
Este artigo é a terceira parte da série "O Maestro". A Parte 2 descreveu a arquitetura escolhida — cinco serviços containerizados em Docker, com LiteLLM como gateway e Open WebUI como interface — e o plano de implantação faseado que guiaria a execução. Esta parte documenta o que aconteceu quando esse plano foi colocado em prática.
O guia de implantação — um documento com todos os comandos em sequência — foi escrito com apoio do Gemini antes da execução começar. Parecia sólido. Ao executá-lo, encontrei problemas que o próprio texto havia introduzido: um `c` faltando no meio de uma URL, um espaço ausente entre dois parâmetros, um comando posicionado antes do contêiner que ele precisava existir. Erros meus, que eu não via porque havia escrito aquelas linhas e as havia relido tantas vezes que o olho já não as enxergava de verdade.
Foi aqui que o uso de IA como ferramenta de diagnóstico mostrou seu valor mais direto. O ciclo era simples: colar a saída do terminal junto com o comando e perguntar o que estava errado. O que eu não conseguia ver depois de dez releituras, o modelo identificava na primeira — não porque fosse mais inteligente, mas porque lia aquilo pela primeira vez. O tempo de diagnóstico que, em experiências anteriores, facilmente consumia horas, passou a ser medido em minutos. Em vários casos, em menos de um minuto.
Há um dado que não passa despercebido: o Gemini ajudou a escrever o documento com os erros. O Claude ajudou a encontrá-los. Ambos são ferramentas, e ferramentas falham e acertam em contextos diferentes. O que esta parte registra é justamente como essa combinação — escrever com IA, executar, depurar com IA — se comportou na prática.
Palavras-chave: Docker; Ubuntu; LiteLLM; guia de implantação; troubleshooting; IA no diagnóstico.
1. Introdução
Este artigo é a Parte 3 de uma série denominada "O Maestro". A Parte 2 apresentou a arquitetura do orquestrador: cinco serviços containerizados (Caddy, Open WebUI, LiteLLM, PostgreSQL e Redis), executados em Docker sobre hardware reutilizado, com um plano de implantação em sete fases que previa validar estabilidade e custos antes de liberar a ferramenta para os 120 colaboradores da empresa.
Antes de começar a execução, preparei um guia de implantação com todos os comandos em ordem. Usei o Gemini para estruturar os passos, revisar a sequência e completar os trechos que eu não tinha certeza. O resultado parecia completo. Havia desde a atualização do sistema operacional até a validação do chat funcionando.
O que esse processo não consegue garantir é que os comandos estejam certos quando executados em um ambiente real. Erros de digitação passam pela revisão da IA porque a IA está avaliando intenção e estrutura, não executando. Problemas de ordem entre comandos só aparecem quando alguém tenta executar o segundo antes do primeiro ter criado o que o segundo precisa. Esse tipo de erro — o que só existe em execução — é o que esta parte documenta.
O artigo está organizado da seguinte forma: a Seção 2 descreve o estado da implantação ao início da execução; a Seção 3 narra o caminho da primeira linha de comando à primeira resposta, agrupado por tipo de problema; e a Seção 4 traz o estado ao término desta fase e a ponte para a Parte 4.
2. Estado inicial da implantação
Ao início da execução, o servidor físico estava disponível — um Dell Inspiron 5675 com AMD Ryzen 7 1700 e Ubuntu Server 24.04 LTS recém-instalado —, os arquivos de configuração haviam sido preparados e as chaves de API para o Google AI Studio estavam em mãos.
O documento de implantação que eu havia escrito estava na sua segunda versão, revisada em julho de 2026 após identificar, em uma releitura crítica, que a primeira versão referenciava modelos descontinuados e não previa configuração HTTPS. A segunda versão endereçava esses pontos. Parecia pronta para execução.
O que se seguiu foi uma sequência de problemas que, individualmente, seriam resolvíveis em minutos por quem os conhecesse de antemão. Em conjunto, somaram dias entre o início da execução e a primeira resposta real. A descrição abaixo é deliberadamente honesta sobre essa distância.
3. Da primeira linha de comando à primeira resposta
3.1 Por que erros triviais são difíceis de encontrar
Há uma regularidade conhecida por quem trabalha com infraestrutura: o problema, uma vez encontrado, é sempre óbvio. O que não é óbvio é o caminho até encontrá-lo.
Erros de digitação em URLs e parâmetros têm uma propriedade peculiar: a mensagem de erro que produzem raramente descreve o erro em si. O sistema não diz "o domínio que você digitou não existe"; diz que o pacote não foi encontrado. Rastrear a causa exige ir dois ou três níveis acima da mensagem recebida — o que, sem referência externa, significa reler o mesmo comando que você mesmo escreveu até o olho encontrar o que a memória muscular escondeu.
É exatamente esse mecanismo que a IA interrompe. Ao colar o erro junto com o comando numa conversa, o modelo lê aquilo pela primeira vez, sem nenhuma expectativa sobre o que deveria estar ali. O que eu não conseguia ver depois de várias releituras, ele identificava imediatamente. Não é uso sofisticado — é o uso mais direto possível, e foi onde o tempo real foi economizado nesta implantação.
3.2 SSH e os primeiros problemas com o Docker
A execução começou com um ponto que eu havia deixado implícito no documento: o Ubuntu Server não instala o OpenSSH por padrão. Foi necessária presença física no servidor para instalar e ativar o serviço. A partir daí, tudo poderia ser feito remotamente via PuTTY.
A instalação do Docker trouxe o primeiro erro real. O comando para adicionar o repositório oficial — que eu havia escrito e revisado — continha dois problemas na mesma linha: o domínio estava como download.docker.om em vez de download.docker.com (um c ausente), e dois parâmetros adjacentes estavam sem o espaço que os separa. O apt update rodou sem erros visíveis. O repositório foi adicionado; só que apontava para um endereço que não existia. A falha só apareceu ao tentar instalar os pacotes:
E: Unable to locate package docker-ce
A mensagem não menciona repositório, URL nem domínio. Diz que o pacote não existe. A suspeita natural é de nome errado ou versão incompatível — não de uma letra faltando num endereço que "eu mesmo havia escrito corretamente". Colar o erro e o comando na IA foi o que apontou para o domínio e para o parâmetro colado.
Ao corrigir e refazer os passos, três comportamentos do procedimento padrão se revelaram frágeis — não por erro de digitação, mas por documentação incompleta do Docker:
-
gpg --dearmor aborta com File exists quando executado pela segunda vez, o que acontece inevitavelmente quando se refaz um passo durante troubleshooting. Silencia com --yes.
-
lsb_release -cs pode não estar presente em instalações minimizadas do Ubuntu. Quando ausente, o codinome da release fica em branco, e o repositório aponta para uma suite inexistente. O erro que aparece menciona os pacotes, não o codinome ausente.
O último ponto desta fase foi o newgrp docker, que aparece em muitos tutoriais como forma de ativar o grupo sem encerrar a sessão. Na prática, ele abre um subshell — a sessão original não é atualizada. A solução que funciona de forma confiável é encerrar o SSH e reconectar.
Com o Docker no ar, a subida da stack expôs dois problemas de natureza diferente: não eram erros de digitação, mas decisões que eu havia tomado no documento e que produziam comportamento inesperado na execução.
O primeiro: eu não havia publicado a porta 4000 do LiteLLM no docker-compose.yml. A omissão fazia sentido como princípio — não expor serviços internos à rede —, mas tornava inoperantes todos os comandos de validação dos passos seguintes, que dependem de curl localhost:4000. A correção foi publicar a porta apenas no endereço de loopback: 127.0.0.1:4000:4000. Acessível do próprio servidor ou por túnel SSH; invisível para a rede.
O segundo era de ordem: coloquei o comando para extrair o certificado raiz do Caddy três seções antes da subida dos contêineres. O contêiner não existia ainda; o certificado, tampouco. O comando falhava. A correção foi mover a extração para depois da primeira subida da stack.
Ainda na configuração, descobri que o Caddyfile que eu havia preparado respondia apenas pelo nome de domínio — e o DNS interno ainda não estava configurado. O acesso pelo navegador dependia de editar o arquivo hosts de cada máquina, o que não escala. A solução adotada para o piloto foi substituir o domínio por :80, tornando o servidor acessível por IP de qualquer máquina na rede sem depender de DNS nem de certificado. O TLS está comentado e documentado como pendente para antes do rollout; enquanto isso, senhas e conversas trafegam em texto claro dentro da rede interna.
Dois comportamentos se repetiram ao longo de toda a configuração e entraram no documento como avisos explícitos. Primeiro: editar o config.yaml do LiteLLM não provoca recarregamento. É preciso recriar o contêiner com --force-recreate, porque variáveis de ambiente são lidas apenas quando o contêiner nasce; restart sobe o mesmo contêiner com o ambiente anterior. Segundo: declarar uma chave de API no .env não é suficiente. A instrução api_key: os.environ/NOME_DA_VARIAVEL no config.yaml faz o LiteLLM buscar a variável no ambiente do contêiner — e para chegar lá, ela precisa ser repassada explicitamente na seção environment: do docker-compose.yml
A integração com o Google AI Studio produziu três problemas encadeados, cada um mascarando o seguinte.
O primeiro foi uma suspeita de credencial inválida: a chave começava com AQ. em vez do prefixo AIza habitual. A dúvida consumiu uma rodada de diagnóstico — até que o código HTTP de resposta a refutou. Um 401 significa credencial rejeitada; o retorno foi 429. Um 429 significa que o provedor reconheceu a credencial e a bloqueou por cota. A mensagem confirmou: o projeto estava em modo pré-pago com saldo zero. O código HTTP é o diagnóstico; o formato da chave não é.
Criado um projeto novo sem faturamento, veio o segundo problema: a cota gratuita é por projeto, não por conta. Um projeto que teve billing ativado em algum momento — mesmo que depois removido, mesmo sem crédito — não recebe o tier gratuito. Foi necessário criar um projeto nunca associado a faturamento. A confirmação veio pela nomenclatura das cotas nas mensagens de erro, que passaram a conter free_tier.
Com projeto e chave corretos, veio o terceiro: o modelo gemini-2.5-flash aparecia no endpoint de listagem e devolvia 404 na chamada direta. O catálogo mostra o que existe globalmente; a chamada mostra o que aquela chave alcança. O projeto antigo tinha acesso herdado a modelos que o novo não alcança. Esse episódio estabeleceu um princípio que se manteve em toda integração seguinte: listar, testar e só então cadastrar.
Dos 44 modelos listados, apenas 10 responderam a texto no tier gratuito — todos variações de Flash. Nenhum modelo Pro tem tier gratuito. O cardápio do piloto ficou honesto mas limitado.
Três problemas completaram esta fase.
Com 44 modelos listados pelo endpoint /v1/models, testar cada um manualmente seria inviável. Para resolver isso, foi escrito um script shell que consultava o LiteLLM para obter a lista de modelos cadastrados e, para cada um, enviava automaticamente uma mensagem de teste via API — registrando quais respondiam, quais retornavam erro e qual era o código HTTP. O objetivo era descobrir, de uma vez, quais modelos do catálogo eram de fato alcançáveis com aquela chave e naquele tier, sem precisar testar um por um pelo chat.
O primeiro problema com esse script estava na forma como ele percorria a lista. A versão original usava for M in $(...):
for M in $(curl -s http://localhost:4000/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
| jq -r '.data[].id'); do
echo -n "$M --> "
curl -s http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"Responda apenas: ok\"}]}" \
| jq -r '.choices[0].message.content // .error.message'
done
Essa construção divide os itens por espaço em branco. Nomes de modelos com espaço, como Gemini 2.5 Flash, eram fragmentados: o script enxergava três tokens separados (Gemini, 2.5, Flash) e tentava testar cada um como se fosse um modelo distinto — todos falhando com "modelo não encontrado". A correção foi substituir o laço por while IFS= read -r, que lê a lista linha a linha e preserva espaços internos:
curl -s http://localhost:4000/v1/models \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
| jq -r '.data[].id' \
| while IFS= read -r M; do
echo -n "$M --> "
curl -s http://localhost:4000/v1/chat/completions \
-H "Authorization: Bearer $LITELLM_MASTER_KEY" \
-H "Content-Type: application/json" \
-d "{\"model\":\"$M\",\"messages\":[{\"role\":\"user\",\"content\":\"Responda apenas: ok\"}]}" \
| jq -r '.choices[0].message.content // .error.message'
done
O efeito colateral — nomes com espaço passaram a exigir aspas em todo comando que os referenciasse — foi aceito como contrapartida da legibilidade no menu do usuário.
O mesmo sintoma — "modelo não encontrado" — apareceu em duas origens distintas: uma renomeação no arquivo de configuração que deixou o LiteLLM servindo um nome diferente do que o Open WebUI pedia, e conversas abertas no navegador presas ao modelo com que nasceram, com a lista em cache. O reinício do contêiner não resolve o segundo caso: o estado está no cliente. Só Ctrl+F5 e uma conversa nova limpam o cache.
O mais inusitado: um teste "falhou" com dezenas de linhas de command not found sem que o comando estivesse errado. O problema era a colagem — a saída anterior havia sido incluída junto com o texto do comando, com o caractere [200~ do protocolo de bracketed paste embaralhando tudo. A saída não fazia nenhum sentido. O diagnóstico com IA foi imediato: [200~ é a assinatura inequívoca do bracketed paste; printf '\e[?2004l' o desabilita na sessão corrente.
A primeira resposta real chegou em espanhol. Sem system prompt definido, o modelo infere o idioma pelo conteúdo do prompt. A configuração foi feita no Admin Panel do Open WebUI — não no painel individual, onde valeria apenas para um usuário.
4. Estado ao final da implantação inicial
Ao término desta fase, os cinco contêineres encontravam-se operacionais:
| Serviço | Estado | Portas expostas |
| Caddy | Ativo | 0.0.0.0:80 (HTTP por IP; TLS pendente) |
| LiteLLM | Ativo | 127.0.0.1:4000 (somente loopback) |
| PostgreSQL | Saudável | Nenhuma (rede interna do Docker) |
| Redis | Saudável | Nenhuma (rede interna do Docker) |
| Open WebUI | Saudável | Nenhuma (acessível via Caddy) |
O diagnóstico por camadas — do centro para a periferia, parando na primeira que falha — foi o que destravou a maioria das investigações:
-
O provedor aceita a chave? — chamada direta, sem LiteLLM
-
O LiteLLM leu o cardápio? — endpoint /v1/models
-
O LiteLLM chama o provedor? — endpoint /v1/chat/completions
-
O Open WebUI alcança o LiteLLM? — curl de dentro do contêiner do WebUI
-
O chat funciona? — navegador
Um achado derivado: log vazio depois de enviar um prompt é um resultado, não a ausência de um. Em um momento de diagnóstico, o log vazio provou que a requisição nem chegava ao LiteLLM — o problema estava em uma camada anterior.
5. Conclusão
Esta terceira parte documentou o caminho entre o início da execução e a obtenção da primeira resposta real. Os problemas encontrados foram, em sua maioria, simples do ponto de vista técnico: letras faltando, parâmetros colados, ordem trocada, comportamentos de ferramentas que diferiam do esperado.
O que essa lista não captura é o tempo que cada um consumiria sem apoio. Não porque fossem complexos, mas porque eram meus próprios pontos cegos. Eu havia escrito aquelas linhas com ajuda do Gemini, as havia relido, e ainda assim os erros passaram. O modelo de linguagem que gerou o documento avalia intenção e estrutura — não executa os comandos. Os erros que só aparecem em execução não aparecem na revisão.
Usar o Claude para diagnosticar o que o Gemini ajudou a escrever não é uma ironia — é uma demonstração de como ferramentas diferentes têm pontos fortes diferentes. Geração e execução são fases distintas; o suporte de IA em cada uma delas resolve problemas diferentes. Essa foi, talvez, a lição mais prática de toda a implantação.
A Parte 4 documenta o que aconteceu após a primeira resposta: a expansão do cardápio com HuggingFace e OpenRouter, a investigação de geração de imagem gratuita, a customização da interface — com os desafios que ainda precisam ser resolvidos antes do rollout.