进阶服务器 / VPS 4 分钟阅读发布于 · 更新于

Nginx 反向代理本地 LLM API

用 Nginx 为 Ollama 或 llama.cpp 提供 TLS、限流和稳定的 /v1 端点。

面向的技术栈

Nginx · certbot 签发 TLS · SSE 需关闭 proxy_buffering · 长读超时 · limit_req 限流

最近一次修改后未重新实机运行 —— 命令请当作起点,而不是已验证的配方。

NginxAPITLSOllamallama.cpp

开始之前

先要有一个已经在本机回环地址上响应的推理服务 —— Ollama 在 11434、llama.cpp 在 8080、vLLM 在 8000 —— 一个指向这台主机的域名,以及一张证书。Nginx 在这里做三件事:终结 TLS、把连接保持得足够久以完成生成、以及成为公网接口上唯一的东西。后端必须继续绑定在 127.0.0.1;如果它监听的是 0.0.0.0,那这层反向代理就只是装饰。

bash
# 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 秒 —— 够短回复用,不够长生成用,而那看起来就像模型崩了。

nginx
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` 既能容纳正常客户端的突发行为,又能挡住脚本。

nginx
# 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 下端点是否有响应、流式请求是不是真的在流式返回而不是最后一次性到达,以及最重要的 —— 后端能不能被外部直接访问到。

bash
# 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` 上,前面做的一切都不作数。

这篇指南用到了什么

它真的跑起来了吗?

复制命令并不等于它能用,所以站内只在这里问一次。除了你的这个回答之外,不收集任何东西。

相关指南

部署指南仅供学习参考。每个模型均有独立许可协议 — 下载或部署前请阅读 Hugging Face 官方模型卡。