Самостоятельно размещаемые окружения находятся в публичной бета-версии на планах Team и Enterprise; раздел Доступность и ограничения охватывает путь включения. Эта страница охватывает запуск флота в production; см. быстрый старт для вашего первого runner и сеанса.
Усиление безопасности развертывания
Самостоятельно размещаемый runner выполняет произвольный, направленный моделью код на вашей инфраструктуре от имени всех, кто может отправить сеанс в его окружение. Это любой член вашей организации Anthropic и любой, кто может запустить сеанс канала Claude Tag в области, которую Owner направил в окружение. Проработайте каждый элемент перед подключением окружения к production-системам:-
Эфемерные контейнеры для каждого сеанса: запускайте каждый процесс runner в свежем контейнере или VM, который уничтожается при выходе процесса, с
--capacity 1и значением по умолчанию--drain-grace-sec 0, чтобы каждый контейнер обслуживал ровно один сеанс. При более высокой емкости или положительном периоде осушения один контейнер обслуживает несколько сеансов от одного заблокированного владельца; см. Runner lifecycle. Не переиспользуйте файловую систему между перезагрузками runner, кроме намеренной настройки pre-warmed checkout, и никогда между владельцами. -
Нет широких учетных данных в образе: не включайте долгоживущие SSH-ключи, учетные данные облачного провайдера или личные токены доступа, которые предоставляют больше, чем нужно сеансу. Создавайте учетные данные, используемые во время сеанса, такие как токены push или API, для каждого сеанса из вашего скрипта-обертки. Для начального клонирования, которое происходит перед запуском обертки, используйте
checkoutlifecycle hook или--use-anthropic-git-proxy; см. Configure git. - Держите секрет окружения в тайне от хостов, запускающих сеансы: секрет окружения может регистрировать runners и получать любой сеанс, поставленный в очередь в окружение. На фиксированном флоте он живет на каждом хосте runner, где код любого сеанса может прочитать файл секрета. Предпочитайте on-demand runners, где секрет остается на хосте оркестратора, который никогда не запускает пользовательский код, и каждый runner получает одноразовый наряд на работу, который регистрирует ровно один runner. На фиксированном флоте рассматривайте файл environment-secret как читаемый каждым сеансом и ротируйте секрет после любого подозреваемого компрометирования сеанса.
- Исходящий трафик по умолчанию запрещен: ограничьте исходящий трафик контейнеров runner и сеанса на границе вашей собственной сети в каждом окружении; Default-deny egress охватывает, что разрешить и почему.
- Наименьшие привилегии хоста IAM: вычислительная идентификация, прикрепленная к хосту runner, такая как профиль экземпляра или учетная запись сервиса узла, должна предоставлять только то, что нужно самому runner. Сеансы должны получать свои собственные учетные данные через ваш скрипт-обертку, а не наследовать идентификацию хоста.
-
Блокируйте конечную точку облачных метаданных от сеансов: чтобы держать сеансы вне хоста идентификации, требуется блокировать их доступ к конечной точке облачных метаданных, и политики исходящего трафика на уровне подсети не перехватывают трафик метаданных link-local, поэтому блокируйте его в самом контейнере:
- IMDSv2 с пределом хопов в один
- GKE Workload Identity с скрытием метаданных
- Явный отказ для
169.254.169.254в пространстве имен сети контейнера сеанса
-
Изоляция файловой системы для каждого runner: каждый процесс runner получает свой собственный рабочий каталог, который никакой другой процесс на хосте не может читать или писать. Сделайте
--hooks-dir, скрипт-обертку и хост~/.claude/доступными только для чтения сеансом, либо встроенными в образ, либо смонтированными только для чтения. -
Отправка не имеет контроля доступа для каждого окружения: любой член вашей организации Anthropic может отправить сеанс в любое из его окружений. Если Owner направляет каналы Claude Tag в окружение, любой, кого допускает параметр доступа Claude Tag, может запустить сеансы канала, которые работают там. По умолчанию это любой в подключенном рабочем пространстве Slack, с учетной записью Claude или без нее. Рассматривайте каждый хост runner как доступный для выполнения кода всеми, кто может отправить в него, и размещайте на хосте runner только данные и учетные данные, которые все эти люди имеют право читать.
--lock-to-accountограничивает, какие сеансы учетной записи выполняет данный хост, но это не сужает, кто может отправить в окружение. Чтобы сделать самостоятельно размещаемые окружения единственным вариантом выбора, Owner может скрыть окружения, размещаемые Anthropic, для всей организации на странице Cloud environments. -
Применяйте защиту repo-settings: выберите режим защиты с помощью
--confine-repo-settings. По умолчаниюwarnрегистрирует нарушение и все еще порождает сеанс,enforceотказывает в сеансе, аoffотключает сканирование. Runner сканирует параметры каждого репозитория для:- Гранта, который разрешается вне рабочего пространства этого сеанса: запись
additionalDirectories, правилоEdit,WriteилиNotebookEditвpermissions.allow, или записьsandbox.filesystem.allowWriteилиallowRead - Непустой блок
env - Переопределение позиции оператора, такое как
sandbox.enabled: false
--trust-workspaceи не охватывает hooks репозитория,.mcp.jsonили правила Bash; см. Permissions and tool approval для того, где должны находиться эти гранты. - Гранта, который разрешается вне рабочего пространства этого сеанса: запись
Список разрешенных IP-адресов вашей организации по умолчанию не охватывает трафик self-hosted runner. Не полагайтесь на него как на сетевой контроль для трафика runner или сеанса; вместо этого применяйте default-deny egress на границе вашей собственной сети и свяжитесь с командой вашего аккаунта Anthropic, если вы хотите применение списка разрешенных IP-адресов для вашей организации.
Требования к сети
Runner и порожденные им дочерние сеансы устанавливают исходящие соединения с хостами ниже. Ограничьте исходящий трафик контейнера сеанса этими хостами и конкретными внутренними сервисами, которые сеансам нужно достичь; Default-deny egress охватывает как и почему. Эти хосты всегда требуются:
Требуются ли эти хосты, зависит от вашей конфигурации:
Runner не достигает
statsig.anthropic.com, *.sentry.io, claude.ai или platform.claude.com. Эти хосты появляются в некоторых старых контрольных списках корпоративной сети, но вам не нужно добавлять их в список разрешений для трафика runner или сеанса: получение флагов функций идет на api.anthropic.com, и runner аутентифицируется с помощью секрета окружения, а не интерактивного OAuth. Два потока на стороне хоста достигают claude.ai, поэтому запускайте их с хоста, чей исходящий трафик это позволяет, а не расширяйте исходящий трафик контейнера сеанса: одностроковый установщик получает install.sh с claude.ai во время установки, и интерактивный claude auth login, который используют guided setup, режим doctor с входом и CI dispatch, входит через claude.ai, claude.com и platform.claude.com. mcp-proxy.anthropic.com тоже не требуется: самостоятельно размещаемые сеансы его не используют, и доставка коннекторов claude.ai вашей организации в сеансы, когда это включено для вашей организации, маршрутизируется через api.anthropic.com. См. MCP servers.
Default-deny egress
Развертывайте контейнеры runner и сеанса в сегменте сети или пространстве имен, чей исходящий трафик ограничен хостами в таблице требований к сети, вашим git-хостом и конкретными внутренними сервисами, которые сеансам нужно достичь. Продукт не может проверить или применить это, поэтому применяйте это на границе вашей собственной сети в каждом окружении. Код сеанса направлен моделью и может попытаться подключиться к произвольным хостам; default-deny egress на сетевом уровне ограничивает, где эти попытки могут приземлиться. Это применяется независимо от режима разрешений: набор инструментов, одобренный по умолчанию, уже включаетBash, поэтому исходящий трафик shell работает без подсказки даже без auto mode.
Для деталей о том, какую телеметрию излучает каждый сеанс и как ее отключить, см. Telemetry.
Аутентификация на исходящий прокси
Некоторые корпоративные исходящие прокси требуют заголовокProxy-Authorization на каждом соединении. Токен в этом заголовке часто ротируется слишком быстро, чтобы писать в URL прокси, который вы устанавливаете в HTTPS_PROXY. Установите HTTPS_PROXY или HTTP_PROXY на URL вашего прокси как обычно, затем установите --proxy-authorization-command или --proxy-authorization-file, чтобы сказать runner, где читать значение заголовка. Оба флага требуют Claude Code v2.1.238 или позже.
Выберите, откуда берется значение Proxy-Authorization
Выберите флаг, который соответствует тому, как вы производите токен Proxy-Authorization:
--proxy-authorization-command <command>: выберите это для токена, который вы генерируете по требованию. Runner запускает команду shell и использует ее обрезанный stdout как значение заголовка, напримерBearer <token>.--proxy-authorization-file <path>: выберите это для токена, который другой процесс ротирует на месте. Runner читает файл и использует его обрезанное содержимое как значение заголовка.
Конфигурации, с которыми runner отказывается запускаться
Каждый флаг также имеет форму переменной окружения, указанную рядом с ним в справочнике флагов CLI runner. Перед тем как runner свяжется с вашим прокси или плоскостью управления, он проверяет флаги и их переменные и отказывается запускаться в трех случаях:- Оба флага установлены: один флаг плюс переменная окружения другого флага считается установкой обоих.
- Нет URL прокси: ни
HTTPS_PROXYниHTTP_PROXYне содержат URLhttp://илиhttps://. Runner читает обе переменные в верхнем или нижнем регистре и не консультируетALL_PROXY. - Любой флаг передан подкоманде orchestrator:
self-hosted-runner orchestratorне принимает флаги или их переменные окружения. Вместо этого передайте флаг каждому runner, который запускает оркестратор.
Что runner изменяет, пока установлен флаг proxy-authorization
С установленным любым флагом runner запускает собственный слушатель и отправляет трафик прокси от себя, своих lifecycle hooks и своих сеансов через этот слушатель. Слушатель добавляет заголовокProxy-Authorization на пути к вашему прокси.
- Слушатель: слушатель является forward proxy на
127.0.0.1. Runner запускает слушатель перед регистрацией в плоскости управления и выходит при запуске, если слушатель не может запуститься. - Переменные прокси: runner переписывает любой из
HTTPS_PROXYиHTTP_PROXY, который вы установили, чтобы он указывал на слушатель. Это переписанное значение достигает самого runner, его lifecycle hooks и каждого сеанса, который он запускает. - Ротация токена: ротированный токен вступает в силу без перезагрузки. Для каждого соединения, которое слушатель открывает для вашего прокси, runner запускает вашу команду или читает ваш файл снова и добавляет результат как заголовок.
- Окружение сеанса: сеанс достигает вашего прокси только через слушатель. В окружении каждого сеанса runner удаляет
ALL_PROXY, удаляет любое написаниеHTTPS_PROXYилиHTTP_PROXY, которое вы не установили, и закрепляетNO_PROXYна собственное значение runner. - Логи: runner никогда не регистрирует значение заголовка.
Конфигурирование git
Runner управляет checkouts репозитория, но по умолчанию не конфигурирует идентификацию git или учетные данные. Вы контролируете образ runner и окружение процесса, поэтому вы контролируете конфигурацию git. Выберите один из двух подходов:- Позвольте runner конфигурировать git: запустите runner с
--configure-git, чтобы он написал ту же идентификацию и конфигурацию подписи коммитов, которые используют сеансы, размещаемые Anthropic - Отправьте конфигурацию git в вашем образе: установите идентификацию и учетные данные push самостоятельно, например, чтобы коммитить под вашей собственной идентификацией бота
--configure-git подпись коммитов SSH требует Git 2.34 или новее, --use-anthropic-git-proxy требует 2.32 или новее, и возобновление сеансов из веток, отправленных --push-outcome-on-release, требует 2.29 или новее. Git 2.24 достаточно, если вы опустите все три и управляете идентификацией git самостоятельно.
Позвольте runner конфигурировать git
Запустите runner с--configure-git или установите SELF_HOSTED_RUNNER_CONFIGURE_GIT=1, чтобы он написал глобальную конфигурацию git при запуске:
user.name = Claudeиuser.email = noreply@anthropic.com, соответствуя сеансам, размещаемым Anthropic- Подпись коммитов и тегов в формате SSH, маршрутизированная через управляемый runner shim, который подписывает каждый коммит через сервис подписи Anthropic, используя собственные учетные данные сеанса. Подписи проверяются на GitHub против опубликованного SSH-ключа подписи Anthropic.
push.negotiate = true, чтобы git спросил ваш git-хост, какие коммиты он уже имеет, перед упаковкой push. Требует Claude Code v2.1.257 или позже.core.hooksPath, указывающий на управляемый runner каталог hooks. Его hookscommit-msgиprepare-commit-msgдобавляют трейлерCo-authored-by:для создателя сеанса к каждому коммиту, построенный из email вCCR_SESSION_ACCOUNT_EMAILи опущенный, когда эта переменная не установлена. Если ваш образ уже устанавливаетcore.hooksPath, runner оставляет вашу настройку на месте, пропускает установку этих hooks и печатает предупреждение[runner:git].
Отправьте конфигурацию git в вашем образе
Идентификация git требуется для любого коммита. Установите ее системно в вашем Dockerfile, чтобы конфигурация применялась независимо от того, какой пользователь запускает процесс runner:git commit не удается с Please tell me who you are и сеансы не могут прогрессировать. Вы можете использовать вместо этого вашу собственную идентификацию бота; runner не переопределяет эти значения.
Не запекайте долгоживущие или широко охватывающие учетные данные push в общий образ runner: учетные данные в образе доступны каждому сеансу, который запускает образ, кто бы его ни запустил. Вместо этого создавайте короткоживущий, наименее охватывающий токен для каждого сеанса из вашего скрипта-обертки, используя идентификацию создателя сеанса, декодированную из session JWT. Сочетайте это с эфемерным контейнером для каждого сеанса, который требует --capacity 1, чтобы никакие учетные данные не пережили сеанс, который их создал; см. раздел усиления безопасности.
Если вы должны конфигурировать учетные данные push на уровне образа, например для ключа развертывания только для чтения, ограничьте их так плотно, как позволяет ваш git-хост:
- SSH-ключ развертывания, ограниченный одним репозиторием с переписью
url.<base>.insteadOf credential.helper, который возвращает минимально охватывающий токенGIT_SSH_COMMAND, указывающий на узко охватывающий ключ
- Runner устанавливает
GIT_TERMINAL_PROMPT=0, чтобы git не спрашивал имя пользователя или пароль. - Runner запускает SSH с
BatchMode=yes, добавленным к вашемуGIT_SSH_COMMAND, если вы его установили, чтобы SSH не спрашивал парольную фразу или подтверждение хоста. - Runner устанавливает
GCM_INTERACTIVE=never, чтобы Git Credential Manager не открывал диалог входа. - Runner очищает
core.askPass, поэтому, если вы используете помощника askpass, установите его через переменную окруженияGIT_ASKPASSвместо этого.
safe.directory:
Используйте прокси git Anthropic
Запустите runner с--use-anthropic-git-proxy или установите CLAUDE_RUNNER_USE_GIT_PROXY=1, чтобы он клонировал через прокси git Anthropic, аутентифицированный с помощью собственного короткоживущего токена сеанса. Для обычных пользовательских сеансов прокси использует токен GitHub или GitHub Enterprise OAuth, сохраненный для создателя сеанса; для сеансов бота и агента он использует токен установки GitHub App вашей организации. В любом случае образ runner не нуждается в учетных данных git вообще: нет SSH-ключей, нет помощника учетных данных, нет .netrc. Это тот же путь аутентификации, который используют окружения, размещаемые Anthropic.
Прокси требует --capacity 1, потому что URL прокси зависит от сеанса, и git 2.32 или новее, потому что более старый git игнорирует механизм конфигурации, который прокси использует для изоляции сеансов друг от друга. Runner отказывается запускаться, если какое-либо требование не выполнено. Поскольку прокси получает со стороны Anthropic, ваш git-хост должен быть доступен из инфраструктуры Anthropic, то же требование, что и для сеансов, размещаемых Anthropic; для git-хоста, который маршрутизируется только внутри вашей сети, используйте вместо этого checkout lifecycle hook. Каждый процесс runner обрабатывает один сеанс за раз, поэтому запускайте больше реплик для параллелизма. Когда прокси включен, --git-host-rewrite и --git-ssh-rewrite не имеют эффекта: URL прокси указывает на api.anthropic.com, а не на ваш git-хост.
Переписывайте URL git для приватных сетей
URL репозитория приходят из плоскости управления как HTTPS с именем хоста вашего git-хоста; для GitHub Enterprise это имя хоста, которое вы конфигурировали для интеграции GitHub Enterprise в параметрах администратора Claude Code на claude.ai. Два повторяемых флага переписывают эти URL перед клонированием:--git-host-rewrite <from>=<to>: для split-horizon DNS, где Anthropic достигает вашего git-хоста через внешнее имя хоста, но runners должны использовать внутреннее--git-ssh-rewrite <host>: для git-хостов, которые принимают только SSH, переписываяhttps://<host>/owner/repoнаgit@<host>:owner/repo
--git-ssh-rewrite, если вам нужны оба. Для полного контроля над checkout используйте checkout lifecycle hook.
Постройте образ runner
Anthropic не публикует предварительно построенный образ runner. Постройте свой собственный вокруг бинарного файлаclaude, наслаивая любой набор инструментов, который нужен вашим репозиториям: языковые среды выполнения, компиляторы, менеджеры пакетов и MCP sidecars.
Рецепты ниже используют --capacity 4, поэтому один контейнер обслуживает до четырех одновременных сеансов от одного заблокированного владельца. Это не обеспечивает изоляцию контейнера для каждого сеанса в разделе усиления безопасности: перед подключением окружения к production-системам либо запустите рецепты на --capacity 1 с одним контейнером на сеанс, либо используйте on-demand runners, которые также держат секрет окружения вне хостов, запускающих сеансы.
Этот Dockerfile является минимальной отправной точкой:
linux-x64 на linux-arm64, если ваши узлы ARM, или на linux-x64-musl или linux-arm64-musl на образе на основе musl, таком как Alpine; см. Alpine Linux setup для дополнительных пакетов, которые нужны образам musl. URL является стандартным местоположением выпуска Claude Code, поэтому вы можете проверить загруженный бинарный файл против подписанного манифеста выпуска, как описано в Binary integrity and code signing. Постройте образ с версией Claude Code 2.1.224 или позже, затем отправьте его в ваш реестр и ссылайтесь на него в рецептах ниже:
Размер CPU и памяти для сеансов
Размер контейнера или хоста runner для сеансов, которые он запускает, а не для самого процесса runner. Runner сам опрашивает работу, подготавливает checkout каждого сеанса, запускает ваши lifecycle hooks и запускает и контролирует процессы сеанса. Нагрузка исходит от сеансов: каждый из них является процессом Claude Code плюс все, что он запускает, такое как сборки, наборы тестов, установки пакетов и MCP servers. Для одного сеанса начните со следующих значений, указанных как запросы и лимиты Kubernetes или эквивалент вашей платформы, и рассматривайте их как отправную точку, а не требование:- Память: запрос и лимит 4 ГиБ каждый, что соответствует минимуму 4 ГБ в системных требованиях Claude Code. Держите их равными, чтобы планировщик учитывал полную память контейнера. Когда контейнер достигает своего лимита памяти, ядро убивает процессы внутри него, что может завершить сеанс в середине задачи.
- CPU: запрос 2 CPU и лимит 4 CPU, чтобы сеанс мог всплеснуть выше запроса во время сборок. Ядро дросселирует контейнер на его лимите CPU, а не убивает процессы в нем, поэтому сеансы на лимите работают медленнее, но продолжают работать.
resources:
--capacity для ограничения количества сеансов, которые он запускает одновременно. Он не делит CPU или память между ними, поэтому сеансы на runner делят CPU и память контейнера. Чтобы ограничить долю одного сеанса, применяйте лимиты из вашего скрипта-обертки. То, что дать одному контейнеру, поэтому зависит от того, сколько сеансов он обслуживает одновременно:
- Один сеанс на runner: дайте каждому контейнеру значения одного сеанса. Используйте эту размер на
--capacity 1, которую раздел усиления безопасности рекомендует, и для on-demand runners, где вы устанавливаете значения на рабочую нагрузку, которую вашspawn-runnerhook отправляет, такую как шаблон pod Kubernetes Job. - Несколько сеансов на runner: на
--capacityвыше одного, умножьте значения одного сеанса на емкость, потому что до такого количества сеансов может работать в контейнере одновременно. Рецепты Kubernetes и Docker Compose запускают--capacity 4без лимитов CPU или памяти, поэтому добавьте лимиты, размер которых соответствует емкости, которую вы запускаете.
Kubernetes
Runner обслуживаетGET /healthz на порту 8080 по умолчанию, конфигурируемом с помощью --health-port, поэтому зонды Kubernetes работают без дополнительной настройки. Конечная точка возвращает 200 всякий раз, когда процесс живой, поэтому зонды ниже обнаруживают мертвый процесс, а не застрявший; чтобы поймать runner, который перестал опрашивать, установите оповещение на серию last_poll_age_seconds из /metrics. Развертывание ниже монтирует секрет окружения из Kubernetes Secret, указывает зонды liveness и readiness на /healthz и устанавливает период завершения 90 секунд. См. Shutdown timing для того, почему период завершения имеет значение.
Манифест не устанавливает resources CPU или памяти на контейнер runner. Добавьте блок, размер которого соответствует емкости, которую вы запускаете, как описано в Size CPU and memory for sessions.
claude-runners. Сначала создайте пространство имен:
(umask 077 && cat > ./environment-secret), вставьте секрет, нажмите Enter, затем Ctrl-D. Затем создайте Secret и удалите файл:
Docker Compose
Сервис Compose ниже перезапускает runner всякий раз, когда он выходит, что охватывает как сбои, так и нормальный выход после осушения. Политика перезагрузки Docker перезапускает тот же контейнер с его записываемым слоем нетронутым, поэтому runner возвращается на переиспользованной файловой системе, а не на свежей, которую рекомендует позиция усиления безопасности; используйте этот рецепт для оценки, и для production либо пересоздайте контейнер за запуск, либо используйте оркестратор, который это делает.Время завершения работы
При полученииSIGTERM runner прекращает принимать новую работу и, если вы не установили --defer-shutdown-max-min, ждёт до --drain-wait-sec, по умолчанию ноль, чтобы завершить выполняемые turns, завершает дерево процессов каждой сессии и запускает post-session lifecycle hook. Это дерево процессов включает команды, которые Claude всё ещё выполнял в сессии.
Полный путь drain требует до --session-stop-grace-sec + --drain-wait-sec + --post-session-hook-timeout-sec, плюс 15 секунд фиксированных накладных расходов на очистку процессов, плюс ещё 30 секунд, когда установлен --push-outcome-on-release. При значениях по умолчанию это 80 секунд, и runner логирует общее время при запуске. Сессии drain параллельно в рамках этого одного бюджета, поэтому общее время не растёт с --capacity.
При значении по умолчанию --drain-wait-sec 0 rolling restart прерывает выполняемые turns; каждая сессия возобновляется на другом runner, теряя неотправленную работу, как описано в разделе Известные проблемы. Установите --drain-wait-sec и увеличьте период grace, чтобы соответствовать ему, чтобы позволить turns завершиться в первую очередь.
На протяжении всего этого пути runner продолжает отправлять heartbeat на control plane с нулевой ёмкостью, поэтому lease сессии не истекает и не переставляется на другой runner, пока hook post-session всё ещё записывает неподтверждённую работу. Heartbeat останавливается непосредственно перед тем, как runner отменяет регистрацию.
Дайте runner по крайней мере общее время, которое он логирует при запуске, прежде чем хост его остановит. Где вы это установите, зависит от того, как ваши хосты останавливаются:
- С периодом grace
SIGTERM: установитеterminationGracePeriodSecondsна Kubernetes,stop_grace_periodна Docker Compose или эквивалент вашего оркестратора по крайней мере на это общее время. Значение по умолчанию Kubernetes в 30 секунд короче, чем путь drain runner, поэтому Kubernetes останавливает pod до того, как runner завершит drain. - С
--retire-at: установите размер margin между временем retire и временем остановки хоста, чтобы охватить типичные turns, плюс background-task hold, который описывает Runner lifecycle, плюс то же самое общее время. Вычислите время retire при каждом запуске, напримерdate +%sплюс предполагаемое время жизни runner. - С
--defer-shutdown-max-min: добавьте две дополнительные части к общему времени drain-path. Первая — это минуты, которые вы настраиваете. Вторая — это post-release grace, который описывает Defer the drain past the first signal, 75 секунд при значениях по умолчанию. Когда флаг установлен, runner также выводит объединённую цифру при запуске, после общего времени drain-path.
Defer the drain past the first signal
Установите--defer-shutdown-max-min <n>, если вы хотите, чтобы runner, который вы перезапускаете, продолжал обслуживать сессии, которые он удерживает, до n минут, вместо того чтобы drain их при первом сигнале. При первом SIGTERM или SIGINT runner прекращает принимать новую работу и продолжает обслуживать сессии, которые он удерживает. Он продолжает опрашивать, чтобы control plane не переставлял эти сессии. Требует Claude Code v2.1.238 или позже.
Что происходит с сессиями, которые runner удерживает после первого сигнала
На первых двух этапах, следующих за сигналом, runner выпускает сессии, и выпущенная сессия возобновляется на свежем runner, когда его пользователь отправляет следующее сообщение. Отсчитывая от первого сигнала, runner проходит через три этапа:- В течение первых
nминут: runner обслуживает свои сессии нормально и продолжает применять--startup-timeout-minи--kill-session-after-min. Если вы также установили--release-idle-session-min, runner выпускает любую сессию, пользователь которой был неактивен столько времени; без этого неактивные сессии остаются на runner. - Когда истекают
nминут: runner выпускает каждую сессию, которую он всё ещё удерживает, неактивную или нет. Runner ждёт, пока turn сессии mid-turn завершится, и до 60 секунд больше для background tasks turn, прежде чем выпустить эту сессию. - Когда истекает post-release grace: runner drain любые сессии, которые он всё ещё удерживает, и control plane переставляет каждую drained сессию на другой runner сразу же. Post-release grace начинается, когда истекают
nминут, и составляет 75 секунд при значениях по умолчанию. Если вы установили--drain-wait-secвыше 60 секунд, post-release grace составляет--drain-wait-secплюс 15 секунд вместо этого.
--defer-shutdown-max-min. Как только drain находится в процессе, следующий сигнал force-exits runner. Это верно, независимо от того, начал ли drain второй сигнал или истекла post-release grace.
Установите timeout остановки
Дайте timeout остановки вашего хоста по крайней мере сумму трёх частей:n минут, которые вы настраиваете, post-release grace и полный drain path, который описывает Shutdown timing. При настройках по умолчанию post-release grace составляет 75 секунд, а drain path составляет 80 секунд, поэтому выделите n минут плюс 155 секунд. Runner выводит эту сумму при запуске всякий раз, когда установлен --defer-shutdown-max-min.
Если timeout остановки истекает до того, как runner завершит работу, хост убивает runner. Сессии, которые он всё ещё удерживает, не получают hook post-session. Runner не отменяет регистрацию, и control plane переставляет сессии примерно через минуту. Если вы не можете дать timeout остановки эту сумму, оставьте --defer-shutdown-max-min неустановленным, чтобы runner drain при первом сигнале вместо этого.
Что достигает работающего post-session hook
Hookpost-session и дочерний процесс Claude сессии каждый запускаются в своей собственной POSIX группе процессов, отдельно от runner, поэтому механизмы остановки достигают их по-разному:
SIGTERMпока runner уже drain: force-exits runner немедленно, пропуская всё, что остаётся от пути drain. Без--defer-shutdown-max-min, это второйSIGTERM, который получает runner. Ничто не сигнализирует работающему hookpost-session, поэтому на голом хосте, где init процесс усыновляет orphans, он завершается самостоятельно, но без надзора: его бюджет timeout больше не применяется, и запись в закрытую трубу логов может убить его сSIGPIPE, поэтому hook, который должен пережить forced exit там, должен перенаправить свой собственный вывод в файл. В рецептах контейнеров на этой странице runner является PID 1 контейнера и его выход завершает контейнер, и при systemd по умолчаниюKillMode=control-groupcgroup-wide kill достигает hook тоже, как описывает запись Cgroup-wide kills; в обоих случаях рассматривайте forced exit как фатальный для hook и полагайтесь на период grace вместо этого.- Process-group-wide signals, такие как
kill -- -<pid>в скрипте-обёртке, shell job control или group-wide watchdog: достигают runner и subprocess mid-checkout-hook, который остаётся group-attached намеренно, но не mid-run hookpost-sessionили дочерний процесс сессии. - Cgroup-wide kills, такие как systemd по умолчанию
KillMode=control-groupилиSIGKILL, который Kubernetes доставляет всему контейнеру, когда истекаетterminationGracePeriodSeconds: достигают всего, включая hook. Изоляция process-group не защищает от этого, поэтому период grace должен охватывать полный путь drain. - Собственный timeout hook: когда hook превышает
--post-session-hook-timeout-sec, runner отправляетSIGTERMна всю группу процессов hook, затемSIGKILLдве секунды спустя, поэтому worker, который fork hook, такой как tar, rsync или git, завершается с оболочкой-обёрткой вместо того, чтобы выжить как orphan. Надзор runner заканчивается, как только stdio hook закрывается: worker, который перенаправил свой собственный вывод в файл и пережил этапSIGTERM, находится вне досягаемости runner.
post-session всё ещё работают, поэтому вы можете отличить тихий drain от того, который находится mid-snapshot.
Держите базовый каталог и емкость идентичными на всех runners
Если runner умирает в середине сеанса, сервер переставляет сеанс в очередь и другой runner в окружении его подхватывает. Этот runner выводит путь checkout из своего собственного--base-dir и --capacity: --capacity 1 проверяет прямо под --base-dir, и --capacity выше 1 использует вместо этого per-session worktrees. Когда runners в одном окружении используют разные значения для любого флага, рабочий каталог возобновленного сеанса изменяется, и абсолютные пути, которые агент записал ранее, в редактированиях, вызовах инструментов или своих собственных заметках, указывают на местоположение, которое больше не существует.
Используйте одинаковые --base-dir и --capacity на каждом runner в окружении и не используйте значение для каждого хоста, такое как ID экземпляра или имя хоста.
Базовый каталог по умолчанию /workspace, с исключением, которое записывает строка справочника --base-dir. Runner нуждается в доступе на запись к нему. При запуске, перед регистрацией, runner создает каталог и подтверждает, что может писать в него, и выходит с cannot create or write to base directory, когда не может. Runner, запущенный как root, создает по умолчанию /workspace сам. Для non-root runner создайте каталог и дайте пользователю runner владение перед запуском runner, или укажите --base-dir на каталог, который этот пользователь уже владеет.
Переиспользуйте pre-warmed checkout
Для больших репозиториев клон может доминировать при запуске сеанса. На--capacity 1 без checkout hook, runner держит один канонический клон на репозиторий на <base-dir>/<repo-owner>/<repo> и переиспользует его на сеансы: он получает запрошенный ref, отсоединяет HEAD и жестко сбрасывает его, что почти мгновенно, когда мало что изменилось. Чтобы пропустить холодный клон, поставьте клон одним из двух способов:
- Клон в образе: постройте клон в образ runner на этом пути. Каждый свежий контейнер затем начинается с теплым клоном без переиспользования диска.
- Клон на постоянном томе: на runners, которые вы предварительно блокируете для учетной записи одного пользователя с
--lock-to-account, укажите--base-dirна постоянный том, поэтому диск только когда-либо обслуживает эту учетную запись. Pre-locked runner никогда не подхватывает сеансы канала Claude Tag, поэтому этот вариант не применяется к runners, которые их обслуживают.
- Любая форма клона работает: полный, неглубокий или однозвездный клон на пути используется как есть. Runner никогда не передает
--depthпри получении в существующий клон, поэтому полный pre-warm держит свою полную историю и неглубокий остается неглубоким.CLAUDE_RUNNER_FETCH_DEPTH(full,0или число; по умолчанию 50) контролирует только холодный клон, который runner делает, когда клон еще не существует. - Отслеживаемые изменения сбрасываются, неотслеживаемые файлы сохраняются: каждый сеанс начинается с жесткого сброса, который стирает предыдущие модификации сеанса, но runner никогда не запускает
git clean, поэтому неотслеживаемые файлы из более ранних сеансов заблокированного владельца остаются в дереве. - С git proxy, сброс становится checkout: с
--use-anthropic-git-proxy, runner дезинфицирует.git/клона перед каждым сеансом, сохраняя хранилище объектов, refs и неглубокое состояние, но удаляя индекс, поэтому каждый сеанс платит полный checkout рабочего дерева вместо почти мгновенного сброса; он все еще никогда не переклонирует. Pre-warms подмодулей не поддерживаются под proxy. - Длинные клоны не нуждаются в обходном пути: runner ограничивает каждую операцию git с помощью 120-секундного наблюдателя без прогресса и 30-минутного жесткого лимита, а не плоского тайм-аута, поэтому медленный холодный клон, который продолжает сообщать о прогрессе, завершается.
Закрепите версию
Процесс дочернего Claude Code каждого сеанса запускает собственный бинарный файл runner, и runner отключает auto-update внутри сеансов, которые он порождает, поэтому каждый сеанс запускает версию, которую вы установили на хосте или встроили в образ. Обновление на уровне хоста вступает в силу в следующий раз, когда runner запускается.- Чтобы держать флот на одной версии: постройте образ с закрепленной версией или на голом хосте установите конкретную версию и отключите auto-updates
- Чтобы обновить: установите более новую версию или пересоздайте образ, затем перезагрузите runners
- Плагины: рынки плагинов тоже не auto-update; установите
FORCE_AUTOUPDATE_PLUGINS=1в окружении runner, чтобы позволить плагинам auto-update, пока бинарный файл остается закрепленным
Масштабируйте флот
Ваш оркестратор решает, когда добавлять или удалять runners. Из-за блокировки one-owner-per-runner, минимальное количество реплик - это количество пользователей и агентов Claude Tag, которых вы ожидаете быть активными одновременно;--capacity контролирует параллелизм в пределах сеансов одного владельца, а не на всех владельцах.
Доступны два подхода к масштабированию:
- Фиксированный флот: запустите статический набор реплик runner и масштабируйте на Prometheus metrics, которые обслуживает каждый runner
- On-demand runners: запустите подкоманду
claude self-hosted-runner orchestrator, которая опрашивает Anthropic для сеансов, которые поставлены в очередь без доступного runner и вызывает ваш hookspawn-runnerдля загрузки одного на сеанс. См. On-demand runners.
Известные проблемы и ограничения
Ниже приведены ограничения в этом выпуске с обходными путями, где они существуют.Трафик коннектора покидает вашу сеть
Anthropic вызывает инструменты коннектора из своей собственной инфраструктуры, а не из вашего runner. Инструменты коннектора - это коннекторы claude.ai, такие как GitHub, Slack и Linear. Когда Claude использует коннектор в самостоятельно размещаемом сеансе, этот трафик идет черезapi.anthropic.com, а не исходит из границы вашей сети.
Чтобы держать коннектор вне самостоятельно размещаемых сеансов, отфильтруйте его с помощью allowedMcpServers и deniedMcpServers параметров политики. Claude Code применяет эти параметры к коннекторам, которые Anthropic доставляет, а также к серверам, которые вы сеете с хоста runner и серверам, которые пользователи добавляют, поэтому если вы развертываете список разрешений для других серверов, 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/*
Некоторые сеансы не считаются неактивными
Сеанс, держащий фоновую задачу, которая никогда не заканчивается, не считается неактивным, поэтому--release-idle-session-min не выпустит слот этого сеанса. Сеанс, который ждет одобрения, запрошенного изнутри работающего вызова инструмента, тоже не считается неактивным. Всегда устанавливайте --kill-session-after-min рядом с ним как жесткий упор, чтобы никакой сеанс не мог держать слот неопределенно долго.
--kill-session-after-min является упором для убегающих сеансов. На runner на v2.1.260 или позже, сеанс, который достигает лимита, не завершается сразу. Runner дает ему окно благодати, 15 минут по умолчанию, которое вы можете изменить с помощью SELF_HOSTED_RUNNER_MAX_LIFETIME_GRACE_MS:
- Если сеанс ждет своего пользователя, или его ход закончился и он держит только фоновые задачи, runner выпускает его сразу же. Сеанс возобновляется, когда его пользователь отправляет свое следующее сообщение.
- Если ход все еще работает, runner ждет, пока ход закончится, или пока сеанс не будет ждать своего пользователя, и затем выпускает его.
- Если сеанс все еще на runner, когда окно благодати истекает, runner завершает его, и любая работа работающего хода потеряна. Ход, ожидающий одобрения, запрошенного изнутри работающего вызова инструмента, - это один способ, которым сеанс переживает окно.
--kill-session-after-min 480 для 8 часов. Чтобы освободить слоты из разговоров, которые становятся неактивными, используйте вместо этого --release-idle-session-min.
Дополнительные ограничения
- Возобновленные сеансы теряют неотправленную работу: когда сеанс выпущен или его runner перезагружен, и пользователь отправляет другое сообщение, сеанс возобновляется на свежем runner, который клонирует репозиторий снова с его начальной ветки, поэтому работа, которую сеанс не отправил, потеряна. Установите
--push-outcome-on-release, чтобы runner сделал best-effort push веток результата сеанса перед выпуском, поэтому возобновленный сеанс начинается с этих коммитов вместо этого; это сохраняет подтвержденную работу, а не грязное рабочее дерево. Перед включением ограничьте, кто может push наclaude/*refs на исходном удаленном, например с помощью набора правил ветки: при возобновлении runner получает ранее отправленную ветку без проверки, кто ее отправил, поэтому любой с доступом push на эти refs может поместить содержимое в возобновленное рабочее пространство. Runner также отбрасывает конфигурацию для каждого сеанса при возобновлении, означая каталог конфигурации Claude сеанса и любое состояние shell, которое сеанс написал;--push-outcome-on-releaseне охватывает это. - Приватные репозитории не могут быть добавлены в середине сеанса: репозиторий, добавленный в сеанс после его запуска, не клонируется с учетными данными на самостоятельно размещаемом runner, поэтому добавление не удается. Выберите каждый репозиторий, который сеансу нужен, когда вы его создаете.
- Некоторые коннекторы не появляются в самостоятельно размещаемых сеансах: коннектор, который вы еще не подключили в параметрах claude.ai, не указан в самостоятельно размещаемом сеансе, и сеанс не будет вас приглашать подключить его. Подключите его в параметрах сначала, затем запустите свежий сеанс. Добавление коннектора в уже работающий сеанс тоже не делает его инструменты доступными для Claude; запустите свежий сеанс, чтобы подхватить недавно добавленный коннектор.
Сообщите о проблеме
Для проблем с самостоятельно размещаемыми окружениями свяжитесь с командой вашего аккаунта Anthropic.Устранение неполадок
Для управляемой диагностики запустите подкоманду doctor на хосте runner. Подкоманда doctor запускает интерактивный сеанс Claude Code с логами и состоянием runner прикрепленными. Сначала войдите сclaude auth login на этом хосте, чтобы сеанс мог запросить ваше окружение, его runners и его поставленные в очередь сеансы. Без этого входа, например, когда хост аутентифицируется с помощью ключа API, он ограничен локальной конечной точкой здоровья, метриками и логом runner, и он читает лог только если вы запустили runner с --log-file.
- Runner не появляется в окружении: подтвердите, что хост может достичь
api.anthropic.comчерез HTTPS, секрет окружения текущий и часы хоста находятся в пределах пяти минут от реального времени; большее смещение вызывает отказ аутентификации. Runner регистрирует[runner:fatal]с причиной отказа при отказе аутентификации. - Runner выходит при запуске с
cannot create or write to base directory: runner не может создать или писать в--base-dir, который по умолчанию/workspace. Исправьте владение каталога или укажите--base-dirна записываемый путь, как описано в Keep the base directory and capacity identical across runners. Если runner вместо этого регистрирует[runner:fatal], говоря, что проверка базового каталога истекла по времени, каталог находится на зависшем монтировании NFS или CSI. Проверьте здоровье монтирования, а не разрешения. Runner печатает оба эти отказа при запуске в stderr перед открытием--log-file, поэтому ищите их в терминале или логах контейнера вашей платформы, а не в файле логов. До v2.1.225, runner не проверял базовый каталог при запуске, и эта неправильная конфигурация не удавалась сеансам после подхвата вместо этого. - Сеансы остаются поставленными в очередь: каждый онлайн runner может быть заблокирован для другого владельца. Проверьте метрику
claude_code_self_hosted_runner_locked_accountкаждого runner metric или полеlocked_accountего строки логов[runner:health], чтобы увидеть, кто его держит. Оба показывают email владельца только после того, как runner получил токен сеанса, несущий претензиюact.email, которую сеансы агента Claude Tag никогда не делают. Без претензии runner не излучает сериюlocked_accountи регистрируетlocked_account=yes, что говорит вам, что runner заблокирован, но не для какого владельца. Добавьте реплики или ждите, пока существующий runner осушится и перезагрузится. Если окружение использует on-demand runners, проверьте оркестратор вместо этого; см. On-demand runners. - Сеансы не удаются сразу после подхвата: откройте сеанс в claude.ai/code, чтобы увидеть ошибку. Наиболее распространенные причины - отсутствие git credentials в образе runner и инструменты сборки, которые не установлены. Неписываемый базовый каталог останавливает runner при запуске вместо того, чтобы не удавались сеансы. См. запись Runner exits at startup with
cannot create or write to base directoryв этом списке. - Сеансы не могут достичь сеть через аутентифицирующий исходящий прокси: когда источник, который вы установили с помощью
--proxy-authorization-commandили--proxy-authorization-file, не удается, истекает по времени после 30 секунд или дает пустое значение, runner отвечает на это соединение502 Bad Gatewayи регистрирует почему. Runner редактирует stderr команды в этом логе и никогда не регистрирует значение заголовка. С--proxy-authorization-command, запустите команду самостоятельно на хосте, чтобы подтвердить, что она печатает все значение заголовка на stdout. Если runner вместо этого выходит при запуске сcould not start the proxy-authorization listener, он не мог открыть свой слушатель loopback. - Runner регистрирует строки
Poll failed, содержащиеrejecting the malformed poll response: runner получил ответ на опрос работы, чье тело не является ожидаемым JSON очереди, чаще всего потому что что-то между runner иapi.anthropic.com, такое как перехватывающий прокси или captive portal, ответил своей собственной страницей. Runner отклоняет ответ, считает его под видомtransportметрикиclaude_code_self_hosted_runner_poll_errors_totalmetric и повторяет попытку по расписанию отказа опроса, описанному в Session lifecycle. Runner продолжает обслуживать свои живые сеансы. Конфигурируйте прокси, чтобы пропустить ответы отapi.anthropic.comбез изменений. До v2.1.246, runner читал такой ответ как пустую очередь работы, которая могла закончить его живые сеансы или заставить его выйти. - Ветка сеанса больше не существует на удаленном: для источника git, который сеанс только читает, runner пропускает этот источник и продолжает на оставшихся. Для источника, на который сеанс отправляет результаты, удаленная ветка, обычно потому что она была объединена и auto-deleted, не удается сеанс с ошибкой, называющей репозиторий и ветку и просящей вас восстановить ветку и повторить попытку. Runner не удается сеанс с той же ошибкой, когда пропуск оставил бы его без репозитория вообще. До v2.1.228, такой сеанс начинался в пустом каталоге.
- Сеансы занимают минуты для запуска: начальный клон обычно доминирует. Смотрите метрику
claude_code_self_hosted_runner_session_init_duration_secondsmetric, чтобы подтвердить, и сократите клон с помощью pre-warmed checkout или меньшегоCLAUDE_RUNNER_FETCH_DEPTH. - Pod убивается в середине осушения: поднимите
terminationGracePeriodSecondsпо крайней мере на значение, которое runner регистрирует при запуске. См. Shutdown timing.
[runner:fatal], в stdout и отладочный вывод в stderr, все как простые текстовые строки, а не JSON. Отказы при запуске, описанные в записях устранения неполадок выше, печатаются в stderr перед этой точкой. Захватите оба потока с помощью --log-file, что также позволяет self-hosted-runner doctor их отслеживать, или с помощью сбора логов вашей платформы. Каждый процесс дочернего сеанса пишет отдельный отладочный лог. При отказе runner сохраняет лог, печатает путь логов в логе runner и выводит хвост логов рядом с сеансом в claude.ai/code.
Что дальше
- Customize sessions: скрипты-обертки, lifecycle hooks, on-demand runners, MCP servers и разрешения
- Test end to end: проверьте новый образ runner из CI перед продвижением
- Reference: каждый флаг CLI, переменная окружения и метрика