.NET (C#) Client Library for interacting with the Docker Registry API (v2).
- Installation
- Quick Start
- Authentication
- Core Operations
- Real-World Scenarios
- Registry-Specific Examples
- Configuration & Advanced Topics
- Error Handling
- Troubleshooting
- API Reference
- Migration Guide
- Contributing
- License
PM> Install-Package Docker.Registry.DotNetdotnet add package Docker.Registry.DotNet<PackageReference Include="Docker.Registry.DotNet" Version="2.0.0" />- .NET Standard 2.0
- .NET 5.0, 6.0, 7.0, 8.0+
using Docker.Registry.DotNet;
var configuration = new RegistryClientConfiguration("http://localhost:5000");
using var client = configuration.CreateClient();
// Get catalog of repositories
var catalog = await client.Catalog.GetCatalog();
foreach (var repo in catalog.Repositories)
{
Console.WriteLine($"Repository: {repo}");
// List tags for each repository
var tags = await client.Tags.ListTags(repo);
foreach (var tag in tags.Tags)
{
Console.WriteLine($" Tag: {tag}");
}
}var configuration = new RegistryClientConfiguration("https://registry.mycompany.com");
configuration.UsePasswordOAuthAuthentication("username", "password");
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();var configuration = new RegistryClientConfiguration("https://hub.docker.com");
using var client = configuration.CreateClient();
// List tags for a specific repository
var tags = await client.Repository.ListRepositoryTags("grafana", "loki-docker-driver");
foreach (var tag in tags.Tags)
{
Console.WriteLine($"{tag.Name} - Last Updated: {tag.LastUpdated}");
}Docker.Registry.DotNet supports multiple authentication methods depending on your registry configuration.
Use this for registries that allow anonymous access or require OAuth tokens without credentials.
var configuration = new RegistryClientConfiguration("https://registry-1.docker.io");
configuration.UseAnonymousOAuthAuthentication();
using var client = configuration.CreateClient();Use this for registries that support HTTP Basic Authentication.
var configuration = new RegistryClientConfiguration("https://registry.mycompany.com");
configuration.UseBasicAuthentication("username", "password");
using var client = configuration.CreateClient();Use this for registries that require OAuth token-based authentication (most common for private registries).
var configuration = new RegistryClientConfiguration("https://registry.mycompany.com");
configuration.UsePasswordOAuthAuthentication("username", "password");
using var client = configuration.CreateClient();| Registry | Authentication Method |
|---|---|
| Docker Hub | UseAnonymousOAuthAuthentication() (public) or UsePasswordOAuthAuthentication() (private) |
| Azure Container Registry (ACR) | UseBasicAuthentication() with username and access token |
| Amazon ECR | Use AWS credentials to obtain a token, then UseBasicAuthentication("AWS", token) |
| Harbor | UseBasicAuthentication() or UsePasswordOAuthAuthentication() |
| GitLab Container Registry | UseBasicAuthentication() with personal access token |
| Local Registry | Often no authentication or UseBasicAuthentication() |
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();
foreach (var repository in catalog.Repositories)
{
Console.WriteLine(repository);
}var parameters = new CatalogParameters
{
Number = 100 // Number of repositories per page
};
var catalog = await client.Catalog.GetCatalog(parameters);
// Check if there are more results
if (!string.IsNullOrEmpty(catalog.Link))
{
Console.WriteLine($"More results available at: {catalog.Link}");
}var allRepositories = new List<string>();
CatalogParameters? parameters = new CatalogParameters { Number = 100 };
do
{
var catalog = await client.Catalog.GetCatalog(parameters);
allRepositories.AddRange(catalog.Repositories);
// Check if there's a next page
if (string.IsNullOrEmpty(catalog.Last))
break;
parameters = new CatalogParameters
{
Number = 100,
Last = catalog.Last // Use last repository as marker
};
} while (true);
Console.WriteLine($"Total repositories: {allRepositories.Count}");var tags = await client.Tags.ListTags("myapp");
foreach (var tag in tags.Tags)
{
Console.WriteLine(tag);
}var parameters = new ListTagsParameters
{
Number = 50 // Tags per page
};
var tags = await client.Tags.ListTags("myapp", parameters);This is useful to find all tags pointing to the same image digest.
var tagsByDigest = await client.Tags.ListTagsByDigests("myapp");
foreach (var digest in tagsByDigest.Manifests)
{
Console.WriteLine($"Digest: {digest.Digest}");
Console.WriteLine($"Tags: {string.Join(", ", digest.Tags)}");
Console.WriteLine($"MediaType: {digest.MediaType}");
Console.WriteLine($"Size: {digest.Size} bytes");
Console.WriteLine();
}var manifestResult = await client.Manifest.GetManifest("myapp", "latest");
Console.WriteLine($"MediaType: {manifestResult.MediaType}");
Console.WriteLine($"Digest: {manifestResult.DockerContentDigest}");
// Access manifest details
if (manifestResult.Manifest is ImageManifest2_2 manifest)
{
Console.WriteLine($"Architecture: {manifest.Config.Platform?.Architecture}");
Console.WriteLine($"OS: {manifest.Config.Platform?.OS}");
// Access layers
foreach (var layer in manifest.Layers)
{
Console.WriteLine($"Layer: {layer.Digest}, Size: {layer.Size} bytes");
}
}To access complete manifest information including history:
var manifestResult = await client.Manifest.GetManifest("myapp", "latest");
if (manifestResult.Manifest is ImageManifest2_2 manifest)
{
// Get the config blob which contains history
var configBlob = await client.Blobs.GetBlob("myapp", manifest.Config.Digest);
using var reader = new StreamReader(configBlob.Stream);
var configJson = await reader.ReadToEndAsync();
// Parse config JSON to access history
var config = JsonConvert.DeserializeObject<ImageConfig>(configJson);
foreach (var historyEntry in config.History)
{
Console.WriteLine($"Created: {historyEntry.Created}");
Console.WriteLine($"Command: {historyEntry.CreatedBy}");
Console.WriteLine();
}
}var rawManifest = await client.Manifest.GetManifestRaw("myapp", "latest");
Console.WriteLine(rawManifest);var digest = await client.Manifest.GetDigest("myapp", "latest");
Console.WriteLine($"Digest: {digest}");var manifest = new ImageManifest2_2
{
SchemaVersion = 2,
MediaType = "application/vnd.docker.distribution.manifest.v2+json",
Config = new Descriptor
{
MediaType = "application/vnd.docker.container.image.v1+json",
Digest = configDigest,
Size = configSize
},
Layers = new List<Descriptor>
{
new Descriptor
{
MediaType = "application/vnd.docker.image.rootfs.diff.tar.gzip",
Digest = layerDigest,
Size = layerSize
}
}
};
var response = await client.Manifest.PutManifest("myapp", "v1.0.0", manifest);
Console.WriteLine($"Manifest uploaded with digest: {response.DockerContentDigest}");// Delete by digest (recommended to avoid deleting wrong manifest)
var digest = await client.Manifest.GetDigest("myapp", "latest");
await client.Manifest.DeleteManifest("myapp", digest);
Console.WriteLine($"Deleted manifest with digest: {digest}");var manifestResult = await client.Manifest.GetManifest("myapp", "latest");
if (manifestResult.Manifest is ImageManifest2_2 manifest)
{
var firstLayer = manifest.Layers.First();
var blob = await client.Blobs.GetBlob("myapp", firstLayer.Digest);
Console.WriteLine($"Content Type: {blob.ContentType}");
Console.WriteLine($"Digest: {blob.DockerContentDigest}");
// Save blob to file
using var fileStream = File.Create($"layer-{firstLayer.Digest}.tar.gz");
await blob.Stream.CopyToAsync(fileStream);
}var exists = await client.Blobs.BlobExists("myapp", "sha256:abc123...");
if (exists)
{
Console.WriteLine("Blob exists in registry");
}await client.Blobs.DeleteBlob("myapp", "sha256:abc123...");
Console.WriteLine("Blob deleted");For smaller blobs, use a monolithic upload:
using var fileStream = File.OpenRead("my-layer.tar.gz");
// Calculate digest
using var sha256 = SHA256.Create();
var hash = await sha256.ComputeHashAsync(fileStream);
var digest = $"sha256:{BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant()}";
fileStream.Position = 0;
// Start upload
var upload = await client.BlobUploads.StartUploadBlob("myapp");
// Complete upload in one request
var imageDigest = new ImageDigest(digest);
var result = await client.BlobUploads.MonolithicUploadBlob(
upload,
imageDigest,
fileStream
);
Console.WriteLine($"Uploaded blob: {result.DockerContentDigest}");For larger blobs, use chunked uploads with resumable capability:
using var fileStream = File.OpenRead("large-layer.tar.gz");
// Calculate final digest
var digest = CalculateSHA256(fileStream);
fileStream.Position = 0;
// Start upload session
var upload = await client.BlobUploads.StartUploadBlob("myapp");
const int chunkSize = 5 * 1024 * 1024; // 5 MB chunks
var buffer = new byte[chunkSize];
long position = 0;
while (position < fileStream.Length)
{
var bytesRead = await fileStream.ReadAsync(buffer, 0, chunkSize);
using var chunkStream = new MemoryStream(buffer, 0, bytesRead);
var from = position;
var to = position + bytesRead - 1;
// Upload chunk
upload = await client.BlobUploads.UploadBlobChunk(
upload,
chunkStream,
from,
to
);
position += bytesRead;
Console.WriteLine($"Uploaded {position}/{fileStream.Length} bytes");
}
// Complete the upload
var imageDigest = new ImageDigest(digest);
var result = await client.BlobUploads.CompleteBlobUpload(upload, imageDigest);
Console.WriteLine($"Upload complete: {result.DockerContentDigest}");Copy a blob from one repository to another without re-uploading:
var parameters = new MountParameters
{
From = "source-repo",
Mount = "sha256:abc123..." // Blob digest to mount
};
var result = await client.BlobUploads.MountBlob("destination-repo", parameters);
if (result.DockerUploadUuid == null)
{
Console.WriteLine("Blob mounted successfully!");
}
else
{
Console.WriteLine("Mount not supported, need to upload blob");
}var upload = await client.BlobUploads.StartUploadBlob("myapp");
// ... something goes wrong ...
await client.BlobUploads.CancelBlobUpload("myapp", upload.UploadUuid);
Console.WriteLine("Upload cancelled");var sourceConfig = new RegistryClientConfiguration("https://source-registry.com");
sourceConfig.UsePasswordOAuthAuthentication("user1", "pass1");
var destConfig = new RegistryClientConfiguration("https://dest-registry.com");
destConfig.UsePasswordOAuthAuthentication("user2", "pass2");
using var sourceClient = sourceConfig.CreateClient();
using var destClient = destConfig.CreateClient();
// Get manifest from source
var manifest = await sourceClient.Manifest.GetManifest("myapp", "v1.0");
if (manifest.Manifest is ImageManifest2_2 imageManifest)
{
// Copy config blob
var configBlob = await sourceClient.Blobs.GetBlob("myapp", imageManifest.Config.Digest);
var configUpload = await destClient.BlobUploads.StartUploadBlob("myapp");
await destClient.BlobUploads.MonolithicUploadBlob(
configUpload,
new ImageDigest(imageManifest.Config.Digest),
configBlob.Stream
);
// Copy each layer
foreach (var layer in imageManifest.Layers)
{
var layerBlob = await sourceClient.Blobs.GetBlob("myapp", layer.Digest);
var layerUpload = await destClient.BlobUploads.StartUploadBlob("myapp");
await destClient.BlobUploads.MonolithicUploadBlob(
layerUpload,
new ImageDigest(layer.Digest),
layerBlob.Stream
);
}
// Push manifest to destination
await destClient.Manifest.PutManifest("myapp", "v1.0", imageManifest);
Console.WriteLine("Image copied successfully!");
}using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();
foreach (var repo in catalog.Repositories)
{
var tags = await client.Tags.ListTags(repo);
foreach (var tag in tags.Tags)
{
Console.WriteLine($"{repo}:{tag}");
}
}using var client = configuration.CreateClient();
var manifest = await client.Manifest.GetManifest("myapp", "latest");
if (manifest.Manifest is ImageManifest2_2 imageManifest)
{
Directory.CreateDirectory("layers");
int layerIndex = 0;
foreach (var layer in imageManifest.Layers)
{
Console.WriteLine($"Downloading layer {layerIndex}: {layer.Digest}");
var blob = await client.Blobs.GetBlob("myapp", layer.Digest);
var fileName = $"layers/layer-{layerIndex:D3}.tar.gz";
using var fileStream = File.Create(fileName);
await blob.Stream.CopyToAsync(fileStream);
Console.WriteLine($"Saved to {fileName} ({layer.Size} bytes)");
layerIndex++;
}
}using var client = configuration.CreateClient();
var cutoffDate = DateTime.UtcNow.AddMonths(-6);
var tagsToDelete = new List<string>();
var tagsByDigest = await client.Tags.ListTagsByDigests("myapp");
foreach (var manifest in tagsByDigest.Manifests)
{
// Note: You'd need to get the actual creation date from the manifest
// This is a simplified example
foreach (var tag in manifest.Tags)
{
if (tag.StartsWith("temp-") || tag.Contains("-old"))
{
tagsToDelete.Add(tag);
}
}
}
foreach (var tag in tagsToDelete)
{
var digest = await client.Manifest.GetDigest("myapp", tag);
if (digest != null)
{
await client.Manifest.DeleteManifest("myapp", digest);
Console.WriteLine($"Deleted tag: {tag}");
}
}var configuration = new RegistryClientConfiguration("https://hub.docker.com");
using var client = configuration.CreateClient();
var tags = await client.Repository.ListRepositoryTags("library", "nginx");var configuration = new RegistryClientConfiguration("https://proget.mycompany.com");
configuration.UsePasswordOAuthAuthentication("username", "password");
using var client = configuration.CreateClient();
var tags = await client.Tags.ListTags("myapp");var configuration = new RegistryClientConfiguration("https://myregistry.azurecr.io");
// Use the admin username and password, or a service principal
configuration.UseBasicAuthentication("username", "password");
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();// First, get an authorization token using AWS SDK
// var ecrClient = new AmazonECRClient();
// var authResponse = await ecrClient.GetAuthorizationTokenAsync(...);
// var token = DecodeBase64(authResponse.AuthorizationData[0].AuthorizationToken);
var configuration = new RegistryClientConfiguration("https://123456789012.dkr.ecr.us-east-1.amazonaws.com");
configuration.UseBasicAuthentication("AWS", token);
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();var configuration = new RegistryClientConfiguration("https://harbor.mycompany.com");
configuration.UseBasicAuthentication("admin", "Harbor12345");
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();var configuration = new RegistryClientConfiguration("https://registry.gitlab.com");
// Use personal access token or deploy token
configuration.UseBasicAuthentication("gitlab-ci-token", "your-token");
using var client = configuration.CreateClient();
var tags = await client.Tags.ListTags("mygroup/myproject");// No authentication typically required
var configuration = new RegistryClientConfiguration("http://localhost:5000");
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();var httpClient = new HttpClient
{
Timeout = TimeSpan.FromMinutes(10)
};
var configuration = new RegistryClientConfiguration("https://registry.mycompany.com");
// Note: The library creates its own HttpClient internally
// For advanced scenarios, consider creating a custom HttpMessageHandlervar handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
// WARNING: Only use this in development/testing environments
return true; // Accept all certificates
}
};
// For production, properly validate the certificate
// or add it to your system's trusted certificate storeThe library includes built-in ActivitySource support for distributed tracing:
using System.Diagnostics;
// Create an ActivityListener to capture traces
var listener = new ActivityListener
{
ShouldListenTo = source => source.Name == "Docker.Registry.DotNet",
Sample = (ref ActivityCreationOptions<ActivityContext> options) => ActivitySamplingResult.AllData,
ActivityStarted = activity => Console.WriteLine($"Started: {activity.DisplayName}"),
ActivityStopped = activity => Console.WriteLine($"Stopped: {activity.DisplayName} ({activity.Duration})")
};
ActivitySource.AddActivityListener(listener);
// Now all registry operations will be traced
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();var handler = new HttpClientHandler
{
Proxy = new WebProxy("http://proxy.company.com:8080"),
UseProxy = true
};
// Use the handler with your configurationusing Docker.Registry.DotNet.Domain.Registry;
try
{
var catalog = await client.Catalog.GetCatalog();
}
catch (UnauthorizedApiException ex)
{
Console.WriteLine("Authentication failed!");
Console.WriteLine($"Status Code: {ex.StatusCode}");
// Inspect WWW-Authenticate headers for more details
foreach (var header in ex.Headers)
{
Console.WriteLine($"{header.Key}: {string.Join(", ", header.Value)}");
}
}try
{
var manifest = await client.Manifest.GetManifest("myapp", "nonexistent-tag");
}
catch (DockerApiException ex)
{
Console.WriteLine($"API Error: {ex.StatusCode}");
Console.WriteLine($"Message: {ex.Message}");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"Network error: {ex.Message}");
}using Polly;
var retryPolicy = Policy
.Handle<HttpRequestException>()
.Or<TaskCanceledException>()
.WaitAndRetryAsync(3, retryAttempt =>
TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
await retryPolicy.ExecuteAsync(async () =>
{
var catalog = await client.Catalog.GetCatalog();
});// Use 'using' statement to ensure proper disposal
using (var client = configuration.CreateClient())
{
var catalog = await client.Catalog.GetCatalog();
// Client is disposed here
}
// Or with 'using' declaration (C# 8.0+)
using var client = configuration.CreateClient();
var catalog = await client.Catalog.GetCatalog();
// Client is disposed at end of scopeProblem: Images built with Docker BuildKit's provenance feature (--provenance true) may fail to retrieve manifests.
Cause: BuildKit creates manifests with OCI media types that may not be fully supported in older versions.
Solution:
-
Build images with
--provenance falsefor compatibility:docker build --provenance false -t myimage:latest .
-
Or ensure you're using the latest version of Docker.Registry.DotNet which includes improved OCI support.
Problem: Authentication fails with a 405 error.
Cause: The registry may not support the authentication method being used.
Solution:
-
Try different authentication methods:
// Try Basic Authentication instead of OAuth configuration.UseBasicAuthentication("username", "password"); // Or try Anonymous OAuth configuration.UseAnonymousOAuthAuthentication();
-
Verify the registry supports the Docker Registry API v2.
Problem: Getting 401 errors even with correct credentials.
Cause: Various authentication issues.
Solutions:
- Verify credentials are correct
- Check if user has permissions for the repository
- For Docker Hub, use your username (not email) and an access token (not password)
- For cloud registries (ACR, ECR, GCR), ensure you're using the correct authentication method
Problem: The SSL connection could not be established
Solution:
// For development only - accept self-signed certificates
// DO NOT use in production!
var handler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
};For production, add the certificate to your system's trusted certificate store.
Problem: Operations timeout when working with large images.
Solution: Increase timeout and use chunked uploads:
// The library handles timeouts internally
// For very large operations, use chunked uploads instead of monolithic uploadsProblem: Getting 404 errors for tags that exist.
Cause: Registry may be using a different API endpoint or authentication scope.
Solution:
- Verify the repository name format (some registries use
namespace/repository) - Check authentication scopes include the repository
- Try using
Repository.ListRepositoryTags()for Docker Hub instead ofTags.ListTags()
The IRegistryClient interface provides access to all registry operations:
Manifest operations for working with image manifests.
GetManifest()- Get an image manifestGetManifestRaw()- Get raw manifest JSONGetDigest()- Get manifest digestPutManifest()- Upload/push a manifestDeleteManifest()- Delete a manifest
Catalog operations for listing repositories.
GetCatalog()- Get list of repositories
Blob operations for downloading image layers.
GetBlob()- Download a blob/layerBlobExists()- Check if a blob existsDeleteBlob()- Delete a blob
Blob upload operations for pushing image layers.
StartUploadBlob()- Start an upload sessionMonolithicUploadBlob()- Upload blob in one requestUploadBlobChunk()- Upload a chunk (for resumable uploads)CompleteBlobUpload()- Complete a chunked uploadCancelBlobUpload()- Cancel an uploadMountBlob()- Mount blob from another repositoryGetBlobUploadStatus()- Get status of resumable upload
Tag operations for working with image tags.
ListTags()- List tags for a repositoryListTagsByDigests()- List tags grouped by digest (shows all tags for each image)
Docker Hub specific repository operations.
ListRepositoryTags()- List tags for a Docker Hub repository
System operations.
- Operations for registry health checks and version info
Version 2.0 introduced several breaking changes for a more modern .NET API:
v1.x:
var catalog = await client.Catalog.GetCatalogAsync();
var tags = await client.Tags.ListImageTagsAsync("myapp", new ListImageTagsParameters());v2.x:
var catalog = await client.Catalog.GetCatalog();
var tags = await client.Tags.ListTags("myapp", new ListTagsParameters());Note: Legacy methods with Async suffix are still available but marked as [Obsolete] to help with migration.
v1.x:
var configuration = new RegistryClientConfiguration("https://registry.com");
var client = new RegistryClient(configuration);v2.x (Recommended):
var configuration = new RegistryClientConfiguration("https://registry.com");
configuration.UsePasswordOAuthAuthentication("user", "pass");
using var client = configuration.CreateClient();Authentication configuration is now done through extension methods:
// Basic Authentication
configuration.UseBasicAuthentication("username", "password");
// Password OAuth
configuration.UsePasswordOAuthAuthentication("username", "password");
// Anonymous OAuth
configuration.UseAnonymousOAuthAuthentication();v2.x adds direct support for:
- .NET 5.0
- .NET 6.0
- .NET 7.0
- .NET 8.0
While maintaining .NET Standard 2.0 support for maximum compatibility.
- ActivitySource support for distributed tracing
- Improved error handling
- Docker Hub registry support via
Repositoryoperations ListTagsByDigests()to get all tags for specific images- Better nullability annotations
- Improved async/await patterns
We welcome contributions!
- Fork the repository
- Create a feature branch
- Make your changes
- Add tests if applicable
- Submit a pull request
If you find a bug or have a feature request, please open an issue on GitHub.
This project is licensed under the Apache License 2.0 - see the LICENSE file for details.
Copyright © Rich Quackenbush, Jaben Cargman and the Docker.Registry.DotNet Contributors 2017-2024
Questions? Check out the sample projects or open an issue!
