☰
Langfuse离线部署实录:内网环境下的大模型可观测平台实践
2026/10/6 4:02:50 网站建设 项目流程

Langfuse离线部署实录:没有外网的服务器上,我是怎么把大模型可观测平台跑起来的

接到这个需求是在一个保密要求极高的内网项目上。客户那边的大模型应用已经上线了小半年,但一直缺一个能看推理链路、统计Token消耗、做Prompt版本管理的平台。选型阶段对比了LangSmith、MLflow、Helmholtz,最后敲定Langfuse——开源、功能贴合、社区活跃。问题随之而来:部署环境是物理隔离的网络,连Docker Hub都访问不了,官方文档里那句"docker compose up -d"根本用不上。

整个部署过程断断续续折腾了三天,踩了不少坑,也积累了一套完整的离线部署流程。这篇文章就把整个思路和操作步骤完整记录下来。如果你也在做大模型应用的运维或交付,需要在隔离网络、内网机房或者专有云环境里部署Langfuse,这篇文章应该能帮你少走很多弯路。

1. 先想清楚:离线部署到底要准备什么

很多人在离线部署上栽跟头,不是因为操作复杂,而是因为第一步就没想清楚"到底要搬哪些东西"。Langfuse不是一个单体应用,它是一组互相依赖的服务集合,离线部署的本质,就是把这一整套依赖完整地搬进内网。

1.1 Langfuse的组件架构与离线部署的关键依赖

Langfuse的官方部署方案基于Docker Compose,最新版本的核心组件包括:

  • Langfuse Web应用:基于Next.js的前端和API服务,提供控制台、数据查询、评测等能力。
  • Langfuse Worker:异步任务消费者,负责处理日志写入、数据聚合、导出任务等。
  • PostgreSQL:主数据库,存储用户、项目、Prompt、观测事件等结构化数据。
  • ClickHouse:分析型数据库,专门存放海量的trace和observation数据,支撑高性能检索和聚合。
  • Redis:作为缓存和消息队列,连接Web应用和Worker。
  • MinIO(可选但强烈建议):S3兼容的对象存储,用于存放导出文件、附件、数据集等。

这六个组件构成了Langfuse的完整运行时。离线部署的核心难点,就是把对应的Docker镜像和依赖包全部准备好。官方还在持续推进的版本可能会引入新的依赖组件,但截至目前,上述六件套就是全部。

注意:ClickHouse和PostgreSQL都各有版本要求,Langfuse对数据库的版本比较敏感,尤其是PostgreSQL,建议直接使用官方docker-compose.yml中lock定的版本,不要随意升级。

1.2 为什么不能只打包应用镜像

我第一次尝试离线部署时,犯过一个典型错误:只把langfuse/langfuse和langfuse/langfuse-worker两个镜像导出带进内网。结果启动后API服务一直报错,日志里是数据库连接失败。这才意识到,整个系统的运行依赖远不止应用本身。

Langfuse的Web应用启动时需要连接PostgreSQL完成Schema校验和数据访问,Worker则需要从Redis拉取任务并写入ClickHouse。任何一个基础组件缺失,应用都无法正常工作。更麻烦的是,Web应用和Worker间通过Redis的队列机制协作,没有Redis整个异步处理链路直接瘫痪。

所以离线部署的第一个原则是:要把完整的运行时依赖一起打包,而不是只搬应用本体。这也是很多开源系统离线部署的共同特点——依赖链是一个整体,缺一环就全盘皆输。

1.3 网络环境的判断:你需要什么样的准备机

离线部署通常需要一个"准备机"——一台能访问外网的机器,用来拉取镜像和依赖包,然后把产物通过移动介质、跳板机或审批通道传入内网。准备机可以是你的办公电脑、一台云上的跳板机,或者客户的临时演示环境。

关键要求只有三个:

  • 能访问Docker Hub(或已配置的镜像仓库)
  • 有足够的磁盘空间(建议至少预留20GB,镜像和临时文件比较多)
  • 装了Docker和docker compose插件

