# Container Registry Pull Reachability and Rate-Budget Ledger (`kingii98/container-registry-pull-reachability-and-rate-budget-ledger`) Actor

Pulls the manifest of every pinned image reference over HTTPS and reports whether the pull works now, why it would fail, which platforms the manifest holds, the TLS name-match verdict, and how much anonymous rate budget the registry has left. HTTP only, n

- **URL**: https://apify.com/kingii98/container-registry-pull-reachability-and-rate-budget-ledger.md
- **Developed by:** [kingii98](https://apify.com/kingii98) (community)
- **Categories:**
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 reachability run starts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Container Registry Pull Reachability and Rate-Budget Ledger

Your build pulls the same images every time. When one pull stops to work, every
build stops. You look at your own code first, then at the runner, and only at
the end at the registry. That search costs an hour.

This Actor holds the registry answer before you need it. It pulls the manifest
of every pinned reference over HTTPS and tells you three things:

1. **Would the pull work now?** Yes, or the exact reason for no.
2. **What changed since the last run?** The ledger holds the previous status
   and the previous digest of each reference.
3. **How much pull budget is left?** The Actor reads the `RateLimit-Limit` and
   `RateLimit-Remaining` headers of each registry host.

HTTP and TLS only. No browser. No proxy. No database. No container runtime.

### What it does for each reference

The Actor walks the same path as a container runtime.

| Step | Call | What it records |
| --- | --- | --- |
| 1 | `GET /v2/<repository>/manifests/<reference>` | The challenge, or the answer at once. |
| 2 | The token endpoint of the challenge | Is the token endpoint reachable? |
| 3 | The manifest call again, with the token | Status, digest, media type, rate budget. |
| 4 | The manifest body | The platform list of the image index. |

The Actor follows each redirect by hand. It records the host chain, and it
removes the `Authorization` header as soon as the host or the port changes, so
a token never reaches a content delivery network. A redirect to a scheme other
than HTTPS stops the probe with an `unreachable` verdict, because a cleartext
hop would show the token, or your registry password, to every device on the
path.

A **blocked** pull gets more work: one more attempt, and a TLS handshake
inspection of the host that failed. That inspection answers the question that
costs the most time in an incident:

> `tls: failed to verify certificate: x509: certificate is not valid for any
> names, but wanted to match docker-images-prod.<hash>.r2.cloudflarestorage.com`

The Actor reports the certificate subject, the subject alternative names and
one clear verdict: `match`, `mismatch` or `unknown`.

### Input

Give the Actor the reference list of your pipeline, or paste your Dockerfile.

```json
{
  "imageRefs": ["alpine:3.20", "ghcr.io/org/app:1.4.2"],
  "dockerfile": "FROM golang:1.23 AS build\nFROM alpine:3.20\n",
  "expectedPlatform": "linux/amd64",
  "failOn": ["unreachable", "unauthorized", "tls_name_mismatch"]
}
```

| Field | Meaning |
| --- | --- |
| `imageRefs` | 1 to 100 references. A reference without a host goes to Docker Hub. |
| `dockerfile` | Optional. The Actor reads the image of each `FROM` line. |
| `expectedPlatform` | Optional, for example `linux/amd64`. |
| `failOn` | The failure classes that make the run verdict `fail`. |
| `registryToken`, `registryUsername`, `registryPassword` | Optional credentials for a private repository that you own. |
| `credentialHosts` | The hosts that may receive those credentials. |
| `ledgerStoreName` | The named key-value store that holds the ledger. |
| `concurrency`, `timeoutSeconds`, `maxRedirects`, `maxResponseBytes` | The bounds of the run. |

Every required field has a default, so a run with the empty input `{}` works.

The Actor reads the Dockerfile carefully. It skips `scratch`, it skips a `FROM`
line that points at an earlier build stage, and it skips a line that still holds
an unexpanded `ARG`.

#### Private repositories

Put your token in `registryToken`, or your user name and password in
`registryUsername` and `registryPassword`. Then name the host in
`credentialHosts`, for example `["ghcr.io"]`. The Actor sends the credentials
only to the token endpoint of a named host. An empty `credentialHosts` list
sends them to every host in your reference list, so fill the list in when the
list mixes a private registry with public ones.

### Failure classes

| Class | Meaning |
| --- | --- |
| `ok` | The pull works now. |
| `unreachable` | The registry or the token endpoint does not answer. |
| `unauthorized` | The registry refuses the credentials. |
| `rate_limited` | The registry answered 429, or it reports no pull budget left. |
| `not_found` | The repository or the tag does not exist. |
| `server_error` | The registry answers 5xx. |
| `tls_name_mismatch` | The certificate is valid, but not for the host that was asked. |
| `tls_error` | Another TLS or certificate chain fault. |
| `platform_missing` | The manifest list does not hold `expectedPlatform`. |
| `invalid_reference` | The text is not a reference, or it points at a private address. |

A registry that answers a status with no better class gets `unreachable`, and
the detail field names the status.

### Output

Three record types go to the dataset.

| Record type | Count | Holds |
| --- | --- | --- |
| `reference` | One for each reference | Host, repository, status, digest, media type, platforms, redirect chain, TLS verdict, response time, failure class, and the change since the last run. |
| `rate-budget` | One for each registry host | `RateLimit-Limit`, `RateLimit-Remaining`, the time to reset, and the use since the last run. |
| `run-summary` | One | The `pass` or `fail` verdict against `failOn`, the counts, and the list of references that changed. |

#### The verdict never fails the run

A blocked pull is the answer that you paid for. The Actor writes the verdict
into the `run-summary` record and into the run status message, and the Apify
run ends **SUCCEEDED**. A FAILED run means a malfunction of the Actor, nothing
else. Read `verdict` in the summary record, or read the status message, to gate
your pipeline.

#### The ledger

The Actor keeps one record for each reference and a short rate-budget history
for each host in a **named** key-value store. Every run with the same
`ledgerStoreName` compares against the same ledger. This gives you the sentence
that a single run cannot give:

> `This reference became "tls_name_mismatch" since the last run.`

The first run writes the ledger and reports the current status only.

### How to use it

1. Put your pipeline reference list into `imageRefs`.
2. Add an Apify schedule. One run each hour is the usual cadence. You can also
   start the Actor before each deployment.
3. Read `verdict` of the `run-summary` record, or read the status message.

<a id="pricing"></a>

### Pricing

The Actor uses pay per event.

| Event | Unit | Price | When |
| --- | --- | --- | --- |
| `reachability-run-start` | run | 0.02 | Once for each run, after the input is read. |
| `image-ref-probed` | image reference probed | 0.006 | Once for each reference that the Actor called a registry for. |
| `blocked-pull-diagnosed` | blocked reference | 0.02 | Once for each reference that stays blocked after the second attempt and the TLS trace. |

Two references are **not** charged as a probe: a reference that cannot be
parsed, and a reference that points at a private or reserved address. The
Actor makes no call for them. A reference that fails once and works on the
second attempt is not charged as a diagnosis, because you receive a working
pull.

The default cap `maxTotalChargeUsd` is 8.00 for one run. A run over 100
references costs at most 0.02 + 0.60 + 2.00 = 2.62, so the cap holds the worst
case, when a whole registry is down and every reference takes the expensive
path.

#### What one month costs

20 healthy references, one run each hour, 30 days:

| Event | Count | Price | Cost |
| --- | --- | --- | --- |
| `reachability-run-start` | 24 runs x 30 days = 720 | 0.02 | 14.40 |
| `image-ref-probed` | 20 refs x 24 runs x 30 days = 14400 | 0.006 | 86.40 |
| `blocked-pull-diagnosed` | 0 blocked = 0 | 0.02 | 0.00 |
| **Total** | | | **100.80** |

The cadence drives the bill, not the reference count alone. Select the schedule
that your pipeline needs:

| Schedule | Runs each day | 20 references | For each reference |
| --- | --- | --- | --- |
| Every hour | 24 | 100.80 | 5.04 |
| Every 6 hours | 4 | 16.80 | 0.84 |
| Once each day | 1 | 4.20 | 0.21 |

A blocked reference adds 0.02 for each run while it stays blocked.

### Limits and rules

- The Actor refuses loopback, private, link-local and reserved addresses, for
  the registry host and for every redirect hop.
- Only HTTPS, for the first call and for every redirect hop. A reference
  carries no URL scheme.
- 1 to 100 references for each run.
- The Actor reads at most `maxResponseBytes` from one answer.
- The Actor does not download image layers. It reads the manifest, and it reads
  the image config blob only when you set `expectedPlatform` and the reference
  resolves to a single-platform manifest.

### Develop

```bash
uv sync
uv run pytest
uv run ruff check .
```

# Actor input Schema

## `imageRefs` (type: `array`):

1 to 100 pinned image references, for example "node:22-alpine", "ghcr.io/org/app:1.4.2" or "alpine@sha256:...". A reference without a registry host goes to Docker Hub. Paste your pipeline list here.

## `dockerfile` (type: `string`):

Optional. Paste a Dockerfile and the Actor reads the image of every FROM line. It skips "scratch", it skips a FROM line that points at an earlier build stage, and it skips a line that holds an unresolved ARG. The result joins the "Image references" list.

## `expectedPlatform` (type: `string`):

Optional, for example "linux/amd64" or "linux/arm64/v8". The Actor then checks that the manifest list of each reference holds this platform, and classifies a reference that misses it as "platform\_missing". Leave it empty to skip the check.

## `failOn` (type: `array`):

The run verdict is "fail" when any reference carries one of these failure classes. The verdict is a dataset record and a status message. The Actor run itself always ends SUCCEEDED, because a blocked pull is an answer and not a malfunction.

## `registryToken` (type: `string`):

Optional. A bearer token or a personal access token for a private repository that you own. The Actor sends it to the token endpoint of the registry as HTTP basic credentials, and it never writes it into the dataset or the log.

## `registryUsername` (type: `string`):

Optional. The user name for a private repository that you own. Use it together with the registry password.

## `registryPassword` (type: `string`):

Optional. The password or access token that belongs to the registry user name.

## `credentialHosts` (type: `array`):

The registry hosts that may receive the credentials above, for example "ghcr.io". An empty list sends the credentials to every host in the reference list. Fill it in when your list mixes a private registry with public ones.

## `ledgerStoreName` (type: `string`):

The named Apify key-value store that holds the previous status and digest of each reference plus a short rate-budget history for each registry host. Every run that uses the same name compares against the same ledger.

## `concurrency` (type: `integer`):

How many references the Actor probes at the same time.

## `timeoutSeconds` (type: `integer`):

Timeout in seconds for one registry request. 1 to 60.

## `maxRedirects` (type: `integer`):

The Actor follows the redirects itself so that it can record the host chain. A chain that passes this bound is a failed pull.

## `maxResponseBytes` (type: `integer`):

Hard cap on the bytes that the Actor reads from one registry answer. A manifest is small, so the default is generous.

## `budgetHistoryLength` (type: `integer`):

How many earlier rate-budget readings the ledger keeps for each registry host.

## Actor input object example

```json
{
  "imageRefs": [
    "alpine:3.20",
    "ghcr.io/astral-sh/uv:latest",
    "mcr.microsoft.com/dotnet/runtime:8.0"
  ],
  "dockerfile": "",
  "expectedPlatform": "linux/amd64",
  "failOn": [
    "unreachable",
    "unauthorized",
    "tls_name_mismatch",
    "tls_error",
    "not_found"
  ],
  "credentialHosts": [],
  "ledgerStoreName": "registry-pull-ledger",
  "concurrency": 5,
  "timeoutSeconds": 20,
  "maxRedirects": 5,
  "maxResponseBytes": 262144,
  "budgetHistoryLength": 12
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "imageRefs": [
        "alpine:3.20",
        "ghcr.io/astral-sh/uv:latest",
        "mcr.microsoft.com/dotnet/runtime:8.0"
    ],
    "dockerfile": "",
    "expectedPlatform": "linux/amd64",
    "failOn": [
        "unreachable",
        "unauthorized",
        "tls_name_mismatch",
        "tls_error",
        "not_found"
    ],
    "credentialHosts": [],
    "ledgerStoreName": "registry-pull-ledger",
    "concurrency": 5,
    "timeoutSeconds": 20,
    "maxRedirects": 5,
    "maxResponseBytes": 262144,
    "budgetHistoryLength": 12
};

// Run the Actor and wait for it to finish
const run = await client.actor("kingii98/container-registry-pull-reachability-and-rate-budget-ledger").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "imageRefs": [
        "alpine:3.20",
        "ghcr.io/astral-sh/uv:latest",
        "mcr.microsoft.com/dotnet/runtime:8.0",
    ],
    "dockerfile": "",
    "expectedPlatform": "linux/amd64",
    "failOn": [
        "unreachable",
        "unauthorized",
        "tls_name_mismatch",
        "tls_error",
        "not_found",
    ],
    "credentialHosts": [],
    "ledgerStoreName": "registry-pull-ledger",
    "concurrency": 5,
    "timeoutSeconds": 20,
    "maxRedirects": 5,
    "maxResponseBytes": 262144,
    "budgetHistoryLength": 12,
}

# Run the Actor and wait for it to finish
run = client.actor("kingii98/container-registry-pull-reachability-and-rate-budget-ledger").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "imageRefs": [
    "alpine:3.20",
    "ghcr.io/astral-sh/uv:latest",
    "mcr.microsoft.com/dotnet/runtime:8.0"
  ],
  "dockerfile": "",
  "expectedPlatform": "linux/amd64",
  "failOn": [
    "unreachable",
    "unauthorized",
    "tls_name_mismatch",
    "tls_error",
    "not_found"
  ],
  "credentialHosts": [],
  "ledgerStoreName": "registry-pull-ledger",
  "concurrency": 5,
  "timeoutSeconds": 20,
  "maxRedirects": 5,
  "maxResponseBytes": 262144,
  "budgetHistoryLength": 12
}' |
apify call kingii98/container-registry-pull-reachability-and-rate-budget-ledger --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kingii98/container-registry-pull-reachability-and-rate-budget-ledger"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WEjMjB13TJQeSgJqz/builds/3E1xaxdBuWv5jSkk9/openapi.json
