用 Docker 在局域网部署 Prompt Optimizer:接入本地 llama.cpp 大模型的完整实战教程

一篇从镜像拉取到接入本地大模型、再到填平 6 个坑的保姆级部署指南。适合想在内网/无公网环境下自建 AI 提示词优化工具的开发者。


📑 目录

  1. 为什么要把提示词优化工具搬到本地
  2. Prompt Optimizer 是什么
  3. 整体架构与准备工作
  4. 第一步:拉取镜像与编写 Compose 配置
  5. 第二步:让本地大模型被识别
  6. 第三步:配置 nginx 反向代理(关键)
  7. 踩坑实录:6 个问题与解决方案
  8. 5 条可直接复用的经验
  9. 最终配置文件一览
  10. 常见问题 FAQ
  11. 后续可扩展方向

为什么要把提示词优化工具搬到本地

用过 ChatGPT、DeepSeek 的人都知道:提示词(Prompt)写得好不好,直接决定 AI 输出质量的天花板。 于是各种”提示词优化工具”应运而生——你丢进去一句大白话,它帮你改写成结构清晰、角色明确、约束完备的专业提示词。

但市面上的在线工具大多有个共同问题:你的提示词和 API Key 要经过别人的服务器。 对于涉及商业机密、客户数据或单纯不想联网的场景,这就很劝退。

有没有办法做到——

  • ✅ 提示词全程不出局域网
  • ✅ AI 模型跑在自己显卡上,零 API 费用
  • ✅ 工具开源、可自托管、可二次集成

答案是:用 Docker 部署开源的 Prompt Optimizer,再接一个本地 llama.cpp 模型服务。 这篇文章记录的就是一次完整的实战部署——包括最后填平的 6 个坑。如果你也想在内网搭一套,照着走一遍能省下至少 3 小时。


Prompt Optimizer 是什么

Prompt Optimizer 是一个基于 Vue 3 + Vite + Naive UI 的开源 AI 提示词优化工具。核心能力如下:

功能说明
一键优化多轮迭代改进提示词,支持”系统模式”和”用户模式”两种思路
对比评估优化前后效果 A/B 对比,直观看到提升
多模型支持OpenAI / Gemini / DeepSeek / 智谱 / Ollama / 自定义模型通吃
图像生成文生图(T2I)、图生图(I2I)、风格迁移
提示词管理模板、收藏、版本历史、导入导出
安全架构纯客户端处理,API Key 不经过服务器

最后一行是重点:它的”纯前端”架构意味着配置都存在浏览器本地(localStorage),这点后面会成为一个坑。

Github地址:https://github.com/linshenkx/prompt-optimizer

processed 2026 07 19T113230.490

整体架构与准备工作

目标架构

我们要搭的是一个”双机协作”的最小可用架构:

┌─────────────────────────────────────────────────────┐
│  局域网                                              │
│                                                     │
│  ┌──────────────┐         ┌──────────────┐         │
│  │ 你的电脑      │         │ Docker 服务器 │         │
│  │ (浏览器)      │         │ <服务器IP>    │         │
│  │              │────────▶│ Portainer     │         │
│  └──────────────┘         │ ┌───────────┐ │         │
│                           │ │prompt-opt │ │         │
│                           │ │:28081→:80 │ │         │
│                           │ └─────┬─────┘ │         │
│                           └───────┼───────┘         │
│                                   │                 │
│  ┌──────────────┐                 │                 │
│  │ llama.cpp    │◀────────────────┘                 │
│  │ <模型机IP>   │  nginx 反代 /llama/               │
│  │ :8080        │                                   │
│  │ 本地大模型   │                                   │
│  └──────────────┘                                   │
└─────────────────────────────────────────────────────┘

局域网你的电脑浏览器Docker 服务器Portainer 管理prompt-optimizer:28081 → :80llama.cpp server本地大模型 :8080访问 :28081nginx 反代 /llama/▲ 图 1:局域网部署架构拓扑——浏览器、Docker 服务器与 llama.cpp 模型机的连接关系

一句话概括:浏览器访问 Docker 服务器上的 Prompt Optimizer,它再通过 nginx 反代去调用另一台机器上的本地大模型。

说明:下文中的 <服务器IP><模型机IP> 是占位符,请替换成你自己局域网里的实际地址。两台机器也可以是同一台。

你需要准备什么

  • 一台装了 Docker + Portainer 的服务器(Linux 即可)
  • 一台跑着 llama.cpp server 的机器(有显卡更好,CPU 也能跑小模型)
  • 局域网内网络互通(能 ping 通彼此)
  • 基本的命令行操作能力

