Saudações. Nesse artigo vamos explorar um item vital para a Internet: um componente HTTP chamado proxy-reverso, suas implementações, principais softwares e exemplos práticos.
Abordaremos os softwares: Nginx, Nginx Proxy Manager, Lighttpd, Traefik, Caddy e Squid.
Pre-requisitos pra praticar os exemplos:
- Servidor, VM ou VPS com Linux Debian 12 e Docker;
- O Linux deve possuir IP público (IPv4) e IPv6 global;
- Ter pelo menos 1 domínio (DNS) para os testes de URLs (vou usar exemplo.com.br e você muda para o seu);
- Comando “curl” para os testes;
Instale os programas básicos que vamos usar:
# Instalar utilitarios de sistema
apt-get -y install psmisc
apt-get -y install iproute2
apt-get -y install net-tools
# Instalar linguagens utilizadas nos exemplos
apt-get -y install python3
# Instalar programas básicos de HTTP
apt-get -y install curl
apt-get -y install wget
apt-get -y install lighttpd
apt-get -y install nginx
apt-get -y install apache2-tools
# Parar sistemas que se auto-iniciaram nas instalacoes acima:
systemctl stop nginx
systemctl disable nginx
systemctl stop lighttpd
systemctl disable lighttpd;
1 – O básico sobre o protocolo HTTP
O protocolo HTTP é de longe o protocolo mais simples e mais usado na Internet. Ele foi concebido para ser uma troca simples de arquivos e acabou dominando a forma de usar a Internet.
O HTTP é uma arquitetura cliente-servidor. O servidor é um software passivo, ele apenas entra em operação quando o cliente envia uma requisição para ele. As requisições e respostas seguem o mesmo formato de texto: tipo (primeira linha) + cabeçalhos (1 por linha) + linha em branco + conteúdo (opcional).
Regras básicas de comunicação entre o cliente e o servidor:
- A primeira linha da requisição deve ser o método, lista de todos os métodos: GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE;
- A primeira linha da resposta deve ser o protocolo em uso e o código de retorno;
- As linhas seguintes devem ser pares: nome do parâmetro de cabeçalho, “:” (dois-pontos) e o valor;
- Cada linha do cabeçalho deve ser finalizada com uma quebra de linha composta pelos caracteres CR+LR (CR = “\r” = Carriage Return, código decimal 13 da tabela ASCII, LF = “\n” = Line Feed código decimal 10 da tabela ASCII);
- Quando os parâmetros de cabeçalho se encerrarem, uma linha em branco deve ser inserida, ou seja, CR+LF do cabeçalho anterior e CR+LF de uma linha vazia, assim a combinação CR+LF+CR+LF marca o fim do cabeçalho.
- Apos a linha em branco, opcionalmente, pode ser enviado mais bytes, esse é o conteúdo anexado na requisição (upload ou envio de formulário) ou, na resposta (o documento solicitado);
Exemplo do requisição:
# Requisicao ao google:
curl -v https://www.google.com/robots.txt
# Retorno do comando acima:
# Estabelecendo conexao TCP...
# Estabelecendo canal TLS...
# Requesicao enviada ao servidor:
GET /robots.txt HTTP/2
Host: www.google.com
User-Agent: curl/8.7.1
Accept: */*
(linha em branco, fim da mensagem)
# Resposta do servidor ao cliente:
HTTP/2 200
accept-ranges: bytes
vary: Accept-Encoding
content-type: text/plain
content-length: 9340
date: Thu, 23 Jan 2025 14:52:05 GMT
expires: Thu, 23 Jan 2025 14:52:05 GMT
cache-control: private, max-age=0
last-modified: Thu, 09 Jan 2025 17:00:00 GMT
server: sffe
(linha em branco)
[texto da resposta, muito longo pra colar aqui!]
A resposta segue o mesmo padrão da requisição, no entanto, a primeira linha informa o protocolo (HTTP) e versão, seguido de um espaço e o código de retorno. Códigos:
- 1xx – resposta informativa (não é usada)
- 2xx – resposta conclusiva – o que você pediu foi atendido
- 3xx – resposta dispersa – você deve ir a outro lugar (nova URL)
- 4xx – resposta negativa – o pedido não pode ser atendido, arquivo não existe ou restrições foram aplicadas para impedir o envio;
- 5xx – resposta de pane – alguma coisa deu errado e o servidor HTTP não consegue processar até o final seu pedido (falha na cadeia de softwares no lado servidor);
Viu como é simples? Os outros detalhes são corriqueiros e simples de aprender.
Vou apresentar os cabeçalhos mais comuns, marcando com [S] os cabeçalhos enviados do servidor para o cliente, [R] nos cabeçalhos que constam na requisição, e [*] para cabeçalhos usados em ambas as operações;
- [R] GET, POST, DELETE, HEAD: o tipo de pedido que o cliente deseja fazer, é a primeira linha da requisição e não é separado por “:”, é seguido do PATH (caminho, exemplo: /chat, /clientes/lista, /client/info?id=1234). Se o caracter “?” estiver presente no PATH, tudo depois do “?” se chama “query” (exemplo: ?id=1234) e costuma ser usado para orientar os detalhes do pedido quando o cliente não deseja submeter isso no corpo;
- GET: o cliente deseja obter um conteúdo;
- POST: o cliente deseja submeter uma informação que irá determinar como o servidor irá responder, usado para criar ou atualizar o lado servidor;
- HEAD: semelhante ao método GET, mas visa obter apenas o cabeçalho da resposta, o servidor não deverá enviar conteúdo;
- DELETE: o cliente deseja remover algum arquivo ou objeto do servidor;
- [S] Server: o servidor HTTP informa ao cliente qual software está processando a requisição (normalmente é o nome do próprio software servidor HTTP);
- [R] User-Agent: nome do software utilizado pelo cliente (nome do navegador ou APP);
- [R] Host: nome de DNS do site desejado, ou IP do site desejado (ex.: google.com, 200.160.2.3);
- [*] – Content-Type: tipo de conteúdo que está sendo negociado. O cliente pode pedir um tipo, mas o servidor pode mandar outro. Tipos mais comuns:
- text/html – páginas HTML;
- text/plain – texto puro;
- text/css – texto com código CSS de personalização das páginas HTML;
- text/csv – texto de planilhas CSV;
- text/javascript – texto com código javascript (software da página);
- image/jpeg – Imagem;
- image/png – Imagem;
- application/json – texto de dados estruturados JSON;
- application/octet-stream – tipo indefinido ou binário detectado (download);
- [*] Content-Length: quando presente, informa qual o tamanho do documento presente no corpo da requisição ou no corpo da resposta. Costuma ser omitido mesmo quando há conteúdo presente;
- [S] Date: data universal de criação do conteúdo;
- [S] Expires: informa a data universal limite de vida do conteúdo, é usado pelo navegador para saber por quanto tempo o arquivo/objeto pode ser mantido no cache do cliente;
- [S] Cache-Control: configurações para que o objeto seja armazenado em cache;
- [S] Location: nova URL a ser usada (redirecionamento), acontece em respostas com código 3xx pois o servidor moveu o conteúdo para outra URL;
Quando você abre um navegador para acessar um site (https://google.com por exemplo), seu navegador envia um “Get /, Host google.com” e recebe “HTTP/2 200, Content-Type: text/html” seguido do texto HTML da página inicial do Google.
Partes de uma URL:

Se a porta (“port”) não for informada, o software cliente presumirá:
- Para o esquema (“scheme”) http, porta 80;
- Para o esquema (“scheme”) https, porta 443;
2 – O servidor HTTP
O servidor HTTP é o software que será executado abrindo alguma porta TCP para escutar os pedidos (requisições) dos clientes, normalmente a porta 80 (HTTP) ou 443 (HTTP+TLS).
Antigamente haviam poucos softwares para isso, dada a simplicidade da função, o Apache dominou por muito tempo.
Eu particularmente gosto muito do Lighttpd e do Nginx para servidores simples e pontuais.
A função do servidor HTTP é mapear o site “www.seusite.com” (DNS apontando para o IP, exemplo: IPv4=45.255.128.2 e IPv6=2804:CAF9:1234::2) em uma pasta do servidor, por exemplo, /var/www/htdocs/.

Ao enviar um pedido “GET /arquivo.txt“, o servidor vai no caminho /var/www/htdocs/arquivo.txt, lê o arquivo, coloca ele no corpo da mensagem que será iniciada assim “HTTP/1 200“. Se o arquivo não existisse, a resposta seria “HTTP/1 404” sem nada no corpo da resposta.
Praticando o exemplo acima:
# Diretorio de base:
mkdir -p /var/www/htdocs
# Arquivos de exemplos:
echo "O rato roeu a roupa do rei de roma" > /var/www/htdocs/arquivo.txt
echo '{ "name": "Patolino", age: "6" }' > /var/www/htdocs/data1.json
echo -n "1" > /var/www/htdocs/1byte.txt
# Configuracao minima do lighttpd:
(
echo 'server.port = 808'
echo 'server.document-root = "/var/www/htdocs"'
) > /etc/lighttpd/test.conf
# Rodar o servidor HTTP com lighttpd para servidor arquivos em uma pasta:
killall lighttpd
lighttpd -f /etc/lighttpd/test.conf
# Testando funcionamento (127.0.0.1 é o ip de loopback, se chama localhost)
curl -v http://localhost:808/arquivo.txt
#* Trying 127.0.0.1:80...
#* Connected to localhost (127.0.0.1) port 80 (#0)
# Requisicao:
#> GET /arquivo.txt HTTP/1.1
#> Host: localhost
#> User-Agent: curl/7.88.1
#> Accept: */*
#>
# Resposta:
#< HTTP/1.1 200 OK
#< Content-Type: application/octet-stream
#< Content-Length: 35
#< Accept-Ranges: bytes
#< Date: Fri, 24 Jan 2025 15:44:03 GMT
#< Server: lighttpd/1.4.69
#<
#O rato roeu a roupa do rei de roma
#* Connection #0 to host localhost left intact
# Parar o lighttpd de teste:
killall lighttpd
O servidor HTTP faz a deteção automática do tipo do arquivo, e responde o tipo MIME no cabeçalho Content-Type, há um tipo padrão informado na configuração e há o arquivo mime com a extensão do arquivo e o tipo (.txt = text/plain, .html = text/html, .css = text/css, …). Na ausência do arquivo MIME o tipo padrão costuma ser “text/html” ou “application/octet-stream”.
Scripts no lado servidor: CGI e FastCGI
Uma tecnologia vital foi a implementação do CGI e FastCGI. Esse recurso permite ao servidor HTTP vincular um caminho da URL a um programa que será executado para produzir o conteúdo. O conteúdo é produzido em tempo real de acordo com os detalhes do pedido, e assim surgiu o PHP e a Web 2.0 (sites de conteúdo dinâmico e interativo).
Observe esta configuração do Lighttpd:
server.port = 810
server.document-root = "/var/www/htdocs"
fastcgi.server += ( ".php" =>
((
"socket" => "/run/php/php-fpm.sock",
"broken-scriptfilename" => "enable"
))
)
Ela informa que toda URL terminada com “.php” deve ser enviada ao SOCKET UNIX do PHP (php-fpm). O php-fpm recebe uma mensagem do servidor HTTP contendo o arquivo solicitado e as variáveis HTTP (Query-String), executará o script PHP, e o retorno do script é devolvido ao servidor HTTP para ser servido ao cliente.
Scripts que rodam como HTTP Server
Outra evolução no uso de HTTP foi que as linguagens de programação passaram a implementar a abertura da porta TCP e a interpretarem diretamente a requisição. Python, NodeJS e Ruby são os mais comuns.
Com essa recurso, seu programa escrito em Python pode abrir a porta TCP, receber os pedidos em HTTP, fazer o “roteamento” de cada pedido para uma função interna.
Algoritmo simples para processar HTTP (implementação de API simples, gravar em /root/py-server-api-ex01.py):
- Abrir a porta TCP 820 como servidor HTTP;
- Criar as rotas para cada tipo de pedido:
- Quando for solicitado “Get /”, acionar a função home_page();
- Um “GET /client/list” aciona a função clients_list();
- Um “GET /client/123” aciona a função client_get(123);
- Um “POST /client/123/disable” aciona a função client_disable(123);
from http.server import HTTPServer, BaseHTTPRequestHandler
import re
# Funções para tratar as rotas
def home_page():
return {"status": "1", "message": "Welcome to the home page!"}
def clients_list():
return {"status": "1", "clients": ["Client A", "Client B", "Client C"]}
def client_get(client_id):
return {"status": "1", "id": client_id, "name": f"Client {client_id}"}
def client_disable(client_id):
return {"status": "1", "id": client_id, "msg": f"Client {client_id} disabled"}
# Manipulador HTTP
class MyRequestHandler(BaseHTTPRequestHandler):
def do_GET(self):
# Rota "/"
if self.path == "/":
response = home_page()
# Rota "/client/list"
elif self.path == "/client/list":
response = clients_list()
# Rota "/client/<id>"
elif match := re.match(r"^/client/(\d+)$", self.path):
client_id = int(match.group(1))
response = client_get(client_id)
# Rota "/client/<id>/desativar"
elif match := re.match(r"^/client/(\d+)/disable$", self.path):
client_id = int(match.group(1))
response = client_disable(client_id)
else:
self.send_response(404)
self.end_headers()
self.wfile.write(b'{"status": "error", "message": "Not Found"}')
return
# Responder com sucesso
self.send_response(200)
self.send_header("Content-type", "application/json")
self.end_headers()
self.wfile.write(bytes(str(response), "utf-8"))
# Iniciar o servidor HTTP
def run_server(port=820):
server_address = ("", port)
httpd = HTTPServer(server_address, MyRequestHandler)
print(f"Starting server on port {port}...")
httpd.serve_forever()
if __name__ == "__main__":
run_server(820)
Rodando script Python e consultando nossa API de teste:
# Rodar servidor API python:
python3 /root/py-server-api-ex01.py \
1>/var/log/pysrv-ex01.log \
2>/var/log/pysrv-ex01.log &
# Chmando API:
curl -v http://localhost:820/client/list
#* Trying 127.0.0.1:820...
#* Connected to localhost (127.0.0.1) port 820 (#0)
# Requisicao:
#> GET /client/list HTTP/1.1
#> Host: localhost:820
#> User-Agent: curl/7.88.1
#> Accept: */*
#>
# Resposta:
#* HTTP 1.0, assume close after body
#< HTTP/1.0 200 OK
#< Server: BaseHTTP/0.6 Python/3.11.2
#< Date: Fri, 24 Jan 2025 16:11:37 GMT
#< Content-type: application/json
#<
#* Closing connection 0
# Conteudo retornado:
#{'status': '1', 'clients': ['Client A', 'Client B', 'Client C']}
Evolução dos ambientes
Algumas coisas aconteceram depois de 2010 que tornaram as coisas mais complexas:
- A invenção do Docker e os ambientes com container;
- A migração em massa da internet HTTP para HTTP+SSL/TLS para proteger as informações como padrão de segurança obrigatório;
- Os navegadores modernos (Firefox, Safari, Google Chrome) passaram a exigir configurações de segurança mais elevadas, forçando HTTPs como padrão;
- A necessidade de balancear a carga de muitas requisições em vários servidores (load-balance), bem como o monitoramento em tempo real de quais servidores internos estão disponíveis para receber esses encaminhamentos;
- Os sistemas se tornaram mais heterogêneos, sites passaram a contar com muitas tecnologias agregadas, frameworks, APIs, web-sockets, anti-bots, atendimento diferenciado por geolocalização (GEOIP DNS Split-Horizon);
- A migração gradual para o IPv6, enquanto mantem o legado IPv4 em produção;
- As metodologias de teste e publicação de código em produção (atualizar um site em partes sem quebrar o funcionamento do sistema);
- A migração em massa para HTTP, WebSockets e JSON como método padrão de interoperabilidade;
- Softwares antigos foram substituídos por softwares mais avançados e flexíveis, como NGINX, Lighttpd, Traefik, Caddy, entre outros, mais focados em recursos de proxy-reverso;
- A implementação de TLS foi automatizada por meio do protocolo ACME (Automated Certificate Management Environment), o Let’s Encrypt surgiu permitindo que todo site obtesse um certificado assinado e reconhecido mundialmente sem custo;
A lista acima vai crescendo. Fica impraticável manter um software “monólito” que faça isso tudo.
É melhor deixar um software cuidando de todas elas, e seu aplicativo ou software fica apenas com o processamento de dados que serão trocados.
3 – O proxy-reverso
Um bom exemplo para entender a necessidade de um proxy-reverso é seguinte caso:
- Temos um site de vendas online, ex.: CompreFlix – compreflix.com;
- Todos os acessos ao diretório (path) /data deve ser responsabilidade do software de CRM;
- Todos os acessos ao diretório /pay devem ser responsabilidade do software de loja virtual;
- Todos os acessos a /support deve ser processada pelo sistema de Help-Desk;
O software que abriu a porta TCP (80 e 443) receberá todas as requisições, mas ele deve rotear a requisição (encaminhar de forma transparente) para diferentes sub-sistemas que estão rodando em outros softwares.

