# Docker Hub Scraper: Images, Tags & Architectures (`arman-bd/docker-hub-scraper`) Actor

Scrape Docker Hub repositories for tags, image sizes, digests, supported architectures, OS and last-pushed dates. Track base-image freshness and platform support.

- **URL**: https://apify.com/arman-bd/docker-hub-scraper.md
- **Developed by:** [Arman Hossain](https://apify.com/arman-bd) (community)
- **Categories:** Developer tools, Automation, MCP servers
- **Stats:** 1 total users, 0 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.37 / 1,000 tag scrapeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Docker Hub Scraper: Images, Tags & Architectures

![Docker Hub Scraper: Every tag of any public Docker Hub repository, compressed size, digest, architectures and last-pushed date, straight from the v2 registry API](https://api.apify.com/v2/key-value-stores/ZQOcNAOHrIgTacAmy/records/docker-hub-scraper.jpg)

**Docker Hub Scraper** lists every tag of any public Docker Hub repository with its compressed size, manifest digest, supported architectures, operating systems and last-pushed date. Point it at `nginx` and you get 1,283 tags; point it at your own namespace and you get an inventory of what you ship.

It reads Docker Hub's own registry data, the canonical source, the same metadata `docker pull` resolves. **No browser, no proxy, no login, no Docker daemon.**

**Agent skill: [SKILL.md](https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/docker-hub-scraper.md)**

```
https://api.apify.com/v2/key-value-stores/t7YoTxpZEJOWvw4Ug/records/docker-hub-scraper.md
```

### What you get

| Output field | Meaning |
|---|---|
| `repository`, `namespace`, `name` | Normalised `namespace/repo`, split out for grouping |
| `tag` | Tag name, e.g. `1.30.4-trixie` |
| `image` | The pull reference you'd type, `nginx:stable` or `bitnami/postgresql:16` |
| `fullSize` | Compressed size in bytes of the tag's primary image |
| `digest` | The tag's manifest (or manifest-list) digest, the immutable identity |
| `architectures` | Runnable architectures, e.g. `["amd64","arm","arm64","386","ppc64le","riscv64","s390x"]` |
| `osList` | Operating systems, e.g. `["linux"]` |
| `variants` | ARM variants present, e.g. `["v5","v7","v8"]` |
| `platforms` | Per-platform detail: os, architecture, variant, size, status, lastPushed, and digest when `includeDigests` is on |
| `mediaType`, `contentType` | e.g. `application/vnd.oci.image.index.v1+json`, `image` |
| `tagStatus` | `active` or `inactive` |
| `lastUpdated`, `tagLastPushed`, `tagLastPulled` | Freshness signals, `tagLastPushed` is the one you want for staleness checks |
| `lastPusher` | Docker Hub username that last pushed the tag |
| `url` | Deep link to the tag on Docker Hub |
| `scrapedAt` | Run timestamp |

A `RUN_SUMMARY` record in the key-value store holds per-run counts, the filters used, any repository that failed, and the rate-limit budget observed on the last request.

### Common use cases

**1. Audit base-image freshness across your services.** Pull the tags you actually depend on and check `tagLastPushed` against your build date.

```json
{
 "repositories": ["nginx", "library/python", "library/redis", "bitnami/postgresql"],
 "maxTagsPerRepo": 50
}
```

**2. Verify arm64 support before migrating.** Anything that survives the filter has a real linux/arm64 image published.

```json
{
 "repositories": ["grafana/grafana", "prom/prometheus", "library/mysql"],
 "architectureFilter": ["arm64"],
 "maxTagsPerRepo": 100
}
```

**3. Track upstream release cadence.** Snapshot on a schedule and diff on `digest`, a changed digest for an unchanged tag means the upstream image was rebuilt under you.

```json
{
 "repositories": ["library/node", "library/golang"],
 "maxTagsPerRepo": 200,
 "includeDigests": true
}
```

### Quick start

Simplest possible run:

```json
{
 "repositories": ["nginx"]
}
```

Several images, arm64 only, small dataset:

```json
{
 "repositories": ["nginx", "bitnami/postgresql", "https://hub.docker.com/r/grafana/grafana"],
 "maxTagsPerRepo": 25,
 "architectureFilter": ["arm64"],
 "includeDigests": false
}
```

### Input

| Field | Type | Default | Notes |
|---|---|---|---|
| `repositories` | array | - | **Required.** `namespace/repo`, a bare official name, a Docker Hub URL, or a `repo:tag` pull reference. All normalise to `namespace/repo`. |
| `maxTagsPerRepo` | integer | `100` | Cap per repository, applied **after** `architectureFilter`. `0` = no limit. |
| `architectureFilter` | array | `[]` | Keep only tags publishing one of these architectures. Accepts `arm64` or `arm/v7`. Empty = all. |
| `includeDigests` | boolean | `true` | Include the sha256 digest of each per-platform image. The tag's own `digest` is always present. |

**Which combinations make sense.** `architectureFilter` runs before the cap, so `architectureFilter: ["riscv64"]` with `maxTagsPerRepo: 20` gives you 20 riscv64-capable tags rather than "whichever of the first 20 tags happened to be riscv64". That does mean a narrow filter on a large repository walks more pages, the Actor stops after 100 pages (10,000 tags) per repository and logs a warning if it gets there. Turn `includeDigests` off when you are only tracking sizes and platforms; it typically halves the dataset size on multi-arch images.

### Finding the right repository name

| What you see | What to pass |
|---|---|
| `docker pull nginx` | `nginx` → resolves to `library/nginx` |
| `docker pull bitnami/postgresql:16` | `bitnami/postgresql` (the `:16` is stripped for you) |
| `https://hub.docker.com/_/redis` | `redis` |
| `https://hub.docker.com/r/grafana/grafana` | `grafana/grafana` |

**The `library` namespace is the catch.** Docker's own official images have no namespace in everyday use, but the API insists on one: `the public API` returns HTTP 404, while `./library/nginx/tags` returns 200. This Actor adds `library/` for you whenever you pass a single-segment name, so you never have to think about it.

### Output example

```json
{
 "repository": "library/nginx",
 "namespace": "library",
 "name": "nginx",
 "tag": "stable-trixie",
 "image": "nginx:stable-trixie",
 "fullSize": 63093193,
 "digest": "sha256:46ccc48fbb1f5a43167f2ee2c279c122b96eec5d976e7f4e1e0780f59a51b4d6",
 "architectures": ["amd64", "arm", "arm64", "386", "ppc64le", "riscv64", "s390x"],
 "osList": ["linux"],
 "variants": ["v5", "v7", "v8"],
 "platforms": [
 {
 "os": "linux",
 "architecture": "amd64",
 "variant": null,
 "size": 63093193,
 "status": "active",
 "lastPushed": "2026-08-05T01:51:00.922797918Z",
 "digest": "sha256:9e4b33ead4c69e9845e53a1f175d0aa70a6cea62425b5d08957ba7eee5dc9b1d"
 },
 {
 "os": "linux",
 "architecture": "arm64",
 "variant": "v8",
 "size": 61387564,
 "status": "active",
 "lastPushed": "2026-08-05T01:51:03.228737506Z",
 "digest": "sha256:9105903bbaf495b6994795c85e415a9cd021381ebcdf9eb2d2c7f2d47172a0cc"
 }
 ],
 "mediaType": "application/vnd.oci.image.index.v1+json",
 "contentType": "image",
 "tagStatus": "active",
 "lastUpdated": "2026-08-05T19:52:15.690518Z",
 "lastPusher": "doijanky",
 "tagLastPushed": "2026-08-05T19:52:15.690518Z",
 "tagLastPulled": "2026-08-06T11:22:04.35119218Z",
 "url": "https://hub.docker.com/_/nginx?tab=tags&name=stable-trixie",
 "scrapedAt": "2026-08-06T12:00:00.000Z"
}
```

`RUN_SUMMARY`:

```json
{
 "repositoriesRequested": 2,
 "repositoriesFailed": 0,
 "failures": [],
 "tagsSaved": 150,
 "rateLimit": {
 "limit": 180,
 "remaining": 176,
 "resetsAt": "2026-08-06T12:10:52.000Z"
 },
 "filters": {
 "repositories": ["library/nginx", "bitnami/postgresql"],
 "maxTagsPerRepo": 100,
 "architectureFilter": [],
 "includeDigests": true
 },
 "finishedAt": "2026-08-06T12:00:03.881Z"
}
```

### Limits and behaviour

- **Anonymous requests share a per-IP budget.** Docker Hub advertises it as rate-limit headers`with a rolling reset; the Actor reads those headers on every response and puts the last observed values in`RUN_SUMMARY.rateLimit\`. One page carries 100 tags, so a 10-repository sweep at the default cap costs about 10 requests, comfortably inside the budget. If you do get a 429. It is retried with a longer backoff than other errors (5s, then 10s) rather than hammering.
- **Attestation manifests are excluded from the platform summary.** Docker Hub lists SBOM and provenance manifests in the same `images[]` array with `os` and `architecture` both set to `"unknown"`. They are not runnable platforms, so `architectures`, `osList`, `variants` and `platforms` cover real images only. This is why a nine-entry `images[]` array can yield five architectures.
- **`fullSize` is compressed, not on-disk.** It is the size of the layer blobs as stored in the registry. The unpacked image on a running host is larger, usually 2-3×.
- **A missing repository doesn't kill the run.** Unknown or private repositories return 404/401, are recorded in `RUN_SUMMARY.failures`, and the run continues. The Actor only errors out if *every* repository fails.
- **Partial results are kept.** If page 7 of a repository fails, the 600 tags already saved stay in the dataset and the failure is still reported.
- **Private repositories are not supported.** This Actor sends no credentials by design. Private repos are reported as failures, not silently skipped.
- **Public data only.** No authentication, no personal data, no access-control bypass.

### API example

```bash
curl -X POST "https://api.apify.com/v2/acts/arman-bd~docker-hub-scraper/run-sync-get-dataset-items?token=YOUR_TOKEN" \
 -H "Content-Type: application/json" \
 -d '{
 "repositories": ["nginx", "bitnami/postgresql"],
 "maxTagsPerRepo": 25,
 "architectureFilter": ["arm64"]
 }'
```

### JavaScript example

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

const client = new ApifyClient({ token: 'YOUR_TOKEN' });
const run = await client.actor('arman-bd/docker-hub-scraper').call({
 repositories: ['nginx', 'library/python'],
 maxTagsPerRepo: 50,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
const stale = items.filter((t) => Date.now() - Date.parse(t.tagLastPushed) > 90 * 864e5);
for (const t of stale) console.log(`${t.image}, last pushed ${t.tagLastPushed}`);
```

### FAQ

**Do I need a proxy?** No. Proxy configuration is not required to run this Actor.

**Do I need a Docker Hub account?** No. You supply no credentials.

**Why did `nginx` work but `mycompany/private-app` fail?** Private repositories need authentication, which this Actor deliberately does not do. It reports them in `RUN_SUMMARY.failures` and carries on.

**What happens if Docker Hub is unavailable?** 429s and 5xx errors are retried with backoff. If a repository still fails. It is recorded in `RUN_SUMMARY.failures` and the run continues with the rest.

**Can I schedule it?** Yes. Diffing `digest` per `repository`+`tag` across scheduled runs is the cleanest way to detect an upstream rebuild.

**Why do the architectures not match what `docker manifest inspect` shows?** They should, with one difference: this Actor omits the `unknown/unknown` attestation entries that `docker manifest inspect` also lists. Compare against the entries with a real platform.

**Does it include pull counts or stars?** No. Those live on the repository object, not the tags endpoint, and this Actor emits one row per tag.

**Can I integrate it with something else?** Yes, Apify API, client libraries, webhooks, scheduled runs, dataset exports (JSON/CSV/Excel) or MCP. Output is structured JSON.

# Actor input Schema

## `repositories` (type: `array`):

One image per entry as namespace/repo, e.g. bitnami/postgresql. Official images have no visible namespace. write 'nginx' and it resolves to library/nginx automatically. Docker Hub URLs (https://hub.docker.com/\_/nginx, https://hub.docker.com/r/bitnami/postgresql) and pull references with a :tag are accepted and normalised.

## `maxTagsPerRepo` (type: `integer`):

Cap the tags saved per repository after filtering. Tags come back newest-pushed first, so a small cap gives you the current releases. Popular images carry over a thousand tags. set 0 for no limit only if you really want all of them.

## `architectureFilter` (type: `array`):

Keep only tags that publish an image for at least one of these architectures. Use the values Docker itself reports (amd64, arm64, arm, 386, ppc64le, s390x, riscv64) or an arch/variant pair such as arm/v7. Leave empty to keep every tag.

## `includeDigests` (type: `boolean`):

Add the sha256 digest of each individual platform image inside the platforms array. The tag's own manifest digest is always included either way. Turn this off for a smaller dataset when you only care about sizes and architectures.

## Actor input object example

```json
{
  "repositories": [
    "nginx",
    "library/python",
    "https://hub.docker.com/r/grafana/grafana"
  ],
  "maxTagsPerRepo": 100,
  "architectureFilter": [
    "arm64",
    "amd64"
  ],
  "includeDigests": true
}
```

# Actor output Schema

## `items` (type: `string`):

Every record the run produced.

## `runsummary` (type: `string`):

The RUN_SUMMARY record from the run's key-value store.

# 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 = {
    "repositories": [
        "nginx",
        "bitnami/postgresql"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("arman-bd/docker-hub-scraper").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 = { "repositories": [
        "nginx",
        "bitnami/postgresql",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("arman-bd/docker-hub-scraper").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 '{
  "repositories": [
    "nginx",
    "bitnami/postgresql"
  ]
}' |
apify call arman-bd/docker-hub-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,arman-bd/docker-hub-scraper"
        }
    }
}
```

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/scemPksjv0QYHSUcO/builds/6IYBfmrlOgN9B7jvp/openapi.json