本地大模型这边

llama.cpp server 的启动不在本文重点范围内,但有几个要点:

  • 模型建议用 GGUF 量化格式(如 Gemma、Qwen 系列的 Q4 量化版,显存占用友好)
  • 启动时监听 0.0.0.0 而非 127.0.0.1,否则局域网访问不到
  • 上下文长度按显存量力而行(12G 显存跑 12B 模型 + 32K 上下文比较稳)

第一步:拉取镜像与编写 Compose 配置

1.1 拉取镜像

官方镜像为 linshen/prompt-optimizer:latest。直接拉:

docker pull linshen/prompt-optimizer:latest

如果拉取报 TLS 握手失败,多半是 Docker daemon 配了失效的镜像加速器。两个办法二选一:

# 办法一:换个能用的加速器,写入 /etc/docker/daemon.json
{
  "registry-mirrors": ["https://docker.1ms.run", "https://docker.xuanyuan.me"]
}
# 然后 sudo systemctl restart docker

# 办法二:手动指定源拉取(临时绕过加速器)
docker pull linshen/prompt-optimizer:latest

1.2 编写 docker-compose

在 Portainer Stacks(或本地 docker-compose.yml)里写如下配置:

services:
  prompt-optimizer:
    image: linshen/prompt-optimizer:latest
    container_name: prompt-optimizer
    restart: unless-stopped
    ports:
      - "28081:80"
    environment:
      # —— 注册一个名为 "llama" 的自定义模型提供商 ——
      - VITE_CUSTOM_API_KEY_llama=dummy_key          # 本地服务不需要真 Key
      - VITE_CUSTOM_API_BASE_URL_llama=http://<服务器IP>:28081/llama/v1
      - VITE_CUSTOM_API_MODEL_llama=your-model.gguf   # 你的模型文件名
      # MCP 默认走自定义模型
      - MCP_DEFAULT_MODEL_PROVIDER=custom
      # 访问认证(强烈建议设置)
      - ACCESS_USERNAME=admin
      - ACCESS_PASSWORD=<设置一个强密码>
    volumes:
      - /opt/prompt-optimizer/nginx-iframe.conf:/etc/nginx/http.d/default.conf:ro

几个要点先记住,后面会解释为什么这么写:

  • 端口 28081:80:把容器内 nginx 的 80 映射到宿主机 28081,避免和别的服务抢 80。
  • VITE_CUSTOM_API_* 环境变量:Prompt Optimizer 启动时会扫描这些变量,自动注册成一个模型提供商。命名规则是 VITE_CUSTOM_API_KEY_<名字>VITE_CUSTOM_API_BASE_URL_<名字>VITE_CUSTOM_API_MODEL_<名字>,三件套配套使用。
  • API Base URL 指向反代路径而不是直接指向模型机——这是绕过 CORS 的核心,下一节详说。
  • volumes 挂载自定义 nginx 配置:为了支持 iframe 嵌入和反代。

第二步:让本地大模型被识别

Prompt Optimizer 注册自定义模型的机制是:容器启动时有个 generate-config.sh 脚本,扫描所有 VITE_CUSTOM_API_* 环境变量,把它们注入到前端的 window.runtime_config 里。

所以你只要把三件套写进环境变量,前端就会多出一个叫 llama 的模型选项。无需改源码、无需重新构建镜像。

- VITE_CUSTOM_API_KEY_llama=dummy_key                              # 占位即可
- VITE_CUSTOM_API_BASE_URL_llama=http://<服务器IP>:28081/llama/v1   # 反代地址
- VITE_CUSTOM_API_MODEL_llama=your-model.gguf                      # 模型名

小贴士:本地 llama.cpp 默认不校验 Key,所以 dummy_key 随便填。但如果以后接云端 API,这里要填真实 Key。


第三步:配置 nginx 反向代理(关键)

这一步是整个部署的”心脏”。如果你跳过它,大概率会卡在”测试连接失败”上(详见踩坑实录第 2 个问题)。

我们要用一份自定义 nginx 配置替换掉镜像自带的,实现两件事:

  1. 允许 iframe 嵌入:移除 X-Frame-Options,方便以后把工具嵌进你的工具站
  2. 反代 /llama/ 路径:让浏览器访问同源地址,彻底绕过 CORS

3.1 在宿主机创建配置文件

