SpyBara
Go Premium

self-hosted-environments-deploy.md 2026-09-11 23:01 UTC to 2026-09-12 03:02 UTC

This page contains 541 additions and 0 deletions.

2026
Sat 12 03:02 Mon 14 22:58 Fri 18 23:58 Sat 19 23:57 Tue 22 23:59

将自托管环境部署到生产环境

在生产环境中运行自托管运行器:安全加固、网络出站流量控制、git 凭证、Kubernetes 和 Compose 配方以及故障排除。

自托管环境在您部署在网络内的运行器上运行 Claude Code 云会话,在生产环境中,这些会话代表所有可以向环境分派会话的人执行模型指导的代码。本页面适用于将工作环境投入生产的操作员。它按部署顺序进行:在连接真实系统之前要锁定什么、队列需要的出站流量、会话如何向您的 git 主机进行身份验证、部署配方本身,以及会话出现故障时要检查什么。

加固您的部署

自托管运行器代表所有可以向其环境分派会话的人在您的基础设施上执行任意的、模型指导的代码。这是您 Anthropic 组织的任何成员,以及任何可以在所有者路由到环境的范围内启动 Claude Tag 频道会话的人。在将环境连接到生产系统之前,请逐项完成以下操作:

  • 临时的、按会话的容器:在新容器或 VM 中运行每个运行器进程,该容器或 VM 在进程退出时被销毁,使用 --capacity 1 和默认的 --drain-grace-sec 0,以便每个容器恰好服务一个会话。在更高的容量或正的 drain grace 下,一个容器为来自同一锁定所有者的多个会话服务;请参阅运行器生命周期。不要在运行器重启之间重用文件系统,除了在刻意的预热检出设置中,并且永远不要跨所有者。

  • 镜像中没有广泛的凭证:不要包含长期的 SSH 密钥、云提供商凭证或授予超过会话需要的个人访问令牌。从您的包装脚本按会话铸造会话期间使用的凭证,例如推送或 API 令牌。对于在包装脚本运行之前发生的初始克隆,使用 checkout 生命周期钩子或 --use-anthropic-git-proxy;请参阅配置 git。

  • 将环境密钥保持在运行会话的主机之外:环境密钥可以注册运行器并获取在环境上排队的任何会话。在固定队列上,它存在于每个运行器主机上,任何会话的代码都可以读取密钥文件。优先使用按需运行器,其中密钥保留在编排器主机上,该主机从不运行用户代码,每个运行器接收单次使用的工作单,恰好注册一个运行器。在固定队列上,将环境密钥文件视为可由每个会话读取,并在任何可疑会话泄露后轮换密钥。

  • 默认拒绝网络出站流量:在每个环境上限制运行器和会话容器的出站流量在您自己的网络边界;默认拒绝出站流量涵盖允许什么以及原因。

  • 最小权限主机 IAM:附加到运行器主机的计算身份(例如实例配置文件或节点服务帐户)应仅授予运行器本身需要的内容。会话应通过您的包装脚本而不是继承主机的身份获取自己的凭证。

  • 阻止会话访问云元数据端点:保持会话不访问主机身份需要阻止它们访问云元数据端点,子网级出站策略不会拦截链接本地元数据流量,因此在容器本身中阻止它:

    • IMDSv2,跳数限制为 1
    • GKE Workload Identity,隐藏元数据
    • 会话容器网络命名空间中 169.254.169.254 的显式拒绝

    该块也适用于您的包装脚本和生命周期钩子,因为它们共享容器。使用会话 JWT针对您自己的令牌服务通过允许列表出站流量验证任何令牌交换,或使用基于文件的 Web 身份,例如 Amazon EKS 上的 IAM Roles for Service Accounts (IRSA)。

  • 按运行器文件系统隔离:每个运行器进程获得自己的工作目录,主机上的其他进程无法读取或写入。使 --hooks-dir、包装脚本和主机的 ~/.claude/ 对会话只读,无论是内置在镜像中还是以只读方式挂载。

  • 分派没有按环境的访问控制:您 Anthropic 组织的任何成员都可以向其任何环境分派会话。如果所有者将 Claude Tag 频道路由到环境,Claude Tag 访问设置允许的任何人都可以启动在那里运行的频道会话。默认情况下,这是连接的 Slack 工作区中的任何人,无论是否有 Claude 帐户。将每个运行器主机视为可由所有可以向其分派的人访问以执行代码,并仅在运行器主机上放置所有这些人都被允许读取的数据和凭证。--lock-to-account限制给定主机执行哪个帐户的会话,但它不会缩小谁可以分派到环境中。要使自托管环境成为唯一的选择器选项,所有者可以从云环境页面为整个组织隐藏 Anthropic 托管的环境。

  • 强制执行 repo-settings 保护:使用 --confine-repo-settings 选择保护模式。默认的 warn 记录违规并仍然生成会话,enforce 拒绝会话,off 禁用扫描。运行器扫描每个存储库的提交设置以查找:

    • 在该会话自己的工作区之外解析的授予:additionalDirectories 条目、permissions.allow 中的 Edit、Write 或 NotebookEdit 规则,或 sandbox.filesystem.allowWrite 或 allowRead 条目
    • 非空的 env 块
    • 操作员态势覆盖,例如 sandbox.enabled: false

    无论 --trust-workspace 如何,保护都会运行,并且不涵盖存储库钩子、.mcp.json 或 Bash 规则;请参阅权限和工具批准了解这些授予的位置。

网络要求

运行器及其生成的会话子进程向以下主机进行出站连接。将会话容器出站流量限制为这些主机和会话需要到达的特定内部服务;默认拒绝出站流量涵盖如何以及为什么。

这些主机始终是必需的:

主机 端口 用途
api.anthropic.com 443,HTTPS;仅 SCM 连接器的 WSS 运行器控制平面和会话流式传输、模型推理、功能标志、产品分析、JWKS 密钥获取、提交签名、设置 --use-anthropic-git-proxy 时的 git 代理,以及设置 --scm-connector-host 时编排器的 SCM 连接器隧道
您的 git 主机,例如 github.com 或您的 GitHub Enterprise 主机 443 或 22 克隆和推送存储库。如果运行器使用 --use-anthropic-git-proxy(通过 api.anthropic.com 路由 git 流量)则不需要。

这些主机是否需要取决于您的配置:

主机 端口 何时需要
downloads.claude.ai 443 在安装时,当您使用本机安装程序在主机上安装或更新 Claude Code 时;install.sh 脚本本身从 claude.ai 提供。在会话运行时,仅当会话从官方 Anthropic 市场安装插件时。
storage.googleapis.com 443 在会话运行时,用于 /plugin 中显示的插件安装计数和元数据。
code.claude.com 和 claude.com 443 内置 claude-code-guide 代理的文档查找和会话期间预批准的 WebFetch 请求。阻止这些主机仅影响文档查找。
*.frame.claudeusercontent.com 443 仅当工件工具对您组织中的会话可用时;默认值因计划而异,请参阅那里的可用性表。在运行器上设置 CLAUDE_CODE_DISABLE_ARTIFACT=1 以保持工具禁用,无论组织设置如何。
registry.npmjs.org 443 当会话安装插件时,用于获取 npm 源插件包和安装插件的 Node.js 依赖项,或当 npx 启动的 MCP 服务器运行时
http-intake.logs.us5.datadoghq.com 443 Anthropic 操作指标。仅当设置 CLAUDE_CODE_BYOC_ENABLE_DATADOG=1 时;在自托管环境中默认关闭。
browser-intake-us5-datadoghq.com 443 Anthropic 错误报告上传,仅在为会话帐户启用错误报告时发送。由 DISABLE_ERROR_REPORTING=1 或 DISABLE_TELEMETRY=1 抑制。

运行器不会到达 statsig.anthropic.com、*.sentry.io、claude.ai 或 platform.claude.com。这些主机出现在一些较旧的企业网络检查清单中,但您不需要为运行器或会话流量允许列表它们:功能标志获取转到 api.anthropic.com,运行器使用环境密钥而不是交互式 OAuth 进行身份验证。两个主机端流程确实到达 claude.ai,因此从其出站允许它的主机运行它们,而不是扩大会话容器出站流量:单行安装程序在安装时从 claude.ai 获取 install.sh,交互式 claude auth login(引导设置、doctor 的已登录模式和 CI 分派使用)通过 claude.ai、claude.com 和 platform.claude.com 登录。mcp-proxy.anthropic.com 也不是必需的:自托管会话不使用它,当为您的组织启用时,您组织的 claude.ai 连接器向会话的交付通过 api.anthropic.com 路由。请参阅 MCP 服务器。

默认拒绝出站流量

在网络段或命名空间中部署运行器和会话容器,其出站流量限制为网络要求表中的主机、您的 git 主机和会话需要到达的特定内部服务。该产品无法验证或强制执行此操作,因此在每个环境的您自己的网络边界应用它。会话代码是模型指导的,可以尝试连接到任意主机;网络层的默认拒绝出站流量限制这些尝试可以到达的位置。这适用于任何权限模式:默认预批准工具集已包括 Bash,因此 shell 出站流量在没有自动模式的情况下运行而不提示。

有关每个会话发出的遥测详情以及如何关闭它,请参阅遥测。

向出站代理进行身份验证

某些企业出站代理在每个连接上需要 Proxy-Authorization 标头。该标头中的令牌通常轮换太快而无法写入您在 HTTPS_PROXY 中设置的代理 URL。像往常一样将 HTTPS_PROXY 或 HTTP_PROXY 设置为您的代理 URL,然后设置 --proxy-authorization-command 或 --proxy-authorization-file 以告诉运行器从何处读取标头值。两个标志都需要 Claude Code v2.1.238 或更高版本。

选择 `Proxy-Authorization` 值的来源

选择与您生成 Proxy-Authorization 令牌的方式相匹配的标志:

运行器拒绝启动的配置

每个标志也有一个环境变量形式,在运行器 CLI 标志参考中列在其旁边。在运行器联系您的代理或控制平面之前,它检查标志及其变量,并在三种情况下拒绝启动:

  • 两个标志都设置:一个标志加上另一个标志的环境变量计为设置两个。
  • 没有代理 URL:HTTPS_PROXY 和 HTTP_PROXY 都不包含 http:// 或 https:// URL。运行器以大写或小写读取两个变量,不查询 ALL_PROXY。
  • 任一标志传递给编排器子命令:self-hosted-runner orchestrator 不接受标志或其环境变量。改为将标志传递给编排器启动的每个运行器。

设置代理授权标志时运行器更改的内容

设置任一标志后,运行器启动自己的侦听器并通过该侦听器发送来自自身、其生命周期钩子和其会话的代理流量。侦听器在到达您的代理的途中添加 Proxy-Authorization 标头。

  • 侦听器:侦听器是 127.0.0.1 上的转发代理。运行器在向控制平面注册之前启动侦听器,如果侦听器无法启动则在启动时退出。
  • 代理变量:运行器重写您设置的 HTTPS_PROXY 和 HTTP_PROXY 中的任何一个,使其指向侦听器。该重写的值到达运行器本身、其生命周期钩子和它运行的每个会话。
  • 令牌轮换:轮换的令牌无需重启即可生效。对于侦听器打开到您的代理的每个连接,运行器再次运行您的命令或读取您的文件并将结果添加为标头。
  • 会话环境:会话仅通过侦听器到达您的代理。在每个会话的环境中,运行器删除 ALL_PROXY,删除您未设置的 HTTPS_PROXY 或 HTTP_PROXY 的任何拼写,并将 NO_PROXY 固定到运行器自己的值。
  • 日志:运行器从不记录标头值。