Essa é a função do proxy-reverso.
O proxy-reverso pode, e deve, ser responsável por gerir o certificado de criptografia, atender nas portas 80 (HTTP), 443 (HTTPs) e demais portas necessárias para gestão (8080 por exemplo) e enviar as requisições programadas para os servidores internos.
4 – Proxy Reverso em ambiente Docker
Embora seja comum rodar o proxy-reverso direto no ambiente do servidor (HOST), isso tem como efeito colateral a dificuldade de criar os apontamentos quando as aplicações HTTP internas estão em containers Docker. Você precisaria fixar o IP de todas elas para ter uma configuração confiável, e perderia na flexibilidade (escala, balanceamento, etc…).
A opção mais encorajada e comum é rodar o proxy-reverso em Docker, em uma rede (docker network) personalizada (“network_public”, “apps”, “br-ias”, etc..), visto que na rede nativa do docker o mapeamento automático de nomes e containers não funciona.
Cuidado: o Docker redireciona portas por meio de firewall do Linux (camada prerouting do netfilter) e isso faz com que a porta redirecionada no Docker e mapeada para um container vença a porta do software que você rodou no HOST.

No exemplo acima, podemos ter o site online (exemplo.com.br) e cada diretório (/clients, /support) será entregue ao container responsável.
A importância de criar uma “docker network”, acima nomeada como “network_public” para colocar todos os containers juntos do proxy-reverso é o recurso de mapeamento de nomes no DNS dos containers. O proxy pode usar “http://api-clients” que a camada de DNS do container mapeará automaticamente para o IP que foi atribuído ao container com o nome “api-clients”.
Para tornar seus sites, APPs e APIs compatíveis com IPv6, basta que o servidor (HOST) tenha IPv6 global e que você entregue um docker network com IPv6 (privado, local). Dessa forma as conexões de entrada em IPv4 e IPv6 serão entregues ao seu proxy-reverso, mas o proxy reverso pode entregar as requisições HTTP para os containers e servidores internos que não possuem IPv6 (somente IPv4):