sudo mkdir -p /opt/prompt-optimizer
sudo tee /opt/prompt-optimizer/nginx-iframe.conf << 'EOF'
server {
    listen 80;   # 注意:硬编码端口,不要用 ${NGINX_PORT}(原因见踩坑第5个)

    # 允许 iframe 嵌入
    add_header Content-Security-Policy "frame-ancestors *" always;

    # —— Llama API 反向代理(核心)——
    location /llama/ {
        auth_basic off;                              # 反代路径不走 Basic Auth
        proxy_pass http://<模型机IP>:8080/;           # 你的 llama.cpp server
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;

        # 完整 CORS 头
        add_header Access-Control-Allow-Origin * always;
        add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
        add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;

        # 处理 OPTIONS 预检请求
        if ($request_method = OPTIONS) {
            return 204;
        }
    }

    # SPA 路由(保留原镜像的认证)
    location / {
        include /etc/nginx/http.d/auth.conf;
        try_files $uri $uri/ /index.html;
    }
}
EOF

3.2 验证反代是否生效

部署后,直接在浏览器访问:

http://<服务器IP>:28081/llama/v1/models

如果返回一段 JSON(里面是你的模型列表),说明反代通了 ✅。


踩坑实录:6 个问题与解决方案

部署过程并非一帆风顺。下面这 6 个坑,每一个都真实踩过,按解决顺序排列。强烈建议先通读一遍再动手,能少走大量弯路。


坑 1:Docker 镜像拉取失败

现象

failed to copy: httpReadSeeker: ... remote error: tls: handshake failure

原因:Docker daemon 配了已失效的镜像加速器,TLS 握手直接挂。

解决:见上文”第一步 1.1″,换可用加速器或手动拉取。

耗时:约 10 分钟|严重度:🔴 阻断


坑 2:模型连接测试失败(CORS 跨域)—— 最烧脑的一个

现象:在 Prompt Optimizer 里点「测试连接」,报:

Llama连接测试失败:API错误: Connection test failed: Connection error.

但奇怪的是,直接 curl 模型机的接口却能正常返回模型列表。

排查过程(建议照着这个顺序套用到任何”连接失败”问题):

步骤结果结论
curl 测 GET /v1/models✅ 返回 JSON模型服务网络通
浏览器打开 llama Web UI✅ 正常显示浏览器到模型机网络通
给 llama.cpp 加 --cors 参数重启❌ 仍失败--cors 不够用

根因(关键洞察):

通过读 Prompt Optimizer 源码(追了 8 个文件)才发现——

  1. curl 测的是 GET /v1/models,这是”简单请求”,不触发 CORS 预检
  2. 工具测的是 POST /v1/chat/completions,还带了 Authorization: Bearer dummy_key 请求头;
  3. 带自定义头的 POST 会触发浏览器的 CORS 预检(OPTIONS 请求)
  4. 而 llama.cpp 的 --cors 参数对”带自定义头的预检请求”支持不完整,返回的 CORS 响应头缺胳膊少腿,浏览器直接拦截。
浏览器 ──POST + Authorization──▶ 模型机:8080
        │
        ├─ 先发 OPTIONS 预检
        ├─ llama.cpp 返回的 CORS 头不完整
        └─ 浏览器拦截 → Connection error

最终方案:用 nginx 反向代理,让浏览器访问同源路径,从根上消灭 CORS:

浏览器 ──同源──▶ <服务器IP>:28081/llama/v1 ──nginx反代──▶ 模型机:8080/v1

直连模型机(触发 CORS)浏览器POST + Auth 头模型机 :8080带 Authorization 头的 POST→ 触发 OPTIONS 预检→ CORS 响应头不完整→ 浏览器拦截,连接失败nginx 反代(同源)浏览器同源请求Docker + nginx服务器转发模型机 :8080无 CORS 预检请求直达,连接成功▲ 图 2:CORS 跨域问题与 nginx 反代方案对比——直连触发预检被拦截 vs 反代同源直达

核心原则:浏览器 CORS 是客户端安全策略,唯一可靠的绕过方式是让请求变成同源。别在源服务上折腾 --cors 参数了,反代才是正解。

耗时:约 2 小时(含源码分析)|严重度:🔴 阻断


坑 3:Portainer Stacks 文件挂载失败

现象

error mounting ".../nginx-iframe.conf" to rootfs at "...": 
not a directory: Are you trying to mount a directory onto a file?

原因:Portainer Stacks 的 Web 编辑器会把相对路径解析到它自己的内部目录(类似 /data/compose/<id>/),而不是你本地的工作目录。结果它找不到文件,就把挂载点当成空目录,容器起不来。

