Ollama本地部署DeepSeek全链路

为什么 2026 年还要自己跑本地大模型

过去两年,几乎每个做 AI 相关工作的团队都经历过同一件事:把一份含有客户名单、合同条款、内部代码的文档贴进在线对话框,然后在某个深夜里突然意识到——这些内容已经离开了自己的电脑。云服务厂商当然有合规承诺,但”承诺”和”数据物理上没离开这台机器”是两件完全不同的事。

Ollama 之所以在开源的本地推理工具里跑得最快,原因很朴素:它把模型下载、量化格式、推理引擎、HTTP 服务四件事打包成了一个你几乎不用配置的黑盒。你敲一行命令,它给你一个监听在 11434 端口的 API;你再配一个 Open WebUI,它给你一个几乎和在线产品一样的聊天界面。对绝大多数非算法岗的工程师、产品经理、法务、医生、教师来说,这就是最低成本的”私有化大模型”入口。

本文只讲一条链路,从装 Ollama 到远程访问,全程可复制执行。中途会穿插大量”别人不会告诉你”的坑:为什么你的 Open WebUI 里模型列表是空的、为什么 11434 端口暴露出去三天后你的 GPU 就在跑别人的任务、为什么 32B 模型在你 16G 显存的机器上”能加载”但一用就崩。这些问题的答案,往往比安装命令本身更值钱。

第一步:安装 Ollama,先搞清楚它到底在跑什么

macOS 与 Linux 的一行安装

Linux 下官方提供了一条 curl 管道脚本,它会下载 Ollama 二进制、创建一个 ollama 系统用户、注册 systemd 服务并开机自启。这是最省事的方式,但你要知道它做了什么,否则后面改监听地址时会找不到入口。

# Linux / WSL2 一行安装(官方脚本)
curl -fsSL https://ollama.com/install.sh | sh

# 安装完成后确认服务状态
systemctl status ollama
# 如果提示服务未运行,手动拉起
sudo systemctl start ollama
sudo systemctl enable ollama

macOS 推荐直接用官网下载的 .dmg 应用包,拖进 Applications 即可。它在菜单栏常驻一个羊驼图标,点击可以看日志、退出、重启。也可以用 Homebrew 安装:

# macOS 用 Homebrew 安装
brew install ollama
# 前台启动服务(适合调试,能看到实时日志)
ollama serve

Windows 安装包与 WSL2 的选择

Windows 用户直接下载 OllamaSetup.exe,双击安装,装完后任务栏右下角会出现 Ollama 图标,服务随之启动。安装程序会把它加到 PATH,所以新开一个 PowerShell 就能用。

这里有一个容易忽略的判断:如果你后面要用 Docker 跑 Open WebUI,Windows 上强烈建议把 Ollama 装在 WSL2 里而不是原生 Windows 里。原因是 Windows 原生 Ollama 监听在 Windows 侧,Docker Desktop 的容器要访问它需要走 host.docker.internal,链路较长;而 WSL2 里的 Ollama 与 Docker 在同一个虚拟网络里,延迟和排查成本都低一个量级。当然,前提是你的显卡驱动和 CUDA 在 WSL2 里跑得通——NVIDIA 卡基本没问题,AMD 卡在 WSL2 里就麻烦得多,这种情况还是老实用原生 Windows 版。

安装后的三条验证命令

装完不要急着拉模型,先用三条命令把地基验一遍。很多人跳过这一步,后面出问题时会误以为是模型或 WebUI 的锅。

# 1. 看版本,确认二进制装上了
ollama --version
# 输出类似:ollama version is 0.9.x

# 2. 看本地已有模型列表(首次为空,正常)
ollama list

# 3. 看服务是否在监听 11434
# Linux / macOS
ss -tlnp | grep 11434
# 或
curl http://127.0.0.1:11434/api/tags

如果第三步返回了 {"models":[]} 这样的 JSON,说明 Ollama 服务已经正常跑起来了,只是还没有模型。这一步是整个链路里最关键的健康检查,后面所有”模型列表为空”的排查,都是从这里开始的。

默认监听 127.0.0.1:11434 意味着什么

Ollama 默认只监听 127.0.0.1,也就是回环地址。这意味着:只有本机能访问它。你的同事在同一个办公室里,用浏览器打开你的 IP 加 11434,是打不开的;同样,Docker 容器里的 Open WebUI 用 http://127.0.0.1:11434 也是打不开的——因为容器里的 127.0.0.1 指的是容器自己。

这个默认值很保守,也很正确。它是整个安全模型的第一道门,几乎所有的远程访问方案,本质上都是在决定”把这道门开到多大”。初学者最常见的错误,是遇到容器连不上就立刻改成 0.0.0.0,结果把门开到公网上去了。正确的做法是只对特定的网络接口、特定的来源开放,这一点在第五节会详细讲。

四个决定性能与体验的环境变量

Ollama 的行为几乎全靠环境变量控制。Linux 下 systemd 服务的变量需要写进 /etc/systemd/system/ollama.service 的 [Service] 段,然后 daemon-reload;macOS 应用版则需要 launchctl setenv 或写入 plist;Windows 在系统环境变量里设置后重启 Ollama 进程。

