Streamlit, Nginx e IPv6: como um simples localhost causou erro intermitente em produção

Streamlit, Nginx e IPv6: como um simples localhost causou erro intermitente em produção

Publicado em 26/06/2026 · Redes

Descubra como um simples uso de localhost entre Nginx e Streamlit gerou erros intermitentes em produção. Veja a investigação completa, a cronologia do troubleshooting, a análise dos logs e a solução definitiva envolvendo IPv4, IPv6 e proxy reverso.

Streamlit, Nginx e IPv6: como um simples localhost causou erro intermitente em produção

Sumário

  1. O problema em produção
  2. Por que o erro parecia aleatório
  3. A primeira suspeita: login, Auth0 ou Streamlit
  4. O papel dos logs do Nginx
  5. A pista decisiva: http://[::1]:8501
  6. Entendendo o problema com localhost
  7. Por que o Streamlit recusava a conexão
  8. A correção no Streamlit
  9. A correção no Nginx
  10. Como validar a solução
  11. Lições aprendidas

Streamlit, Nginx e IPv6: como um simples localhost causou erro intermitente em produção

Em ambientes de produção, alguns problemas parecem grandes demais para serem causados por uma configuração pequena. Às vezes a aplicação funciona, às vezes falha. Às vezes o login carrega, às vezes não. Às vezes a página abre parcialmente, mas os arquivos JavaScript, recursos estáticos ou componentes internos quebram sem uma explicação óbvia.

Foi exatamente esse o cenário encontrado em um web app feito com Streamlit, publicado atrás de um Nginx atuando como proxy reverso.

O sintoma principal era simples de observar, mas difícil de interpretar: usuários tinham erros aparentemente aleatórios ao acessar a aplicação. O app não estava totalmente fora do ar. Esse era o detalhe traiçoeiro. Se estivesse tudo quebrado, o diagnóstico seria mais direto. O problema real era pior: ele falhava parcialmente.

O sintoma inicial: erro intermitente em produção

O aplicativo funcionava em muitos acessos, mas alguns usuários encontravam problemas como:

  • erro durante o login;
  • página parcialmente carregada;
  • JavaScript não carregando;
  • arquivos estáticos falhando;
  • comportamento instável;
  • falhas aparentemente sem padrão.

No Nginx, a mensagem recorrente era:

connect() failed (111: Connection refused)

Esse tipo de erro indica que o Nginx tentou se conectar ao backend, mas não encontrou ninguém escutando naquele endereço e naquela porta.

Em outras palavras: o Nginx bateu na porta, mas não havia serviço respondendo ali.

A primeira hipótese: Auth0, login ou queda do Streamlit

Como parte dos erros aparecia durante o login, a primeira suspeita natural foi o fluxo de autenticação. Poderia ser o Auth0? Poderia ser sessão expirada? Poderia ser reinício do Streamlit? Poderia ser instabilidade do servidor?

Essas hipóteses faziam sentido no começo, mas não explicavam tudo.

Se fosse apenas autenticação, os erros deveriam estar mais concentrados no fluxo de login. Se fosse queda total do Streamlit, o app ficaria indisponível de forma mais clara. Se fosse reinício do serviço, os erros apareceriam em janelas específicas, próximos aos horários de restart.

Mas o padrão era diferente.

Os erros apareciam todos os dias, durante um período longo, sem parecerem presos a um único evento operacional.

A cronologia dos logs mostrou que o problema era antigo

A investigação começou pelos logs do Nginx. Esse foi o ponto decisivo.

A primeira ocorrência encontrada era de:

2026/06/12 00:00:15

A última ocorrência analisada era de:

2026/06/26

Ou seja, o problema não era novo. Ele vinha acontecendo por aproximadamente duas semanas.

A contagem diária mostrava recorrência constante:

12/06 -> 41
13/06 -> 33
14/06 -> 34
15/06 -> 53
16/06 -> 48
17/06 -> 62
18/06 -> 39
19/06 -> 23
20/06 -> 45
21/06 -> 32
22/06 -> 45
23/06 -> 34
24/06 -> 24
25/06 -> 30
26/06 -> 39

O total encontrado foi:

582 erros

Depois de remover os próprios acessos, sobraram:

452 erros externos

Isso mudou completamente a interpretação. Não era apenas um erro local, nem algo causado por testes do próprio administrador. Usuários externos também estavam sendo afetados.

A pista principal: o upstream era IPv6

A descoberta mais importante apareceu dentro da própria linha de erro do Nginx.

Todas as mensagens apontavam para:

upstream: "http://[::1]:8501"

Esse detalhe é tudo.

O Nginx não estava tentando acessar:

127.0.0.1:8501

Ele estava tentando acessar:

[::1]:8501

O endereço ::1 é o equivalente IPv6 de localhost. Já 127.0.0.1 é o loopback em IPv4.