配置 git

运行器管理存储库检出但默认不配置 git 身份或凭证。您控制运行器的镜像和进程环境,因此您控制 git 配置。选择两种方法之一:

  • 让运行器配置 git:使用 --configure-git 启动运行器,使其写入 Anthropic 托管会话使用的相同身份和提交签名配置
  • 在镜像中提供 git 配置:自己设置身份和推送凭证,例如在您自己的机器人身份下提交

运行器主机上的 Git 版本下限:--configure-git SSH 提交签名需要 Git 2.34 或更高版本,--use-anthropic-git-proxy 需要 2.32 或更高版本,从 --push-outcome-on-release 推送的分支恢复会话需要 2.29 或更高版本。如果您省略所有三个并自己管理 git 身份,Git 2.24 就足够了。

让运行器配置 git

使用 --configure-git 启动运行器,或设置 SELF_HOSTED_RUNNER_CONFIGURE_GIT=1,使其在启动时写入全局 git 配置:

  • user.name = Claude 和 user.email = noreply@anthropic.com,与 Anthropic 托管会话匹配
  • SSH 格式提交和标签签名,通过运行器管理的垫片路由,使用会话自己的凭证通过 Anthropic 的签名服务签署每个提交。签名可在 GitHub 上针对 Anthropic 的已发布 SSH 签名密钥进行验证。
  • push.negotiate = true,所以 git 在打包推送之前询问您的 git 主机它已经拥有哪些提交。需要 Claude Code v2.1.257 或更高版本。
  • core.hooksPath 指向运行器管理的钩子目录。其 commit-msg 和 prepare-commit-msg 钩子为每个提交添加 Co-authored-by: 预告片,用于会话的创建者,从 CCR_SESSION_ACCOUNT_EMAIL 构建,当该变量未设置时省略。如果您的镜像已设置 core.hooksPath,运行器保留您的设置,跳过安装这些钩子,并打印 [runner:git] 警告。

提交签名需要 git 2.34 或更高版本;运行器在启动时检查并在您的 git 较旧时以错误退出。此标志不配置推送凭证,您仍然在镜像中提供。

在镜像中提供 git 配置

git 身份对任何提交都是必需的。在您的 Dockerfile 中系统范围设置它,以便配置适用于运行器进程运行的任何用户:

RUN git config --system user.name "Claude" && \
    git config --system user.email "noreply@anthropic.com"

没有身份,git commit 失败并显示 Please tell me who you are,会话无法取得进展。您可以改用自己的机器人身份;运行器不会覆盖这些值。

不要将长期或广泛范围的推送凭证烘焙到共享运行器镜像中:镜像中的凭证可用于镜像运行的每个会话,无论谁启动它。相反,从您的包装脚本按会话铸造短期、最小范围的令牌,使用从会话 JWT 解码的会话创建者的身份。将其与临时的按会话容器配对,这需要 --capacity 1,因此没有凭证超过铸造它的会话;请参阅加固部分。

如果您必须在镜像级别配置推送凭证,例如对于只读部署密钥,请尽可能紧密地限制它们:

  • SSH 部署密钥限制为一个存储库,带有 url.<base>.insteadOf 重写
  • 返回最小范围令牌的 credential.helper
  • GIT_SSH_COMMAND 指向狭义范围的密钥

您配置的任何机制都必须无需提示即可工作,因为运行器的内置克隆和获取禁用 git、SSH 和 Git Credential Manager 否则会显示的提示:

  • 运行器设置 GIT_TERMINAL_PROMPT=0,所以 git 不要求用户名或密码。
  • 运行器使用 BatchMode=yes 运行 SSH,如果您设置了一个,则附加到您的 GIT_SSH_COMMAND,所以 SSH 不要求密码短语或主机确认。
  • 运行器设置 GCM_INTERACTIVE=never,所以 Git Credential Manager 不打开登录对话框。
  • 运行器清除 core.askPass,所以如果您使用 askpass 助手,改为通过 GIT_ASKPASS 环境变量设置它。

如果您的 git 主机拒绝凭证,或您没有配置凭证,运行器重试几次然后失败存储库准备。运行器不会将这些设置传递到会话的环境中。

如果检出目录由与运行器进程不同的 uid 拥有,git 拒绝对其进行操作;添加 safe.directory:

RUN git config --system --add safe.directory '*'

使用 Anthropic git 代理

使用 --use-anthropic-git-proxy 启动运行器,或设置 CLAUDE_RUNNER_USE_GIT_PROXY=1,使其通过 Anthropic 的 git 代理克隆,使用会话自己的短期令牌进行身份验证。对于普通用户会话,代理使用为会话创建者存储的 GitHub 或 GitHub Enterprise OAuth 令牌;对于机器人和代理会话,它使用您组织的 GitHub App 安装令牌。无论哪种方式,运行器镜像根本不需要 git 凭证:没有 SSH 密钥、没有凭证助手、没有 .netrc。这是 Anthropic 托管环境使用的相同身份验证路径。

代理需要 --capacity 1,因为代理 URL 是按会话的,以及 git 2.32 或更高版本,因为较旧的 git 忽略代理用来隔离会话的配置机制。如果任一要求未满足,运行器拒绝启动。因为代理从 Anthropic 端获取,您的 git 主机必须可从 Anthropic 基础设施到达,与 Anthropic 托管会话相同的要求;对于仅在您的网络内可路由的 git 主机,改用 checkout 生命周期钩子。每个运行器进程一次处理一个会话,因此运行更多副本以获得并行性。启用代理后,--git-host-rewrite 和 --git-ssh-rewrite 无效:代理 URL 指向 api.anthropic.com,而不是您的 git 主机。

