.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.DotNet
dotnet 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 HttpMessageHandler
var 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 store
The 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 configuration
using 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 scope
Problem: Images built with Docker BuildKit's provenance feature (--provenance true, the default in recent Docker versions) fail to retrieve manifests with a 404 Not Found error.
Cause: BuildKit publishes these images with OCI media types (application/vnd.oci.image.index.v1+json and application/vnd.oci.image.manifest.v1+json). Versions of Docker.Registry.DotNet without OCI support did not request or recognize these media types (#28).
Solution: Upgrade to a version with OCI manifest support. The OCI image index is returned as a ManifestList and OCI image manifests as ImageManifest2_2, so code written against the Docker media types works unchanged.
If you are pinned to an older version, build with --provenance false as a workaround:
docker build --provenance false -t myimage:latest .
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 uploads
Problem: 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!