传输方式上,我见过有人用U盘拷贝tar包,也见过通过审批后的FTP通道上传,还有人用内网自建的Harbor作为中转。无论哪种方式,核心都是两个动作:docker save导出和docker load导入。后面我会给出完整命令。

2. 镜像的批量获取与运输:唯一硬卡点在这里

离线部署真正有技术含量的环节,就是镜像获取。这步处理得好,后续基本就是顺水推舟;处理不好,可能在传输环节就卡死。

2.1 获取完整的镜像清单

我建议直接在联网机器上拉取官方docker-compose.yml,然后从中提取镜像列表。这样做的好处是版本完全对齐,不会出现自己随便配的版本和官方不兼容的情况。

实际操作如下:

# 克隆或下载官方部署仓库(任选其一) git clone https://github.com/langfuse/langfuse.git # 或者直接下载 docker-compose.yml wget https://raw.githubusercontent.com/langfuse/langfuse/main/docker-compose.yml # 查看文件中所有镜像的定义 grep -E '^\s+image:' docker-compose.yml

以当前较新的版本为例,镜像清单大致是:

组件镜像名用途
Weblangfuse/langfuse:2.x主应用前后端
Workerlangfuse/langfuse-worker:2.x异步任务处理
PostgreSQLpostgres:15.x主数据库
ClickHouseclickhouse/clickhouse-server:24.x分析数据库
Redisredis:7.x缓存与队列
MinIOminio/minio:latest对象存储

有个细节需要注意:langfuse/langfuse和langfuse/langfuse-worker的版本号必须一致,否则可能出现API和Worker之间数据格式不兼容的问题。

2.2 拉取、打包、导出的完整操作流程

在准备机上执行:

# 1. 先拉取所有镜像(逐条执行) docker pull langfuse/langfuse:2.29.0 docker pull langfuse/langfuse-worker:2.29.0 docker pull postgres:15.6 docker pull clickhouse/clickhouse-server:24.3.3.102 docker pull redis:7.2.4 docker pull minio/minio:RELEASE.2024-06-13T22-53-52Z # 2. 使用 docker save 批量导出 # 建议用tar归档再压缩,体积能小不少 docker save \ langfuse/langfuse:2.29.0 \ langfuse/langfuse-worker:2.29.0 \ postgres:15.6 \ clickhouse/clickhouse-server:24.3.3.102 \ redis:7.2.4 \ minio/minio:RELEASE.2024-06-13T22-53-52Z \ -o langfuse-images.tar # 3. 压缩传输包 gzip langfuse-images.tar # 得到 langfuse-images.tar.gz,此时可以传入内网

如果单个压缩包太大,超出了传输介质的限制,可以用split命令分卷:

# 按500MB分卷 split -b 500M -d langfuse-images.tar.gz langfuse-part- # 内网端合并 cat langfuse-part-* > langfuse-images.tar.gz

这个过程中我踩过一个比较典型的坑:docker save不加-o参数时,会把镜像流输出到stdout,如果终端环境有编码转换,很可能导致tar包损坏。所以务必使用-o指定输出文件。

2.3 内网机器导入镜像

到内网目标机器后执行:

# 解压 gunzip langfuse-images.tar.gz # 导入镜像,耐心等待(这一步会有点久,取决于服务器磁盘性能) docker load -i langfuse-images.tar # 验证导入结果 docker images

docker load完成后,会逐条输出Loaded image信息。建议核对一下镜像名和tag是否齐全,尤其是ClickHouse的镜像名带官方命名空间(clickhouse/clickhouse-server),漏了或拼错了后面起容器时会报image not found。

2.4 拉取镜像失败的两个替代方案

内网环境除了完全隔离,还有一种常见情况是"半隔离"——容器运行时可以访问内网自建的镜像仓库。如果是这种情况,可以先把镜像推送到内网Harbor或Nexus,然后修改目标机器的Docker配置指向内网仓库。

