Saudações.
Nesse tutorial vou ensinar como colocar o um sistema que roda em seu servidor caseiro na Internet por meio de túnel particular entre ele e uma VPS.
Vou demonstrar meu caso particular, um Mac Mini rodando LM Studio com IA local.
1 – Meu ambiente
O Mac Mini (Apple M4 Pro, 24G de RAM compartilhada com GPU/NPU), está atrás no meu escritório atras de NAT (roteador Mikrotik hAP AX2, 2 links: fibra GPON e StarLink).

O LM Studio abre por padrão a porta 1234/TCP (HTTP) em localhost, o suficiente para que qualquer aplicativo rodando no mesmo Mac tenha acesso à API de inferência, usarei o modelo google/gemma-4-e4b para os testes.
1.1 – Abrir servidor HTTP do LM Studio
Precisamos ativar o servidor do LM Studio para que ele abra a porta 1234/TCP para escutar as chamadas HTTP.

Alem de ativar, é necessário duas operações críticas:
- Crítico – Colocar chave de API, por padrão o LM Studio não exige chave e isso pode resultar em abusos por qualquer cliente que encontre sua URL;
- O LM Studio deve iniciar automaticamente durante o boot do Mac, caso ele seja reiniciado;
Criando chave de API: Ative o “Require Authentication” e clique em “Manage Tokens“:

Criar chave de API: Na tela de gestão de tokens, clique em “Create new token“:

Coloque o nome da chave, bloqueio o acesso às tools/mcp do LM Studio:

Copie a chave da API:

Copie a chave: sk-lm-M6BHzAzQ:IkhpDtr6dZdmk8Cop1bV
Pronto, chave disponível para uso:

1.2 – Testando API do LM Studio
Agora vamos conferir o funcionamento da API localmente antes de continuar para a publicação remota.
# Chave de API
LM_API_TOKEN="sk-lm-M6BHzAzQ:IkhpDtr6dZdmk8Cop1bV";
# Listar modelos disponiveis
curl \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
-X GET \
"http://127.0.0.1:1234/api/v1/models";
# Listar apenas nome dos modelos
curl \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
-X GET \
"http://127.0.0.1:1234/api/v1/models" | jq '.models.[].key';
Testar inferência com o modelo google/gemma-4-e4b:
# Chave de API
LM_API_TOKEN="sk-lm-M6BHzAzQ:IkhpDtr6dZdmk8Cop1bV";
# Enviar requisicao ao modelo
curl http://localhost:1234/v1/responses \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
-d '{
"model": "google/gemma-4-e4b",
"input": "Why sky is blue? Short answer."
}';
# Resposta:
# {
# "id": "resp_04e9077a14631e04e9bd90822e30f578fd8a3666a9032bf3",
# ...
# "output": [
# {
# "id": "rs_li4z968kg1jwz0vf9kbfvl",
# "type": "reasoning",
# "status": "completed",
# "summary": [],
# "content": [
# {
# "type": "reasoning_text",
# "text": "(texto do pensamento aqui, muito grande)"
# }
# ]
# },
# {
# "id": "msg_2v0dqupfuqb0ookgv5j97r",
# "type": "message",
# "role": "assistant",
# "status": "completed",
# "content": [
# {
# "type": "output_text",
# "text": "... As sunlight enters our atmosphere ...",
# "annotations": [],
# "logprobs": []
# }
# ]
# }
# ],
# ...
# }
Com o LM Studio pronto e escutando na loopback, vamos continuar.
1 – Túnel reverso
Para que nosso Mac Mini na LAN privada atras de NAT seja alcançado pela Internet, o primeiro passo é criar um software que rode no Mac e se conecte ao servidor, criando um túnel que possa trafegar dados entre o servidor (Linux na VPS/VM) e o cliente (Mac Mini).
Para isso podemos usar alguns sistemas:
- FRP – Fast Reverse Proxy;
- Rathole;
- Chisel;
- SSH;
- Pangolin;
- Bore;
- Cloudflare Tunnel;
- Tailscale Funnel;
Cada solução acima requer um estudo para entender o paradigma que ela opera.
O mais simples e rápido é o SSH, ele permite rodar o cliente SSH no Mac conectando-se ao servidor SSH, ordenando ao servidor a abertura de uma porta (porta no servidor) que conduz toda conexão que chega até ela até um IP e porta no lado cliente, esse modo “Proxy TCP” do SSH permite publicar instantaneamente qualquer serviço.
Vou usar o FRP nesse tutorial.
1.1 – Usando FRP
O frp (Fast Reverse Proxy) tem duas peças:
- O frps é o servidor, na ponta com IP público;
- O frpc é o cliente, na ponta presa atrás do NAT. Ele inicia a conexão do Mac até o servidor e o servidor entra por esse túnel para alcançar o cliente.
Diagrama base:

As portas envolvidas no lado servidor (Linux, Docker) são:
- 7700: vhostHTTPPort, ao ser acessada a conexão entra no servidor e sai no cliente;
- 7701: bindPort do frps, publicada na Internet para o frpc montar o tunel;
- 7500: dashboard do frps;
Configuração do nosso container no lado servidor, o container “frp-lmstudio“:
# Nome do container
NAME="frp-lmstudio";
# URL para acesso global
# Crie o nome no DNS apontando para o IP publico do servidor
SERVER_FQDN="lmstudio.dominio.com";
# Imagem do container
IMAGE="fatedier/frps:v0.71.1";
# Segredos
# - Gere com o comando: openssl rand -hex 32
FRP_TOKEN="a34923f8aff02ca4d0b4b79148352f69a73743006c37a753704bff87d444f9ae";
# - Gere com o comando: openssl rand -hex 16
DASH_PASS="f7f2606c6599d25d828c1949e5344838";
# Volume
# Pasta de configuracao para ser o volume do container
DATADIR="/storage/$NAME";
# Criar diretorio no host, somente o root le esse arquivo
mkdir -p $DATADIR;
touch $DATADIR/frps.toml;
chmod 600 $DATADIR/frps.toml;
# Configuracao do servidor frp
(
# escuta clientes frpc
echo 'bindAddr = "0.0.0.0"';
echo 'bindPort = 7701';
echo;
# por de entrada que sera entregue ao frpc via tunel
echo 'vhostHTTPPort = 7700';
echo 'vhostHTTPTimeout = 600';
echo;
# controle de acesso/autenticacao
echo 'auth.method = "token"';
echo "auth.token = \"$FRP_TOKEN\"";
echo 'transport.tls.force = true';
echo;
# servidor de controle para manutencao e monitoramento
echo 'webServer.addr = "0.0.0.0"';
echo 'webServer.port = 7500';
echo 'webServer.user = "admin"';
echo "webServer.password = \"$DASH_PASS\"";
echo;
# log de atividades
echo 'log.to = "console"';
echo 'log.level = "info"';
echo;
) > $DATADIR/frps.toml;
# Obter imagem
docker image pull $IMAGE;
# Renovar/criar/rodar
docker container rm -f frp-lmstudio 2>/dev/null;
docker container run \
--name frp-lmstudio \
--hostname frp-lmstudio.intranet.br \
--restart unless-stopped \
--network network_public \
--detach \
--read-only \
\
--publish 7701:7701/tcp \
--publish 127.0.0.1:7500:7500/tcp \
\
--volume $DATADIR/frps.toml:/etc/frp/frps.toml:ro \
\
--label "traefik.enable=true" \
\
--label "traefik.http.routers.lmstudio.rule=Host(\`${SERVER_FQDN}\`)" \
--label "traefik.http.routers.lmstudio.entrypoints=websecure" \
--label "traefik.http.routers.lmstudio.service=lmstudio" \
--label "traefik.http.routers.lmstudio.tls=true" \
--label "traefik.http.routers.lmstudio.tls.certresolver=letsencrypt" \
\
--label "traefik.http.services.lmstudio.loadbalancer.server.port=7700" \
\
$IMAGE \
-c /etc/frp/frps.toml;
# Acesso:
echo;
echo "URL de acesso publico: https://$SERVER_FQDN";
echo;
echo "Endereco para configuracao do FRP Cliente:";
echo " Endereco do servidor: $SERVER_FQDN";
echo " Porta TCP frps......: 7001";
echo;
Agora vamos configurar o cliente frpc
Configurações importantes no cliente:
- Endereço lmstudio.dominio.com: serverAddr, servidor (DNS ou IP);
- Porta 7701: serverPort porta que o servidor abriu para atender clientes frpc;
- localIP e localPort aponta para a loopback (127.0.0.1) e porta do LM STudio (1234);
- loginFailExit: com false o cliente insiste em reconectar em vez de morrer na primeira falha;
- customDomains: o domínio que o frps usa para achar esse túnel, precisa ser igual ao da label do Traefik;
- hostHeaderRewrite: faz o LM Studio receber o Host como se o acesso fosse local.
No Mac OS vamos precisar do Brew (https://brew.sh/), com o comando brew instalaremos o frpc:
# Atualizar primeiro, sempre
brew update;
brew upgrade;
# Instalar FRP Cliente
brew install frpc;
Agora vamos criar o arquivo de configuração do frpc:
# Segredo, mesmo TOKEN do servidor
FRP_TOKEN="a34923f8aff02ca4d0b4b79148352f69a73743006c37a753704bff87d444f9ae";
# Endereco do servidor
SERVER_FQDN="lmstudio.dominio.com";
# Diretorio de configuracao
mkdir -p ~/.config/frpc;
# Somente o seu usuario le esse arquivo
touch ~/.config/frpc/frpc.toml
chmod 600 ~/.config/frpc/frpc.toml;
# Configuracao do cliente FRP
(
echo;
echo "serverAddr = \"$SERVER_FQDN\"";
echo 'serverPort = 7701';
echo 'loginFailExit = false';
echo;
echo 'auth.method = "token"';
echo "auth.token = \"$FRP_TOKEN\"";
echo 'transport.tls.enable = true';
echo;
echo 'log.to = "console"';
echo 'log.level = "info"';
echo;
echo '[[proxies]]';
echo 'name = "lmstudio"';
echo 'type = "http"';
echo 'localIP = "127.0.0.1"';
echo 'localPort = 1234';
echo "customDomains = [\"$SERVER_FQDN\"]";
echo 'hostHeaderRewrite = "127.0.0.1"';
echo;
) > ~/.config/frpc/frpc.toml;
# Visualizar
cat ~/.config/frpc/frpc.toml;
Testar se está tudo correto:
# Validar sintaxe
frpc verify -c ~/.config/frpc/frpc.toml;
# frpc:
# the configuration file
# /Users/patrickbrandao/.config/frpc/frpc.toml syntax is ok
Rodar frpc para publicar o LM Studio via servidor:
# Conectar (CTRL+C para sair)
frpc -c ~/.config/frpc/frpc.toml;
# [I] [client/service.go:355] login to server success, get run id [...]
# [I] [proxy/proxy_manager.go:183] proxy added: [lmstudio]
# [I] [client/control.go:174] [lmstudio] start proxy success
Agora vamos testar o LM Studio pela URL pública:
# Chave de API
LM_API_TOKEN="sk-lm-M6BHzAzQ:IkhpDtr6dZdmk8Cop1bV";
# Endereco do servidor
SERVER_FQDN="lmstudio.dominio.com";
# Listar modelos disponiveis
curl \
-X GET \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
"https://$SERVER_FQDN/api/v1/models";
# Listar apenas nome dos modelos
curl \
-X GET \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
"https://$SERVER_FQDN/api/v1/models" | jq '.models.[].key';
# Resposta (modelos):
# "google/gemma-4-e4b"
# "qwen/qwen3.5-9b"
# "laya-multilingual"
# "mys/laya-multilingual"
# "fr0stbit3/laya-multilingual"
# "qwen/qwen3.8-27b"
# "vibethinker-3b"
# "google/gemma-4-26b-a4b"
# "google/gemma-4-12b"
# "stable-diffusion-3.5-large"
# "nvidia/nemotron-3-nano-4b"
# "text-embedding-nomic-embed-text-v1.5"
# "mistralai/ministral-3-3b"
# "mistralai/ministral-3-14b-reasoning"
# "microsoft_fara-7b"
# "qwen/qwen3-vl-8b"
# "qwen/qwen3-vl-4b"
# "codegemma-1.1-7b-it"
# "openai/gpt-oss-20b"
# Chat com streaming
curl \
-X POST \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
"https://$SERVER_FQDN/v1/chat/completions" \
-d '{
"model": "google/gemma-4-e4b",
"stream": true,
"messages": [ { "role": "user", "content": "Quem foi Patolino?" } ]
}';
# data: {"choices":[{"delta":{"content":"Pat"}}], ...}
# data: {"choices":[{"delta":{"content":"olino"}}], ...}
# data: [DONE]
# Testar modelo system-one
# modelo: ggml-org/Laya-GGUF, type: 'choice' | 'score' | 'noul'
JSON_QUESTIONS='
{
"model": "ggml-org/Laya-GGUF",
"state": "Hi, we were billed twice for March. Please refund the duplicate today or we will cancel our plan.",
"questions": {
"department": {
"type": "choice",
"instructions": "Which department should handle this?",
"criteria": {
"billing": "invoices, payments, refunds",
"technical": "bugs, outages, system errors",
"other": "everything else"
}
},
"urgency": {
"type": "score",
"instructions": "How urgent is this?",
"criteria": [
"not urgent",
"soon",
"blocking"
]
},
"churn_risk": {
"type": "noul",
"instructions": "Does the user threaten to cancel or leave?"
}
}
}';
JSON_PAYLOAD=$(echo $JSON_QUESTIONS | jq -c);
curl \
-X POST \
-H "Content-type: application/json" \
-H "Authorization: Bearer $LM_API_TOKEN" \
"https://$SERVER_FQDN/v1/systemone" \
-d "$JSON_PAYLOAD";
#
# Retorno:
#
# {
# "model": "laya",
# "answers": {
# "department": {
# "type": "choice",
# "choice": "billing",
# "probabilities": {
# "billing": 0.9870735189679403,
# "technical": 0.007643838803052039,
# "other": 0.005282642229007625
# },
# "confidence": 0.9806102784519104
# },
# "urgency": {
# "type": "score",
# "score": 1.7765817804244373,
# "legend": {
# "0": "not urgent",
# "1": "soon",
# "2": "blocking"
# },
# "probabilities": {
# "0": 0.03936197723519105,
# "1": 0.1446942651051806,
# "2": 0.8159437576596283
# },
# "confidence": 0.6648726706366559
# },
# "churn_risk": {
# "type": "noul",
# "noul": 0.8768382231230579
# }
# },
# "usage": {
# "input_tokens": 164,
# "output_tokens": 0
# }
# }
Nossa API está online, agora temos que garantir que o frpc seja iniciado como serviço persistência no Mac.
# Pasta de agentes do usuario
mkdir -p ~/Library/LaunchAgents;
# Servico do frpc
(
echo '<?xml version="1.0" encoding="UTF-8"?>';
echo '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">';
echo '<plist version="1.0">';
echo '<dict>';
echo ' <key>Label</key><string>local.frpc</string>';
echo ' <key>ProgramArguments</key>';
echo ' <array>';
echo " <string>$HOME/frp/frpc</string>";
echo ' <string>-c</string>';
echo " <string>$HOME/frp/frpc.toml</string>";
echo ' </array>';
echo ' <key>RunAtLoad</key><true/>';
echo ' <key>KeepAlive</key><true/>';
echo ' <key>ThrottleInterval</key><integer>10</integer>';
echo " <key>StandardOutPath</key><string>$HOME/frp/frpc.log</string>";
echo " <key>StandardErrorPath</key><string>$HOME/frp/frpc.log</string>";
echo '</dict>';
echo '</plist>';
) > ~/Library/LaunchAgents/local.frpc.plist;
# Validar o arquivo
plutil -lint ~/Library/LaunchAgents/local.frpc.plist;
# /Users/voce/Library/LaunchAgents/local.frpc.plist: OK
# Ativar
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.frpc.plist;
# Conferir
tail -3 ~/frp/frpc.log;
# Reiniciar
launchctl kickstart -k gui/$(id -u)/local.frpc;
# Desativar
launchctl bootout gui/$(id -u)/local.frpc;
# Nunca dormir (a tela pode apagar normalmente)
sudo pmset -a sleep 0;x
x
x
x
x
(parei aqui)
x
x
x
x
6.1 – O timeout de 60 segundos
O frps espera por padrão 60 segundos pelo cabeçalho de resposta. Depois disso, entrega 504. Em LLM isso acontece fácil: o modelo sendo carregado na memória somado a uma resposta longa sem streaming estoura esse tempo, e o cliente recebe erro enquanto o Mac continua fritando a GPU à toa.
Por isso coloquei vhostHTTPTimeout = 600 no capítulo 2.2. Ajuste para o seu pior caso.
6.2 – Não use httpUser e httpPassword
O frp oferece Basic Auth no proxy e parece uma camada extra de graça. Não é: ele usa o mesmo cabeçalho Authorization do token Bearer, e o resultado é 401 para todo cliente da API, mesmo com o token certo.
Se quiser outra camada, use o middleware ipAllowList do Traefik para restringir por IP de origem.
6.3 – 404 ou 504 com o Mac desligado?
Com o Mac fora do ar, o frps responde 404. O mesmo vale quando o frpc está conectado e o LM Studio está fechado. Para quem consome a API, esse código é enganoso: parece URL errada, não servidor ausente.
Para responder 504 Gateway Timeout, faça a gambiarra no Traefik: adicione estas labels ao container do capítulo 2.3, antes da imagem:
--label "traefik.http.routers.lmstudio.middlewares=lmstudio-504" \
--label "traefik.http.middlewares.lmstudio-504.errors.status=404" \
--label "traefik.http.middlewares.lmstudio-504.errors.service=lmstudio" \
--label "traefik.http.middlewares.lmstudio-504.errors.query=/" \
--label "traefik.http.middlewares.lmstudio-504.errors.statusRewrites.404=504" \
CUIDADO: o Traefik não distingue a origem do 404. Uma rota inexistente no próprio LM Studio também vira 504. É gambiarra assumida; eu aceito o preço porque ninguém deveria chamar rota errada nessa API.
6.4 – Diagnóstico rápido
Procure o sintoma na tabela:
| Sintoma | Causa | Conserto |
|---|---|---|
| 404 na URL pública | frpc desconectado, LM Studio fechado ou domínio diferente em customDomains | Consulte ~/frp/frpc.log e confira o servidor do LM Studio |
| 401 com token certo | httpUser e httpPassword ativos no frpc | Remova os dois parâmetros |
| 504 após 60 segundos | vhostHTTPTimeout no valor padrão | Aumente no frps.toml e reinicie o container |
| Log: token in login doesn’t match | auth.token diferente nos dois lados | Copie o mesmo valor e reinicie o frpc |
| Log: connect to local service error: connection refused | Servidor do LM Studio parado ou em outra porta | Ligue o servidor e confira localPort |
Conclusão
Recomendo o frp para esse cenário: um único binário em cada ponta, roteamento por domínio e dashboard embutido. Quando precisar publicar outro serviço do Mac, basta um novo bloco [[proxies]] no frpc.toml e novas labels no Traefik.
Não esqueça que agora o seu Mac Mini é um servidor na Internet: use token forte, troque-o se vazar e acompanhe o uso pelo dashboard.
Tudo pronto, agora você pode abusar do seu LM Studio de qualquer lugar do mundo. Agora é contigo. Use sua criatividade.
Terminamos por hoje!
Patrick Brandão, patrickbrandao@gmail.com
[right] “Eu só posso lhe mostrar a porta. Você é quem tem que atravessá-la.“
Morpheus

Deixe um comentário