解决所有 volume 挂载一律用绝对路径

volumes:
  - /opt/prompt-optimizer/nginx-iframe.conf:/etc/nginx/http.d/default.conf:ro

并在宿主机上手动创建好该文件(见第三步 3.1)。

耗时:约 30 分钟|严重度:🟡 中断


坑 4:网站完全打不开

现象:部署后 http://<服务器IP>:28081/ 直接访问不了。

原因:上个坑的 volumes 挂载失败导致容器没起来,而旧容器已经被销毁,新容器又起不来——服务真空了。

解决:应急处理——先注释掉 volumes 让服务恢复,deploy 起来后,再去宿主机创建配置文件,最后取消注释重新 deploy:

# volumes:
#   - /opt/prompt-optimizer/nginx-iframe.conf:/etc/nginx/http.d/default.conf:ro

教训:改配置有风险时,先保证”能跑起来”的基线,再逐步叠加。别一步到位把服务搞挂。

耗时:约 15 分钟|严重度:🔴 阻断


坑 5:Nginx 崩溃(环境变量没被替换)

现象:挂上自定义 nginx 配置后,容器起来了但网站还是打不开。日志显示:

WARN exited: nginx (exit status 1; not expected)

原因:我照着原版配置抄了 listen ${NGINX_PORT};,但——

  • 原版镜像的 entrypoint 用 envsubst 处理 nginx 模板文件(在 /etc/nginx/templates/ 目录);
  • 而我直接把配置挂载到了最终路径 /etc/nginx/http.d/default.conf跳过了模板处理
  • 于是 nginx 把 ${NGINX_PORT} 当成字面字符串,解析不出端口号,启动崩溃。

解决:把变量硬编码为实际值

sudo sed -i 's/listen ${NGINX_PORT};/listen 80;/' /opt/prompt-optimizer/nginx-iframe.conf

重新 deploy 后,访问 /llama/v1/models 返回了模型 JSON,反代正式生效 ✅。

最佳实践:挂载自定义 nginx 配置到最终路径时,把所有 ${VARIABLE} 替换成实际值,别指望 envsubst 帮你处理。

耗时:约 30 分钟|严重度:🔴 阻断


坑 6:测试连接仍失败(localStorage 缓存)

现象:反代明明生效了(浏览器直访 /llama/v1/models 返回 JSON),但工具 UI 里的「测试连接」还是失败。

原因:Prompt Optimizer 是纯前端应用,模型配置存在浏览器 localStorage 里。之前手动测试时保存了一个错误的旧地址,而环境变量注入的默认值不会覆盖已保存的 localStorage

解决:在工具的模型设置 UI 里,手动把 API Base URL 改成反代地址:

http://<服务器IP>:28081/llama/v1

(或者直接清浏览器 localStorage / 用无痕窗口重试。)

耗时:约 15 分钟|严重度:🟡 中断


踩坑速查表

#问题严重度耗时根因类别
1Docker 镜像拉取失败🔴 阻断10min网络环境
2CORS 跨域连接失败🔴 阻断2h浏览器安全策略
3Portainer 文件挂载失败🟡 中断30minPortainer 路径解析
4网站完全打不开🔴 阻断15min容器未启动
5Nginx 崩溃🔴 阻断30min环境变量替换
6测试连接仍失败🟡 中断15minlocalStorage 缓存

总耗时约 3.5 小时(含源码分析)。如果提前看了这篇,估计 40 分钟搞定。


5 条可直接复用的经验

经验 1:CORS 问题,反代永远是正解

❌ 在源服务加 --cors 参数        → 对预检请求支持不完整
❌ 在源服务加 --allow-origin     → 同上
✅ 中间层加 nginx 反向代理       → 同源请求,彻底解决

经验 2:Portainer Stacks 不认相对路径

Web 编辑器会把所有相对路径解析到内部目录。记住一句话:在 Portainer Stacks 里,volume 挂载永远用绝对路径。

经验 3:Docker 镜像的模板变量会”失灵”

很多官方镜像用 envsubst 处理 nginx 模板变量(如 ${NGINX_PORT}),但直接挂载到最终路径会跳过模板处理。挂载自定义配置时,把所有 ${VARIABLE} 写成实际值。

经验 4:纯前端应用的 localStorage 是”隐形缓存”

像 Prompt Optimizer 这类纯前端应用,配置存在 localStorage,环境变量注入的默认值不会覆盖已保存值。改了环境变量后,记得清 localStorage 或在 UI 里手动更新。

