'How to find the creation date of an image in a (private) Docker registry (API v2)?

I would like to find out the latest timestamp for an image in a private Docker registry using the v2 API without first pulling the image to my local host.



Solution 1:[1]

A minor improvement to the answer from @snth : print the date more user friendly by using date:

date --date=$(curl -s -X GET http://$REGISTRY:5000/v2/$IMAGE/manifests/$TAG | \
  jq -r '.history[].v1Compatibility' | jq -r '.created' | sort | tail -n 1 )

Prints something like:

Fr 12. Okt 15:26:03 CEST 2018

Solution 2:[2]

Compiling the comments and answers above here is a complete solution

With Authentication

# Setup variables to make this more usalbe
USERNAME=docker_user
PASSWORD=docker_pass
DOCKER_REGISTRY=http://my-registry.myorg.org:9080
REPO=myrepo
TAG=atag

# Query Registry and pipe results to jq

DOCKER_DATE=$(curl -s -u $USERNAME:$PASSWORD -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' -X GET http://$REGISTRY_URL/v2/circle-lb/manifests/master | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')
echo "Date for $REPO:$TAG is $DOCKER_DATE"

Without Authentication

DOCKER_DATE=$(curl -s -H 'Accept: application/vnd.docker.distribution.manifest.v1+json' -X GET http://$REGISTRY_URL/v2/circle-lb/manifests/master | jq -r '[.history[]]|map(.v1Compatibility|fromjson|.created)|sort|reverse|.[0]')

And lastly if you want to parse with the date command (gnu date)

on linux:

date -d $DOCKER_DATE

on mac:

gdate -d $DOCKER_DATE

Solution 3:[3]

With the V2 image manifest, schema version 2, the response from http://registry:5000/v2/<IMAGE>/manifests/<TAG> does not contain the 'history' field, and I haven't found a way to retrieve the image creation date in one request. However one can use another request to obtain it, based on the image manifest config's digest.

First, get the config's digest (notice the header which specifies the schema version):

digest=$(curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
 http://registry:5000/v2/<IMAGE>/manifests/<TAG> \
 | jq -r '.config.digest')

The value of digest will be something similar to sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736.

Then, get the blob of the object identified by the digest, i.e. the configuration object for a container, and retrieve the creation date from there:

curl -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \
  http://registry:5000/v2/<IMAGE>/blobs/$digest | jq -r '.created'

This should produce:

2021-10-27T12:18:24.105617451Z

Bonus: Getting the creation dates using skopeo

I'm not sure if the above is the only or the best way, but by the way, it is apparently more or less what skopeo does under the hood when one runs:

skopeo inspect docker://registry:5000/<IMAGE>:<TAG>

The above command returns information about the image tag, among them the creation date:

{
  "Name": "registry:5000/<IMAGE>",
  "Digest": "sha256:655721ff613ee766a4126cb5e0d5ae81598e1b0c3bcf7017c36c4d72cb092fe9",
  "RepoTags": [...],
  "Created": "2021-10-27T12:18:24.105617451Z",
  ...
}

Solution 4:[4]

Here is dotnet core (C#) implementation in case anyone is interested:

    public class DockerHub{
    public DockerRegistryToken GetRegistryToken(string image){
        Console.WriteLine("authenticateing with dockerhub");
        using HttpClient client = new ();   
        var url = string.Format($"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{image}:pull");
        //var response = client.Send(new HttpRequestMessage(HttpMethod.Get, url));
        var result = client.GetStringAsync(url);            
        var drt = JsonSerializer.Deserialize<DockerRegistryToken>(result.Result);        
        return drt;
    }

    public DateTime GetRemoteImageDate(string image, string tag, DockerRegistryToken token){

        using HttpClient client = new ();           
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token.Token);
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.docker.distribution.manifest.list.v2+json"));
        
    
        var manifestResult = client.GetStringAsync(string.Format($"https://registry-1.docker.io/v2/{image}/manifests/{tag}"));  
        var json = JsonDocument.Parse(manifestResult.Result);

        var created = json.RootElement.GetProperty("history").EnumerateArray()
                    .Select(m => JsonDocument.Parse(m.GetProperty("v1Compatibility").ToString()).RootElement.GetProperty("created"))
                    .Select(m => DateTime.Parse(m.GetString()))
                    .OrderByDescending(m => m)
                    .FirstOrDefault(); // I challange you to improve this
        Console.WriteLine("Date recieved: {0}",created);
        return created.ToUniversalTime();

    }   
}

public class DockerRegistryToken{
    [JsonPropertyName("token")]
    public string Token { get; set; }

    /// always null
    [JsonPropertyName("access_token")]
    public string AccessToken  {get; set; }

    [JsonPropertyName("expires_in")]
    public int ExpiresInSeconds { get; set; }

    [JsonPropertyName("issued_at")]
    public DateTime IssuedAt { get; set; }

}

To use

        var client = new DockerHub();
        var tok = client.GetRegistryToken("your/reponame");
        var remoteImageDate = client.GetRemoteImageDate("your/reponame","latest",tok);

Solution 5:[5]

Some idea for C#:

using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;

namespace DockerApi;

public class Manifests
{
    [JsonPropertyName("schemaVersion")]
    public long SchemaVersion { get; set; }

    [JsonPropertyName("name")]
    public string Name { get; set; }

    [JsonPropertyName("tag")]
    public string Tag { get; set; }

    [JsonPropertyName("architecture")]
    public string Architecture { get; set; }

    [JsonPropertyName("fsLayers")]
    public FsLayer[] FsLayers { get; set; }

    [JsonPropertyName("history")]
    public History[] History { get; set; }

    [JsonPropertyName("signatures")]
    public Signature[] Signatures { get; set; }

    public V1Compatibility V1Compatibility
    {
        get
        {
            var jsonNode = JsonNode.Parse(History.Where(m => m.V1Compatibility.Contains("architecture")).First().V1Compatibility);
            return JsonSerializer.Deserialize<V1Compatibility>(jsonNode);
        }
    }
}
public partial class FsLayer
{
    [JsonPropertyName("blobSum")]
    public string BlobSum { get; set; }
}

public partial class History
{
    [JsonPropertyName("v1Compatibility")]
    public string V1Compatibility { get; set; }
}

public partial class Signature
{
    [JsonPropertyName("header")]
    public Header Header { get; set; }

    [JsonPropertyName("signature")]
    public string SignatureSignature { get; set; }

    [JsonPropertyName("protected")]
    public string Protected { get; set; }
}

public partial class Header
{
    [JsonPropertyName("jwk")]
    public Jwk Jwk { get; set; }

    [JsonPropertyName("alg")]
    public string Alg { get; set; }
}

public partial class Jwk
{
    [JsonPropertyName("crv")]
    public string Crv { get; set; }

    [JsonPropertyName("kid")]
    public string Kid { get; set; }

    [JsonPropertyName("kty")]
    public string Kty { get; set; }

    [JsonPropertyName("x")]
    public string X { get; set; }

    [JsonPropertyName("y")]
    public string Y { get; set; }
}
public partial class V1Compatibility
{
    [JsonPropertyName("architecture")]
    public string Architecture { get; set; }

    [JsonPropertyName("config")]
    public Config Config { get; set; }

    [JsonPropertyName("created")]
    public DateTimeOffset Created { get; set; }

    [JsonPropertyName("id")]
    public string Id { get; set; }

    [JsonPropertyName("os")]
    public string Os { get; set; }

    [JsonPropertyName("parent")]
    public string Parent { get; set; }

    [JsonPropertyName("throwaway")]
    public bool Throwaway { get; set; }
}

public partial class Config
{
    [JsonPropertyName("Env")]
    public string[] Env { get; set; }

    [JsonPropertyName("Cmd")]
    public string[] Cmd { get; set; }

    [JsonPropertyName("WorkingDir")]
    public string WorkingDir { get; set; }

    [JsonPropertyName("ArgsEscaped")]
    public bool ArgsEscaped { get; set; }

    [JsonPropertyName("OnBuild")]
    public object OnBuild { get; set; }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Sebastian Wagner
Solution 2
Solution 3 natka_m
Solution 4 sbeskur
Solution 5 zimbres