O problema, então, deixou de ser “o Streamlit caiu?” e passou a ser: o Nginx está tentando falar com o Streamlit por IPv6, mas o Streamlit está escutando nesse endereço?

A resposta era não.

O detalhe traiçoeiro: localhost não significa apenas IPv4

A configuração original do Nginx usava:

proxy_pass http://localhost:8501;

À primeira vista, isso parece correto. Afinal, o Streamlit está na mesma máquina. Então usar localhost parece natural.

Mas existe uma armadilha aí.

No arquivo /etc/hosts, havia:

127.0.0.1 localhost
::1 localhost

Isso significa que localhost pode resolver tanto para IPv4 quanto para IPv6.

Dependendo do processo, da configuração do sistema, da ordem de resolução e do comportamento da aplicação, localhost pode virar:

127.0.0.1

ou:

::1

No caso analisado, os logs provaram que o Nginx estava tentando várias conexões para:

::1

Só que o Streamlit não estava escutando em IPv6.

Como o erro acontecia na prática

A lógica do erro era esta:

Usuário acessa o site
        ↓
Nginx recebe a requisição
        ↓
Nginx usa proxy_pass para localhost:8501
        ↓
localhost resolve para ::1
        ↓
Nginx tenta conectar em [::1]:8501
        ↓
Streamlit não está escutando nesse endereço
        ↓
Connection refused

O problema não era a porta 8501 em si. O problema era a combinação entre:

  • Nginx usando localhost;
  • localhost podendo resolver para IPv6;
  • Streamlit não escutando em IPv6;
  • backend configurado de forma não explícita;
  • proxy reverso dependendo da resolução de nomes.

Essa é a parte mais importante do raciocínio: o erro não estava no fato de usar proxy reverso, nem no Streamlit, nem no Nginx isoladamente. O erro estava na falta de alinhamento entre o endereço usado pelo frontend e o endereço realmente escutado pelo backend.

O impacto real nos usuários

Foram contabilizadas:

2319 requisições

Removendo os próprios acessos, sobraram:

1655 requisições externas

Dessas, houve:

452 erros

A taxa aproximada foi:

27%

É importante interpretar esse número corretamente.

