1. 为什么要在 Docker 里折腾 OpenWebUI + one-api + Langfuse
如果你正在找一个能自己掌控的 ChatGPT 式界面,OpenWebUI 基本是绕不开的选择。它本身是一个自托管的 Web 前端,能对接 Ollama、OpenAI 兼容接口,也能通过管道扩展出联网搜索、代码执行这类能力。但真正把它放到团队或本地开发环境里用,问题就来了:模型来源太杂。OpenAI、Azure OpenAI、通义、DeepSeek、本地 Ollama,每家的接口格式、鉴权方式、endpoint 路径都不一样,OpenWebUI 里一个个配连接既乱又难维护。这时候 one-api 就派上用场了——它把各种大模型统一成 OpenAI 格式的 API,OpenWebUI 只需要认一个地址、一个 Key。
另一个容易被忽略的是可观测性。你在 OpenWebUI 里聊了什么、调用了哪个模型、耗时多少、token 消耗多少,默认是看不到调用链的。Langfuse 正好补上这块,它能把每次对话以 trace 的形式记录下来,方便排查“为什么这个请求这么慢”“哪个模型在偷偷烧钱”。
这篇就聚焦 OpenWebUI 在 Docker 中的开发部署流程,把 one-api 做统一模型接入、Langfuse 做调用链追踪这两件事串起来。我会给出可复制的 docker-compose 配置、环境变量清单,以及接口连通性验证步骤。适合已经会用 Docker、想搭一套本地可观测 AI 开发环境的人。整个过程我在 CentOS 服务器上实测过,踩的坑会一并写出来。
需要先说明一个前提:OpenWebUI 官方文档里的开发指南分“本机部署”和“Docker Compose 部署”两条路。本机部署是 conda 起前端、sh dev.sh 起后端,适合改源码;Docker 部署更适合环境隔离和长期运行。本文走 Docker 路线,但会保留开发模式的热更新能力,这样你改前端 src 或后端代码能直接生效。
2. 前置准备:TaoToken 统一接入与 Docker 环境
在动手写 compose 之前,先把“模型从哪来”这件事定下来。one-api 的作用是统一分发,但它自己不带模型,需要你往里面填上游渠道。如果你手头有多个厂商的 Key,一个个配渠道比较繁琐;更省事的做法是先用一个聚合接入层把模型统一暴露成 OpenAI 兼容接口,再让 one-api 去对接。
我这边用的是 TaoToken 的接入方式,它的 API 地址是https://taotoken.net/api,兼容 OpenAI 的/v1/chat/completions格式。你可以在 one-api 里把它当成一个 OpenAI 类型的渠道填进去,Base URL 填https://taotoken.net/api,Key 填你申请到的令牌,模型名按需填。这样 one-api 对外就有一套统一的模型列表,OpenWebUI 只连 one-api 就行。
具体操作上,先拿到 Key:打开https://taotoken.net/api-keys(带 utm 的完整链接是https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=),创建一个令牌并复制。这个 Key 后面会填到 one-api 的渠道配置里,不要直接写进 OpenWebUI。
Docker 环境这边,CentOS 上装 Docker 按官方文档走即可。装完后建议配一下镜像加速,否则拉ghcr.io和docker.io的镜像会非常慢。我实测可用的配置如下,写入/etc/docker/daemon.json:
{ "registry-mirrors": [ "https://docker.1ms.run", "https://doublezonline.cloud" ] }然后重载并重启:
sudo systemctl daemon-reload sudo systemctl restart dockerNode 版本是个隐藏坑。OpenWebUI 前端要求 Node 18.17.0 左右,版本不对npm ci会卡在 pyodide 相关依赖上。用 nvm 管理最稳:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | bash source ~/.bashrc nvm install 18.17.0 nvm use 18.17.0 nvm alias default 18.17.0确认node -v输出 v18.17.0 再继续。如果 npm 版本低于 6,执行npm install -g npm@latest。这些准备工作做完,后面 compose 启动才不会因为依赖问题反复失败。
3. 可复制配置:docker-compose 编排 OpenWebUI、one-api、Langfuse
这一节是核心,直接给可复制的配置。整体思路是三个服务:open-webui(开发模式,挂载源码)、one-api(统一模型网关)、langfuse(追踪)。为了减少依赖,Langfuse 这里用它的云端服务做追踪后端,本地只跑 OpenWebUI 的 pipelines 容器来转发 trace,这样不用自己维护 Postgres、ClickHouse 那一套。
先看 OpenWebUI 的开发 compose。文件名建议compose-dev.yaml,放在 open-webui 源码根目录:
name: open-webui-dev services: frontend: build: context: . target: build command: ["npm", "run", "dev"] depends_on: - backend ports: - "3000:5173" extra_hosts: - host.docker.internal:host-gateway volumes: - ./src:/app/src backend: build: context: . target: base command: ["bash", "dev.sh"] env_file: ".env" environment: - ENV=dev - WEBUI_AUTH=False - OPENAI_API_BASE_URL=http://host.docker.internal:3001/v1 - OPENAI_API_KEY=sk-oneapi-local ports: - "8080:8080" extra_hosts: - host.docker.internal:host-gateway volumes: - ./backend:/app/backend - data:/app/backend/data volumes: data: {}几个关键点。前端3000:5173是容器外 3000、容器内 5173,Vite 开发服务器默认监听 5173,映射错了会打不开。extra_hosts里的host.docker.internal:host-gateway让容器能访问宿主机上的 one-api,这是 Linux 下必须加的,否则容器里解析不了这个域名。WEBUI_AUTH=False在开发阶段跳过登录,省得反复认证。
one-api 的 compose 单独放一个目录,比如one-api/docker-compose.yml:
services: one-api: image: justsong/one-api:latest container_name: one-api restart: always ports: - "3001:3000" volumes: - ./data:/data environment: - TZ=Asia/Shanghai - SQL_DSN=root:123456@tcp(mysql:3306)/oneapi depends_on: - mysql mysql: image: mysql:8.0 container_name: one-api-mysql restart: always environment: - MYSQL_ROOT_PASSWORD=123456 - MYSQL_DATABASE=oneapi volumes: - ./data/mysql:/var/lib/mysqlone-api 对外映射 3001,容器内是 3000。它默认管理员账号root、密码123456,首次登录后务必改掉。登录进去后,在“渠道”里新建一个 OpenAI 类型渠道,Base URL 填https://taotoken.net/api,Key 填你在 TaoToken 拿到的令牌,模型名按你实际要用的填,比如gpt-4o-mini、claude-3-5-sonnet之类。保存后测试渠道,返回绿色即通。
Langfuse 这边,先在https://cloud.langfuse.com注册项目,拿到 public key 和 secret key。然后在 OpenWebUI 里启用 pipelines 容器:
docker run -p 9099:9099 \ --add-host=host.docker.internal:host-gateway \ -v pipelines:/app/pipelines \ --name pipelines \ --restart always \ ghcr.io/open-webui/pipelines:main启动后,在 OpenWebUI 管理设置里新建一个 OpenAI API 连接,URL 填http://host.docker.internal:9099,密码填0p3n-w3bu!。这是 pipelines 的标准密码,不是你的 Langfuse key。Langfuse 的 key 在 pipelines 的管道配置里填,具体在 OpenWebUI 的“管道”页面找到 Langfuse 过滤器,把 public key、secret key、host 填进去。
环境变量清单整理成表格方便对照:
| 变量 | 作用 | 示例值 |
|---|---|---|
| OPENAI_API_BASE_URL | OpenWebUI 指向 one-api | http://host.docker.internal:3001/v1 |
| OPENAI_API_KEY | one-api 的访问令牌 | sk-oneapi-local |
| WEBUI_AUTH | 开发阶段跳过登录 | False |
| LANGFUSE_PUBLIC_KEY | Langfuse 项目公钥 | pk-lf-xxx |
| LANGFUSE_SECRET_KEY | Langfuse 项目私钥 | sk-lf-xxx |
| LANGFUSE_HOST | Langfuse 服务地址 | https://cloud.langfuse.com |
配置写完后,启动顺序建议:先起 one-api 和 mysql,确认 3001 能访问;再起 pipelines;最后起 OpenWebUI 开发环境。这样排查问题时边界清晰。
4. 验证请求:从 OpenWebUI 发一条消息看 trace 是否落库
配置写完不代表通了,得实际发一条请求验证。启动 OpenWebUI 开发环境:
docker compose -f compose-dev.yaml up -d等容器起来后,访问http://localhost:3000。如果前端没起来,进容器看日志:
docker exec -it open-webui-dev-frontend-1 sh netstat -tuln | grep 5173容器内 5173 有监听说明 Vite 起来了。宿主机上curl http://localhost:3000能返回 HTML 就对了。
接着验证 one-api 连通性。在宿主机上直接打 one-api 的接口:
curl http://localhost:3001/v1/chat/completions \ -H "Authorization: Bearer sk-oneapi-local" \ -H "Content-Type: application/json" \ -d '{ "model": "gpt-4o-mini", "messages": [{"role": "user", "content": "ping"}] }'返回里带choices字段就说明 one-api 到上游是通的。如果返回 401,检查 Key 是否和 one-api 里创建的令牌一致;如果返回model not found,检查渠道里模型名是否填对。
然后在 OpenWebUI 界面里,左下角设置里确认模型列表能看到 one-api 暴露的模型。选一个模型发一条“你好”,正常返回即打通。这时候去 Langfuse 的 Traces 页面刷新,应该能看到一条新的 trace,点进去有 input、output、latency、model 这些字段。如果没看到,检查 pipelines 容器日志:
docker logs pipelines --tail 50常见的是 Langfuse key 填错,日志里会有 401 或unauthorized。另外 pipelines 容器和 OpenWebUI 容器之间的网络要通,host.docker.internal在两边都要能解析。
验证成功后,你改一下./src下的前端代码,Vite 会热更新,浏览器自动刷新。改./backend下的 Python 代码,dev.sh会重载。这就是开发模式的价值——不用每次重建镜像。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth
部署过程中最容易卡住的几个报错,我按实际遇到的整理出来。
401 Unauthorized。出现在 OpenWebUI 调 one-api 时,多半是OPENAI_API_KEY和 one-api 里创建的令牌不匹配。one-api 的令牌是在“令牌”页面生成的,不是渠道里的 Key。渠道里的 Key 是上游厂商的,令牌才是给下游用的。两者别搞混。另外 one-api 默认令牌有额度限制,额度用完也会 401,去令牌页面看剩余额度。
local proxy failed。这个报错通常出现在 OpenWebUI 容器里访问host.docker.internal失败。Linux 下 Docker 默认不解析这个域名,必须在 compose 里加extra_hosts: - host.docker.internal:host-gateway。如果加了还不行,检查 Docker 版本,20.10 以上才支持host-gateway。实在不行就用宿主机内网 IP 替代。
reading choices 报错。一般是 one-api 返回的响应格式和 OpenWebUI 预期不一致。检查 one-api 渠道的 Base URL 是否带了/v1。TaoToken 的地址是https://taotoken.net/api,one-api 里填这个就行,它会自动补/v1/chat/completions。如果填成https://taotoken.net/api/v1可能重复。另外模型名要和上游实际支持的完全一致,大小写敏感。
OAuth 相关报错。如果你开了WEBUI_AUTH=True又配了 OAuth,回调地址不对会报错。开发阶段直接WEBUI_AUTH=False跳过。生产环境再配 OAuth,回调地址要填http://你的域名/auth/oauth/...,和 OpenWebUI 里配的一致。
还有一个跨域问题要提。OpenWebUI 前后端分容器时,浏览器直接访问后端接口会有 CORS。官方后端 config 里不允许cross_origin配*,所以开发阶段可能遇到跨域拦截。我试过用浏览器插件临时放行access-control-allow-origin,但这只是开发权宜。更稳的做法是让前端通过 Vite 的 proxy 转发到后端,在vite.config.ts里配server.proxy,把/api指到http://backend:8080。这样浏览器只和 3000 通信,不跨域。
端口问题也常见。云服务器要在控制台安全组放行 3000、3001、8080、9099。服务器本机防火墙firewalld或iptables也要放行。容器内端口映射别写反,3000:5173是宿主 3000 对容器 5173。进容器用netstat -tuln确认监听端口,宿主机用curl确认能通。
6. 后续怎么用:从开发环境到长期编码与 Agent
环境搭好之后,日常开发就是改代码、看 trace、调模型。Langfuse 的 trace 能帮你定位慢请求——比如某个模型首 token 延迟高,或者某次对话 token 消耗异常。one-api 的日志能看到每个渠道的调用量和失败率,方便你决定要不要换渠道。
如果你后面要做长期编码或者 Agent 类应用,OpenWebUI 的 pipelines 可以扩展出函数调用、工具链。这时候模型接入的稳定性就很重要。TaoToken 的 Coding Plan 适合这种持续调用的场景,地址是https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,它按套餐走,比按量计费更适合高频开发。模型对话调试可以用https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,快速验证某个模型是否可用。控制台在https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,能看用量和余额。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=,里面有各语言的调用示例。
如果你用 Claude Code 做开发,它的 Anthropic 兼容接入可以参考https://taotoken.net/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。API Keys 管理在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=。
最后说个实际经验:开发环境别用生产数据库。OpenWebUI 的datavolume 和 one-api 的data/mysql分开挂载,清库重建不心疼。Langfuse 云端项目也分 dev 和 prod 两个,trace 别混在一起。这样你折腾坏了随时能重来,不影响线上。