# Linux:编辑 systemd 服务,加入环境变量
sudo systemctl edit ollama.service

# 在打开的编辑器里粘贴(注意要保留 --- 标记或直接写入 [Service] 段)
[Service]
Environment="OLLAMA_HOST=127.0.0.1:11434"
Environment="OLLAMA_MODELS=/data/ollama/models"
Environment="OLLAMA_KEEP_ALIVE=30m"
Environment="OLLAMA_NUM_PARALLEL=2"
Environment="OLLAMA_MAX_LOADED_MODELS=2"

# 保存后重载并重启
sudo systemctl daemon-reload
sudo systemctl restart ollama

OLLAMA_HOST:改监听地址

格式是 host:port,例如 0.0.0.0:11434 表示监听所有网卡,192.168.1.20:11434 表示只监听那一块网卡。只监听某一块网卡的 IP 是一种被严重低估的做法:如果你的机器同时连着公司内网和咖啡馆 Wi-Fi,只绑定内网网卡,就能在公共网络环境下天然避免暴露。这是比”记得开防火墙”更可靠的物理隔离。

OLLAMA_MODELS:改模型存储路径

默认路径是 macOS 的 ~/.ollama/models 和 Linux 的系统用户目录 /usr/share/ollama/.ollama/models。一个 32B 的 Q4 模型大概 20GB,再加几个不同规格的模型,系统盘很快就满了。把它挪到数据盘是必要操作。注意挪完之后要先把旧模型目录里的内容拷贝过去,否则 ollama list 会变成空的,但磁盘空间还没释放——这是新手最常困惑的场景之一。