经验 5:分层排查法——连接失败的万能套路

遇到”连接失败”类问题,按这个顺序排查最高效:分层排查法:连接失败排查顺序1curl 测试排除网络层问题2浏览器直接访问排除浏览器到目标的网络3F12 Network 抓包看 Method · URL · Status · Response4看容器日志看服务端是否收到请求5阅读源码看前端到底发了什么请求适用于几乎所有「前端调后端调不通」的场景▲ 图 3:连接失败分层排查法——从 curl 测试到阅读源码的五步排查顺序

这套打法适用于几乎所有”前端调后端调不通”的场景。


最终配置文件一览

部署完成后,你手上应该有这两个核心文件:

文件路径作用
Docker ComposePortainer Stacks / docker-compose.yml服务编排与环境变量注入
Nginx 配置/opt/prompt-optimizer/nginx-iframe.confiframe 支持 + 模型反代

运行状态检查清单:

组件检查方式期望结果
Prompt Optimizer浏览器访问 http://<服务器IP>:28081打开登录页
llama.cpp servercurl http://<模型机IP>:8080/v1/models返回模型 JSON
nginx 反代浏览器访问 http://<服务器IP>:28081/llama/v1/models返回模型 JSON
模型连接工具内「测试连接」通过 ✅
一键优化输入一句提示词点优化AI 生成结构化提示词 ✅

常见问题 FAQ

Q1:不用 Portainer,纯命令行能部署吗?
能。把 docker-compose.yml 放任意目录,docker compose up -d 即可。volumes 路径用相对路径也没问题(相对路径的坑只在 Portainer Stacks 里出现)。

Q2:模型机和 Docker 服务器是同一台行不行?
完全可以。把 <模型机IP> 换成 127.0.0.1 或 localhost,反代照常工作。注意容器内访问宿主机要用 host.docker.internal 或宿主机的局域网 IP。

Q3:没有显卡,能用 CPU 跑模型吗?
能,但慢。建议选小模型(7B 以下)+ 低量化,上下文调小。纯 CPU 跑 12B 模型优化一个提示词可能要等一两分钟。

Q4:不想自建模型,能接云端 API 吗?
当然。Prompt Optimizer 原生支持 OpenAI / DeepSeek / 智谱等。把环境变量换成对应云端的 Key 和 Base URL 即可。DeepSeek 性价比很高,适合做备用。

Q5:iframe 嵌入到自己的网站报错怎么办?
确认 nginx 配置里加了 add_header Content-Security-Policy "frame-ancestors *" always;,并移除了 X-Frame-Options: SAMEORIGIN。本文第三步的配置已经处理好这两点。

Q6:改了环境变量为什么没生效?
大概率是 localStorage 缓存(见坑 6)。清浏览器缓存、用无痕窗口,或在工具 UI 里手动改配置。

Q7:支持多个模型同时切换吗?
支持。注册多个 VITE_CUSTOM_API_*_<名字> 三件套即可,比如同时注册 llama 和 qwen 两个提供商,UI 里可下拉切换。


后续可扩展方向

部署跑通只是起点,后续还能往这些方向扩展:

  • 工具站集成:把 Prompt Optimizer 作为新标签页嵌入到你的个人工具站首页
  • 多模型切换:同时挂载 Gemma、Qwen 等多个本地模型,按场景切换
  • 云端兜底:加一个 DeepSeek 等云端 API 作为备用,本地模型挂了自动顶上
  • 自动更新:在 Portainer 配置 Watchtower 之类的工具实现镜像自动升级
  • HTTPS:通过反代或 Let’s Encrypt 给内网服务加上 HTTPS
  • 模型负载均衡:跑多个 llama 实例,用 nginx 做轮询负载均衡
  • MCP 集成:把内置的 MCP Server 接入工作流自动化

写在最后

这次部署最深刻的体会是:“连接失败”这四个字背后,可能藏着五层问题。 从网络层、浏览器安全策略、容器路径解析、模板变量处理,到前端缓存——任何一环掉链子,表现都是同样的”失败”。

好在每填一个坑,就多攒一条可复用的经验。希望这篇整理能帮你绕过这些弯路,把精力花在真正有意思的事上——比如调教出更厉害的提示词。

如果你也在折腾本地 AI 部署,欢迎在评论区交流你的踩坑故事。部署愉快 🚀


本文基于一次真实部署实践整理,配置均已脱敏。如需引用请注明出处。

Leave a Comment