运行器还在注册时向 Anthropic 报告选择加入,在启动时打印 Registering as opted in to Anthropic-managed git (--use-anthropic-git-proxy)。选择加入运行器上的每个会话然后使用 Anthropic 管理的 git 或按会话代理 URL。当会话使用按会话代理 URL 时,运行器记录一行 [runner:warn] 说明这一点。

为专用网络重写 git URL

存储库 URL 从控制平面作为 HTTPS 到达,带有您的 git 主机的主机名;对于 GitHub Enterprise,这是您在 claude.ai 上的 Claude Code 管理设置中为 GitHub Enterprise 集成配置的主机名。两个可重复的标志在克隆之前重写这些 URL:

  • --git-host-rewrite <from>=<to>:对于分割视界 DNS,其中 Anthropic 通过外部主机名到达您的 git 主机,但运行器必须使用内部主机名
  • --git-ssh-rewrite <host>:对于仅接受 SSH 的 git 主机,将 https://<host>/owner/repo 重写为 git@<host>:owner/repo

主机重写首先运行,因此如果您需要两者,请在 --git-ssh-rewrite 中列出内部主机名。为了完全控制检出,使用 checkout 生命周期钩子。

构建运行器镜像

Anthropic 不发布预构建的运行器镜像。围绕 claude 二进制文件构建您自己的,分层您的存储库需要的任何工具链:语言运行时、编译器、包管理器和 MCP 边车。

下面的配方使用 --capacity 4,所以一个容器为来自同一锁定所有者的最多四个并发会话服务。这不提供加固部分中的按会话容器隔离:在将环境连接到生产系统之前,要么以 --capacity 1 运行配方,每个会话一个容器,要么使用按需运行器,它也将环境密钥保持在会话运行主机之外。

这个 Dockerfile 是一个最小的起点:

