开始之前
先要有一个已经在本机回环地址上响应的推理服务 —— Ollama 在 11434、llama.cpp 在 8080、vLLM 在 8000 —— 一个指向这台主机的域名,以及一张证书。Nginx 在这里做三件事:终结 TLS、把连接保持得足够久以完成生成、以及成为公网接口上唯一的东西。后端必须继续绑定在 127.0.0.1;如果它监听的是 0.0.0.0,那这层反向代理就只是装饰。
# The backend must NOT be publicly bound. Check before you proxy:
ss -tlnp | grep -E "11434|8080|8000"
# Expect 127.0.0.1:11434, not 0.0.0.0:11434
sudo certbot --nginx -d llm.example.com会弄坏流式输出的两个设置
这是最容易配错的一段,而且症状很迷惑人:用 curl 发非流式请求时 API「是好的」,一发流式请求就像卡住了。Nginx 默认会缓冲被代理的响应,于是 SSE 事件不再逐 token 到达,而是在最后一次性吐出来。另外默认读超时是 60 秒 —— 够短回复用,不够长生成用,而那看起来就像模型崩了。
server {
# Works on every Nginx. The newer `http2 on;` needs 1.25.1+, and Ubuntu
# 22.04/24.04 ship 1.18/1.24, where it is an unknown directive.
listen 443 ssl http2;
server_name llm.example.com;
ssl_certificate /etc/letsencrypt/live/llm.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/llm.example.com/privkey.pem;
location /v1/ {
proxy_pass http://127.0.0.1:11434;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Stream tokens instead of buffering the whole reply:
proxy_buffering off;
proxy_cache off;
# A long generation is not a hung connection:
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
}在它前面加一道门
一个暴露在公网上的开放端点,等于把你的 GPU 交给任何人花。Nginx 能做掉其中便宜的那一半 —— 一个共享密钥加上限流 —— 而且应该做,因为这些后端大多自己没有任何鉴权。带 burst 的 `limit_req` 既能容纳正常客户端的突发行为,又能挡住脚本。
# These two go in the http {} context, e.g. a file in /etc/nginx/conf.d/:
limit_req_zone $binary_remote_addr zone=llm:10m rate=10r/m;
map $http_authorization $api_ok {
default 0;
"Bearer YOUR_LONG_RANDOM_TOKEN" 1;
}
# …and this inside the server {} block from above:
location /v1/ {
if ($api_ok = 0) { return 401; }
limit_req zone=llm burst=5 nodelay;
# …proxy settings from above…
}确认它真的生效了
要检查三件事,因为可能出问题的是三个不同的地方:TLS 下端点是否有响应、流式请求是不是真的在流式返回而不是最后一次性到达,以及最重要的 —— 后端能不能被外部直接访问到。
# 1. Does it answer at all?
curl -H "Authorization: Bearer YOUR_LONG_RANDOM_TOKEN" \
https://llm.example.com/v1/models
# 2. Does it stream? Tokens should appear progressively, not in one burst.
curl -N -H "Authorization: Bearer YOUR_LONG_RANDOM_TOKEN" \
-H "Content-Type: application/json" \
-d '{"model":"llama3.1:8b","messages":[{"role":"user","content":"count to 20"}],"stream":true}' \
https://llm.example.com/v1/chat/completions
# 3. From ANOTHER machine — this must fail:
curl --max-time 5 http://llm.example.com:11434/api/tags数字大概该是什么样
反向代理不会给生成过程增加可测量的延迟 —— 时间花在 GPU 读取权重上,不是转发字节上。真正变化的是客户端感受到的首 token 时间:`proxy_buffering on` 时它等于整次生成的时长,关掉之后它是毫秒级。如果通过代理测到的吞吐和本地测到的差距超出正常波动,你看到的是缓冲问题,不是网络问题。
出问题时
流式请求的内容在最后一次性到达:那个 location 块里漏了 `proxy_buffering off`。长生成进行到一半返回 504:`proxy_read_timeout` 还是默认的 60 秒。每个请求都 502:后端没有监听在 `proxy_pass` 指向的地址,或者绑到了别的网卡 —— 用 `ss -tlnp` 确认,不要靠猜。客户端带着正确的 token 却收到 401:请求头是精确匹配的,多一个尾随空格或者 `Bearer` 大小写不同都会失败。还有,如果上面第 3 步在另一台机器上成功了,请先停下来解决那个问题:后端本身对公网开放时,前面加多少层代理都保护不了任何东西。
常见问题
为什么套上 Nginx 之后流式输出就不工作了?
Nginx 默认会缓冲被代理的响应,于是 SSE 事件被攒着,在生成结束时一次性发出。请在代理 API 的那个 location 块里设置 `proxy_buffering off`(以及 `proxy_cache off`)。由于请求本身是成功的,这个问题通常会先被误判成客户端的 bug。
长生成为什么会返回 504?
`proxy_read_timeout` 默认是 60 秒,而本地模型的长回复很容易超过这个时长 —— 于是 Nginx 在模型还在工作时就关掉连接并报网关超时。把 `proxy_read_timeout` 和 `proxy_send_timeout` 调到与你最长的真实回复相称的值。
一层反向代理足以保护本地大模型 API 吗?
只有在「没有它就访问不到后端」的前提下才够。Ollama、llama.cpp 和 vLLM 默认都不要求鉴权:llama-server 和 vLLM 可以加 `--api-key`,Ollama 则根本没有密钥选项。所以反向代理必须是唯一的入口 —— 把后端绑定到 `127.0.0.1`,并从另一台机器验证它的端口拒绝连接。在代理层加 token 校验和 `limit_req` 限流能解决问题中便宜的那一半;而把后端留在 `0.0.0.0` 上,前面做的一切都不作数。
这篇指南用到了什么
它真的跑起来了吗?
复制命令并不等于它能用,所以站内只在这里问一次。除了你的这个回答之外,不收集任何东西。
相关指南
在 €20/月 VPS 上运行 Llama 3.1 8B
使用 llama.cpp 服务模式在低价 Linux VPS 上搭建私有 LLM API 的完整教程。
用 vLLM 在 RTX 4090 上搭建多模型 API 服务
用 vLLM 的连续批处理技术,在单张 RTX 4090 上提供生产级多模型 API 服务。
TabbyAPI:EXL3 的 OpenAI 兼容服务端
TabbyAPI 现在服务的是 ExLlamaV3(EXL3 和 FP16),不再是 EXL2 —— 变了什么、现在怎么跑,以及手里的 EXL2 模型该怎么办。