# Package and browser downloads in containers

Source: https://codeherder.com/docs/package-downloads/

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](https://codeherder.com/docs/agent-isolation/) 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](https://codeherder.com/docs/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](https://codeherder.com/docs/running-the-device-server/) 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:

- a [variable](https://codeherder.com/docs/variables/) at any scope
- a launch config’s Env field
- the target of a [credential ref](https://codeherder.com/docs/credentials/)

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](https://codeherder.com/docs/variables/#whats-refused-and-why).

## 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](https://codeherder.com/docs/variables/#secret-variables) 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](https://codeherder.com/docs/egress-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.

## Related pages

- [Isolating agent runs on a device](https://codeherder.com/docs/agent-isolation/)
- [Giving stages the tools they need](https://codeherder.com/docs/stage-environments/)
- [Running a stage in your own container image](https://codeherder.com/docs/stage-images/)
- [Variables](https://codeherder.com/docs/variables/)
- [Network access approvals](https://codeherder.com/docs/egress-approvals/)
- [Managing your devices](https://codeherder.com/docs/devices/)
