Docker Registry HTTP API V2とは?
コンテナイメージリポジトリには様々な種類があります。最もよく聞くのは老舗のDocker Hub、他にはQuay.ioやGitHub Container Registry(ghcr)などもあります。
これらはそれぞれ別の実装ですが、これらで共通して利用できるが、Docker Registry HTTP API V2です。
仕様についてはDocer社のページにまとまっています
タグの一覧を取得する
上記文書によるとタグの取得はGET /v2/<name>/tags/list
でできそうです。
このAPIはページネーションをサポートしておりn
パラメータと、last
パラメータを使うことで複数ページの結果を取得できます。
というのが、文書にある内容なのですが、意外とこれが、各コンテナレジストリで違うふるまいをしたので、メモ的に状況を紹介します。
skopeo
各コンテナレジストリのAPIの振る舞いを観察するために、skopeo実行時のログを活用することにしました。このツールはPodmanの開発コミュニティを中心にメンテされているもののようです。
skopeoはコンテナイメージレジストリの操作を行うためのCLIツールで、コンテナレジストリの違いを意識せずに、操作するためのものです。
引数に--debug
をつけると、詳細なログを出しつつ動いてくれるので、この動作を追うことで各コンテナレジストリの実装について動作を確かめてみました。
ログからわからない動作については、以下のライブラリのソースコードを読むことも解析の助けになりました。
DockerHub
まずはskopeoでの実行をしてみます
$ docker run --rm quay.io/skopeo/stable:v1.9.2 list-tags --debug docker://ubuntu
time="2023-03-28T01:47:19Z" level=debug msg="Using registries.d directory /etc/containers/registries.d"
time="2023-03-28T01:47:19Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf\""
time="2023-03-28T01:47:19Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf.d/000-shortnames.conf\""
time="2023-03-28T01:47:19Z" level=debug msg="No credentials matching docker.io/library/ubuntu found in /tmp/auth.json"
time="2023-03-28T01:47:19Z" level=debug msg="No credentials matching docker.io/library/ubuntu found in /root/.config/containers/auth.json"
time="2023-03-28T01:47:19Z" level=debug msg="No credentials matching docker.io/library/ubuntu found in /root/.docker/config.json"
time="2023-03-28T01:47:19Z" level=debug msg="No credentials matching docker.io/library/ubuntu found in /root/.dockercfg"
time="2023-03-28T01:47:19Z" level=debug msg="No credentials for docker.io/library/ubuntu found"
time="2023-03-28T01:47:19Z" level=debug msg=" Lookaside configuration: using \"default-docker\" configuration"
time="2023-03-28T01:47:19Z" level=debug msg=" No signature storage configuration found for docker.io/library/ubuntu:latest, using built-in default file:///var/lib/containers/sigstore"
time="2023-03-28T01:47:19Z" level=debug msg="Looking for TLS certificates and private keys in /etc/docker/certs.d/docker.io"
time="2023-03-28T01:47:19Z" level=debug msg=" Sigstore attachments: using \"default-docker\" configuration"
time="2023-03-28T01:47:19Z" level=debug msg="GET https://registry-1.docker.io/v2/"
time="2023-03-28T01:47:19Z" level=debug msg="Ping https://registry-1.docker.io/v2/ status 401"
time="2023-03-28T01:47:19Z" level=debug msg="GET https://auth.docker.io/token?scope=repository%3Alibrary%2Fubuntu%3Apull&service=registry.docker.io"
time="2023-03-28T01:47:20Z" level=debug msg="GET https://registry-1.docker.io/v2/library/ubuntu/tags/list"
{
"Repository": "docker.io/library/ubuntu",
"Tags": [
"10.04",
"12.04",
"12.04.5",
...jsonでタグが列挙されている...
DockerHubのリポジトリのイメージはドメイン名なしでubuntu:latest
のように指定できます。
このURLは docker.io/library/ubuntu
と解釈され、そのイメージはhttps://registry-1.docker.io/v2/library/ubuntu
にあると解釈されるようです。
(この部分のロジックは実装にベタ書きされているように見えました)
まず以下にアクセスすると401が返却されます。
$ curl -v "https://registry-1.docker.io/v2/"
...
< HTTP/1.1 401 Unauthorized
< content-type: application/json
< docker-distribution-api-version: registry/2.0
< www-authenticate: Bearer realm="https://auth.docker.io/token",service="registry.docker.io"
< date: Tue, 28 Mar 2023 01:40:10 GMT
< content-length: 87
< strict-transport-security: max-age=31536000
<
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":null}]}
公開リポジトリであってもこのエラーが返るようなので認証が必要そうです。
といっても、ログインをする必要はなさそうです。
認証のための情報は www-authenticate
ヘッダに記載されています。
この情報を頼りにアクセスのためのトークンを取得します。
https://auth.docker.io/token?scope=repository%3Alibrary%2Fubuntu%3Apull&service=registry.docker.io
このURLをよく見てみると、以下のようにアクセスしたいリソースを指定しているようです。
- scope=repository/library/ubuntu:pull
- これはこれからアクセスしようとするリポジトリから類推している?
- service=registry.docker.io
- これは上記www-authenticateヘッダから取得したもの
この仕様を探すと・・以下の文書が見つかりました
Docker Registry v2 authentication via central service
ということで、これもDocker Registry HTTP API V2にアクセスするための仕様の一部のようです。
ということでアクセスしてみます。出力はjsonなのでjqで見やすく加工します。
$ curl "https://auth.docker.io/token?scope=repository%3Alibrary%2Fubuntu%3Apull&service=registry.docker.io"| jq -r
{
"token": ....
"access_token: ...
"expires_in: 300,
"issued_at": ...
}
それっぽいトークンを得ることが出来ました。必要に応じてユーザ認証情報も渡す必要があるようでしたが、publicなイメージの場合は不要のようでした。
token
とaccess_token
は同じトークンがセットされていました。
トークンが得られたので、それを使ってDocker Registry HTTP API V2のAPIを利用してみます。
ちなみにこのトークンは300sで有効期限が切れてしまうので注意してください。(上記のexpires_in
)
$ curl -H "Authorization: Bearer <上記のtoken>" "https://registry-1.docker.io/v2/library/ubuntu/tags/list"
{"name":"library/ubuntu","tags":["10.04","12.04","12.04.5","12.10","13.04",...
無事にタグの一覧を取得出来ました。
ちなみにここでn
を指定すると取得する個数を制限でき、無指定だとすべてのタグが取得できました。
$ curl -v -H "Authorization: Bearer <上記のaccess_token>" "https://registry-1.docker.io/v2/library/ubuntu/tags/list?n=2"
...
< HTTP/1.1 200 OK
< content-type: application/json
< docker-distribution-api-version: registry/2.0
< link: </v2/library/ubuntu/tags/list?last=12.04&n=2>; rel="next"
< date: Tue, 28 Mar 2023 05:24:22 GMT
< content-length: 51
< strict-transport-security: max-age=31536000
<
{ [51 bytes data]
100 51 100 51 0 0 64 0 --:--:-- --:--:-- --:--:-- 64
* Connection #0 to host registry-1.docker.io left intact
* Closing connection 0
{
"name": "library/ubuntu",
"tags": [
"10.04",
"12.04"
]
}
さらにn
と一緒にlast
に前回リクエストの最後のtagの値を指定することで、次のページを取得することが出来ました。このアクセス方法は上記レスポンスのlink
ヘッダに記載されているものです。
$ curl -v -H "Authorization: Bearer <上記のaccess_token>" "https://registry-1.docker.io/v2/library/ubuntu/tags/list?last=12.04&n=2"
...
< HTTP/1.1 200 OK
< content-type: application/json
< docker-distribution-api-version: registry/2.0
< link: </v2/library/ubuntu/tags/list?last=12.10&n=2>; rel="next"
< date: Tue, 28 Mar 2023 05:25:52 GMT
< content-length: 53
< strict-transport-security: max-age=31536000
<
{ [53 bytes data]
100 53 100 53 0 0 62 0 --:--:-- --:--:-- --:--:-- 62
* Connection #0 to host registry-1.docker.io left intact
* Closing connection 0
{
"name": "library/ubuntu",
"tags": [
"12.04.5",
"12.10"
]
}
Docker Hubの独自API
Docker Registry HTTP API V2とは別にDocker HubのAPIというものもあります
https://registry.hub.docker.com/v2/repositories
がエントリポイントのようです。
Docker Registry HTTP API V2とはドメインが違います。
GitHub Container Registry (ghcr.io)
DockerHubと同じか?、、と思うとちょっと振る舞いが違いました。
$ docker run --rm quay.io/skopeo/stable:v1.9.2 list-tags --debug docker://ghcr.io/linuxserver/hedgedoc
time="2023-03-28T05:28:58Z" level=debug msg="Using registries.d directory /etc/containers/registries.d"
time="2023-03-28T05:28:58Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf\""
time="2023-03-28T05:28:58Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf.d/000-shortnames.conf\""
time="2023-03-28T05:28:58Z" level=debug msg="No credentials matching ghcr.io/linuxserver/hedgedoc found in /tmp/auth.json"
time="2023-03-28T05:28:58Z" level=debug msg="No credentials matching ghcr.io/linuxserver/hedgedoc found in /root/.config/containers/auth.json"
time="2023-03-28T05:28:58Z" level=debug msg="No credentials matching ghcr.io/linuxserver/hedgedoc found in /root/.docker/config.json"
time="2023-03-28T05:28:58Z" level=debug msg="No credentials matching ghcr.io/linuxserver/hedgedoc found in /root/.dockercfg"
time="2023-03-28T05:28:58Z" level=debug msg="No credentials for ghcr.io/linuxserver/hedgedoc found"
time="2023-03-28T05:28:58Z" level=debug msg=" Lookaside configuration: using \"default-docker\" configuration"
time="2023-03-28T05:28:58Z" level=debug msg=" No signature storage configuration found for ghcr.io/linuxserver/hedgedoc:latest, using built-in default file:///var/lib/containers/sigstore"
time="2023-03-28T05:28:58Z" level=debug msg="Looking for TLS certificates and private keys in /etc/docker/certs.d/ghcr.io"
time="2023-03-28T05:28:58Z" level=debug msg=" Sigstore attachments: using \"default-docker\" configuration"
time="2023-03-28T05:28:58Z" level=debug msg="GET https://ghcr.io/v2/"
time="2023-03-28T05:28:58Z" level=debug msg="Ping https://ghcr.io/v2/ status 401"
time="2023-03-28T05:28:58Z" level=debug msg="GET https://ghcr.io/token?scope=repository%3Alinuxserver%2Fhedgedoc%3Apull&service=ghcr.io"
time="2023-03-28T05:28:58Z" level=debug msg="Increasing token expiration to: 60 seconds"
time="2023-03-28T05:28:58Z" level=debug msg="GET https://ghcr.io/v2/linuxserver/hedgedoc/tags/list"
time="2023-03-28T05:28:59Z" level=debug msg="GET https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=amd64-version-1.8.1&n=0"
time="2023-03-28T05:28:59Z" level=debug msg="GET https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=arm32v7-1.9.1&n=0"
time="2023-03-28T05:28:59Z" level=debug msg="GET https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=1.9.3-ls56&n=0"
time="2023-03-28T05:28:59Z" level=debug msg="GET https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=1.9.5-ls73&n=0"
{
"Repository": "ghcr.io/linuxserver/hedgedoc",
"Tags": [
"amd64-1.7.0-ls1",
"arm32v7-1.7.0-ls1",
"arm64v8-1.7.0-ls1",
"amd64-version-1.7.0",
認証情報が必要か確認する
まず https://ghcr.io/v2/
にアクセスし、401をもらいます
HTTPのレスポンスヘッダの情報を参照すると・・
www-authenticate: Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull"
DockerHubの場合と違いscopeの記載があります
アクセス用のトークンの取得
次のURLにアクセスしトークンを取得します https://ghcr.io/token?scope=repository%3Alinuxserver%2Fhedgedoc%3Apull&service=ghcr.io
- scope
- repository/linuxserver/hedgedoc:pull
- 上記で得たscopeを使わず、リポジトリ単位のスコープを取得している
- service
- ghcr.io
curl "https://ghcr.io/token?scope=repository%3Alinuxserver%2Fhedgedoc%3Apull&service=ghcr.io" |jq
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 73 100 73 0 0 264 0 --:--:-- --:--:-- --:--:-- 263
{
"token": ...
}
- DockerHubのものとはレスポンスが違い、こちらはtokenというキーのみが入っています
- Docker Registry v2 authentication via central serviceの仕様を見ると
access_token
など他のパラメータはoptionalのようなので、これも仕様どおりの振る舞いです
この後Increasing token expiration to: 60 seconds
というログが出ており、トークンの使用期限を伸ばしているようでしたが、この操作をせずとも次のコマンドが動作したので、深く動作を追っていません。
取得したトークンを使いタグ一覧を取得する
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list
を呼び出してタグの一覧を取得します。
$ curl -H "Authorization: Bearer djE6bGludXhzZXJ2ZXIvaGVkZ2Vkb2M6MTY3OTk4MTkzNDQwODE1MzI0OA==" -v "https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?n=0"|jq
...
> GET /v2/linuxserver/hedgedoc/tags/list?n=0 HTTP/2
> Host: ghcr.io
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: Bearer djE6bGludXhzZXJ2ZXIvaGVkZ2Vkb2M6MTY3OTk4MTkzNDQwODE1MzI0OA==
>
* Connection state changed (MAX_CONCURRENT_STREAMS == 100)!
< HTTP/2 200
< content-type: application/json
< docker-distribution-api-version: registry/2.0
< link: </v2/linuxserver/hedgedoc/tags/list?last=amd64-version-1.8.1&n=0>; rel="next"
< date: Tue, 28 Mar 2023 05:41:54 GMT
< content-length: 1908
< x-github-request-id: 0539:3FEC:59AD6:B0ACD:64227E21
<
{ [1119 bytes data]
100 1908 100 1908 0 0 5695 0 --:--:-- --:--:-- --:--:-- 5695
* Connection #0 to host ghcr.io left intact
* Closing connection 0
{
"name": "linuxserver/hedgedoc",
"tags": [
"amd64-1.7.0-ls1",
"arm32v7-1.7.0-ls1",
"arm64v8-1.7.0-ls1",
n
パラメータの指定がないときは100件のようです。
また、レスポンスヘッダのlink
の指定を見るとn=0
というパラメータが付いており、これはnの指定がないものと同じ意味のようでした。(おそらくn=100
でも同じ)
若干不気味ですが、次々とlinkヘッダが続く限りアクセスすることで、すべてのタグを取得しています。
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=amd64-version-1.8.1&n=0
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=arm32v7-1.9.1&n=0
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=1.9.3-ls56&n=0
https://ghcr.io/v2/linuxserver/hedgedoc/tags/list?last=1.9.5-ls73&n=0
GitHub Container Registryの独自API
GitHub Container RegistryはGitHubのPackagesという枠組みの機能の1つであるため、GitHubのpackagesのAPIで操作することが可能です。
エントリポイントはhttps://api.github.com/orgs/<ORG>/packages
のようです。
Docker Registry HTTP API V2とはドメインが違います。
Quay.io
前2つと同じか・・?と思いきやこれまた少し挙動が違います。
$ docker run --rm quay.io/skopeo/stable:v1.9.2 list-tags --debug docker://quay.io/ansible/creator-ee
time="2023-03-28T05:48:29Z" level=debug msg="Using registries.d directory /etc/containers/registries.d"
time="2023-03-28T05:48:29Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf\""
time="2023-03-28T05:48:29Z" level=debug msg="Loading registries configuration \"/etc/containers/registries.conf.d/000-shortnames.conf\""
time="2023-03-28T05:48:29Z" level=debug msg="No credentials matching quay.io/ansible/creator-ee found in /tmp/auth.json"
time="2023-03-28T05:48:29Z" level=debug msg="No credentials matching quay.io/ansible/creator-ee found in /root/.config/containers/auth.json"
time="2023-03-28T05:48:29Z" level=debug msg="No credentials matching quay.io/ansible/creator-ee found in /root/.docker/config.json"
time="2023-03-28T05:48:29Z" level=debug msg="No credentials matching quay.io/ansible/creator-ee found in /root/.dockercfg"
time="2023-03-28T05:48:29Z" level=debug msg="No credentials for quay.io/ansible/creator-ee found"
time="2023-03-28T05:48:29Z" level=debug msg=" Lookaside configuration: using \"default-docker\" configuration"
time="2023-03-28T05:48:29Z" level=debug msg=" No signature storage configuration found for quay.io/ansible/creator-ee:latest, using built-in default file:///var/lib/containers/sigstore"
time="2023-03-28T05:48:29Z" level=debug msg="Looking for TLS certificates and private keys in /etc/docker/certs.d/quay.io"
time="2023-03-28T05:48:29Z" level=debug msg=" Sigstore attachments: using \"default-docker\" configuration"
time="2023-03-28T05:48:29Z" level=debug msg="GET https://quay.io/v2/"
time="2023-03-28T05:48:30Z" level=debug msg="Ping https://quay.io/v2/ status 401"
time="2023-03-28T05:48:30Z" level=debug msg="GET https://quay.io/v2/auth?scope=repository%3Aansible%2Fcreator-ee%3Apull&service=quay.io"
time="2023-03-28T05:48:31Z" level=debug msg="Increasing token expiration to: 60 seconds"
time="2023-03-28T05:48:31Z" level=debug msg="GET https://quay.io/v2/ansible/creator-ee/tags/list"
time="2023-03-28T05:48:32Z" level=debug msg="GET https://quay.io/v2/ansible/creator-ee/tags/list?n=50&next_page=<謎のハッシュ値>"
{
"Repository": "quay.io/ansible/creator-ee",
"Tags": [
"8f2ef3e55d91258eca4eaef9222505bd401ff86d-1473632019-15-1",
"v0.2.0a1",
認証情報が必要か確認する
$ curl -v https://quay.io/v2/
...
< HTTP/2 401
< date: Tue, 28 Mar 2023 05:49:53 GMT
< content-type: text/html; charset=utf-8
< content-length: 4
< server: nginx/1.20.1
< www-authenticate: Bearer realm="https://quay.io/v2/auth",service="quay.io"
< docker-distribution-api-version: registry/2.0
<
* Connection #0 to host quay.io left intact
true* Closing connection 0
他と同様に401が返却され、認証に必要な情報が返却されました。
こちらはDockerHubと同様にscopeの情報は有りません
認証トークンの取得
$ curl -v "https://quay.io/v2/auth?scope=repository%3Aansible%2Fcreator-ee%3Apull&service=quay.io"|jq
...
< HTTP/2 200
< date: Tue, 28 Mar 2023 05:51:37 GMT
< content-type: application/json
< content-length: 841
< server: nginx/1.20.1
< cache-control: no-cache, no-store, must-revalidate
< x-frame-options: DENY
< strict-transport-security: max-age=63072000; preload
<
{ [841 bytes data]
100 841 100 841 0 0 1086 0 --:--:-- --:--:-- --:--:-- 1085
* Connection #0 to host quay.io left intact
* Closing connection 0
{
"token": "..."
}
- scope
- repository/ansible.creator-ee:pull
- service
- quay.io
返却されるjsonのキーはtokenのみで、これはGitHub Container Registryと同じような振る舞いです。
この後Increasing token expiration to: 60 seconds
というログが出ており、トークンの使用期限を伸ばしているようでしたが、この操作をせずとも次のコマンドが動作したので、深く動作を追っていません。
取得したトークンを使いタグ一覧を取得する
$ curl -H "Authorization: Bearer <上記で取得したtoken>" -v "https://quay.io/v2/ansible/creator-ee/tags/list"|jq
...
< server: nginx/1.20.1
< link: </v2/ansible/creator-ee/tags/list?n=50&next_page=gAAAAABkIoEP_XSQJENT2gJCiC_jnL4WZFxUQv2mDs7WRBF_21b-Lg5erskE1MWMUmfv5fdCP3o8JqmQEMhmlylx8edelVqeND299G2ZvCxaCPh5McC-0Xg%3D>; rel="next"
< x-frame-options: DENY
< strict-transport-security: max-age=63072000; preload
<
{ [1885 bytes data]
100 1885 100 1885 0 0 2026 0 --:--:-- --:--:-- --:--:-- 2024
* Connection #0 to host quay.io left intact
* Closing connection 0
{
"name": "ansible/creator-ee",
"tags": [
"8f2ef3e55d91258eca4eaef9222505bd401ff86d-1473632019-15-1",
"v0.2.0a1",
"8f2ef3e55d91258eca4eaef9222505bd401ff86d-1477382381-16-1",
"8f2ef3e55d91258eca4eaef9222505bd401ff86d",
"v0.2.0",
"67b9e06a5462acd665a9260f5082f39f07fc2ef2-1968203497-17-1",
"67b9e06a5462acd665a9260f5082f39f07fc2ef2",
"v0.3.0",
"52d2ac2718da1fe739a657e73a6094da18227ecb-2005081881-18-1",
"52d2ac2718da1fe739a657e73a6094da18227ecb",
"v0.3.1",
...
デフォルトの件数は50件のようで、n
を増やしても50件以上は取得できませんでした。
link
ヘッダの値を利用して次のページを取得するのですが、ここの値の様子が他のコンテナレジストリと大きく違いました。n
パラメータは50で、これは違和感がありませんが、last
ではなくnext_page
というパラメータ名がついており、その値も最後のtagの名前ではなく謎のハッシュ値となっています。
まぁlink
ヘッダの中身をそのままリクエストすれば次のページの情報が得られるというのは同じなので、そういう処理を書いていれば問題はないのですが、ちょっと不思議な実装です。
ということで以下のURLで次ページを呼び出していました。
https://quay.io/v2/ansible/creator-ee/tags/list?n=50&next_page=<謎のハッシュ値>
Quayの独自API
Quayの独自のAPIもあります
https://quay.io/api/v1/
がエンドポイントです。
(Docker Registry HTTP API V2がhttps://quay.io/v2
なので、並んでいるとややこしいですが、独立したAPIです)
実は認証情報は不要
色々試していて気づいたのですが、実はQuay.ioは公開リポジトリに関しては認証トークンなしでもアクセスできました。
$ curl -v "https://quay.io/v2/ansible/creator-ee/tags/list"|jq
...
< HTTP/2 200
< date: Tue, 28 Mar 2023 06:03:47 GMT
< content-type: application/json
< content-length: 1885
< server: nginx/1.20.1
< link: </v2/ansible/creator-ee/tags/list?n=50&next_page=gAAAAABkIoNDsbasB-22vQs84yeAQtwR_OWxNscrx43fnYYUZ7T5lN0lGdy0xGzBU8P_mAVNwTHYVvRpjKmvkKQPcgRYUrOLNNKL9JCieJrtwBlUA-Dt5EM%3D>; rel="next"
< x-frame-options: DENY
< strict-transport-security: max-age=63072000; preload
<
{ [1885 bytes data]
100 1885 100 1885 0 0 2607 0 --:--:-- --:--:-- --:--:-- 2607
* Connection #0 to host quay.io left intact
* Closing connection 0
{
"name": "ansible/creator-ee",
"tags": [
"8f2ef3e55d91258eca4eaef9222505bd401ff86d-1473632019-15-1",
"v0.2.0a1",
"8f2ef3e55d91258eca4eaef9222505bd401ff86d-1477382381-16-1",
"8f2ef3e55d91258eca4eaef9222505bd401ff86d",
"v0.2.0",
...
まとめ
コンテナレジストリの種類を問わず利用できるDocker Registry HTTP API V2は便利ですが、コンテナレジストリごとに微妙に振る舞いが違うので、そのあたりを考慮して実装しないとおかしなことになるということがわかりました。