Exemplo de criação da rede “network_public” no Docker, IPv4-Only e IPv4+IPv6:
# Criar rede para APPs atendidos por proxy-reverso
# - No linux, a interface se chamará br-net-public
# - No docker, a rede se chamara network_public
# - o ICC=true ativa a comunicacao entre containers
# - Colocar MTU em 9000 para acelerar a comunicação entre containers
# - As opcoes '-o' podem ser removidas, se desejar
# Somente IPv4:
docker network create \
-d bridge \
-o "com.docker.network.bridge.name"="br-net-public" \
-o "com.docker.network.bridge.enable_icc"="true" \
-o "com.docker.network.driver.mtu"="9000" \
--subnet 100.90.0.0/16 \
network_public
# Dual-Stack - IPv4 + IPv6:
docker network create \
-d bridge \
-o "com.docker.network.bridge.name"="br-net-public" \
-o "com.docker.network.bridge.enable_icc"="true" \
-o "com.docker.network.driver.mtu"="9000" \
--subnet 100.90.0.0/16 \
--ipv6 --subnet=2001:db8:100:90::/64 \
network_public
# Comandos para verificar a rede no Linux (apos criar com os comandos acima):
# ip addr show dev br-net-public
# ip neigh show dev br-net-public
# brctl show br-net-public
# tcpdump -pnevas0 -i brctl show br-net-public
5 – Nginx Proxy Manager (NPM)
O NPM – Nginx Proxy Manager é um dos melhores. Baseado em NGINX e com plugins que tornam a configuração e a automação muito simples.
Pontos fortes:
- Interface muito amigável e fácil de operar;
- Configura automaticamente os certificados TLS usando provedores gratuitos (ACME – Let’s Encrypt);
- Possui recursos de segurança para restringir acessos a URLs;
- Agrupa sites em certificados wildcard (certificado *.exemplo.com.br pode ser usado em vários containers, permitindo uso de wildcard de DNS);
Pontos fracos:
- Difícil de automatizar para que containers criados com “labels” sejam automaticamente mapeados no NPM sem necessidade de criação manual do mapeamento da URL;
Criar container NPM:
# Criar container NPM na rede network_public
# npm = nginx-proxy-manager
# Criar diretorio para armazenar no HOST os dados produzidos dentro do container
mkdir -p /storage/npm-app/data
mkdir -p /storage/npm-app/letsencrypt
# Remover container caso exista:
docker rm -f npm-app
# Remover imagem atual para forçar reconstrução com imagem atualizada:
docker rmi jc21/nginx-proxy-manager:latest
# Baixar imagem nova (opcional, o comando docker run abaixo ja faz isso)
docker pull jc21/nginx-proxy-manager:latest
# Criar container:
# - nome: npm-app
# - portas:
# 80 = HTTP
# 81 = Gerencia
# 443 = HTTPs (HTTP+TLS)
# - pastas:
# /storage/npm-app/data mapeada em /data dentro do container
# /storage/npm-app/letsencrypt mapeada em /etc/letsencrypt dentro do container
#
# - banco de dados de login e mapeamentos salvos no arquivo
# /data/database.sqlite dentro do container, que será mapeado no caminho
# /storage/npm-app/data/database.sqlite
#
docker run \
-d --restart=unless-stopped \
\
--name npm-app -h npm-app.intranet.br \
--network network_public \
\
-p 80:80 \
-p 81:81 \
-p 443:443 \
\
-e DB_SQLITE_FILE=/data/database.sqlite \
\
-v /storage/npm-app/data:/data \
-v /storage/npm-app/letsencrypt:/etc/letsencrypt \
\
jc21/nginx-proxy-manager:latest
# Acesse......: http://ip-do-servidor:81/
# Login padrao: admin@example.com / changeme
# Nota:
# - sempre faça backup do /storage do HOST
# - destruir e recriar o container do NPM nao deve ser problema
# pois os dados sempre ficam fora do container e serão os mesmos
# toda vez que você roda-lo
Tela de login inicial (porta TCP 81), usuário admin@example.com senha changeme:

Altere o email no primeiro login (o email será o novo usuário para autenticação):

Altere a senha (senha padrão changeme):

O primeiro passo no NPM é cadastrar os nomes de DNS (FQDN) que serão usados no menu de certificados “SSL Certificates“. Você deve cadastrar 1 nome de cada vez, evite agregar vários nomes juntos (não funciona todas as vezes por restrições no provedor ACME / Let’sEncrypt).

Troque “exemplo.com.br” pelo seu domínio real que aponta para o IPv4/IPv6 do seu servidor rodando NPM. Registre em seguida o nome “www.exemplo.com.br“.

Certificado registrado e funcional via Let’s Encrypt. O NPM renovará o certificado automaticamente antes do vencimento. Você não precisa se preocupar nunca mais!

Precisamos de um container para hospedar o site em si, mesmo que de exemplo (hello-world), vamos fazer isso com um container lighttpd para servidor uma página inicial de exemplo:
# Criar a pasta no HOST:
mkdir -p /storage/hello-world-home
echo 'Ola mundo, deu certo' > /storage/hello-world-home/index.html
# Rodar o container chamado hello-world-home
# - Observe que não há porta mapeada, esse container é 100% privado
docker run -d \
-d --restart=unless-stopped \
--name hello-world-home -h hello-world-home.intranet.br \
--network network_public \
\
-v /storage/hello-world-home:/var/www/html:ro \
\
rtsp/lighttpd
Vamos criar um redirecionamento no NPM de tudo que for para o “exemplo.com.br” para o container “hello-world-home” porta 80. Vá no menu “Hosts” -> “Proxy Hosts“:

Adicione o nome de DNS (exemplo.com.br) em “Domain Names” (pode ser mais de um). NÃO clique em “SAVE”, vá em “SSL” em seguida.

Selecione o certificado registrado com o mesmo nome configurado:

Salve clicando em “SAVE”. O apontamento foi realizado com sucesso. Testando:
# Teste:
curl -v https://exemplo.com.br/
#* Using Stream ID: 1 (easy handle 0x5611e3deace0)
#> GET / HTTP/2
#> Host: exemplo.com.br
#> user-agent: curl/7.88.1
#> accept: */*
#>
#< HTTP/2 200
#< server: openresty
#< date: Fri, 24 Jan 2025 22:20:52 GMT
#< content-type: text/html
#< content-length: 21
#< etag: "2574584986"
#< last-modified: Fri, 24 Jan 2025 21:20:35 GMT
#< accept-ranges: bytes
#< strict-transport-security: max-age=63072000;includeSubDomains; preload
#< x-served-by: exemplo.com.br
#<
#Ola mundo, deu certo
Com o domínio inteiro recebido, agora vamos redirecionar uma pasta específica do site para outro container, vamos criar um container exemplo de API:
# Criar a pasta no HOST:
mkdir -p /storage/hello-world-api
echo '{ "nome": "Patolino", "age": "6" }' > /storage/hello-world-api/index.json
# Rodar o container chamado hello-world-home
# - Observe que não há porta mapeada, esse container é 100% privado
docker run -d \
-d --restart=unless-stopped \
--name hello-world-api -h hello-world-api.intranet.br \
--network network_public \
\
-v /storage/hello-world-api:/var/www/html:ro \
\
rtsp/lighttpd
Edite o HOST que responde pelo domínio “exemplo.com.br” inteiro, menu “Hosts” -> “Proxy hosts” -> vá na linha do Host, no final da linha clique nos três pontinhos “…” e vá no “Edit“:

Vá na guia “Custom locations” e adicione uma localização em “Add location“, no exemplo abaixo, tudo que for para “/api” será enviado para o container “hello-world-api” porta 80. Você pode clicar várias vezes em “Add location” para adicionar mais redirecionamentos de pastas do seu domínio para um container específico:

Com isso aprendemos os principais recursos de proxy-reverso do NPM, navegue pelo software para aprender novos recursos como autenticação, lista de acessos, etc…
Eu recomendo que você evite os redirecionamentos de pastas “/api-client“, no lugar, prefira criar entradas de DNS como “api-client.exemplo.com.br” e entregar o nome inteiro para o container alvo. Isso facilita hospedar sub-domínios em locais diferentes sem um depender do outro no fluxo de conexões TCP/HTTP.
Pouca configuração é melhor que muita configuração!
Destrua tudo que foi construído nesse capítulo para praticar o próximo no mesmo ambiente:
# Remover NPM
docker stop npm-app
docker rm npm-app
# Remover containers lighttpd
docker rm -f hello-world-home
docker rm -f hello-world-api
6 – Traefik
O favorito de quem sobe muitos containers e não tem tempo a perder!
Embora o Traefik consiga fazer tudo que o NPM faz, ele é MUITO chato de configurar na mão, em contra-partida, você consegue subir todo serviço de maneira automática sem tocar no Traefik, tudo isso graças ao mapeamento de “labels” dos containers.
O Traefik pode ser conectar ao Docker e detectar novos containers que tenham os labels instruindo o redirecionamento desejado.
Rodando o Traefik:
# Criar container Traefik (básico)
# Diretorio de dados persistentes
mkdir -p /storage/traefik-app/letsencrypt
# Remover container caso exista:
docker rm -f traefik-app
# Remover imagem atual para forçar download com imagem atualizada:
docker rmi traefik:latest
# Obter imagem (no momento, latest = v3.x):
# - opcional, o comando docker run abaixo ja faz isso
docker pull traefik:latest
# Criar container:
# - nome: traefik-app
# - Colocar na rede network_public
# - portas:
# 80 = HTTP
# 443 = HTTPs (HTTP+TLS)
# - volume mapeado:
# arquivo /var/run/docker.sock mapeado em /var/run/docker.sock no container
# - argumento especial '--providers.docker' instrue o Traefik a consultar
# o socket unix do Docker para obter suas configurações.
#
docker run \
-d --restart=unless-stopped \
\
--name traefik-app -h traefik-app.intranet.br \
--network network_public \
\
-p 80:80 \
-p 443:443 \
\
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /storage/traefik-app/letsencrypt:/etc/letsencrypt \
\
traefik:latest \
--api=true \
--providers.docker=true \
--entrypoints.web.address=:80 \
--entrypoints.websecure.address=:443 \
--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
--certificatesresolvers.letsencrypt.acme.email=seu-email@exemplo.com.br \
--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json
Observe que mapeamos o caminho do socket unix (/var/run/docker.sock) do Docker (HOST) no mesmo caminho dentro do container, em modo somente leitura (“:ro”). Isso dá ao Traefik acesso à configuração do Docker para consultar o JSON de todos os containers em execução.
O Traefik consultará todos os containers em busca de “labels” que o instrua como prover o encaminhamento de URLs.
Objetivamente: cada container “puxa” o trafego para si nesse tipo de ambiente.
Vamos criar dois containers de teste com manifesto de labels.
Primeiro container para a página inicial:
# Lighttpd para receber o site exemplo.com.br e www.exemplo.com.br
# Criar a pasta no HOST:
mkdir -p /storage/hello-world-home
echo 'Ola, deu certo pelo Traefik' > /storage/hello-world-home/index.html
# Rodar o container chamado hello-world-home
# - Observe que não há porta mapeada, esse container é 100% privado
# - Observe os labels do Traefik:
docker run -d \
-d --restart=unless-stopped \
--name hello-world-home -h hello-world-home.intranet.br \
--network network_public \
\
-v /storage/hello-world-home:/var/www/html:ro \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.exemplo.rule=Host(\`exemplo.com.br\`) || Host(\`www.exemplo.com.br\`)" \
--label "traefik.http.routers.exemplo.priority=100" \
--label "traefik.http.routers.exemplo.entrypoints=websecure" \
--label "traefik.http.routers.exemplo.tls=true" \
--label "traefik.http.routers.exemplo.tls.certresolver=letsencrypt" \
--label "traefik.http.services.exemplo.loadbalancer.server.port=80" \
\
rtsp/lighttpd
Segundo container para a pasta “/api” para o site “exemplo.com.br” (e o “www” dele):
# Lighttpd para receber o site exemplo.com.br/api e api.exemplo.com.br
# Criar a pasta no HOST:
mkdir -p /storage/hello-world-api
echo '{ "nome": "Patolino", "age": "6" }' > /storage/hello-world-api/index.json
# Remover:
docker rm -f hello-world-api
# Rodar o container chamado hello-world-home
# - Observe que não há porta mapeada, esse container é 100% privado
docker run -d \
-d --restart=unless-stopped \
--name hello-world-api -h hello-world-api.intranet.br \
--network network_public \
\
-v /storage/hello-world-api:/var/www/html:ro \
\
--label "traefik.enable=true" \
--label "traefik.http.routers.api.rule=(Host(\`exemplo.com.br\`) || Host(\`www.exemplo.com.br\`)) && PathPrefix(\`/api\`)" \
--label "traefik.http.routers.api.priority=200" \
--label "traefik.http.routers.api.entrypoints=websecure" \
--label "traefik.http.routers.api.tls=true" \
--label "traefik.http.routers.api.tls.certresolver=letsencrypt" \
--label "traefik.http.services.api.loadbalancer.server.port=80" \
\
rtsp/lighttpd
Observe o “priority“, quanto mais específica for seu filtro, aumente o valor de prioridade para que a regra seja processada primeiro em relação à configuração de outros containers.
O Traefik é assim mesmo, você roda ele de forma simples, e deixa o resto pra cada container declarar suas capturas. Trabalho 100% automático!
O ponto fraco do Traefik é que você tem que reconfigurar todos os seus containers para incluir as declarações de labels.
Ao declarar um novo container, o Traefik pode demorar entre 1 e 2 minutos para consultar a API remota do Let’s Encrypt para obter o certificado para ele. Garanta que o DNS foi configurado corretamente ANTES de criar o container, pois há o risco do excesso de tentativas sem sucesso resultar no bloqueio do seu IP na API do Let’s Encrypt.
Traefik personalizado na mão junto com o modo automático
Você pode rodar o Traefik especificando todas as configurações no CMD (command) do container, veja um exemplo bem completo e exaustivo:
# Criar container Traefik (completo)
# - Diretorios mapeados:
mkdir -p /storage/traefik-app/letsencrypt
mkdir -p /storage/traefik-app/logs
mkdir -p /storage/traefik-app/config
# - Arquivo de configuracao manual
touch /storage/traefik-app/config/traefik.yml
# - Remover atual:
docker rm -f traefik-app
# - Rodar
docker run \
-d --restart=unless-stopped \
\
--name traefik-app -h traefik-app.intranet.br \
--network network_public \
\
-p 80:80 \
-p 443:443 \
\
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /storage/traefik-app/letsencrypt:/etc/letsencrypt \
-v /storage/traefik-app/config:/etc/traefik \
-v /storage/traefik-app/logs:/logs \
\
traefik:latest \
\
--global.checkNewVersion=false \
--global.sendAnonymousUsage=false \
\
--api.dashboard=true \
--api.insecure=true \
\
--log.level=INFO \
--log.filePath=/logs/error.log \
\
--accessLog.filePath=/logs/access.log \
\
--entrypoints.web.address=:80 \
--entrypoints.web.http.redirections.entryPoint.to=websecure \
--entrypoints.web.http.redirections.entryPoint.scheme=https \
--entrypoints.web.http.redirections.entryPoint.permanent=true \
\
--entrypoints.websecure.address=:443 \
\
--providers.docker=true \
--providers.file.directory=/etc/traefik \
\
--certificatesresolvers.letsencrypt.acme.email=seu-email@exemplo.com.br \
--certificatesresolvers.letsencrypt.acme.storage=/etc/letsencrypt/acme.json \
--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
No exemplo acima, implementamos:
- Logs: vão ficar armazenados no HOST no diretório /storage/traefik-app/logs, tome cuidado com o crescimento e armazenamento deles em obediência ao Marco Civil;
- Configuração personalizada: arquivos .yml (formato YAML) podem ser criados e configurados no HOST no diretório /storage/traefik-app/config com as personalizações manuais do seu ambiente;
Com o uso de Docker Compose, você não precisará se preocupar com a quantidade de argumentos do container, já que um único arquivo de stack resolverá todas as suas necessidades.
Outra forma de resolver é transformar a configuração em parte dos dados persistentes do container, convertendo a configuração de argumentos do Traefik em um arquivo de configuração. Esse método permite que você edite o arquivo sem reiniciar o container pois o Traefik monitora alterações em tempo real. O único defeito é que se você errar a configuração o Traefik bug ao vivo!
Arquivo de configuração no HOST em /storage/traefik-app/config/traefik.yml:
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
insecure: true
log:
level: INFO
filePath: /logs/error.log
accessLog:
filePath: /logs/access.log
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
certificatesResolvers:
letsencrypt:
acme:
email: seu-email@exemplo.com.br
storage: /etc/letsencrypt/acme.json
httpChallenge:
entryPoint: web
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: "/etc/traefik/config"
watch: true
Rodando o container Traefik usando o arquivo acima:
# Criar container Traefik (completo)
# - Diretorios mapeados:
mkdir -p /storage/traefik-app/letsencrypt
mkdir -p /storage/traefik-app/logs
mkdir -p /storage/traefik-app/config
# - Arquivo de configuracao manual
touch /storage/traefik-app/config/traefik.yml
# - Remover atual:
docker rm -f traefik-app
# - Rodar
docker run \
-d --restart=unless-stopped \
\
--name traefik-app -h traefik-app.intranet.br \
--network network_public \
\
-p 80:80 \
-p 443:443 \
-p 8080:8080 \
\
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /storage/traefik-app/letsencrypt:/etc/letsencrypt \
-v /storage/traefik-app/config:/etc/traefik \
\
traefik:latest \
--configFile=/etc/traefik/traefik.yml
Com isso encerramos os principais exemplos de uso do Traefik!
Apagar Traefik:
# Remover Traefik (traefik-app)
docker stop traefik-app
docker rm traefik-app
7 – Caddy
O Caddy segue a mesma ideia do Traefik de ser “zero-touch” e prover redirecionamentos automáticos por meio de labels mas também permitindo personalizações manuais.
Ele parece simples, mas é mais esquisito de configurar que o Traefik, logo ele não se encaixará em todas as soluções. Sua configuração é escrita em blocos e os labels são mais curtos.
O Caddy também faz a obtenção automática de certificados assinados usando ACME-Let’sEncrypt.
Rodando Caddy:
# Criar container Traefik (básico)
# Diretorio de dados persistentes
mkdir -p /storage/caddy-app
mkdir -p /storage/caddy-app/config
mkdir -p /storage/caddy-app/logs
mkdir -p /storage/caddy-app/data
# Remover container caso exista:
docker rm -f caddy-app
# Remover imagem atual para forçar download com imagem atualizada:
docker rmi lucaslorentz/caddy-docker-proxy:latest
# Obter imagem (no momento, latest = v2.9.x):
# - opcional, o comando docker run abaixo ja faz isso
docker pull lucaslorentz/caddy-docker-proxy:latest
# Criar container:
# - nome: caddy-app
# - Colocar na rede network_public
# - portas:
# 80 = HTTP
# 443 = HTTPs (HTTP+TLS)
# - volumes mapeados:
# arquivo /var/run/docker.sock mapeado em /var/run/docker.sock no container
# diretorios de config e logs do host mapeados dentro do container
#
docker run \
-d --restart=unless-stopped \
\
--name caddy-app -h caddy-app.intranet.br \
--network network_public \
\
-p 80:80 \
-p 443:443 \
\
-e CADDY_INGRESS_NETWORKS=network_public \
\
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /storage/caddy-app/data:/data \
-v /storage/caddy-app/logs:/logs \
\
lucaslorentz/caddy-docker-proxy:latest
Criando um container para puxar automaticamente uma URL pelo Caddy:
# Lighttpd para receber o site exemplo.com.br e www.exemplo.com.br
# Criar a pasta no HOST:
mkdir -p /storage/hello-world-home
echo 'Ola, deu certo pelo Caddy' > /storage/hello-world-home/index.html
# Remover container hello-world-home anterior:
docker rm -f hello-world-home
# Rodar o container chamado hello-world-home
# - Observe que não há porta mapeada, esse container é 100% privado
# - Observe os labels do Caddy:
docker run -d \
-d --restart=unless-stopped \
--name hello-world-home -h hello-world-home.intranet.br \
--network network_public \
\
-v /storage/hello-world-home:/var/www/html:ro \
\
--label caddy=example.com \
--label caddy.reverse_proxy=http://hello-world-home:80 \
\
rtsp/lighttpd
Embora pareça simples e mágico, o Caddy tem suas limitações, o Traefik e o NPM ainda são superiores.
Conclusão
Recomendo focar no Traefik, ele roda sem toque, 100% invisível, suporta todos os recurso de automatização e segurança e deixa com que o serviços (containers) assumam seus nomes e diretórios HTTP para escuta.
Até a próxima!
Patrick Brandão, patrickbrandao@gmail.com