I know that I can use this command $ docker images --tree docker history
to view the layers of a Docker image, but how do I do that for images on Docker Hub without pulling it? This is so that I know what is on an image before I download it.
E.g., for the Tomcat repo, https://registry.hub.docker.com/_/tomcat/, the webpage doesn't seem to show what is on the image. I have to look at the Dockerfile on Github to find out.
Update I see this repo https://registry.hub.docker.com/u/tutum/tomcat/ has more tabs. The "Dockerfile" tab shows how it is created, but only seems to show the latest version. Is there no choice to view the file for other tags?
You can get this for any registry by using the registry API. Within the image manifest is a pointer to the image config, and that config is a json blob that includes the history steps. Note that this history is not authoritative, and depending on the build tooling, could report different commands than what were actually used to build the image layers.
A shell script version of this using the auth for Docker Hub looks like:
#!/bin/sh
ref="${1:-library/ubuntu:latest}"
repo="${ref%:*}"
tag="${ref##*:}"
token=$(curl -s "https://auth.docker.io/token?service=registry.docker.io&scope=repository:${repo}:pull" \
| jq -r '.token')
digest=$(curl -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
-H "Authorization: Bearer $token" \
-s "https://registry-1.docker.io/v2/${repo}/manifests/${tag}" \
| jq -r .config.digest)
curl -H "Accept: application/vnd.docker.container.image.v1+json" \
-H "Authorization: Bearer $token" \
-s -L "https://registry-1.docker.io/v2/${repo}/blobs/${digest}" | jq .history
Behind the scenes, what's happening is the registry first resolves any manifest list to a single manifest for the default architecture, but you could pull any architecture manually:
$ regctl image manifest --list localhost:5000/library/alpine:latest --format '{{jsonPretty .}}'
{
"manifests": [
{
"digest": "sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a",
"mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "amd64",
"os": "linux"
},
"size": 528
},
{
"digest": "sha256:18c29393a090ba5cde8a5f00926e9e419f47cfcfd206cc3f7f590e91b19adfe9",
"mediaType": "application\/vnd.docker.distribution.manifest.v2+json",
"platform": {
"architecture": "arm",
"os": "linux",
"variant": "v6"
},
"size": 528
},
...
Then that platform specific manifest includes the config and layers:
$ regctl image manifest localhost:5000/library/alpine@sha256:69704ef328d05a9f806b6b8502915e6a0a4faa4d72018dc42343f511490daf8a --format '{{jsonPretty .}}'
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"size": 1471,
"digest": "sha256:14119a10abf4669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab"
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"size": 2814446,
"digest": "sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e"
}
]
}
And pulling the config blob shows all of the contents you may recognize from the history and inspect commands:
$ regctl blob get localhost:5000/library/alpine sha256:14119a10abf4669e8cdbdff324a9f9605d99697215a0d21c360fe8dfa8471bab | jq .
{
"architecture": "amd64",
"config": {
"Hostname": "",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh"
],
"Image": "sha256:d3e0b6258ec2f725c19668f11ae5323c3b0245e197ec478424ec6a87935690eb",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": null
},
"container": "330289c649db86f5fb1ae5bfef18501012b550adb0638b9193d4a3a4b65a2f9b",
"container_config": {
"Hostname": "330289c649db",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
],
"Cmd": [
"/bin/sh",
"-c",
"#(nop) ",
"CMD [\"/bin/sh\"]"
],
"Image": "sha256:d3e0b6258ec2f725c19668f11ae5323c3b0245e197ec478424ec6a87935690eb",
"Volumes": null,
"WorkingDir": "",
"Entrypoint": null,
"OnBuild": null,
"Labels": {}
},
"created": "2021-08-27T17:19:45.758611523Z",
"docker_version": "20.10.7",
"history": [
{
"created": "2021-08-27T17:19:45.553092363Z",
"created_by": "/bin/sh -c #(nop) ADD file:aad4290d27580cc1a094ffaf98c3ca2fc5d699fe695dfb8e6e9fac20f1129450 in / "
},
{
"created": "2021-08-27T17:19:45.758611523Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
}
],
"os": "linux",
"rootfs": {
"type": "layers",
"diff_ids": [
"sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68"
]
}
}
The regctl commands above come from my regclient project, but I believe you can do similar commands with skopeo and crane. The advantage of these tools over curl commands is handling the authentication that varies between registries (the bearer token from Hub won't work for other registries).
Lastly, the one-line answer using regctl is a formatted image config command, which performs all the above steps without needing jq:
$ regctl image config localhost:5000/library/alpine:latest --format '{{jsonPretty .History}}'
[
{
"created": "2021-08-27T17:19:45.553092363Z",
"created_by": "/bin/sh -c #(nop) ADD file:aad4290d27580cc1a094ffaf98c3ca2fc5d699fe695dfb8e6e9fac20f1129450 in / "
},
{
"created": "2021-08-27T17:19:45.758611523Z",
"created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
"empty_layer": true
}
]
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With