FROM debian:bookworm-slim
ARG CLAUDE_CODE_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends git curl ca-certificates openssh-client \
 && rm -rf /var/lib/apt/lists/*
RUN curl -fsSL "https://downloads.claude.ai/claude-code-releases/${CLAUDE_CODE_VERSION:?set with --build-arg CLAUDE_CODE_VERSION}/linux-x64/claude" \
      -o /usr/local/bin/claude && chmod +x /usr/local/bin/claude
RUN git config --system user.name "Claude" \
 && git config --system user.email "noreply@anthropic.com" \
 && git config --system --add safe.directory '*'
ENTRYPOINT ["claude"]

如果您的节点是 ARM,将 linux-x64 交换为 linux-arm64,或在 Alpine 等 musl 基础镜像上交换为 linux-x64-musl 或 linux-arm64-musl;请参阅 Alpine Linux 设置了解 musl 镜像需要的额外包。URL 是标准 Claude Code 发布位置,因此您可以根据二进制完整性和代码签名中描述的发布的已签名清单验证下载的二进制文件。使用 Claude Code 版本 2.1.224 或更高版本构建镜像,然后将其推送到您的注册表并在下面的配方中引用它:

docker build --build-arg CLAUDE_CODE_VERSION=2.1.224 -t <your-registry>/claude-runner:latest .

为会话调整 CPU 和内存大小

为运行器运行的会话而不是运行器进程调整运行器的容器或主机大小。运行器本身轮询工作、准备每个会话的检出、运行您的生命周期钩子,以及启动和监督会话进程。负载来自会话:每个都是一个 Claude Code 进程加上它启动的任何东西,例如构建、测试套件、包安装和 MCP 服务器。

对于一个会话,从以下值开始,表示为 Kubernetes 请求和限制或您平台的等效值,并将它们视为起点而不是要求:

  • 内存:请求和限制各 4 GiB,满足 Claude Code 系统要求中的 4 GB 最小值。保持两者相等,以便调度程序考虑容器的完整内存。当容器达到其内存限制时,内核杀死其中的进程,这可能会结束会话中途。
  • CPU:请求 2 个 CPU,限制 4 个 CPU,所以会话可以在构建期间突发超过请求。内核在其 CPU 限制处限制容器,而不是杀死其中的进程,所以会话在限制处运行较慢但继续运行。

在 Kubernetes 容器规范中,使用以下 resources 块设置这些起始值:

resources:
  requests:
    cpu: "2"
    memory: 4Gi
  limits:
    cpu: "4"
    memory: 4Gi

构建和测试通常是会话负载中最大和最可变的部分,因此运行您的存储库的代表性构建,测量其峰值 CPU 和内存,并提高任何在该峰值之上没有为 Claude Code 进程留出空间的起始值。

运行器使用 --capacity 来限制它一次运行多少个会话。它不在它们之间分割 CPU 或内存,所以运行器上的会话共享容器的 CPU 和内存。要限制一个会话的份额,从您的包装脚本应用限制。因此,给一个容器什么取决于它一次服务多少个会话:

  • 每个运行器一个会话:给每个容器一个会话的值。在 --capacity 1 使用此大小,加固部分推荐,以及对于按需运行器,您在您的 spawn-runner 钩子提交的工作负载上设置值,例如 Kubernetes Job 的 pod 模板。
  • 每个运行器多个会话:在 --capacity 高于 1 时,将一个会话的值乘以容量,因为最多那么多会话可以在容器中同时运行。Kubernetes 和 Docker Compose 配方以 --capacity 4 运行,没有 CPU 或内存限制,因此添加为您运行的容量调整大小的限制。

Kubernetes

运行器默认在端口 8080 上提供 GET /healthz,可使用 --health-port 配置,因此 Kubernetes 探针无需额外设置即可工作。端点在进程活着时返回 200,所以下面的探针检测死进程,而不是卡住的进程;要捕获停止轮询的运行器,请在 /metrics 的 last_poll_age_seconds 系列上发出警报。下面的 Deployment 从 Kubernetes Secret 挂载环境密钥,将活跃度和就绪探针指向 /healthz,并设置 90 秒的终止宽限期。请参阅关闭时序了解为什么宽限期很重要。

清单在运行器容器上设置没有 CPU 或内存 resources。添加为您运行的容量调整大小的块,如为会话调整 CPU 和内存大小所述。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: claude-runner
  namespace: claude-runners
spec:
  replicas: 3
  selector:
    matchLabels:
      app: claude-runner
  template:
    metadata:
      labels:
        app: claude-runner
        app.kubernetes.io/part-of: claude-code-self-hosted-runner
    spec:
      terminationGracePeriodSeconds: 90
      containers:
        - name: runner
          image: <your-registry>/claude-runner:latest
          args:
            - self-hosted-runner
            - --environment-secret-file
            - /etc/claude/environment-secret
            - --capacity
            - "4"
          volumeMounts:
            - name: environment-secret
              mountPath: /etc/claude
              readOnly: true
          ports:
            - name: health
              containerPort: 8080
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 5
            periodSeconds: 10
          livenessProbe:
            httpGet:
              path: /healthz
              port: 8080
            initialDelaySeconds: 30
            periodSeconds: 30
      volumes:
        - name: environment-secret
          secret:
            secretName: claude-runner-environment-secret

上面的 Deployment 存在于 claude-runners 命名空间中。首先创建命名空间:

kubectl create namespace claude-runners

从保存您在管理 UI 的复制环境密钥步骤中复制的值的本地文件创建支持 Secret,以便密钥永远不会出现在您的 shell 历史记录中。运行 (umask 077 && cat > ./environment-secret),粘贴密钥,按 Enter,然后按 Ctrl-D。然后创建 Secret 并删除文件:

kubectl create secret generic claude-runner-environment-secret -n claude-runners --from-file=environment-secret=./environment-secret

Docker Compose

下面的 Compose 服务在运行器退出时重启它,这涵盖崩溃和正常退出后的 drain。Docker 重启策略重启同一容器及其可写层完整,所以运行器以重用的文件系统而不是加固态势推荐的新文件系统回来;为评估使用此配方,对于生产要么每次运行重新创建容器,要么使用执行此操作的编排器。

services:
  claude-runner:
    image: <your-registry>/claude-runner:latest
    command:
      - self-hosted-runner
      - --environment-secret-file
      - /run/secrets/environment-secret
      - --capacity
      - "4"
    secrets:
      - environment-secret
    restart: always
    stop_grace_period: 90s

secrets:
  environment-secret:
    file: ./environment-secret

关闭时序

在 SIGTERM 上,运行器停止接收新工作,除非你设置了 --defer-shutdown-max-min,否则最多等待 --drain-wait-sec(默认为零)以完成进行中的轮次,终止每个会话的进程树,并运行 post-session 生命周期钩子。该进程树包括 Claude 仍在会话中运行的命令。

完整的排空路径最多需要 --session-stop-grace-sec + --drain-wait-sec + --post-session-hook-timeout-sec,加上 15 秒的固定进程清理开销,加上当设置 --push-outcome-on-release 时的额外 30 秒。在默认设置下为 80 秒,运行器在启动时记录总时间。会话在这一个预算下并行排空,因此总时间不会随 --capacity 增长。

在默认的 --drain-wait-sec 0 下,滚动重启会中断进行中的轮次;每个会话在另一个运行器上恢复,丧失未推送的工作,如 已知问题 中所述。设置 --drain-wait-sec,并提高宽限期以匹配,以让轮次先完成。

在整个路径中,运行器以零容量持续向控制平面发送心跳,因此会话租约不会过期并被重新排队到另一个运行器,同时 post-session 钩子仍在写出未提交的工作。心跳在运行器注销前停止。

在主机停止运行器之前,至少给运行器启动时记录的总时间。你在哪里设置这个取决于你的主机如何停止:

  • 使用 SIGTERM 宽限期:在 Kubernetes 上设置 terminationGracePeriodSeconds,在 Docker Compose 上设置 stop_grace_period,或你的编排器的等效项至少为该总时间。Kubernetes 的默认值 30 秒短于运行器的排空路径,因此 Kubernetes 在运行器完成排空前停止 pod。
  • 使用 --retire-at:调整退休时间和主机停止时间之间的边距以覆盖典型轮次,加上 Runner lifecycle 描述的后台任务保持,加上相同的总时间。在每次启动时计算退休时间,例如 date +%s 加上运行器的预期生命周期。
  • 使用 --defer-shutdown-max-min:向排空路径总时间添加两个额外部分。第一个是你配置的分钟数。第二个是 Defer the drain past the first signal 描述的发布后宽限期,默认为 75 秒。设置该标志后,运行器也会在启动时打印组合数字,在排空路径总时间之后。

延迟排空超过第一个信号

如果你想要重启的运行器继续为其持有的会话服务长达 n 分钟,而不是在第一个信号上排空它们,请设置 --defer-shutdown-max-min <n>。在第一个 SIGTERM 或 SIGINT 上,运行器停止接收新工作并继续为其持有的会话服务。它持续轮询,以便控制平面不会重新排队这些会话。需要 Claude Code v2.1.238 或更高版本。

第一个信号后运行器持有的会话会发生什么

在信号后的前两个阶段,运行器释放会话,释放的会话在其用户发送下一条消息时在新运行器上恢复。从第一个信号开始计数,运行器经过三个阶段:

  • 在前 n 分钟内:运行器正常为其会话服务,并继续强制执行 --startup-timeout-min 和 --kill-session-after-min。如果你也设置了 --release-idle-session-min,运行器会释放任何用户空闲该长时间的会话;没有它,空闲会话保留在运行器上。
  • 当 n 分钟用完时:运行器释放它仍然持有的每个会话,无论是否空闲。运行器等待中途轮次的轮次结束,以及最多 60 秒的轮次后台任务,然后释放该会话。
  • 当发布后宽限期用完时:运行器排空它仍然持有的任何会话,控制平面立即将每个排空的会话重新排队到另一个运行器。发布后宽限期从 n 分钟用完时开始,默认为 75 秒。如果你设置 --drain-wait-sec 超过 60 秒,发布后宽限期是 --drain-wait-sec 加 15 秒。

在任何阶段,运行器一旦不持有任何会话就以 0 退出。第二个信号缩短阶段:运行器立即排空,就像在没有 --defer-shutdown-max-min 的第一个信号上一样。一旦排空开始,下一个信号强制退出运行器。这适用于第二个信号或发布后宽限期用完是否启动了排空。

调整停止超时

给你的主机停止超时至少三个部分的总和:你配置的 n 分钟、发布后宽限期和 Shutdown timing 描述的完整排空路径。使用默认设置,发布后宽限期为 75 秒,排空路径为 80 秒,因此允许 n 分钟加 155 秒。当设置 --defer-shutdown-max-min 时,运行器在启动时打印此总和。

如果停止超时在运行器完成前用完,主机会杀死运行器。它仍然持有的会话不会获得 post-session 钩子。运行器不会注销,控制平面大约一分钟后重新排队会话。如果你无法给停止超时该总和,请不设置 --defer-shutdown-max-min,以便运行器在第一个信号上排空。

什么到达运行的 post-session 钩子

post-session 钩子和 Claude 会话子进程各自在自己的 POSIX 进程组中运行,与运行器分离,因此停止机制以不同方式到达它们:

  • 运行器已在排空时的 SIGTERM:立即强制退出运行器,跳过排空路径的任何剩余部分。没有 --defer-shutdown-max-min,那是运行器接收的第二个 SIGTERM。没有信号发送到正在运行的 post-session 钩子,因此在采用孤儿的初始进程的裸主机上,它自行完成,但不受监督:其超时预算不再适用,写入关闭的日志管道可以用 SIGPIPE 杀死它,因此需要在强制退出时存活的钩子应该将其自己的输出重定向到文件。在此页面的容器配方中,运行器是容器的 PID 1,其退出结束容器,在 systemd 的默认 KillMode=control-group 下,cgroup 范围的杀死到达钩子,如 Cgroup-wide kills 条目所述;在两者中,将强制退出视为对钩子致命,并依赖宽限期。
  • 进程组范围的信号,例如包装脚本中的 kill -- -<pid>、shell 作业控制或组范围的看门狗:到达运行器和正在进行的 checkout 钩子子进程(故意保持组附加),但不到达正在运行的 post-session 钩子或会话子进程。
  • Cgroup 范围的杀死,例如 systemd 的默认 KillMode=control-group 或当 terminationGracePeriodSeconds 过期时 Kubernetes 传递给整个容器的 SIGKILL:到达一切,包括钩子。进程组隔离不能防止这些,这就是为什么宽限期必须覆盖完整排空路径。
  • 钩子自己的超时:当钩子超过 --post-session-hook-timeout-sec 时,运行器向钩子的整个进程组发送 SIGTERM,然后两秒后发送 SIGKILL,因此钩子分叉的工作进程(例如 tar、rsync 或 git)与包装 shell 一起终止,而不是作为孤儿存活。运行器的监督在钩子的 stdio 关闭后结束:将其自己的输出重定向到文件并在 SIGTERM 阶段后存活的工作进程超出运行器的范围。

当排空开始时,以及在强制退出时,运行器记录仍在运行的 post-session 钩子数量,因此你可以区分安静的排空和正在进行快照的排空。

在运行器之间保持基目录和容量相同

如果运行器在会话中途死亡,服务器重新排队会话,环境中的另一个运行器获取它。该运行器从其自己的 --base-dir 和 --capacity 派生检出路径:--capacity 1 直接在 --base-dir 下检出,--capacity 高于 1 改用按会话 worktrees。当同一环境中的运行器对任一标志使用不同的值时,恢复的会话的工作目录更改,代理之前记录的绝对路径(在编辑、工具调用或其自己的笔记中)指向不再存在的位置。

在环境中的每个运行器上使用相同的 --base-dir 和 --capacity,并且不要使用按主机的值,例如实例 ID 或主机名。

基目录默认为 /workspace,除了 --base-dir 参考行记录的例外。运行器需要对其的写入访问。在启动时,在注册之前,运行器创建目录并确认它可以写入,当它不能时以 cannot create or write to base directory 退出。以 root 身份启动的运行器自己创建默认 /workspace。对于非 root 运行器,在启动运行器之前创建目录并给运行器的用户所有权,或将 --base-dir 指向该用户已拥有的目录。

重用预热的检出

对于大型仓库,克隆可能会主导会话启动。在 --capacity 1 且没有 checkout hook 的情况下,运行器在 <base-dir>/<repo-owner>/<repo> 处为每个仓库保持一个规范克隆,并在会话间重用它:它获取请求的引用,分离 HEAD,并硬重置到该引用,当变化不大时这几乎是瞬间完成的。要跳过冷克隆,可以通过以下两种方式之一提供克隆:

  • 在镜像中克隆:在该路径处将克隆构建到运行器镜像中。每个新容器随后都会以预热克隆启动,而无需重用磁盘。
  • 在持久卷上克隆:在使用 --lock-to-account 预锁定到一个用户账户的运行器上,将 --base-dir 指向持久卷,这样磁盘只为该账户服务。预锁定的运行器永远不会接收 Claude Tag 频道会话,因此此选项不适用于为其服务的运行器。

重用路径的保证和不保证的内容:

  • 任何克隆形状都可以工作:路径处的完整、浅层或单分支克隆按原样使用。运行器在获取到现有克隆时永远不会传递 --depth,因此完整的预热保持其完整历史,浅层克隆保持浅层。CLAUDE_RUNNER_FETCH_DEPTH(full、0 或一个数字;默认 50)仅控制当尚不存在克隆时运行器进行的冷克隆。
  • 跟踪的更改重置,未跟踪的文件保留:每个会话从硬重置开始,该重置会清除前一个会话的跟踪修改,但运行器永远不会运行 git clean,因此来自锁定所有者早期会话的未跟踪文件保留在树中。
  • 使用 git 代理时,重置变成检出:使用 --use-anthropic-git-proxy,运行器在每个会话前清理克隆的 .git/,保留对象存储、引用和浅层状态,但删除索引,因此每个会话需要进行完整的工作树检出而不是近乎瞬间的重置;它仍然永远不会重新克隆。代理下不支持子模块预热。
  • 长克隆不需要解决方法:运行器使用 120 秒无进度监视器和 30 分钟硬上限来限制每个 git 操作,而不是平面超时,因此保持报告进度的缓慢冷克隆会完成。

固定版本

每个会话的子 Claude Code 进程运行运行器自己的二进制文件,运行器在它生成的会话内关闭自动更新,所以每个会话运行您在主机上安装或构建到镜像中的版本。主机级更新在运行器下次启动时生效。

  • 将队列保持在一个版本上:使用固定版本构建镜像,或在裸主机上安装特定版本并禁用自动更新
  • 升级:安装较新版本或重建镜像,然后重启运行器
  • 插件:插件市场也不自动更新;在运行器的环境中设置 FORCE_AUTOUPDATE_PLUGINS=1 以让插件自动更新,同时二进制保持固定

扩展队列

您的编排器决定何时添加或删除运行器。由于每个运行器一个所有者锁,最小副本计数是您期望并发活跃的用户和 Claude Tag 代理数;--capacity 控制一个所有者内的并行性,而不是跨所有者。

两种扩展方法可用:

  • 固定队列:运行静态运行器副本集并在每个运行器提供的 Prometheus 指标上扩展
  • 按需运行器:运行 claude self-hosted-runner orchestrator 子命令,它轮询 Anthropic 以查找没有可用运行器排队的会话,并调用您的 spawn-runner 钩子为每个会话启动一个。请参阅按需运行器。

已知问题和限制

以下是此版本中的限制,其中存在解决方法。

连接器流量离开您的网络

Anthropic 从其自己的基础设施而不是从您的运行器调用连接器工具。连接器工具是 claude.ai 连接器,例如 GitHub、Slack 和 Linear。当 Claude 在自托管会话中使用连接器时,该流量通过 api.anthropic.com 而不是源自您的网络边界内。

要将连接器排除在自托管会话之外,使用 allowedMcpServers 和 deniedMcpServers 策略设置过滤它。Claude Code 将这些设置应用于 Anthropic 交付的连接器以及您从运行器主机播种的服务器和用户添加的服务器,所以如果您为其他服务器部署允许列表,Claude Code 也会阻止交付的连接器。要在 URL 基础允许列表旁边保持连接器可用,添加与 Anthropic 代理路径匹配的条目以获得交付的连接器:

  • https://api.anthropic.com/v2/ccr-sessions/*
  • https://api.anthropic.com/v1/code/sessions/*
  • https://api.anthropic.com/v1/code/mcp/*

如果工具流量必须保留在您的网络内,改为在运行器镜像上作为本地 MCP 服务器运行等效工具。请参阅 MCP 服务器。

某些会话不计为空闲

持有永不完成的后台任务的会话不计为空闲,所以 --release-idle-session-min 不会释放该会话的槽。等待从运行中工具调用内请求的批准的会话也不计为空闲。始终将 --kill-session-after-min 与其一起设置作为硬后挡,以便没有会话可以无限期地持有槽。

--kill-session-after-min 是失控会话的后挡。在 v2.1.260 或更高版本的运行器上,达到限制的会话不会立即终止。运行器给它一个宽限窗口,默认 15 分钟,您可以使用 SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS 更改:

  • 如果会话等待其用户,或其转向已结束并仅持有后台任务,运行器立即释放它。会话在其用户发送下一条消息时恢复。
  • 如果转向仍在运行,运行器等待转向完成,或会话下一次等待其用户,然后释放它。
  • 如果会话在宽限窗口结束时仍在运行器上,运行器终止它,任何运行转向的工作丢失。等待从运行中工具调用内请求的批准的转向是会话超过窗口的一种方式。

释放的会话从新克隆恢复,所以它未推送的工作无论如何都消失了;请参阅恢复的会话丢失未推送的工作。在 v2.1.260 之前,运行器在限制处终止每个会话,最多等待宽限窗口以完成运行转向。

将该标志设置为高于您预期的最长会话时长,例如 --kill-session-after-min 480 为 8 小时。要从空闲的对话释放槽,改用 --release-idle-session-min。

其他限制

  • 恢复的会话丢失未推送的工作:当会话被释放或其运行器重启,用户发送另一条消息时,会话在新运行器上恢复,该运行器从其起始分支再次克隆存储库,所以会话未推送的工作消失。设置 --push-outcome-on-release 使运行器在释放之前尽力推送会话的结果分支,所以恢复的会话从这些提交开始;这保留提交的工作,而不是脏工作树。在启用之前,限制谁可以推送到源远程上的 claude/* refs,例如使用分支规则集:在恢复时,运行器获取之前推送的分支而不验证谁推送了它,所以任何有推送访问这些 refs 的人都可以将内容放入恢复的工作区。运行器也在恢复时丢弃按会话配置,意味着会话的 Claude 配置目录和会话写入的任何 shell 状态;--push-outcome-on-release 不涵盖这些。
  • 私有存储库无法在会话中途添加:在自托管运行器上,添加到已启动会话的存储库不使用凭证克隆,所以添加失败。创建会话时选择会话需要的每个存储库。
  • 某些连接器不出现在自托管会话中:您在 claude.ai Settings 中尚未连接的连接器不在自托管会话中列出,会话不会提示您连接它。首先在 Settings 中连接它,然后启动新会话。向已运行的会话添加连接器也不会使其工具对 Claude 可用;启动新会话以获取新添加的连接器。

报告问题

对于自托管环境的问题,请联系您的 Anthropic 帐户团队。

故障排除

为了获得引导诊断,在运行器主机上运行 doctor 子命令。doctor 子命令启动交互式 Claude Code 会话,附加运行器的日志和状态。首先在该主机上使用 claude auth login 登录,以便会话可以查询您的环境、其运行器和其排队的会话。没有该登录,例如当主机使用 API 密钥进行身份验证时,它仅限于本地健康端点、指标和运行器的日志,并且仅在您使用 --log-file 启动运行器时读取日志。

claude self-hosted-runner doctor

常见问题:

  • 运行器不出现在环境中:确认主机可以通过 HTTPS 到达 api.anthropic.com,环境密钥是最新的,主机时钟在真实时间的五分钟内;更大的偏差导致身份验证失败。运行器在身份验证失败时使用拒绝原因记录 [runner:fatal]。
  • 运行器在启动时以 cannot create or write to base directory 退出:运行器无法创建或写入 --base-dir,默认为 /workspace。修复目录的所有权或将 --base-dir 指向可写路径,如在运行器之间保持基目录和容量相同所述。如果运行器改为记录 [runner:fatal] 说基目录检查超时,目录在挂起的 NFS 或 CSI 挂载上。检查挂载健康而不是权限。运行器在打开 --log-file 之前将这两个启动失败打印到 stderr,所以在终端或您的平台的容器日志中查找它们,而不是日志文件。在 v2.1.225 之前,运行器在启动时不检查基目录,此错误配置在获取后失败会话。
  • 会话保持排队:每个在线运行器可能被锁定到不同的所有者。检查每个运行器的 claude_code_self_hosted_runner_locked_account 指标或其 [runner:health] 日志行的 locked_account 字段以查看谁持有它。两者仅在运行器被发出携带 act.email 声明的会话令牌后显示所有者的电子邮件,Claude Tag 代理的会话永远不会这样做。没有声明,运行器发出没有 locked_account 系列并记录 locked_account=yes,这告诉您运行器被锁定但不是对哪个所有者。添加副本,或等待现有运行器 drain 并重启。如果环境使用按需运行器,改为检查编排器;请参阅按需运行器。
  • 会话在获取后立即失败:在 claude.ai/code 中打开会话以查看错误。最常见的原因是运行器镜像中缺少 git 凭证和未安装的构建工具。不可写的基目录在启动时停止运行器,而不是失败会话。请参阅此列表中的运行器在启动时以 cannot create or write to base directory 退出条目。
  • 会话无法通过身份验证的出站代理到达网络:当您使用 --proxy-authorization-command 或 --proxy-authorization-file 设置的源失败、在 30 秒后超时或产生空值时,运行器以 502 Bad Gateway 应答该连接并记录原因。运行器在该日志中编辑命令的 stderr,从不记录标头值。使用 --proxy-authorization-command,在主机上自己运行命令以确认它在 stdout 上打印整个标头值。如果运行器改为在启动时以 could not start the proxy-authorization listener 退出,它无法打开其环回侦听器。
  • 运行器记录 Poll failed 行包含 rejecting the malformed poll response:运行器接收工作轮询响应,其主体不是队列的预期 JSON,最常见的是因为运行器和 api.anthropic.com 之间的某些东西(例如拦截代理或强制门户)用其自己的页面应答。运行器拒绝响应,在 claude_code_self_hosted_runner_poll_errors_total 指标的 transport 类型下计数,并在会话生命周期中描述的失败轮询计划上重试。运行器继续为其活跃会话服务。配置代理以从 api.anthropic.com 通过未更改的响应。在 v2.1.246 之前,运行器将此类响应读取为空工作队列,这可能会结束其活跃会话或使其退出。
  • 会话的分支不再存在于远程:对于会话仅从中读取的 git 源,运行器跳过该源并继续其余的。对于会话推送结果的源,删除的分支(通常因为它被合并和自动删除)使会话失败,错误命名存储库和分支,并要求您恢复分支并重试。当跳过会使其没有存储库时,运行器使用相同的错误使会话失败。在 v2.1.228 之前,此类会话在空目录中启动。
  • 会话需要数分钟才能启动:初始克隆通常主导。观看 claude_code_self_hosted_runner_session_init_duration_seconds 指标以确认,并使用预热检出或较小的 CLAUDE_RUNNER_FETCH_DEPTH 切割克隆。
  • Pod 在 drain 中途被杀死:将 terminationGracePeriodSeconds 提高到至少运行器在启动时记录的值。请参阅关闭时序。

日志初始化后,运行器将其生命周期日志(包括 [runner:fatal] 行)写入 stdout,调试输出写入 stderr,都作为纯文本行而不是 JSON。上面故障排除条目中描述的启动失败在该点之前打印到 stderr。使用 --log-file 捕获两个流,这也让 self-hosted-runner doctor 尾随它们,或使用您的平台的日志收集。每个会话的子进程写入单独的调试日志。失败时运行器保留日志,在运行器日志中打印日志的路径,并在 claude.ai/code 中的会话旁边显示日志的尾部。

接下来

  • 自定义会话:包装脚本、生命周期钩子、按需运行器、MCP 服务器和权限
  • 端到端测试:在推广新运行器镜像之前从 CI 验证它
  • 参考:每个 CLI 标志、环境变量和指标