Langfuse 自托管部署:VPS 配置、TLS 与备份
在自有 VPS 上运行 Langfuse v4,了解实际资源下限、固定镜像标签、TLS 配置、ClickHouse 保留策略,以及防止磁盘写满并可恢复的备份方案。
为什么要跟踪 AI agent
自行托管 Langfuse,可以查看 agent 在一次运行中实际执行了什么。Langfuse 是一款开源 LLM(大语言模型)可观测性工具。它会记录每条提示词、每次模型响应、每次工具调用和每个 token,然后将这些记录归入一个可打开查看的跟踪记录中。在您自己的 VPS 上运行 Langfuse,意味着这些提示词不会离开您控制的服务器。
这样做的原因很简单。看不到成本问题或质量问题,就无法修复。服务商账单只能告诉您,周二的费用是周一的4倍。跟踪记录可以告诉您,是哪次 agent 运行导致了这笔费用,哪个提示词增长到了40,000个 token,以及哪个重试循环在放弃前运行了9次。账单给出数字,跟踪记录揭示产生这些数字的代码。
本指南会使用3个术语。跟踪记录(trace) 是 agent 的一次端到端运行。观测记录(observation) 是该运行中的一个步骤:普通代码对应一个 span,调用模型对应一次 generation。评分(score) 是附加到跟踪记录上的一个数值,可以来自人工审核或自动评估器。Langfuse 支持 OpenTelemetry(OTel),这是分布式跟踪的厂商中立标准,因此您已有的检测代码可以将数据发送到 Langfuse。
自托管 Langfuse 实际运行的组件
Langfuse v4 不是单个容器。它由两个应用容器和四个存储服务组成。在单台 VPS 上,这 6 个组件都会运行在您的服务器上。
langfuse-web提供 Web 界面和数据摄取 API。langfuse-worker在后台处理队列。它解析数据摄取批次、计算成本,并运行每晚的保留任务。- Postgres 保存用户、组织、项目、API 密钥和提示词等事务数据。
- ClickHouse 保存跟踪数据本身,即观测记录和评分。它是为分析查询构建的列式存储,因此即使仪表板查询超过一亿行,仍能快速返回结果。
- Redis 是位于 Web 容器和 worker 之间的队列与缓存。
- MinIO 在服务器上提供兼容 S3 的对象存储。它保存每个原始传入事件,以及您附加的所有媒体文件。
Langfuse 为实际执行工作的 3 个组件公布了最低资源要求。
The data behind this chart
[
{
"label": "ClickHouse",
"cpu_cores": 2,
"memory_gib": 8
},
{
"label": "Langfuse web",
"cpu_cores": 2,
"memory_gib": 4
},
{
"label": "Langfuse worker",
"cpu_cores": 2,
"memory_gib": 4
}
]仅 ClickHouse 就需要 8 GiB 内存。Web 容器和 worker 各需要 4 GiB。这些是 Langfuse 进行资源规划的 3 个组件的最低要求,Postgres、Redis 和 MinIO 还需要额外占用内存。项目自带的 Docker Compose 指南建议使用配备 4 个 CPU 核心、16 GiB 内存和约 100 GiB 存储空间的机器。这与上述计算结果一致,并没有额外留出太多余量。
不要在 2 GiB 规格的计划上尝试运行。ClickHouse 会启动,并在一段时间内接受写入,但随后会在后台合并期间退出,因为合并会将表的大型数据分片加载到内存中。您会看到 docker compose ps 将 clickhouse 容器报告为 restarting,dmesg 输出类似 Out of memory: Killed process 1234 (clickhouse-serv) 的日志行,并且 Langfuse 的每个仪表板都返回 500。在负载较轻时,ClickHouse 可能直接拒绝查询,并记录 DB::Exception: Memory limit (total) exceeded。8 GiB 内存足以支持一名开发者每天发送几千条跟踪数据。规划部署时应按 16 GiB 内存计算。
使用 Docker Compose 部署 Langfuse
克隆仓库。堆栈、服务连接关系和默认环境变量都在其中的 docker-compose.yml 中。
git clone https://github.com/langfuse/langfuse.git
cd langfuse该文件中所有必须修改的值都标记为 # CHANGEME。先生成三个应用密钥。
openssl rand -base64 32 # NEXTAUTH_SECRET
openssl rand -base64 32 # SALT
openssl rand -hex 32 # ENCRYPTION_KEYENCRYPTION_KEY 必须是使用 64 个十六进制字符表示的 256 位值,这正是 openssl rand -hex 32 的输出结果。它用于加密静态存储的敏感值,包括您存储在实例中的 LLM 提供商密钥。数据写入后再修改它将导致这些数据行无法解密,因此从首次启动开始就应将其视为永久值。SALT 用于对 Langfuse API 密钥进行哈希处理,因此修改它会使代理当前使用的所有密钥失效。
然后设置 POSTGRES_PASSWORD、CLICKHOUSE_PASSWORD、REDIS_AUTH 和 MINIO_ROOT_PASSWORD。MinIO 密码会出现 4 次:首次作为 MINIO_ROOT_PASSWORD,随后还会作为 LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY、LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY 和 LANGFUSE_S3_BATCH_EXPORT_SECRET_ACCESS_KEY。漏掉任何一处,MinIO 都会返回 SignatureDoesNotMatch 拒绝该客户端;此错误会写入 worker 日志,而 Web 界面仍可能显示正常。将这些值放在 env 文件中,而不是纳入版本控制的 compose 文件中,是 Docker Compose env 文件和密钥中介绍的做法。
启动前固定镜像标签
随附文件使用 langfuse/langfuse:4 和 langfuse/langfuse-worker:4。这些标签会随时间变化。Langfuse 会在启动时自动执行 Postgres 和 ClickHouse 迁移,因此数月后例行执行一次 docker compose pull,可能变成针对当天未备份数据库的非计划架构迁移。将两者都固定到 docker-compose.override.yml 中的同一个版本。Compose 会将该文件合并到随附文件之上,因此后续执行 git pull 时不会覆盖您的修改。
services:
langfuse-web:
image: docker.io/langfuse/langfuse:4.3.1
langfuse-worker:
image: docker.io/langfuse/langfuse-worker:4.3.1截至 2026 年 8 月,当前版本为 4.3.0。请查看项目的 GitHub releases 页面,固定为部署当天的最新版本,然后有计划地更新该版本号。随附文件中的存储镜像已经固定到主版本,包括 postgres:17、clickhouse-server:25.12 和 redis:7;它们也应采用相同的处理方式。
启动服务。
docker compose up -d
docker compose ps
docker compose logs -f langfuse-worker首次启动会执行迁移,因此请等待 1 到 2 分钟后再访问服务。docker compose ps 应列出 6 个状态为 running 的服务。如果 worker 不断重启,请查看其日志获取原因:CLICKHOUSE_MIGRATION_URL 使用端口 9000 上的 ClickHouse 原生协议,而不是 HTTP 端口 8123;将其指向 8123 会导致该服务失败,但 Web 容器仍可能显示正常。
从服务器本机检查健康状态。
curl -s "http://localhost:3000/api/public/health?failIfDatabaseUnavailable=true"
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:3000/api/public/ready直接执行 /api/public/health 只能证明 API 进程正在运行,因为该请求会有意跳过数据库检查,使服务在 Postgres 短暂异常时仍能继续提供服务。failIfDatabaseUnavailable=true 形式才适合交给监控系统;数据库不可访问时,它会返回 503。迁移完成且容器可以接受流量后,/api/public/ready 会返回 200。这两项都是普通的 HTTP 检查,因此 Uptime Kuma 状态页面可以监控它们,并在您的代理发现问题前报告整个堆栈已停止。
在前端配置 TLS 并关闭额外端口
附带的 Compose 文件会为 Web 容器发布 3000:3000,为 MinIO 发布 9090:9000。这两个端口都绑定到所有网络接口。在公网 IP 上,这意味着任何扫描 3000 端口的人都能访问注册页面,任何扫描 9090 端口的人都能访问存放原始提示词的存储桶。
仅设置防火墙规则无法关闭这些端口。Docker 会将自己的 DNAT 规则写入 nat 表,并且这些规则会在数据包到达 ufw 的 filter 规则之前生效,因此 ufw deny 3000 仍会让已发布的端口保持开放。这个问题十分常见,因此已有专门的指南说明:为什么 Docker 发布的端口会绕过 ufw。请在 override 文件中改为绑定到 loopback。
services:
langfuse-web:
ports:
- "127.0.0.1:3000:3000"
environment:
NEXTAUTH_URL: https://langfuse.example.com
minio:
ports:
- "127.0.0.1:9090:9000"
- "127.0.0.1:9091:9001"NEXTAUTH_URL 必须是包含 scheme 的完整公网地址,因为登录流程会根据该值生成回调 URL。如果在 HTTPS 代理后仍将其保留为 http://localhost:3000,登录往返过程中浏览器会被重定向到无法访问的地址。
现在将反向代理指向 127.0.0.1:3000,并由它管理证书。同一 Compose 项目中的 Traefik 通常是最合适的选择,其路由标签参见在同一个 Traefik 反向代理后运行多个应用。如果 Langfuse 是服务器上唯一的应用,使用 Caddy 也可以通过两行配置完成相同的工作。使用 curl -sI https://langfuse.example.com/api/public/ready 验证,然后从另一台机器确认 curl http://YOUR_IP:3000 现在会超时。
MinIO 有一个需要注意的事项。Langfuse 会通过指向该 S3 endpoint 的预签名 URL,将关联的媒体内容提供给浏览器。因此,如果多模态 trace 包含图像或音频,MinIO 仅绑定到 loopback 会导致这些附件无法加载。在配置反向代理前,请先阅读 blob storage 配置页面,因为写入预签名 URL 的 endpoint 必须与对外发布的 endpoint 一致。纯文本 trace 不受影响。
首次访问时创建您的账户,然后确保实例由您管理。将 LANGFUSE_ALLOWED_ORGANIZATION_CREATORS 设置为您自己的电子邮件地址,这样访问该页面的陌生人就无法在您的服务器上创建组织。
发送您的第一条追踪记录
在 Web 界面中创建项目,然后从项目设置中复制其公钥和密钥。Python SDK 会读取 3 个环境变量。
export LANGFUSE_PUBLIC_KEY="pk-lf-..."
export LANGFUSE_SECRET_KEY="sk-lf-..."
export LANGFUSE_BASE_URL="https://langfuse.example.com"LANGFUSE_BASE_URL 是 SDK v4 中的变量名。SDK v4 于 2026 年 3 月发布。旧代码和旧指南使用 LANGFUSE_HOST。如果追踪记录被发送到 Langfuse Cloud,而不是您的服务器,原因是基础 URL 未设置,因为默认值指向托管实例。
pip install langfuse opentelemetry-instrumentation-anthropic anthropicimport os
from anthropic import Anthropic
from langfuse import get_client, observe
from opentelemetry.instrumentation.anthropic import AnthropicInstrumentor
AnthropicInstrumentor().instrument()
langfuse = get_client()
client = Anthropic(api_key=os.environ["ANTHROPIC_API_KEY"])
@observe(as_type="tool")
def lookup_order(order_id: str) -> str:
return f"order {order_id}: shipped"
@observe()
def handle_request(question: str) -> str:
context = lookup_order("A-1042")
message = client.messages.create(
model="claude-haiku-4-5",
max_tokens=512,
messages=[{"role": "user", "content": f"{context}\n\n{question}"}],
)
return message.content[0].text
if __name__ == "__main__":
assert langfuse.auth_check()
print(handle_request("Where is my order?"))
langfuse.flush()@observe 装饰器会在函数周围创建一个观测,捕获函数参数和返回值,并将其嵌套在当前已激活的观测下。AnthropicInstrumentor 是 Anthropic 客户端的 OpenTelemetry 插桩。它会将每次 messages.create 调用转换为一条生成记录,其中包含模型名称、令牌用量和延迟。调用位置无需修改。
两个调用可以代您完成检查。langfuse.auth_check() 会在密钥错误或基础 URL 错误时返回 False。这样无需在仪表板为空时反复排查原因。langfuse.flush() 会阻塞,直到队列中的 span 发送完成。短生命周期进程必须调用它,因为 SDK 会在后台批量发送数据;如果脚本立即退出,尚未发送的批次也会随进程一起丢失。
为什么 ClickHouse 会持续增长?
Trace 是大多数人自行托管时增长最快的数据。每次 agent 运行都会为每个步骤写入一行,并完整保存输入和输出。因此,提示词较长且输出较多的 agent 每天产生的字节数,可能远超它所监控的应用。若不加处理,ClickHouse 会占满磁盘;磁盘写满后,数据写入会停止,而不是变慢。
这里有两类独立增长项,需要分别处理。
第一类是您自己的 trace 数据,解决方法是配置保留期限。在 Web 界面中打开项目设置,并以天为单位设置数据保留期限。Langfuse 接受的最短期限为 3 天。随后,系统会通过每晚运行的任务,选择超过该期限的 traces、observations、scores 和媒体资产,并从 ClickHouse 及 blob storage 中删除。该任务需要对 bucket 具有 DeleteObject 权限;默认 compose 文件中的 MinIO root 凭据已经具备此权限。删除操作不可恢复。如果需要长期保存历史数据,请先配置 blob storage 导出。不要在 Langfuse 自有表上手写 TTL 子句:保留任务负责让 ClickHouse 与 bucket 保持同步,而手动设置 TTL 只会删除其中一侧的数据。
根据实际用途选择保留期限。成本和质量评审通常针对几天前的数据,而不是几个月前的数据。对于小型团队,30 天是合理的起点。如果只有在出现故障时才查看 trace,保留 14 天就足够。
第二类是 ClickHouse 自身的系统日志表。这个问题经常令人意外,因为即使配置了保留期限,磁盘仍会持续增长。ClickHouse 会为自身诊断写入 trace_log、text_log、opentelemetry_span_log、metric_log 和 asynchronous_metric_log。这些表默认没有 TTL,Langfuse 也不会读取它们。首先确认磁盘空间实际消耗在哪里。
SELECT table, formatReadableSize(size) AS size, rows FROM (
SELECT table, database, sum(bytes) AS size, sum(rows) AS rows
FROM system.parts
WHERE active
GROUP BY table, database
ORDER BY size DESC
)使用 docker compose exec clickhouse clickhouse-client --password "$CLICKHOUSE_PASSWORD" 运行。如果系统表接近列表顶部,请通过配置覆盖文件关闭它们,因为 ClickHouse 启动时会将 /etc/clickhouse-server/config.d/ 中的每个文件合并到主配置之上。
<clickhouse>
<trace_log remove="1"/>
<text_log remove="1"/>
<opentelemetry_span_log remove="1"/>
<asynchronous_metric_log remove="1"/>
<metric_log remove="1"/>
</clickhouse>挂载该配置覆盖文件,然后重启 ClickHouse。
services:
clickhouse:
volumes:
- ./clickhouse-config.d/system-logs.xml:/etc/clickhouse-server/config.d/system-logs.xml:ro这样可以停止新的写入。磁盘上已有的行仍会保留,因此请使用 DROP TABLE IF EXISTS system.trace_log 显式回收空间;对删除的每个表执行相同操作。如果希望保留诊断数据,也可以为每个表设置激进的 TTL,而不是使用 remove="1";Langfuse 的扩展文档对此有具体说明。
还有一张表值得了解。blob_storage_file_log 记录上传到 bucket 的事件文件。如果您同时为 bucket 设置了生命周期策略,请为该表设置匹配的 TTL,避免两者逐渐不一致。
ALTER TABLE blob_storage_file_log MODIFY TTL created_at + INTERVAL 30 DAY DELETE;同时为数据磁盘设置普通的 df -h 告警。Trace 不会平稳增长。发布新 agent 的当天,数据量可能突然增加;首次发现这一点不应是数据写入已经失败。
备份 Postgres 和 ClickHouse
Langfuse 备份包含 3 个部分。Postgres 保存用户、组织、项目和 API 密钥。ClickHouse 保存追踪数据。MinIO 保存原始事件。只恢复 Postgres,您可以正常登录,但看不到历史数据。只恢复 ClickHouse,您可以恢复历史数据,但没有人能够登录查看。
Postgres 是普通的 pg_dump,这也是 Langfuse 备份文档推荐的方式。
docker compose exec -T postgres pg_dump -U postgres postgres \
| gzip > langfuse-pg-$(date +%F).sql.gzClickHouse 需要更谨慎,因为在合并运行时复制正在使用的数据目录,得到的不是一致的备份。在单台服务器上,最简单的方法是停止容器并归档卷。
docker compose stop clickhouse
docker volume ls | grep clickhouse
docker run --rm -v langfuse_langfuse_clickhouse_data:/data -v "$PWD":/backup alpine \
tar czf /backup/langfuse-ch-$(date +%F).tar.gz -C /data .
docker compose start clickhouse使用 docker volume ls 输出的卷名,不要使用 YAML 中写出的名称。文件声明了 langfuse_clickhouse_data,Compose 会在前面加上项目名称,因此在名为 langfuse 的目录中克隆后,会生成 langfuse_langfuse_clickhouse_data。如果名称错误,docker run 会直接创建一个新的空卷,不会报告错误,归档文件中也不会包含任何数据。
Web 容器会先将每个收到的事件写入存储桶,再由 worker 处理。因此,短时间停止 ClickHouse 通常只会导致 worker 稍后重试。在低流量时段执行,并尽量缩短停止时间。对于负载更高的实例,ClickHouse 自带的 BACKUP DATABASE default TO S3(...) 语句可以在不停止服务器的情况下创建一致的备份。MinIO 是第三个部分,使用 mc mirror 或将 MinIO 复制到服务器外的存储桶即可覆盖这一部分。无论生成哪种备份,都应将其移出服务器;这正是 VPS 上的加密 restic 备份 的用途。
Redis 无需备份。它保存队列和缓存,因此丢失 Redis 只会丢失当前正在处理的事件,不会丢失更早的数据。
一致性方面的限制确实存在,应该明确说明。Postgres 和 ClickHouse 在不同时间点导出,因此恢复后可能出现项目记录存在但没有追踪数据,或追踪数据属于已经不存在的项目。Langfuse 可以容忍这种情况,但仍应在相近时间点、低流量时段完成两个导出。事件存储桶才是真正的安全保障,因为 Langfuse 会在处理每个收到的事件之前,先将其持久化到存储桶中。
至少应将备份恢复到一次临时堆栈中。这样可以现在发现卷名错误,而不是等到服务中断时才发现。
首先查看什么
以下4项值得在第一周重点关注。
- 每条 trace 的成本。 Langfuse 根据模型名称和 token 使用量计算成本,因此应按成本对 trace 排序,并从头到尾查看成本最高的那条。通常可以发现 prompt 变长了:整个文档被粘贴到了上下文中,或者对话历史没有经过裁剪。问题一旦可见,控制 AI agent 的成本就会从猜测变成工程任务。
- 按输入和输出拆分的 token 使用量。 输入 token 数量多但价格低,输出 token 数量少但价格高,缓存的输入 token 价格更低。Claude Code 如何计算 token 使用量对同一计费方式进行了详细说明,该方式也适用于您自行编写的任何 agent。
- 延迟百分位数。 中位数会掩盖问题。超时通常出现在 p95 和 p99,而在 agent 循环中,p95 的一次缓慢工具调用还会乘以迭代次数。
- 失败的工具调用。 按级别
ERROR过滤观测记录。工具有5%的失败率时,汇总成功率可能无法体现问题,但在 trace 中非常明显:您可以看到模型重试,然后消耗 token 绕过该工具继续执行。
在部署当天设置保留时长,并选择每周固定同一天检查的 dashboard。没人打开的可观测性工具,最终只是一个会占满磁盘的数据库。
FAQ
自托管 Langfuse 需要多少内存?
规划 4 个 CPU 核心和 16 GiB 内存。这是 Langfuse Docker Compose 指南针对单台虚拟机的建议配置,此外还需要约 100 GiB 存储空间。已发布的组件最低配置要求为:ClickHouse 需要 8 GiB,web 和 worker 容器各需要 4 GiB。Postgres、Redis 和 MinIO 还需要额外内存。8 GiB 可以运行单个开发者实例。2 GiB 不够:ClickHouse 在后台合并期间会被内核终止,并且 dmesg 会显示 Out of memory: Killed process。
设置数据保留期限后,为什么 ClickHouse 磁盘仍会持续占满?
保留设置只适用于 Langfuse 自身的数据。ClickHouse 还会单独写入诊断表 trace_log、text_log、opentelemetry_span_log、metric_log 和 asynchronous_metric_log,这些表默认没有 TTL。按表对 system.parts 分组查询,找出占用空间最大的表。然后在 /etc/clickhouse-server/config.d/ 下的文件中添加 remove="1" 条目,禁用不使用的表,重启 ClickHouse,并删除现有表,以回收已经占用的空间。
Langfuse 的最短数据保留期限是多少?
3 天。可以在项目设置中按项目设置保留期限,也可以通过 projects API 设置。每晚运行的任务会从 ClickHouse 和 blob storage 中删除超过保留期限的 traces、observations、scores 和媒体资源。删除操作无法撤销。如果需要保留超过该期限的历史数据,请先配置 blob storage 导出。
是否必须同时备份 Postgres 和 ClickHouse?
是,因为两者保存的数据不同。Postgres 保存用户、组织、项目和 API keys,ClickHouse 保存 trace 数据本身。仅恢复 Postgres 后,您可以登录实例,但其中没有任何 trace 数据。还应备份 MinIO bucket,因为其中保存了 Langfuse 接收后持久化的原始事件。在整个技术栈中,这些数据最接近事实来源。
是否可以将现有的 OpenTelemetry 配置指向自托管 Langfuse?
可以。Langfuse v4 及其 v4 SDK 基于 OpenTelemetry 构建,Anthropic 和 OpenAI OTel instrumentations 可以直接向其导出数据。在 Python 中运行 pip install langfuse opentelemetry-instrumentation-anthropic,在启动时调用一次 AnthropicInstrumentor().instrument(),并将 LANGFUSE_PUBLIC_KEY、LANGFUSE_SECRET_KEY 和 LANGFUSE_BASE_URL 设置为您自己的主机。使用 langfuse.auth_check() 确认配置,然后再排查缺少的 dashboard。