Isso não significa que 452 usuários perderam acesso. Uma única visita ao Streamlit pode gerar várias requisições:

  • /;
  • /_stcore/*;
  • /static/*;
  • /media/*;
  • websocket;
  • arquivos JavaScript;
  • recursos internos da aplicação.

Então uma única sessão problemática pode gerar vários erros no log.

Mesmo assim, o volume diário e a persistência por duas semanas indicavam impacto real. O bug não era cosmético. Ele podia afetar login, carregamento da interface, navegação e experiência geral do usuário.

A correção: tornar tudo explícito

A solução foi simples, mas conceitualmente muito importante: parar de depender de localhost.

Em produção, principalmente em proxy reverso, é melhor ser explícito.

Se o backend deve escutar apenas em IPv4 local, então configure o backend para isso e configure o Nginx para apontar exatamente para esse endereço.

Correção no Streamlit

A unit do systemd antes estava assim:

ExecStart=/home/streamlit/football.hacking/venv/bin/streamlit run /home/streamlit/football.hacking/app.py

Depois da correção, passou a ser:

ExecStart=/home/streamlit/football.hacking/venv/bin/streamlit run /home/streamlit/football.hacking/app.py --server.address=127.0.0.1 --server.port=8501

Com isso, o Streamlit passou a escutar explicitamente em:

127.0.0.1:8501

Isso também melhora a segurança, porque o Streamlit não precisa ficar exposto em todas as interfaces da máquina. Quem deve receber acesso externo é o Nginx. O backend pode ficar restrito ao loopback local.

Correção no Nginx

A configuração original era:

proxy_pass http://localhost:8501;

Ela foi alterada para:

proxy_pass http://127.0.0.1:8501;

Essa mudança remove a ambiguidade.

Agora o Nginx não precisa resolver localhost. Ele não escolhe entre IPv4 e IPv6. Ele simplesmente conecta no endereço exato onde o Streamlit está escutando.

A arquitetura fica assim:

Usuário
  ↓
Nginx
  ↓
127.0.0.1:8501
  ↓
Streamlit

Simples, direto e previsível.

Aplicando a correção com segurança

Antes de alterar arquivos em produção, foram feitos backups.

Backup da configuração do Nginx:

sudo cp /etc/nginx/sites-available/streamlit \
/etc/nginx/sites-available/streamlit.bak.$(date +%F-%H%M)

Backup da unit do systemd:

sudo cp /etc/systemd/system/streamlit.service \
/etc/systemd/system/streamlit.service.bak.$(date +%F-%H%M)

Depois da alteração na unit:

sudo systemctl daemon-reload

Reinício do Streamlit:

sudo systemctl restart streamlit

Reload do Nginx:

sudo systemctl reload nginx

Esse fluxo evita fazer mudanças cegas. Primeiro preserva o estado anterior, depois aplica a alteração e por fim recarrega apenas o necessário.

Verificando se o Streamlit está no endereço correto

Após a correção, o comando principal de validação foi:

sudo ss -ltnp | grep 8501

O resultado esperado era algo como:

127.0.0.1:8501

Esse comando responde uma pergunta objetiva: em qual endereço e porta o processo está escutando?

Se aparecer 127.0.0.1:8501, o backend está restrito ao loopback IPv4.

Se aparecer 0.0.0.0:8501, ele está escutando em todas as interfaces IPv4.

Se aparecer [::]:8501 ou [::1]:8501, há IPv6 envolvido.

Verificando se o erro voltou

Para monitorar especificamente o problema original:

sudo grep '\[::1\]:8501' /var/log/nginx/error.log

Ou, de forma mais ampla:

sudo grep 'Connection refused' /var/log/nginx/error.log

O esperado é não aparecerem novas ocorrências apontando para:

http://[::1]:8501

Se esse padrão não voltar, a causa raiz foi eliminada.

A lógica intuitiva do diagnóstico

A linha de raciocínio correta foi:

  1. Existe erro no usuário.
  2. O erro aparece no Nginx como Connection refused.
  3. Connection refused significa que o Nginx tentou conectar, mas não havia serviço naquele destino.
  4. O destino real não era 127.0.0.1:8501, mas [::1]:8501.
  5. [::1] é IPv6.
  6. O Streamlit não estava escutando nesse IPv6.
  7. O Nginx usava localhost.
  8. localhost podia resolver para IPv4 ou IPv6.
  9. A correção era remover a ambiguidade.
  10. Nginx e Streamlit passaram a usar explicitamente 127.0.0.1:8501.

Essa é a diferença entre trocar configurações no chute e resolver o problema pela causa raiz.

Lições aprendidas

A primeira lição é que localhost parece simples, mas nem sempre é explícito. Em sistemas modernos, ele normalmente aponta para IPv4 e IPv6.

A segunda lição é que proxy reverso deve ser previsível. Se o backend escuta em 127.0.0.1, o proxy deve apontar para 127.0.0.1.

A terceira lição é que logs não servem apenas para confirmar erro. Eles contam a história do erro. Nesse caso, o detalhe upstream: "http://[::1]:8501" foi a chave de todo o diagnóstico.

A quarta lição é que problemas intermitentes nem sempre são realmente aleatórios. Muitas vezes existe um padrão escondido. O trabalho é encontrar onde esse padrão aparece.

A quinta lição é que pequenas ambiguidades em produção podem gerar impacto real. Um simples localhost foi suficiente para causar falhas durante dias.

Conclusão

Esse problema envolvendo Streamlit, Nginx, localhost e IPv6 mostra como uma falha aparentemente aleatória pode nascer de uma configuração pequena, mas ambígua.

O Nginx estava configurado para enviar requisições para localhost:8501. Porém, no servidor, localhost podia resolver tanto para 127.0.0.1 quanto para ::1. Como o Streamlit não estava escutando em IPv6, parte das conexões feitas pelo Nginx para [::1]:8501 falhava com connect() failed (111: Connection refused).

A correção foi tornar tudo explícito: configurar o Streamlit para escutar em 127.0.0.1:8501 e configurar o Nginx para usar proxy_pass http://127.0.0.1:8501.

No fim, a solução técnica foi curta. Mas o valor real esteve no processo: observar o sintoma, abandonar hipóteses fracas, seguir os logs, identificar o endereço real do upstream e alinhar frontend e backend no mesmo protocolo.

Em produção, clareza vence conveniência. E, muitas vezes, trocar localhost por 127.0.0.1 não é detalhe: é a diferença entre um serviço instável e um serviço previsível.

Perguntas comuns

1. Por que localhost pode causar problema no Nginx?

Porque localhost pode resolver tanto para IPv4 quanto para IPv6. Se o backend não estiver escutando nos dois protocolos, parte das conexões pode falhar.

2. O erro era do Streamlit?

Não exatamente. O Streamlit funcionava, mas não estava escutando no endereço IPv6 usado em algumas tentativas do Nginx.

3. Por que usar 127.0.0.1 é melhor nesse caso?

Porque elimina a resolução de nomes e força o uso de IPv4 local, exatamente onde o Streamlit está escutando.

4. Isso significa que IPv6 é ruim?

Não. O problema não é o IPv6. O problema é o desalinhamento: o Nginx tentou usar IPv6, mas o backend não estava preparado para isso.

5. Como evitar esse erro em outros serviços?

Configure explicitamente o endereço do backend e use o mesmo endereço no proxy reverso. Evite localhost quando o serviço não estiver escutando em IPv4 e IPv6.