# 在准备机上打tag并推送 docker tag langfuse/langfuse:2.29.0 harbor.internal.com/library/langfuse:2.29.0 docker push harbor.internal.com/library/langfuse:2.29.0 # 在目标机上直接pull docker pull harbor.internal.com/library/langfuse:2.29.0

还有一种情况更特殊:那台准备机连Docker Hub也访问不了,只能通过HTTP代理访问有限白名单域名。此时可以在/etc/docker/daemon.json里配置代理,然后重启Docker:

{ "proxies": { "http-proxy": "http://proxy.internal.com:8080", "https-proxy": "http://proxy.internal.com:8080" } }

不过坦白说,如果网络限制到这个程度,通常走审批流程让人工传递tar包反而更快。离线部署本质上是"绕过网络依赖"而不是"对抗网络策略",选择最省力的路径才是正确的工程判断。

3. 内网编排:那些必须手工调整的配置

镜像导入后,工作重点就转移到编排文件上。直接拿官方docker-compose.yml用,在内网环境大概率起不来。原因集中在几个方面:环境变量缺失、组件间域名解析、外网遥测请求超时、以及数据库持久化配置。这里逐个说明。

3.1 环境变量:Langfuse跑起来的七个关键配置

Langfuse的配置项非常多,完整列表可以查官方文档。但内网部署只需要关注下面这几个,它们直接决定服务能否启动:

配置项作用内网部署建议
ENCRYPTION_KEY加密数据库中的敏感字段(API密钥等)必须设置,长度32字节,用openssl rand -base64 32生成
SALT密码哈希的盐值必须设置,同样用openssl rand -base64 32生成
NEXTAUTH_URLNextAuth回调地址必须设置为内网访问地址,如http://192.168.1.100:3000,否则登录跳转异常
NEXTAUTH_SECRETNextAuth会话签名密钥必须设置,openssl rand -base64 32生成
DATABASE_URLPostgreSQL连接串格式postgresql://postgres:password@db:5432/postgres
REDIS_URLRedis连接串格式redis://redis:6379
CLICKHOUSE_URLClickHouse连接串格式http://clickhouse:8123
TELEMETRY_ENABLED是否发送遥测数据内网必须设为false,否则启动时会尝试外连

生成密钥的具体命令:

openssl rand -base64 32 # 返回一串类似 xyz123abc456... 的字符串 openssl rand -hex 16 # 返回32位十六进制字符串,用作SALT

这里特别强调一下NEXTAUTH_URL。我遇到过最诡异的现象是:所有容器都healthy,但访问登录页面提交后一直报NEXTAUTH_URL相关的redirect错误。原因就是我在内网用IP访问,但环境变量里写的是容器名或旧域名。在内网环境,NEXTAUTH_URL必须填用户实际访问的地址——如果通过域名访问就填域名,如果是IP访问就填IP。这是一条容易忽略但影响极大的配置。

3.2 内网DNS与容器间通信

Docker Compose默认会创建内部网络,容器之间通过服务名互相解析。所以docker-compose.yml中依赖项连接串里的host名,必须和Compose中的服务名一致——比如DATABASE_URL里的host写成db,服务名就是db;REDIS_URL里写redis,服务名就必须是redis。

很多内网环境的路由和DNS策略比较严,如果目标机器本身还跑着其他容器,要注意端口冲突。官方配置默认映射:

  • 3000端口:Web应用
  • 8123端口:ClickHouse HTTP接口
  • 9000端口:MinIO API
  • 9001端口:MinIO控制台

如果这些端口已经被占用的,需要改成宿主机的其他端口,比如"13000:3000"。修改后NEXTAUTH_URL也要跟着改为http://内网IP:13000。

3.3 持久化存储:离线环境最怕重启丢数据

