Seu serviço do Compose expira contra uma API que o navegador na mesma máquina abre sem qualquer reclamação. Essa diferença é a pista: a requisição que funciona sai de um host com túnel VPN, e a que trava sai de um contêiner com sua própria pilha de rede. Nada dentro do contêiner conhece as rotas do túnel, então o destino privado continua inalcançável, não importa como o nome seja escrito. A correção é um salto deliberado pelo host, não uma tabela de rotas mais esperta.

Comece por por que os dois caminhos diferem e depois decida onde o túnel deve viver.

Por que o host funciona e o contêiner não

Subir o túnel dá ao host uma interface, um conjunto de rotas e, normalmente, uma entrada de resolvedor fornecida pela VPN. Um contêiner não herda nada disso: ele começa com uma interface na ponte do motor e uma rota padrão que aponta para essa ponte. As rotas instaladas no host são invisíveis para tudo o que roda dentro desse espaço de nomes.

Por isso os sintomas parecem inconsistentes e não simplesmente quebrados. O host resolve o nome privado e conecta, porque ali existem tanto o nome quanto a rota. O contêiner envia seu pacote para um endereço privado que nada no caminho da ponte consegue alcançar, e a requisição expira. Adicionar o nome ao arquivo hosts do contêiner não muda nada, porque o próprio endereço é inalcançável de onde o contêiner está.

Alcance o host de dentro do contêiner

Um processo no contêiner que disca localhost está discando o contêiner. O Docker Desktop publica host.docker.internal como um nome que aponta de volta para a máquina que executa o motor; os motores Linux o deixam indefinido a menos que o serviço peça, e é isso que o mapeamento abaixo faz. Com isso, a aplicação mira um nome e uma porta em vez do endereço privado.

services:
  app:
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      INTERNAL_API_HOST: "host.docker.internal:8666"

O túnel fica no lado do host do salto, e a porta nessa variável é aquela em que o proxy vai escutar. Um contêiner que perde o túnel agora falha em um único salto conhecido em vez de tentar uma rota que nunca teve.

Escolha onde o túnel vive

Três formas são possíveis para um serviço do Compose que precisa de uma API privada, e elas diferem em qual processo é dono do túnel. Um cliente VPN pode rodar dentro da imagem do contêiner, o que exige expor um dispositivo de túnel e conceder a capacidade de administração de rede que vem com ele. A aplicação pode compartilhar o espaço de nomes de rede de um contêiner de túnel separado, de modo que os dois processos veem uma pilha e uma tabela de rotas. Ou o túnel pode ficar no host, com um encaminhador estreito do host para o contêiner.

As duas primeiras puxam arquivos de perfil, comportamento de reconexão e política de resolvedor para dentro do projeto, e fazem cada serviço do arquivo depender da saúde do túnel. É acoplamento demais para uma única chamada de API. Para uma pilha de desenvolvimento que lê um único endpoint interno, o encaminhador do lado do host é a menor mudança que ainda mantém as credenciais onde a equipe de plataforma as administra.

Publique um único caminho estreito no host

Comece pelo que o encaminhador herda: execute-o com a conta que é dona do túnel e seus soquetes de saída assumem as rotas do host e o resolvedor do host. Um bloco location do nginx cobre um caminho.

server {
    listen 8666;
    location /search/ {
        proxy_pass http://10.20.30.40:8090/;
        proxy_set_header Host internal-api.example;
        proxy_read_timeout 60s;
    }
}

Trate o endereço e a porta como substitutos e coloque o par em que a API realmente escuta. Cada upstream adicional merece seu próprio bloco location, e cada bloco deve admitir apenas os caminhos que este serviço chama, para que o proxy não se torne uma porta geral para a rede privada. Amarre o ouvinte ao endereço da ponte para clientes que chegam via host-gateway, ao loopback só quando o cliente compartilha o espaço de nomes de rede do host, e nunca a 0.0.0.0. Escolha também uma porta ainda livre: um número já ocupado deixa o proxy sem escutar enquanto o contêiner continua expirando, o que é indistinguível da falha de rede com que você começou.

Portas, nomes e a decisão de TLS

A resolução de nomes se divide do mesmo jeito que o roteamento. As consultas públicas do contêiner passam pelo resolvedor do motor, então um nome privado que resolve no host não vai resolver dentro do contêiner. O proxy é o componente que resolve o nome privado, porque herda o resolvedor do host.

TLS é a decisão que é adiada e depois explica a próxima falha. Se o proxy encaminha os bytes intactos, o contêiner vê o certificado da própria API e precisa da AC interna em seu armazenamento de confiança. Se o proxy termina o TLS, essa confiança passa para o host e a aplicação começa a montar links a partir do cabeçalho host que recebe. Escolha um arranjo e aplique-o a cada recurso, porque um redirecionamento emitido pelo outro aponta para um nome de host que o contêiner não consegue resolver.

Verifique de dentro do serviço

Envie a requisição do contêiner, não do host: um curl simples contra o caminho do proxy e depois a verificação de saúde da própria aplicação. Quando o proxy responde mas a aplicação ainda falha, compare três coisas: a URL base que ela monta, qualquer variável de proxy em seu ambiente e a AC em que ela confia. Todas estão no ambiente e nos registros do contêiner, e corrigir qualquer uma delas deixa a VPN em paz.

Escreva o arranjo escolhido no README do projeto enquanto está fresco. Senão, a próxima pessoa que esbarrar nesse tempo esgotado passará a mesma tarde redescobrindo que o túnel vive no host.

Artigos relacionados