# 迁移模型目录的正确步骤
sudo mkdir -p /data/ollama/models
sudo cp -r /usr/share/ollama/.ollama/models/* /data/ollama/models/
sudo chown -R ollama:ollama /data/ollama/models
# 然后再设置 OLLAMA_MODELS 并重启服务
# 确认无误后再手动删除旧目录,不要提前删

OLLAMA_KEEP_ALIVE:模型常驻时长

默认是 5 分钟。超过这个时间没有请求,模型就从显存卸载。对于个人偶尔用一次的场景很合理,但对频繁交互的场景就是灾难:每次提问前都要等 5 到 20 秒把模型重新加载进显存,体感像在用 2010 年的网页。设成 30m 或 -1(永久常驻,注意显存会被一直占用)能显著改善体验。代价是显存不释放,如果你还要跑别的模型或做训练,就别设 -1。

OLLAMA_NUM_PARALLEL:并发数

这个参数决定同一个模型能同时处理几个请求。默认值会根据显存自动推断,通常是 1 或 2。调到 4 会让吞吐量明显提升,但每个请求能用的上下文长度会按比例缩水——因为总显存是固定的,并行度越高,每个槽位分到的 KV Cache 越少。这是 Ollama 里最反直觉的一个权衡:并发数和上下文长度在抢同一块显存。团队共享场景调到 2 到 4 比较务实,个人使用保持 1 反而更好。

第二步:下载并试用 DeepSeek 模型

选哪个规格:7B、14B、32B 还是 70B

DeepSeek-R1 系列在 Ollama 上的蒸馏版本覆盖了多个规格。基础的拉取命令非常统一:

# 按需选择,先拉小的验证链路,再拉大的
ollama pull deepseek-r1:7b
ollama pull deepseek-r1:14b
ollama pull deepseek-r1:32b
ollama pull deepseek-r1:70b

# 也可以用更小的嵌入模型做向量检索
ollama pull nomic-embed-text

需要提醒的是:具体可用的 tag 以 ollama.com/library 页面为准,模型在持续更新,新的量化版本、新的蒸馏规格、甚至模型命名都可能变化。如果你 pull 时报 manifest unknown,第一反应应该是去官网库页面看一眼当前 tag 列表,而不是怀疑网络。

显存估算表:别只看参数量

网上大量文章会告诉你”7B 需要 8G 显存、70B 需要 48G”,这个说法粗糙到会误导人。真实显存占用由三部分构成:权重(参数量 × 每参数字节数)、KV Cache(与上下文长度、层数、隐藏维度成正比)、以及推理时的临时缓冲。下面这张表按每参数字节数给出一个务实区间,假设上下文为 8K,实际会因批次和上下文而浮动。

参数量 量化等级 每参数字节 权重显存 建议显卡显存 实际体感
7B Q4_K_M 约 0.55 B 约 4.5 GB 8 GB 笔记本独显可流畅,手机级别不现实
7B Q8_0 约 1.05 B 约 8 GB 12 GB 接近原模型的推理质量,速度略降
14B Q4_K_M 约 0.55 B 约 9 GB 12 GB 16G 显存的最佳甜点,日常问答够用
14B Q8_0 约 1.05 B 约 16 GB 24 GB 论文级文本处理明显更稳
32B Q4_K_M 约 0.55 B 约 20 GB 24 GB 单机最强的性价比档,需要 24G 卡
32B Q8_0 约 1.05 B 约 35 GB 48 GB 单卡基本无缘,需双卡或专业卡
70B Q4_K_M 约 0.55 B 约 43 GB 48 GB 起 Mac Studio 64G 统一内存可跑,速度一般
70B Q8_0 约 1.05 B 约 78 GB 96 GB 起 个人硬件基本不现实,接近服务器级

表里”建议显存”比”权重显存”高出一截,原因就是 KV Cache。以 32B 模型为例,14G 左右的富余量,在 32K 上下文、并发 4 的情况下会迅速吃满。很多人在 24G 卡上跑 32B Q4 第一个问题很流畅,聊到第二十轮突然卡死或报 OOM,就是这个原因。

Mac 统一内存是另一种算法

Apple Silicon 的显存和内存是共享的,理论上 64G 内存的 Mac Studio 能跑 70B Q4。但实际体验要打折扣:统一内存的带宽远低于独立显卡的 HBM,token 生成速度通常在 8 到 15 token/s 量级,比 RTX 4090 跑 32B 慢不少。它的优势是”能跑”和”安静低功耗”,劣势是”跑得慢”。如果你是想给团队做一个 24 小时在线的服务,Mac 不是好选择;如果是个人离线写作和代码辅助,Mac 反而是性价比最高的方案。

首次运行与进程观察

拉完模型先不要急着配置界面,直接命令行试一下,这是确认”模型本身能不能用”最快的方式。

# 交互式对话
ollama run deepseek-r1:14b
# 进入后直接输入问题,按 Ctrl+D 退出

# 单次非交互提问
ollama run deepseek-r1:7b "用三句话解释什么是向量数据库"

# 查看当前已加载到显存的模型
ollama ps
# 输出会显示 NAME / SIZE / PROCESSOR(100% GPU 表示全部在显存里,
# 出现 CPU 字样说明有部分被换到了内存,速度会掉一个数量级)

# 查看磁盘占用
ollama list   # 每个模型的大小
du -sh ~/.ollama/models   # 总占用

ollama ps 是最被低估的一条命令。它的 PROCESSOR 列会告诉你模型是否真的全在 GPU 上。当你发现回答速度突然从 30 token/s 掉到 3 token/s,第一件事就是看这里——大概率是显存不够,模型的部分层被卸载到了 CPU,推理被串行化了。

磁盘占用的现实

一个 32B Q4 模型约 20GB,14B Q4 约 9GB,7B Q4 约 4.7GB。看起来还好,但 Ollama 拉取时会有临时的 blob 文件,并且不同 tag 的模型会完整重复存储。如果你试了 7B、14B、32B、70B 各一个,轻松超过 70GB。删模型用 ollama rm deepseek-r1:7b,删完之后记得跑一次 du 确认空间真的释放了。如果发现空间没变,去看看 OLLAMA_MODELS 指向的目录是不是有两个副本。

第三步:用 Docker 装 Open WebUI

一条命令启动,但参数要改

Open WebUI 的官方一条命令长这样,但直接抄会遇到”模型列表为空”的问题,因为 OLLAMA_BASE_URL 的默认值在容器里指向的是容器自身。

# 推荐版本:显式传 Ollama 地址,并挂载数据卷
docker run -d \
  -p 3000:8080 \
  -v open-webui:/app/backend/data \
  --name open-webui \
  --restart unless-stopped \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  ghcr.io/open-webui/open-webui:main

注意这里的关键点:OLLAMA_BASE_URL 不能写 127.0.0.1。原因前面说过,容器有自己的网络命名空间,127.0.0.1 是容器自己。在 Docker Desktop(macOS / Windows)里,宿主机有一个特殊的别名 host.docker.internal,容器通过它可以访问宿主机上的服务。这个别名在 Linux 上默认不存在,要么在 run 命令里加 --add-host=host.docker.internal:host-gateway,要么直接改用局域网 IP。

Linux 上的三种正确写法

# 写法 A:host 网络模式,最简单,容器直接用宿主机网络栈
docker run -d --network=host \
  -v open-webui:/app/backend/data \
  --name open-webui \
  -e OLLAMA_BASE_URL=http://127.0.0.1:11434 \
  ghcr.io/open-webui/open-webui:main
# 注意:host 模式下端口由环境变量 PORT 控制,默认 8080

# 写法 B:加 host-gateway 映射,保持桥接网络
docker run -d \
  -p 3000:8080 \
  --add-host=host.docker.internal:host-gateway \
  -v open-webui:/app/backend/data \
  --name open-webui \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  ghcr.io/open-webui/open-webui:main

# 写法 C:直接用宿主机局域网 IP(需要 Ollama 监听 0.0.0.0)
docker run -d \
  -p 3000:8080 \
  -v open-webui:/app/backend/data \
  --name open-webui \
  -e OLLAMA_BASE_URL=http://192.168.1.20:11434 \
  ghcr.io/open-webui/open-webui:main

三种写法里,我个人最推荐写法 B。写法 A 最省心但会占用宿主机端口、丢失端口隔离,和其他服务冲突时很难排查;写法 C 要求 Ollama 监听所有网卡,安全面变大。写法 B 兼顾了隔离和连通性,是目前 Linux 上的最优解。

首次访问与管理员注册

浏览器打开 http://localhost:3000,会看到注册页。第一个注册的账号会自动成为管理员,之后 Open WebUI 默认关闭新用户注册(可以在设置里打开)。这一步是安全的,因为如果在你注册之前有人先访问了这台机器,他就能抢走管理员。所以部署完立刻注册,不要留空窗期。

注册完成后,左上角的模型选择器应该能看到 DeepSeek 的模型。如果能看到但对话报错,问题通常在 Ollama 侧的地址或权限;如果根本看不到,问题在连接。

模型列表为空的排查顺序

这是 Open WebUI 领域最高频的问题,几乎每个新部署的人都会遇到。按下面的顺序排查,不要跳步:

  1. Ollama 是否在跑。在宿主机上执行 curl http://127.0.0.1:11434/api/tags。没返回 JSON,就 systemctl status ollama 看服务状态。
  2. Ollama 是否有模型。返回 {"models":[]} 说明服务正常但没拉模型,先 ollama pull。
  3. 容器能否访问到 Ollama 地址。进容器里测:docker exec -it open-webui sh -c "curl http://host.docker.internal:11434/api/tags"。注意容器里可能没有 curl,可以用 wget -qO- 或 python -c。
  4. Open WebUI 里连接的 URL 是否写对。登录后在设置 → 模型 → 连接 Ollama 里看一眼,改完点保存,必要的时候刷新 Ollama 连接。
  5. 环境变量是否被旧容器缓存。改完 -e 必须 docker rm -f open-webui 重建容器,不要以为 docker restart 会生效。

第五点是最容易踩的坑:很多人改完参数执行 docker restart open-webui,然后盯着还是空白的列表发火。环境变量在容器创建时就固化了,restart 不会重新读取 run 命令里传的 -e 参数——你得删掉重建。

进容器测连通的正确命令

# 进容器内部手动测连通性
docker exec -it open-webui python -c "import urllib.request;print(urllib.request.urlopen('http://host.docker.internal:11434/api/tags',timeout=5).read().decode())"

# 如果报 Connection refused:Ollama 没监听或被防火墙拦
# 如果报 Name or service not known:host.docker.internal 别名不存在(Linux 未加 --add-host)
# 如果超时:网络路由问题,检查是否有 VPN 或其他网卡干扰

第四步:API 调用,把 Ollama 变成你自己的后端

原生 API 的四个核心端点

Ollama 自带的 HTTP API 设计得非常直白,全部是 POST 或 GET 的 JSON 接口,没有 OAuth、没有签名、没有限流——这也正是后面安全问题的根源。

端点 方法 用途 关键参数
/api/tags GET 列出本地模型 无,直接返回 models 数组
/api/generate POST 单次补全,不带对话历史 model、prompt、stream、options
/api/chat POST 多轮对话,传完整 messages model、messages、stream、tools
/api/pull POST 拉取模型,可编程式批量下载 name、stream

/api/generate 适合”一问一答”场景,比如批量摘要、格式化转换;/api/chat 才是对接前端聊天的正确选择,因为你需要维护消息历史。两者都支持 stream: true,返回换行分隔的 JSON(NDJSON),前端逐行解析就能实现打字机效果。

OpenAI 兼容端点:零成本迁移

Ollama 在 /v1 路径下实现了一套 OpenAI 兼容接口。这意味着你手上所有基于 openai SDK 写的代码,只要改两个参数就能跑在本地:

from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama",  # 随便填,Ollama 不校验
)

resp = client.chat.completions.create(
    model="deepseek-r1:14b",
    messages=[{"role": "user", "content": "你好"}],
)
print(resp.choices[0].message.content)

api_key 必须传但不会被校验,这是兼容层的妥协。别指望它能帮你做鉴权,它只是让 SDK 不报错。如果有一天你要把这个端点暴露给外部,请自己在前置网关上加一层 key 校验,不要在本地裸奔。

可运行的 Python 示例

示例一:普通对话

# 普通同步对话,最基础的调用
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

def ask(question, model="deepseek-r1:14b"):
    resp = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": question}],
        temperature=0.6,
    )
    return resp.choices[0].message.content

if __name__ == "__main__":
    print(ask("用一段话解释什么叫 KV Cache,以及它为什么吃显存"))

示例二:流式输出

# 流式输出,适合做终端工具或命令行助手
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

def ask_stream(question, model="deepseek-r1:14b"):
    stream = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": question}],
        stream=True,
        temperature=0.6,
    )
    for chunk in stream:
        delta = chunk.choices[0].delta.content
        if delta:
            print(delta, end="", flush=True)
    print()

if __name__ == "__main__":
    ask_stream("写一段 Python 代码,把 CSV 按列分组成多个文件")

示例三:带历史的多轮对话

# 多轮对话:自己维护 messages 列表
from openai import OpenAI

client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")

class LocalChat:
    def __init__(self, model="deepseek-r1:14b", system=None):
        self.model = model
        self.history = []
        if system:
            self.history.append({"role": "system", "content": system})

    def send(self, text):
        self.history.append({"role": "user", "content": text})
        resp = client.chat.completions.create(
            model=self.model,
            messages=self.history,
            temperature=0.6,
        )
        answer = resp.choices[0].message.content
        self.history.append({"role": "assistant", "content": answer})
        return answer

if __name__ == "__main__":
    bot = LocalChat(system="你是一位严谨的数据库架构师,回答尽量简短。")
    print(bot.send("我有一张订单表,一千万行,查询慢"))
    print("---")
    print(bot.send("刚才说的表,加索引有什么讲究?"))  # 能接上前文

注意 LocalChat 里的 history 会一直增长,长对话会迅速把上下文撑满。生产代码里要做截断:保留 system 消息 + 最近 N 轮,或者做摘要压缩。这也是本地模型最大的工程细节之一,比在线 API 更需要你自己管上下文预算。

原生 API 的直连写法

如果你不想装 SDK,用 requests 直连也一样干净:

import requests, json

# 原生 /api/chat
r = requests.post(
    "http://localhost:11434/api/chat",
    json={
        "model": "deepseek-r1:7b",
        "messages": [{"role": "user", "content": "你好"}],
        "stream": False,
    },
    timeout=120,
)
print(r.json()["message"]["content"])

# 拉模型的编程式写法
r = requests.post(
    "http://localhost:11434/api/pull",
    json={"name": "deepseek-r1:7b", "stream": False},
    timeout=None,
)
print(json.dumps(r.json(), ensure_ascii=False)[:200])

第五步:远程访问,在”能用”和”安全”之间选边

局域网访问的正确姿势

让同一 Wi-Fi 下的手机、平板、同事的电脑访问你的模型,最常见的做法是把 OLLAMA_HOST 改成 0.0.0.0:11434。这确实能成功,但它同时打开了所有网卡。更克制的做法是只绑定局域网网卡的 IP:

# 更安全的写法:只绑内网网卡
OLLAMA_HOST=192.168.1.20:11434

# 然后让 Open WebUI 也监听局域网
docker run -d \
  -p 0.0.0.0:3000:8080 \
  -v open-webui:/app/backend/data \
  --add-host=host.docker.internal:host-gateway \
  --name open-webui \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  ghcr.io/open-webui/open-webui:main

Open WebUI 通过 -p 控制监听地址,0.0.0.0:3000:8080 表示所有网卡 3000 端口都监听。如果你只想让内网访问,可以写 192.168.1.20:3000:8080,Docker 会只在这一块网卡上转发。这是很多人不知道的能力:-p 的第一个冒号前可以写 IP。

安全警告:公网暴露 11434 = 把模型送人

这是我必须用最重的语气说的一件事:Ollama 的 11434 端口没有任何身份验证。没有 token、没有 key、没有 IP 白名单。任何人只要知道你的 IP 和端口,就能:

  • 用 /api/tags 列出你所有模型,知道你在研究什么
  • 用 /api/pull 给你拉一堆几十 G 的模型,把你的磁盘撑爆
  • 用 /api/generate 无限次调用,把你的 GPU 占满
  • 用 /api/delete 删掉你的模型

更讽刺的是,互联网上一直有专门扫描 11434 端口的爬虫,它们会替你完成上面这些操作。我在一些安全审计里见过真实案例:一台被临时映射到公网测试的机器,三天内 GPU 利用率常年 100%,模型列表里多出好几个自己没拉过的模型,账单里多出好几度电。这不是”理论上可能”,这是”一定会”。

Open WebUI 则好一些,它自带账号体系,注册后需要登录。但它的默认配置并不够强:没有强制强密码、没有登录失败锁定、没有 2FA(早期版本)。对于小团队可以接受,对于公网仍然不够。

几种远程访问方式的对比

下面这张表是本文最值得收藏的一张:

方案 攻击面 需要公网 IP 部署成本 适用场景 风险等级
直接映射 11434 到公网 整个 Ollama API 裸奔 是 极低 无人应在生产使用 极高
映射 Open WebUI 到公网 登录页 + 账号体系 是 低 个人临时试用 中高
Nginx 反代 + HTTPS 证书 加密传输,登录仍可被爆破 是 中 给外部客户交付的小服务 中
防火墙白名单 + 内网 IP 段 仅限指定来源 否 低 公司内网、家庭内网 低
Tailscale / WireGuard 组网 不暴露公网端口,零信任 否 低 异地团队、远程办公 很低
SSH 隧道端口转发 复用 SSH 认证 否 极低 单人临时远程 很低

从这张表能读出一个结论:凡是能在不暴露公网端口的前提下实现远程的方案,安全性都自动高一档。Tailscale、WireGuard、SSH 隧道,本质上都是在公网之上套一层加密的虚拟内网,外部扫描器根本看不到你的 11434。

推荐做法:反代加 HTTPS 加来源限制

如果确实需要一个对外的访问入口(比如给客户演示),完整的安全栈应该长这样:

# Nginx 配置示例:只暴露 Open WebUI,不碰 Ollama
server {
    listen 443 ssl http2;
    server_name ai.example.com;

    ssl_certificate     /etc/letsencrypt/live/ai.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ai.example.com/privkey.pem;

    # 只允许公司出口 IP 和指定 VPN 段
    allow 203.0.113.0/24;
    allow 10.8.0.0/24;
    deny all;

    # 基础限流,防暴力尝试
    limit_req zone=one burst=10 nodelay;
    client_max_body_size 20m;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 600s;   # 流式生成长,超时要放宽
    }
}

注意 proxy_read_timeout 600s 这一行。本地模型生成长回答很慢,Nginx 默认 60 秒超时,会出现”前端还在等,连接已经被掐断”的诡异现象。这是自建本地大模型服务最容易被忽略的运维细节之一。

远程团队的场景:Tailscale 是更优解

如果你的团队里有成员在外地、在家里、在客户现场,Tailscale 或 WireGuard 的体验远好于端口映射。Tailscale 的 Tailnet 给每台设备分配一个 100.x 网段的内网 IP,你只需要在 Ollama 所在的机器上装 Tailscale,其他设备加入同一个 Tailnet,就能像访问局域网一样访问。

# 在服务端设备上
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale ip -4   # 拿到 100.x.y.z 的地址

# 服务端只监听 Tailscale 这一块网卡
# Linux systemd:
Environment="OLLAMA_HOST=100.x.y.z:11434"

# 团队成员电脑安装 Tailscale 后,直接访问:
# http://100.x.y.z:3000   Open WebUI
# http://100.x.y.z:11434  Ollama API

这种做法的好处是:公网上没有任何端口对外开放,扫描器扫不到;Tailscale 内部是 WireGuard 加密,流量在传输层就保密;设备准入由 Tailnet 管理,丢一台设备直接移除即可。对于”数据不出本机”这个诉求,它在网络层面完全满足——所有流量只在你的设备之间加密传递,不经过第三方的推理服务。

第六步:”数据不出本机”到底有多真

拉模型时的那一次外网下载

“数据不出本机”这句话需要精确化。准确的表述是:推理过程不进云,但模型本体和元数据来自云。拉模型时,Ollama 会从 registry.ollama.ai 下载模型权重,这是一个几百 MB 到几十 GB 的下载,走 HTTPS,内容本身不含你的任何数据,但你的 IP、下载时间、下载的模型名会被服务器记录。这点和 apt install 没有本质区别。

拉完之后,ollama run、/api/generate、Open WebUI 的聊天,全部在本机完成。你的问题、回答、上传的文档,都不会离开这台机器。这点是可以验证的:断开网络,把模型拔到外网都不可达的环境,模型依然能正常回答。

Open WebUI 可能的外连

Open WebUI 在默认配置下有几个可能外连的点,需要在安全评审时明确:

  • 更新检查:容器启动时会检查新版本,可以设置 ENABLE_VERSION_UPDATE_CHECK=false 关掉。
  • 模型列表同步:如果你配置了 ollama.com 作为模型来源,它会去拉公开模型库列表,设置 OFFLINE_MODE=true 可全局关闭。
  • 社区模型/插件市场:在设置里绑定 ollama.com 账号后,会同步个人模型和工具;不绑定就不会。
  • 匿名使用统计:默认开启,可在设置里关闭,也可以用环境变量 ANONYMIZED_TELEMETRY=false 关掉。
  • 容器镜像拉取:docker pull 本身是外连,这属于基础设施范畴。要完全离线,需要用 docker save/docker load 传镜像。
# 完全离线的 Open WebUI 启动参数
docker run -d \
  -p 127.0.0.1:3000:8080 \
  --add-host=host.docker.internal:host-gateway \
  -v open-webui:/app/backend/data \
  --name open-webui \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  -e OFFLINE_MODE=true \
  -e ENABLE_VERSION_UPDATE_CHECK=false \
  -e ANONYMIZED_TELEMETRY=false \
  ghcr.io/open-webui/open-webui:main

需要诚实地说:OFFLINE_MODE 能关掉大部分功能,但它也会让某些依赖外部服务的功能不可用,比如某些模型的图标、在线文档检索。这是权衡,不是缺陷。

日志与会话记录的本地存储

所有聊天记录、用户、配置都存在 /app/backend/data/webui.db,这是一个 SQLite 数据库,挂在 open-webui 这个 Docker volume 里。它就在你自己的磁盘上,这是”数据不出本机”最实在的部分——但同时也意味着,备份和清理是你的责任。

# 备份数据库:先停容器,再拷贝,避免写一半
docker stop open-webui
docker run --rm -v open-webui:/data -v $(pwd):/backup \
  alpine tar czf /backup/openwebui-$(date +%F).tar.gz -C /data .
docker start open-webui

# 查看数据库体积
docker exec open-webui du -sh /app/backend/data/webui.db

# 清理:删掉 30 天前的对话(谨慎操作,先备份)
docker exec -it open-webui python -c "
import sqlite3, datetime
db = sqlite3.connect('/app/backend/data/webui.db')
cutoff = (datetime.datetime.now() - datetime.timedelta(days=30)).isoformat()
cur = db.execute('DELETE FROM chat WHERE updated_at < ?', (cutoff,))
db.commit()
print('deleted rows:', cur.rowcount)
db.close()
"

注意:直接操作 SQLite 前一定要停掉容器或至少确认没有正在写入,否则可能损坏数据库。企业场景更稳妥的做法是重建一个新 volume,把旧数据导出成 JSON 后再导回去。Ollama 自身也会在 ~/.ollama/logs/ 下写日志,其中会记录请求的 prompt 和响应摘要,如果你处理的是敏感数据,这些日志也要定期清理,或者干脆把 OLLAMA_DEBUG 保持关闭。

量化模型和全精度模型的真实落差

本地跑模型的人都会问:Q4 量化的 32B 和原版 32B,效果差多少?诚实的答案是:在绝大多数任务上差距很小,但在长上下文和细节精确度上有可感知的下降。

具体来说:Q8 相比原版基本无损,几乎没有理由不用 Q8(如果显存够);Q4_K_M 在常识问答、摘要、改写、代码生成上表现稳定,但在数学推理的中间步骤、长文档的细节召回、复杂 JSON 格式保持上,错误率会升高。有趣的是,量化对"知识"影响小,对"推理链"影响大——因为推理过程依赖大量的浮点运算稳定性,权重的细微抖动会在多步推理中放大。

所以选模型时,我的建议是:优先用更大的模型跑低量化,而不是小模型跑高量化。14B Q4 往往比 7B Q8 效果好得多,而显存需求几乎一样。这是本地部署里最反直觉的一条经验。

上下文长度受显存限制的现实

模型标签上写的 "128K 上下文",在本地往往跑不到。原因就是 KV Cache:上下文越长,缓存越大。在 24G 显存的卡上跑 32B 模型,实际能稳定用的上下文通常只有 8K 到 16K,虽然可以设 num_ctx 到 32K,但会面临 OOM 或者性能急剧下降。

# 显式设置上下文长度(越小越省显存)
ollama run deepseek-r1:14b
>>> /set parameter num_ctx 8192
# 或者在 API 里传
curl http://localhost:11434/api/chat -d '{
  "model": "deepseek-r1:14b",
  "messages": [{"role":"user","content":"hi"}],
  "options": {"num_ctx": 8192, "num_predict": 2048}
}'

Open WebUI 里也有类似设置,在管理员面板 → 设置 → 模型 → 高级参数里,可以给每个模型设默认上下文。默认值往往偏大,需要根据你的硬件调小。

大多数人做错的地方

错误一:把 11434 端口直接暴露到公网

这是本文最想强调的一条。前面已经详细说过后果,这里补充一个观察:做这件事的人往往不是不懂安全,而是"临时测试一下"。路由器上做个端口映射,本意是下班后在家里试试,结果忘了关。这类事故的共同特征是"临时变成永久"。

正确的习惯是:任何需要临时开放的端口,在开放的那一刻就设一个闹钟,24 小时后必须检查并关闭;或者干脆用 Tailscale 代替,因为它压根不需要开放端口。

错误二:把 API key 设为 "ollama" 却当作安全措施

很多人看到 SDK 里 api_key="ollama" 能跑通,就以为这是某种密钥。它不是。你改成 api_key="mysecret" 也一样能跑,因为服务端压根不读这个字段。真正的鉴权必须在网关层做。

错误三:只看参数量,不看 KV Cache

买卡时盯着"32B 需要 20G",买了 24G 卡,以为万事大吉。实际跑起来发现长对话必崩,或者速度骤降。这是只算了 weight 没算 cache 的典型失败。选卡时至少预留 40% 余量给 KV Cache 和缓冲。

错误四:以为 Open WebUI 的遥测"无所谓"

对于个人使用确实无所谓,但对于医疗、法律、金融这类强合规场景,任何外连都需要在数据流图上画出来。默认的匿名统计哪怕只传版本号和功能使用频次,也要在安全评审里申报。关掉它只需要一个环境变量,但很多人从来没打开过设置页。

错误五:把 Ollama 当成数据库,模型和业务数据混在一个盘

OLLAMA_MODELS 和 Open WebUI 的 webui.db 如果都放在系统盘、跟业务代码的盘混在一起,备份策略会变得非常混乱。建议把模型目录、Open WebUI 数据目录、备份目录三者物理隔离,各自有独立的状态和清理逻辑。

错误六:用 docker restart 代替重建容器

前面提过,但我认为值得单独列出来。Open WebUI 的所有配置几乎都靠环境变量,而环境变量只在容器创建时注入。改配置 = 删容器重建。这条经验能省掉很多人一个下午的排查时间。

错误七:忽视 ollama ps 这个诊断入口

几乎所有"为什么突然变慢"的问题,答案都在 ollama ps 的 PROCESSOR 列。100% GPU 才算全速,出现 CPU 说明有层被卸载,这是性能骤降的直接证据。养成改配置后第一时间看它的习惯。

进阶:让这套方案更稳

Compose 编排,把配置固化下来

用 docker run 一条命令验证可以,但长期使用应该写成 compose 文件,把版本、卷、环境变量、重启策略都固化,避免"重装一次环境又调半天"。

# docker-compose.yml
services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    restart: unless-stopped
    ports:
      - "127.0.0.1:3000:8080"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    volumes:
      - open-webui:/app/backend/data
    environment:
      - OLLAMA_BASE_URL=http://host.docker.internal:11434
      - ENABLE_VERSION_UPDATE_CHECK=false
      - ANONYMIZED_TELEMETRY=false
      - WEBUI_AUTH=true

volumes:
  open-webui:

注意 ports 里写的是 127.0.0.1:3000,这意味着 Open WebUI 只对本机开放,对外的入口必须经过 Nginx 或 Tailscale。这是默认安全的配置,需要远程时再显式打开。

安全检查清单

每次部署完成、每次改动网络配置后,照下面的清单过一遍。这一份可以直接贴进团队的 SOP 文档。

# ===== Ollama 本地部署安全检查清单 =====

[ ] 1. Ollama 未监听 0.0.0.0(除非确实需要,并已加防火墙)
    ss -tlnp | grep 11434
    # 期望看到 127.0.0.1:11434 或内网 IP,不是 0.0.0.0

[ ] 2. 路由器/云安全组未将 11434 映射到公网
    # 登录路由器后台,检查端口转发列表,确认无 11434

[ ] 3. 若必须对外,只暴露 Open WebUI 端口,不暴露 11434
    # Open WebUI 有账号体系,Ollama 没有

[ ] 4. Open WebUI 监听地址收紧
    docker port open-webui
    # 期望 127.0.0.1:3000 或内网 IP

[ ] 5. 已设置强密码并关闭开放注册
    # 管理员面板 → 设置 → 用户 → 关闭 "允许新用户注册"

[ ] 6. 遥测与更新检查已关闭
    docker inspect open-webui | grep -E "TELEMETRY|OFFLINE"
    # 期望看到 "false" / "true"

[ ] 7. Tailscale / WireGuard 组网已启用(远程访问场景)
    tailscale status
    # 所有客户端都在同一 Tailnet

[ ] 8. 反向代理只允许白名单 IP
    nginx -t
    # 检查 allow/deny 规则

[ ] 9. 日志清理策略已配置
    du -sh ~/.ollama/logs
    # 定期清理,或确认轮转已生效

[ ] 10. 数据库备份已跑过一次
    ls -lh openwebui-*.tar.gz
    # 定期备份,确认可以恢复

常见问题 FAQ

Ollama 装好了,但是拉模型特别慢,有没有加速的方法?

Ollama 的模型仓库没有国内镜像,下载速度取决于你到 registry.ollama.ai 的连接质量。可以尝试:一是设置代理,在启动 Ollama 的进程里设 HTTPS_PROXY 环境变量(注意是 Ollama 服务进程,不是你的 shell);二是在网络状态好的时段拉取,大模型往往一次拉完就长期复用;三是使用 Hugging Face 上同模型的 GGUF 版本手动导入,绕过 Ollama 仓库。优先级上,代理方案最稳,手动导入更适合内网完全隔离的场景。

7B 和 32B 的差距到底有多大,值得为了 32B 换显卡吗?

在常识问答、简单改写这类任务上,7B 已经够用,差距不明显。但在需要多步推理、长文档理解、代码重构的场景里,32B 相比 7B 是质变级别的提升,而与 14B 相比则是明显提升但不夸张。换卡的判断标准不是"参数更大",而是"你当前的工作是否卡在推理深度上"。如果只是写邮件、总结会议记录,24G 卡的投入完全没必要;如果是要替代一部分真正的分析工作,24G 是这条线的门槛。

Open WebUI 里能同时连接多个 Ollama 实例吗?

可以。Open WebUI 的连接设置里支持添加多个 Ollama 地址,每个地址给一个自定义名称,模型列表会按来源合并显示。这个能力适合"一台跑 7B 的轻量机 + 一台跑 32B 的工作站"的组合,让不同任务自动落到合适的机器上。需要注意的时,模型名在不同实例可能重名,建议在命名上做区分,否则选择器里会分不清。

DeepSeek-R1 的思考过程很长,能不能关掉或者缩短?

R1 系列的推理链是模型本身训练出来的行为,Ollama 层面没有"官方开关",但可以通过设置较小的 num_predict 限制总输出长度,避免它一直反思下去。在 Open WebUI 里,思考过程通常会单独折叠显示,不影响阅读。实际使用中,对于简单问题,强制它"直接给答案,不要推理"往往会导致质量下降,因为你在让它违背训练目标;更好的做法是选择非推理型的模型版本用于简单任务,推理型模型留给复杂问题。

本机能跑模型,但同事的浏览器打不开,怎么排查?

按顺序查三件事:第一,在同事电脑上 ping 你的内网 IP,确认网络层通;第二,确认 Open WebUI 的端口映射不是 127.0.0.1:3000:8080,这个写法只对本机开放,要改成 0.0.0.0:3000:8080 或内网 IP;第三,检查宿主机防火墙,Linux 的 ufw 或 firewalld 默认可能拦截 3000 端口。三步都过了还打不开,多半是公司网络做了隔离,需要换用 Tailscale 这类组网方案。

升级 Ollama 或 Open WebUI 会不会把模型和数据弄丢?

Ollama 升级二进制不会删除 ~/.ollama/models 里的模型,Open WebUI 升级镜像也不会删除 volume 里的数据库。但有两个细节要注意:一是升级 Ollama 时如果 systemd 配置被覆盖,OLLAMA_HOST 等环境变量可能丢失,升级后要检查;二是 Open WebUI 跨大版本升级会有数据库迁移,迁移失败可能导致数据损坏,升级前务必先备份 volume。这两条都是踩过坑的经验,不是理论风险。

THE END
喜欢就支持一下吧
点赞13 分享
评论 抢沙发
头像-知了AI
欢迎您留下宝贵的见解!
提交
头像-知了AI

昵称

取消
昵称表情代码快捷回复

    暂无评论内容