官方docker-compose.yml里MinIO和PostgreSQL、ClickHouse都定义了volume。离线环境因为镜像无法轻易重新获取,持久化更要谨慎。有三种选择:

  1. 使用volume(推荐):跟随Docker管理,备份时用docker run --volumes-from导出。
  2. 绑定挂载到宿主机目录(最直观):适合后续对接客户的备份机制,目录肉眼可见。
  3. 内网存储服务:将数据目录挂载到NFS或其他共享存储上。

我倾向于绑定挂载到宿主机目录,尤其是在交付后需要移交给客户运维的场景。看着/data/langfuse/{postgres,clickhouse,redis,minio}这样清晰的目录结构,接手的人心里踏实。绑定挂载只需把volume声明改为:

volumes: - /data/langfuse/postgres:/var/lib/postgresql/data - /data/langfuse/clickhouse:/var/lib/clickhouse - /data/langfuse/redis:/data - /data/langfuse/minio:/data

提示:ClickHouse的权限要求比较严格,绑定挂载时需要确保宿主机目录属主是101:101(ClickHouse容器内用户),否则容器启动时可能报权限错误。最简单的办法:先创建目录并chown为101:101。

4. 第一次启动:从迁移到健康检查的完整链路

配置就绪后,第一次启动是最容易出现问题的环节。Langfuse官方部署手册里有一句"Run database migrations",这一步很多人会漏掉,或者不知道什么时候该执行。我梳理了一条完整的启动链路,照这个顺序做基本不会翻车。

4.1 数据库迁移:必须先于应用启动

Langfuse使用Prisma ORM管理PostgreSQL的Schema。新部署必须执行迁移命令,否则Web应用启动后会一直报"database is not in sync with the schema"之类的错误。

进入项目目录,执行:

# 确保镜像已导入,然后执行迁移 docker compose run --rm api db-migrate

迁移命令会读取.env或compose配置中的DATABASE_URL,自动创建表结构、索引和外键。注意这个命令执行完毕后,PostgreSQL里已经有了完整的Schema,后续再启动所有服务就不会报Schema问题了。

如果在内网的数据库服务器上做了额外的安全策略(比如限制了源IP),迁移时会遇到连接超时。这种情况可以临时把网络策略加上Docker网段,或者用--network host模式跑迁移命令:

docker run --rm --network host \ -e DATABASE_URL="postgresql://postgres:password@宿主机IP:5432/postgres" \ langfuse/langfuse:2.29.0 db-migrate

4.2 组件启动顺序与实际依赖关系

官方用depends_on定义了依赖顺序,但depends_on默认只保证"容器启动了",并不保证"容器内的服务就绪了"。比如PostgreSQL容器已经进入运行状态,但不代表5432端口已经能接受连接。

稳妥的做法是启动后主动等待健康检查通过:

# 后台启动所有服务 docker compose up -d # 查看容器状态 docker compose ps # 等待片刻后检查健康状态,STATUS列会显示healthy watch -n 2 docker compose ps

官方镜像通常内置了HEALTHCHECK指令,容器状态会从starting变为healthy。如果长时间停在starting或直接unhealthy,优先看日志:

docker compose logs api docker compose logs worker

4.3 ClickHouse的内存与日志问题

ClickHouse是整组容器中资源占用最大的角色。默认配置下,它可能会占用宿主机大量的内存,对于只有8GB内存的服务器,这可能造成其他容器被OOM杀掉。建议在compose文件中给ClickHouse加上内存限制:

clickhouse: image: clickhouse/clickhouse-server:24.3.3.102 ulimits: nofile: soft: 262144 hard: 262144 mem_limit: 4g

同样,Langfuse的Web应用也可以加mem_limit: 2g,避免内存竞争导致整个Docker守护进程异常。

ClickHouse的日志默认滚动策略比较保守,长时间运行后会占据较多磁盘空间。建议在挂载的ClickHouse配置目录里放一个config.d/logger.xml:

<clickhouse> <logger> <level>warning</level> <size>100M</size> <count>5</count> </logger> </clickhouse>

这个文件需要在启动前就放到挂载目录中,否则容器内的配置不会自动生成。

4.4 首次登录与功能验证清单

所有容器healthy后,在内网浏览器里访问http://内网IP:3000。首次访问会要求注册管理员账号,注意这一步涉及发邮件确认——内网环境没有配置SMTP的话,会出现"邮箱验证失败"之类的提示。

Langfuse支持跳过邮件验证的配置AUTH_DISABLE_SIGNUP和AUTH_DISABLE_EMAIL_VALIDATION的组合。推荐在.env里设置:

AUTH_DISABLE_SIGNUP=false AUTH_DISABLE_EMAIL_VALIDATION=true

这样注册账号后不需要邮件验证就能直接登录,适合内网交付初期快速验证。

功能验证的核心检查项如下:

  • 能正常创建Project并生成API密钥
  • 能通过SDK(Python或JS)向/api/public/traces写入一条测试数据
  • 控制台能看到写入的trace,且Token消耗统计正确
  • 创建一个Prompt版本并发布,确认Worker能同步处理
# 用Python SDK快速验证(内网机器上执行) pip install langfuse python -c " from langfuse import Langfuse langfuse = Langfuse( public_key='pk-xxx', secret_key='sk-xxx', host='http://内网IP:3000' ) langfuse.trace(name='offline-test').update(output='ok') langfuse.flush() "

能在控制台看到这条offline-test的trace记录,说明整条链路已经通了。

5. 版本升级与日常运维:离线环境下的后续管理

部署成功只是开始,更考验人的是后续的升级和维护。离线环境下没有"拉新镜像"的便捷路径,凡事都得提前规划。

5.1 离线升级的完整操作路径

Langfuse迭代速度较快,新版本经常带来新的观测功能和安全修复。离线升级的基本思路是"准备机拉新包、导出、内网导入、重启"。

# 准备机:拉取新版本镜像(以2.31.0为例) docker pull langfuse/langfuse:2.31.0 docker pull langfuse/langfuse-worker:2.31.0 # 导出新镜像(基础组件没变就不需要重新打包) docker save langfuse/langfuse:2.31.0 langfuse/langfuse-worker:2.31.0 -o langfuse-upgrade.tar # 内网机器:导入新镜像 docker load -i langfuse-upgrade.tar # 在docker-compose.yml中修改镜像版本号 vim docker-compose.yml # 把 langfuse/langfuse:2.29.0 改为 langfuse/langfuse:2.31.0 # 把 langfuse/langfuse-worker:2.29.0 改为 langfuse/langfuse-worker:2.31.0 # 执行迁移(升级通常伴随Schema变更) docker compose run --rm api db-migrate # 重建并启动 docker compose up -d

升级前务必备份PostgreSQL和ClickHouse的数据目录。我通常在备份时直接停掉应用,避免数据不一致:

docker compose stop api worker # PostgreSQL仍在运行,此时用pg_dump备份 docker exec -i langfuse-db pg_dump -U postgres postgres > backup_$(date +%Y%m%d).sql

ClickHouse的数据备份没有PostgreSQL那么方便,最简单可靠的方式是直接打包数据目录:

tar czf clickhouse-backup.tar.gz -C /data/langfuse/clickhouse .

5.2 日志采集与故障诊断的实用技巧

离线环境没有外部的日志聚合服务,但Langfuse运行过程中会产生几种关键日志,必须知道去哪里看:

  • Web应用日志:docker compose logs api,排查登录、API请求、数据库连接问题
  • Worker日志:docker compose logs worker,排查异步队列消费、ClickHouse写入问题
  • PostgreSQL慢查询日志:如果页面加载慢,优先排查数据库

