在 VPS 上运行 Ollama:自托管一个 LLM
用 Ollama 在 VPS 上自托管开放 LLM:诚实的 CPU 容量评估、安装、systemd、为什么绝不要暴露 11434 端口,以及放在 TLS 后面的 Open WebUI。
您将搭建的内容
一个开放权重的语言模型,运行在您自己拥有的服务器上,通过 HTTP API 提供服务,如果您愿意,还可以在浏览器里通过一个聊天页面访问。Ollama 就是负责下载模型、把它加载到内存并在 http://127.0.0.1:11434 上响应请求的那一部分。安装只需一条命令。这件事真正的难点都在别处:为您的 VPS 挑选一个内存真正装得下的模型,以及不要不小心把一个没有身份验证的推理服务器发布到整个互联网上。
先说两个实话提醒。仅有 CPU 的 VPS 运行小模型也很慢,而且这个 API 完全没有内置的身份验证。下面都会详细讲到,因为这两点正是人们踩坑的地方。
用实实在在的数字做一次容量核对
一个模型占用的内存大致等于它的文件大小,加上大约一个 GB 的运行时开销,再加上一些用于上下文窗口的内存。Ollama 的默认模型采用 4 位量化(标记为 Q4),每十亿参数大约消耗半个 GB 的内存。所以算法很简单,而它决定了一切。
一个 3B 模型,例如 llama3.2:3b,下载大小约 2 GB,运行时需要大约 4 GB 空闲内存。一个 7B 或 8B 模型,例如 mistral:7b 或 llama3.1:8b,磁盘占用约 5 GB,需要大约 8 GB 内存,16 GB 才算宽裕。一个 13B 或 14B 模型大约需要 16 GB。30B 到 70B 区间的任何模型都需要一台大内存的机器,或者现实一点说,需要 GPU:在 CPU 的 VPS 上它要么装不下,要么回答慢到毫无用处。
现在说速度,因为这部分人们最容易低估。CPU 推理受内存带宽限制,而不是主频,而共享 vCPU 的 VPS 带宽有限。每秒生成的 token 数预期在个位数到十几之间:一个 7-8B 的 Q4 模型也许能达到每秒 4 到 10 个 token,一个 3B 模型每秒 10 到 25 个。GPU 大约要快一个数量级。这些数字有意给得很粗略,诚实的做法是测量您自己的机器,下面的运行步骤会演示怎么做。相信您自己的 eval rate,而不是任何文章里的数字,包括这一篇。
实际的结论是:如果您能接受这个速度,CPU 上的小型量化模型对起草、总结和分类都确实有用。对于更大或更快的需求,请为 GPU 实例预留预算。
要针对某台具体机器权衡某个具体模型,在这里估算它的内存占用:
安装 Ollama
有两种干净的方式。在一台裸 VPS 上,官方脚本最简单:
curl -fsSL https://ollama.com/install.sh | sh这会创建一个名为 ollama 的系统用户,把二进制文件安装到 /usr/local/bin/ollama,并注册一个名为 ollama.service 的 systemd 服务,它会开机自启并绑定 127.0.0.1:11434。确认它已经启动:
systemctl status ollama
ollama --version如果您已经在运行 Docker,那就改用容器:
docker run -d --name ollama \
-p 127.0.0.1:11434:11434 \
-v ollama:/root/.ollama \
--restart always \
ollama/ollama注意端口映射上的 127.0.0.1: 前缀。它把端口只绑定到本地回环地址。写成 -p 11434:11434 则会把它发布到每一个网络接口上,那正是安全章节所警告的错误。只选一种安装方式;不要同时运行脚本和容器,否则两个进程会争抢同一个端口。
拉取并运行您的第一个模型
ollama pull llama3.2:3b
ollama run llama3.2:3bpull 把模型分层下载到磁盘(这个模型约 2 GB)。run 把它们加载进内存,并把您带到一个 >>> 提示符前。输入一个问题。第一个 token 可能要等好几秒,因为权重正从磁盘加载进内存,之后答案就会流式输出。输入 /bye 离开聊天;Ollama 会继续在后台运行。
查看已加载的内容以及它是怎么装下的:
ollama psPROCESSOR 这一列说的是实话。100% CPU 表示没有用到 GPU,而慢就来自这里。用 verbose 标志测量真实速度:
ollama run --verbose llama3.2:3b "Write two sentences about Linux."末尾打印的 eval rate 那一行,就是这台硬件上每秒生成的 token 数。这才是您做规划时要依据的数字。
模型存放在哪里,以及该买多少磁盘
由脚本安装并作为服务运行时,模型存放在 ollama 用户的主目录里:
sudo du -sh /usr/share/ollama/.ollama/models以您自己的用户交互式运行时,它们位于 ~/.ollama/models。在容器里,它们位于名为 ollama 的卷中。这一点很重要,因为量化后的权重累积得很快:一个 3B 约 2 GB,一个 7-8B 约 5 GB,一个 14B 约 9 GB。为了对比而拉取四个模型,您就会不知不觉花掉 20 GB。按您打算保留的模型来规划磁盘大小,并用 ollama rm <model> 删掉其余的。
把它作为一个您能掌控的服务来运行
安装脚本已经注册了 ollama.service,所以它无需额外操作就会开机重启。值得改动的设置是模型在内存里驻留多久,以及在某些环境下,绑定地址,两者都放进一个 systemd drop-in,这样 Ollama 升级时就不会把它们覆盖掉:
sudo systemctl edit ollama.service在编辑器展示给您的 [Service] 标题下面添加这一段:
[Service]
Environment="OLLAMA_KEEP_ALIVE=30m"OLLAMA_KEEP_ALIVE 是模型在最后一次请求之后在内存里停留多久(默认 5 分钟)。如果您整天都在查询这台机器,就把它调大,以免每次都重新加载权重;在内存紧张的机器上把它设为 0,一旦请求完成就立刻释放内存。systemctl edit 会替您重新加载单元文件,所以重启一下让改动生效:
sudo systemctl restart ollama最要紧的那个安全要点
默认情况下 Ollama 绑定 127.0.0.1:11434,所以只有 VPS 本机上的进程才能访问它。那个默认值是对的。保持它。
这个 API 没有身份验证。一点都没有。没有 API 密钥,没有登录,没有速率限制,没有白名单。任何能访问到 11434 端口的人,都可以运行您已经拉取的任何模型、拉取新模型、删除它们,并让您的 CPU 或 GPU 无限期地满载运转。像 Shodan 这样的扫描器成千上万地索引开放的 Ollama 实例,一个暴露在外的实例在几小时之内就会被发现并被滥用。
所以这里有一个绝对不能犯的错误:不要设置 OLLAMA_HOST=0.0.0.0 并在防火墙里打开 11434。那会把一个没有身份验证的推理服务器发布给整个互联网。再多的配置也无法让 0.0.0.0 上裸露的 11434 变得安全,因为 Ollama 里根本没有东西可配置,身份验证根本就不存在。
有三种安全的方式可以从本机以外的地方访问模型:
- 保持在本地。如果唯一的调用方是同一台 VPS 上的另一个程序,比如一个 cron 脚本、一个机器人、一个把您的工具桥接到模型的 MCP 服务器,那就把绑定保持在
127.0.0.1,让那个程序去调用http://127.0.0.1:11434。什么都不暴露,也不需要别的东西。 - 通过私有隧道访问。把 VPS 接入一个您自己搭建的 WireGuard VPN,把
OLLAMA_HOST设为隧道地址(例如10.8.0.1,而不是0.0.0.0),这样只有 VPN 对端才能连接。公网在 11434 上仍然什么都看不到。 - 在前面放一个带身份验证的反向代理。在 nginx、Traefik 或 Caddy 上终止 TLS 并要求密码或令牌,然后代理到
127.0.0.1:11434。Ollama 保持它的本地回环绑定;代理是唯一监听公共端口的东西。这跟在任何本地服务前面放上一张装在 nginx 上的 Let's Encrypt 证书是同一种形态。
反向代理这个选项,正是聊天界面接下来要给您的东西,而且带着一个真正的登录。
用 Open WebUI 加一个聊天界面,放在 TLS 后面
Open WebUI 是一个自托管的聊天界面。用 Docker 运行它,并让它指向本地的 Ollama:
docker run -d \
--name open-webui \
--network=host \
-e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
-v open-webui:/app/backend/data \
--restart always \
ghcr.io/open-webui/open-webui:main--network=host 标志是 Linux VPS 上的关键细节。它把容器放进宿主机的网络命名空间,于是容器内部的 127.0.0.1 就是宿主机自己的回环地址,容器就能在 127.0.0.1:11434 访问到 Ollama,而无需 Ollama 监听任何其他接口。您在别处会看到的那种桥接网络配方,--add-host=host.docker.internal:host-gateway 配上 OLLAMA_BASE_URL=http://host.docker.internal:11434,在这里行不通:那个名字会解析到 Docker 桥接网关,而一个绑定在宿主机 127.0.0.1 上的服务是无法跨桥接访问的,于是 Open WebUI 就只会干等着,报告它连不上 Ollama。
host 网络模式的代价是,Open WebUI 现在会在宿主机的 8080 端口上、在每一个接口上监听;任何 -p 映射都会被丢弃,Docker 会打印一条警告说明这一点。所以在宿主机和服务商防火墙两处都把 8080 关掉,让 TLS 反向代理成为唯一的公共入口。第一次访问时,Open WebUI 会要求您创建一个管理员账户,那个账户就是您的身份验证层,所以请选一个强密码。
要从您的笔记本电脑上通过 HTTPS 打开聊天界面,在 127.0.0.1:8080 前面放一个 TLS 反向代理。如果您已经在这台机器上路由好几个 Docker 应用,用 Traefik 为多个应用自动配置 TLS是最贴合的选择:一个标签块就能签发证书并把 chat.example.com 路由到 Open WebUI。安全章节里的规则依然成立,代理掌管公共端口和登录,而 Ollama 留在本地回环上,Open WebUI 自己的 8080 也留在防火墙后面。
从您的代码调用 OpenAI 兼容的端点
Ollama 在 /v1 上说一套 OpenAI 聊天 API 的子集,所以大多数 OpenAI 客户端库在改动两处之后就能用:基础 URL 和一个随便填的密钥。
from openai import OpenAI
client = OpenAI(base_url="http://127.0.0.1:11434/v1", api_key="ollama")
resp = client.chat.completions.create(
model="llama3.2:3b",
messages=[{"role": "user", "content": "Name three Linux distributions."}],
)
print(resp.choices[0].message.content)api_key 是客户端库要求的,但 Ollama 会忽略它,所以任何字符串都行。model 必须是您已经拉取过的名字;未知的名字会返回 model "x" not found, try pulling it first。用普通的 curl 调用也是一样的思路:
curl http://127.0.0.1:11434/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama3.2:3b","messages":[{"role":"user","content":"Hello"}]}'这也是把模型接入 agent 和编辑器工具链的方式。如果您已经在这台机器上做开发,一个本地模型可以给脚本和插件提供支撑,与在 VPS 上跑在 tmux 里的 Claude Code并肩工作,把廉价、私密的起草工作留在付费 API 之外,同时把繁重的推理交给托管模型。
各种故障表现,以及您会看到的确切字符串
进程在生成到一半时被 "Killed"。 您启动一个大模型,终端打印出 Killed,或者服务器日志显示 llama runner process has terminated: signal: killed。是 Linux 的 OOM killer 把它停掉了,因为这个模型需要的内存比机器拥有的还多。用 sudo dmesg | grep -i oom 确认原因,您会看到类似 Out of memory: Killed process ... (ollama) 的一行。解决办法是换一个更小或量化更狠的模型,用 llama3.2:3b 代替一个 13B,或者加上交换空间,让一个刚好超出物理内存的负载慢慢地撑下去,而不是直接死掉。交换空间把一次瞬间崩溃变成一个缓慢的回答;它并不能让一个 70B 模型在 4 GB 上变得可用。
"Error: model requires more system memory"。 Ollama 拒绝启动模型,并打印 Error: model requires more system memory (X GiB) than is available (Y GiB)。这是上面那种崩溃的礼貌版本:Ollama 算了一笔账,与其让 OOM killer 动手,不如自己停下来。它甚至把两个数字都递给了您。选一个需求低于您空闲内存的模型(用 free -h 查看),缩小上下文长度,或者换一台更大的 VPS。没有哪个标志能让模型装得下,内存是实打实的。
第一个 token 等了很久,之后就正常了。 一个冷启动的模型有五到三十秒什么都不打印,然后就正常流式输出。那段停顿是权重第一次从磁盘加载进内存,慢速存储会让它更糟。一旦加载完成,模型会在 OLLAMA_KEEP_ALIVE 的时长内保持驻留,所以第二个提示会立刻回答。如果这些间隔让您烦,就把那个值调大,并用 ollama ps 查看某个模型当前是否已加载。
就是单纯地慢。 每秒十个 token 或更少,而且完全没有报错。那就是 CPU 推理在做它本该做的事。ollama ps 显示 100% CPU,意味着没有 GPU。这不是 bug,也没有哪个设置能修好它,因为瓶颈是内存带宽,而不是配置错误。换一个更小的模型,接受这个速度,或者换到一个 GPU 实例,并在断定哪里出问题之前,用 --verbose 测量您的真实速率。
从另一台机器连接被拒绝。 从您的笔记本电脑上,您会得到 curl: (7) Failed to connect to <ip> port 11434: Connection refused。这是设计使然:Ollama 只绑定本地回环。不要用绑定 0.0.0.0 的方式去“修”它,那恰恰就是上面说的暴露错误。请改为通过 VPN 或带身份验证的代理来访问模型。
您把 11434 暴露给了互联网。 如果您确实设置了 OLLAMA_HOST=0.0.0.0、打开了防火墙,现在看到自己从没启动过的模型拉取,或者 CPU 被未知客户端顶在 100%,那您已经被发现并被利用了。这是头号错误,不是什么边缘情况。把绑定改回 127.0.0.1 或 VPN 地址,在防火墙里关掉 11434,并在前面加上身份验证。凡是在它开放期间可以访问到那个地址的东西,都要假设已经被陌生人查询过。
备份与升级
没什么状态可丢。模型可以重新下载,所以唯一值得备份的,是 Open WebUI 的数据卷,账户、聊天记录、设置,以及您写过的任何 systemd drop-in。用一个用完即弃的容器备份这个卷:
docker run --rm -v open-webui:/data -v "$PWD":/backup alpine \
tar czf /backup/open-webui.tgz -C /data .升级 Ollama 就重新运行安装脚本;升级 Open WebUI 就用 docker pull ghcr.io/open-webui/open-webui:main,然后重新创建容器。什么都不要长期锁定:模型质量和运行时都在快速变化,所以请阅读发布说明并在您自己的机器上重新做基准测试,而不是相信上个季度的数字。
FAQ
仅有 CPU 的 VPS 真的能跑 LLM 吗?
能,但有限度。3B 到 8B 区间的小型量化模型能在 CPU 上运行,对起草、总结和分类都确实有用,只是慢,在共享 vCPU 上每秒只有个位数到十几个 token。13B 往上的任何模型都会慢得让人痛苦,或者根本装不进内存。要想真正的速度或更大的模型,您需要一个 GPU 实例。
每个模型需要多少内存?
对默认的 4 位量化模型有一条粗略法则:权重每十亿参数约 0.5 GB 内存,再加上大约 1 GB 开销以及一点用于上下文的内存。所以一个 3B 模型需要约 4 GB 空闲,一个 7-8B 模型约 8 GB,一个 14B 模型约 16 GB。用 free -h 查看您的余量,并为操作系统和机器上其他东西留出空间。
Ollama 的 API 有身份验证吗?
没有。Ollama 没有任何内置的身份验证、API 密钥或速率限制,任何能访问到 11434 端口的人都对它拥有完全的控制权。这正是它默认绑定 127.0.0.1 的原因,也是您绝不能把 11434 暴露在 0.0.0.0 上、面向互联网的原因。请在本地访问它,通过私有 VPN,或者通过一个加了登录的反向代理。
我要怎么加一个网页聊天界面?
用 --network=host 在 Docker 里运行 Open WebUI,让它共享宿主机的回环地址,从而在 http://127.0.0.1:11434 访问到原生的 Ollama,然后在它的 8080 端口前面放一个 TLS 反向代理,以便从您的笔记本电脑访问。把 8080 在防火墙里关着,让代理成为唯一的公共入口。Open WebUI 自己的管理员账户提供登录,您在首次启动时设置它的密码。
我要怎么从自己的应用里调用它?
用 http://127.0.0.1:11434/v1 上的 OpenAI 兼容端点。把任何 OpenAI SDK 指向那个基础 URL,传入任何字符串作为 API 密钥(因为它会被忽略),并把 model 设为一个您已经拉取过的名字。现有的 OpenAI 代码除了基础 URL 和密钥之外,通常无需改动就能运行。