CodeHerderSearch⌘KRequest access →

Package and browser downloads in containers

How the device download cache speeds up container-stage installs, which keys are refused, and how to use a private registry or the GitLab registry.

When a device runs each stage in its own container, CodeHerder gives that device a download cache. Package installs and browser downloads in a stage go through it. A second install of the same package on the same device is fast, and the registry sees fewer requests.

This page covers when the cache applies, what it serves, how to turn it off or cap it, which environment keys CodeHerder refuses because they would change where installs come from, and how to use a private registry.

When the cache applies

The cache runs on a device started with ch device-server --docker-executor while stage network filtering is on, which is the default. See Isolating agent runs on a device for the two Docker options.

  • One cache serves every task and workspace on the device.
  • It does not apply to devices that run stages as plain host processes, or to the trusted --docker mode.
  • It is on by default. You don’t set anything up.

What the cache serves

Tool Served
npm and pnpm Yes
Yarn 2 and later Yes
Yarn 1 (classic) No
Playwright browser downloads Yes
Go modules, and the Go checksum database Yes
PyPI downloads for pip, uv and other Python clients Yes
Puppeteer browser downloads No. Puppeteer uses the browser that ships in the stage image.

Every other download goes straight to its host, as it does without a cache.

Your project’s config still wins

The cache fills in a default only where your project has not chosen. These settings take precedence:

  • A registry or scope registry in a committed .npmrc.
  • A project .yarnrc.yml.
  • A GOPROXY you set yourself, for example with go env -w.
  • A pip index from your own config or requirements file.

For uv and PyPI, package URLs, lock-file sources and hashes don’t change.

What gets stored

The cache stores only public, anonymous downloads.

  • A request that carries credentials goes straight to its host. The cache never stores it.
  • A private registry goes straight to its host too.
  • npm packages are checked against the registry’s integrity value, and PyPI files against their published digests, before the cache stores them. Playwright publishes no checksum, so those downloads aren’t checked this way.

When the cache is down

If the cache isn’t running, installs go straight to the internet. Nothing fails because of it.

The device’s Health panel shows Pull cache as a warning. The warning never blocks work. See Managing your devices for the readiness checks. If you run the device yourself, docker logs ch-pullcache shows why the cache stopped.

Settings

Set these on the device server. See Running the device server as a service for where environment variables go. Restart the device server after you change one.

Setting Default What it does
CH_PULL_CACHE true Set to false to turn the cache off. Installs then fetch from the internet every time.
CH_PULL_CACHE_MAX_SIZE 20g Caps the cache. Use a number with an optional k, m, g or t suffix. When the cache is full, the least recently used entries go first.

When the cache is on, a CH_PULL_CACHE_MAX_SIZE value it can’t parse stops the device server at start.

When the device reaches its disk floor, CodeHerder can empty the cache, late in its cleanup order. It refills as stages ask for packages.

Keys CodeHerder refuses

A key that points installs at another host could send every stage’s downloads somewhere you didn’t intend. CodeHerder refuses a fixed set of keys wherever you can set an environment value:

The save fails with the error code variable_key_refused. The check ignores case, so npm_config_registry is refused too. It applies on every device, whether or not the cache is running.

These are exact names, not patterns. A key not listed here, such as PIP_TIMEOUT, isn’t refused by this rule.

npm, yarn and bun registries

NPM_CONFIG_REGISTRY, NPM_CONFIG_REPLACE_REGISTRY_HOST, NPM_CONFIG_USERCONFIG, NPM_CONFIG_GLOBALCONFIG, YARN_NPM_REGISTRY_SERVER, YARN_REGISTRY, BUN_CONFIG_REGISTRY

Python indexes

PIP_INDEX_URL, PIP_EXTRA_INDEX_URL, PIP_CONFIG_FILE, UV_INDEX_URL, UV_DEFAULT_INDEX, UV_EXTRA_INDEX_URL, UV_INDEX

Go modules

GOPROXY, GOSUMDB, GOPRIVATE, GONOSUMDB, GONOSUMCHECK, GOINSECURE, GOFLAGS, GOENV, GOTOOLCHAIN, GOROOT