一个比较推荐的定位技巧是:在.env里把LOG_LEVEL设置为debug(默认是info),能输出SQL语句和API请求的详细信息。但注意生产环境不要长期开debug,日志量会暴涨,磁盘很快就满。

5.3 备份策略与容灾安排

离线环境的数据一旦丢失,恢复成本比联网环境高得多。我的建议是至少做双层备份:

  1. 物理备份:每天凌晨用tar打包PostgreSQL和ClickHouse的数据目录,存到内网备份服务器。
  2. 逻辑备份:每周用pg_dump导出SQL文件,用于应对"物理文件损坏但数据库服务还能启动"的情况。

可以在宿主机上写一个简单的定时脚本:

#!/bin/bash # /opt/scripts/langfuse-backup.sh DATE=$(date +%Y%m%d_%H%M%S) BACKUP_DIR=/data/backups/langfuse mkdir -p $BACKUP_DIR # 备份PostgreSQL docker compose -f /opt/langfuse/docker-compose.yml exec -T db \ pg_dump -U postgres postgres > $BACKUP_DIR/postgres_$DATE.sql # 备份ClickHouse数据目录 tar czf $BACKUP_DIR/clickhouse_$DATE.tar.gz \ -C /data/langfuse/clickhouse . # 保留最近30天的备份 find $BACKUP_DIR -name "*.sql" -mtime +30 -delete find $BACKUP_DIR -name "*.tar.gz" -mtime +30 -delete

然后加到crontab:

0 2 * * * /opt/scripts/langfuse-backup.sh

5.4 内网时间同步的一个隐蔽坑

这个问题我差点漏掉。Langfuse的Worker对任务有时间戳校验,而ClickHouse内部也强依赖时间排序。如果内网服务器的时间与真实时间偏差过大(比如超过几分钟),可能出现"trace写进去了但控制台查不到"或者"任务一直堆积不消费"的怪象。

检查方法:

timedatectl status # 确认NTP服务是否在运行 timedatectl show -p NTPSynchronized

离线环境访问不了公网NTP服务,需要在局域网内部搭建时间服务器,或者手动校准所有机器的时间。最省心的做法是在内网找一台机器作为NTP服务器,其他机器都指向它同步。具体搭建方式这里不展开,但如果你的Langfuse出现"数据写入无反应"而容器都正常的情况,先查时间同步,这个排查顺序能省不少事。

6. 部署完成后的几点经验总结

整个流程走通之后回头看,离线部署Langfuse的成败其实只取决于几个关键点:

**第一,镜像版本必须统一。**应用和worker不一致、PostgreSQL版本不对、ClickHouse镜像名拼错,这些都是最常见的坑。拉取前把完整清单列出来,逐项核对再动手。

第二,环境变量里藏着80%的问题。NEXTAUTH_URL不对导致登录异常、TELEMETRY_ENABLED没关导致启动时外连超时、AUTH_DISABLE_EMAIL_VALIDATION没设导致注册卡壳。这些配置在官方文档里都有,但内网环境把它们从"可选项"变成了"必选项"。

**第三,数据库迁移必须在应用启动前执行。**这一步漏掉,后面所有容器都会陷入启动失败的重启循环,而排查半天可能都想不到是Schema的问题。

**第四,离线环境的运维要更保守。**升级前必须完整备份,改动配置前先导出当前docker compose config留底,养成"先备份再操作"的习惯。

我在实际交付中还有一个体会:建议交付时随环境附上一份部署说明文档,把镜像清单、环境变量清单、备份恢复步骤、常见故障排查都写清楚。这样后续客户运维团队接手时不至于手足无措。毕竟离线环境里出问题,查资料都查不了,文档就是唯一的救命稻草。

如果后续你有机会接触到Langfuse的新版本,建议优先关注它的评测功能和数据集管理模块——这两个方向在内部模型迭代场景下特别有用。离线部署的核心流程不会变,变的无非是镜像版本和一些新增的环境变量,掌握了这套方法论,换任何版本都能快速上手。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询