Proxies

HTTP_PROXY, HTTPS_PROXY, ALL_PROXY, NO_PROXY

Certificate trust

SSL_CERT_FILE, SSL_CERT_DIR, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, NODE_EXTRA_CA_CERTS, PIP_CERT, UV_SYSTEM_CERTS, UV_NATIVE_TLS

Browser downloads and browser binaries

PLAYWRIGHT_DOWNLOAD_HOST, any PLAYWRIGHT_<BROWSER>_DOWNLOAD_HOST (for example PLAYWRIGHT_CHROMIUM_DOWNLOAD_HOST), PLAYWRIGHT_BROWSERS_PATH, PLAYWRIGHT_MCP_EXECUTABLE_PATH, PUPPETEER_DOWNLOAD_HOST, PUPPETEER_DOWNLOAD_BASE_URL, PUPPETEER_EXECUTABLE_PATH, CHROME_PATH, AGENT_BROWSER_EXECUTABLE_PATH

Other keys are refused for other reasons, such as keys that change which program a shell runs. See Variables.

Using a private registry

Put the registry in your project’s own config and commit it. The refusal above doesn’t touch committed files.

  • npm: add a scope registry to the project .npmrc, for example @acme:registry=https://npm.acme.example/.
  • Yarn 2 and later: set npmScopes or npmRegistryServer in the project .yarnrc.yml.
  • pip and uv: put the index in the repo’s pip or uv config, or at the top of a requirements file.

Keep the auth token out of the repo. In .npmrc, reference an environment variable:

//npm.acme.example/:_authToken=${NPM_TOKEN}

Then add NPM_TOKEN as a secret variable in the workspace. A name that ends in _TOKEN must be saved with Secret turned on. NPM_TOKEN isn’t on the refused list.

GitLab package registry

Private npm packages on gitlab.com work in a container stage with no stage secret. CodeHerder relays the registry reads and adds a read-only token itself. The token never enters the stage: not its environment, its files or its logs.

  • It covers the instance-level path /api/v4/packages/npm/ and the project-level path /api/v4/projects/<id>/packages/npm/. Metadata and tarballs both work.
  • The relay sends GET and HEAD only. Publish, dist-tag changes, unpublish and every other gitlab.com API path stay refused, also for a listed repo.
  • The token is a group deploy token with only the read_package_registry scope. It reads packages in the GitLab connection’s Group or namespace, and in the subgroups below it. It cannot read other groups.
  • Your .npmrc can keep its _authToken=${NODE_AUTH_TOKEN} line. pnpm only warns when the variable is empty. The npm CLI fails when NODE_AUTH_TOKEN is unset, so set it to any placeholder for npm.

What it needs:

  • A workspace GitLab connection on gitlab.com with the Group or namespace set to the group that owns the packages.
  • A connection token with the api scope, and the Maintainer or Owner role on that group, so CodeHerder can create the deploy token.
  • Sandbox git tokens not set to off. Only the server mints this token. When it mints none, the relay refuses the read: a device never uses its own mint key or the token pool for it.
  • A device with a current egress proxy. An older proxy gives the stage no registry route, and installs behave as before.

Self-hosted GitLab hosts are not covered. Use a committed registry config and a secret variable for those.

A writable stage runs with a strict list of allowed hosts, so a private registry host can be blocked at first. When a stage tries it, CodeHerder can turn the block into a request that a person approves. See Network access approvals.

Browsers in container stages

  • Puppeteer uses the browser in the stage image. You need no setup. Don’t set executablePath.
  • Playwright with its own browser needs npx playwright install chromium once per task. The download comes through the device cache and stays for the rest of that task.
  • Playwright with channel: 'chrome' needs no install.

Don’t set PLAYWRIGHT_BROWSERS_PATH or PUPPETEER_EXECUTABLE_PATH. Both are refused.

Last updated

CodeHerder

Round up your herd.

Bring every human and every agent onto one table. Watch the work move. Costs update as it happens.

Try "pricing", "connect a device", or "who reviews the code"

↑↓ move · ↵ open · esc close