Photo by: kyohei ito, License: [CC BY-SA 2.0]
Have you ever encountered this scenario? You're working on a system or platform, and you slave over it for months, getting everything to work. When you finally deploy it, the client is irate because nothing works. And the only reply you can come up with is "But it works on my machine."
If you've been in the software development scene for years, then you're no stranger to this. Fortunately, there's a tech that will enable you to efficiently manage and supervise all the work your team is doing and help you avoid this problem. It’s called Docker.
Docker tech was first developed by Solomon Hykes as an internal project while working for dotCloud, a European company based in Berlin, Germany. It is an open-source platform used for developing, running, and shipping applications in isolated packages called containers. And these containers are hosted in a software called Docker Engine.
By using Docker tech, you can develop, test, or deploy code simultaneously but in different isolated environments, essentially streamlining different stages of the software development life cycle.
Docker Engine API is a RESTful API used to interact with the Docker daemon, especially to ones running in a remote machine. It allows you to do operations that you typically do with the Docker CLI (command line interface), such as creating a docker swarm, starting and stopping containers, pushing and pulling images, and many more using the HTTP(S) protocol. You may find it useful if you’re trying to automate the building and scaling of your docker applications.
Since the Docker Engine API is a RESTful API, you’d be working with HTTP calls and referencing URLs. For a small project, this is manageable. For a larger one, though, this can become cumbersome and overwhelming. Fortunately, Docker offers some official SDKs to developers for Go and Python. And as for .NET, there is an unofficial SDK called Docker.DotNet, which has tons of support from the community.
Docker.DotNet is practically just a mirror of the official API, as it’s structured similarly to Docker Engine API via the DockerClient class. However, instead of JSON objects, you will be working with C# objects. And instead of handling the HTTP requests yourself, you would only be calling methods to an object (e.g., Image, Container, Network) that you need to access. You can refer to the official documentation if you would like to learn more about an operation, such as the parameters to pass and the structure of the data returned. You may also check the source code and use it as a reference.
Currently, we’re developing a desktop application that will allow us to manage our docker hosts remotely. It will enable us to start containers from images, deploy services to a docker swarm, push and pull images to and from private registries, start and stop containers, and many more operations. It is no small feat, given the requirements. All these actions can be accomplished with either HttpClient or Docker.DotNet.
If we choose to use HttpClient, it would mean we have to work on the API layer ourselves, which includes making sure the structure of the request is correct such as the URL query parameters, request body, and the response. With Docker.DotNet, on the other hand, it will save us the time of coding the layer of the API ourselves, and we can focus more on working on the business logic of our application.
Using the HttpClient library is simple enough. Although, we'd have to manually deserialize the response data before we can use it to deal with the docker images. On the other hand, in Docker.DotNet implementation, we only need to call the ListImagesAsync method and it automatically implements the conversion.
Case in point, look at the differences below:
To show you more about what I’m talking about, I have some examples below. For these, I have set up a VM with a Docker daemon listening on port 2375 unencrypted for the sake of simplicity. It’s only for development purposes and is something you should not do in production. You can refer to the official documentation on how to secure the Docker daemon. Moreover, in the examples utilizing HttpClient, we are directly instantiating an HttpClient instance to simplify the code. You may refer to HttpClientFactory which is recommended by Microsoft.
using System;
using System.Text.Json;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
var uri = "http://172.18.115.143:2375/images/json";
var client = new HttpClient();
var response = await client.GetAsync(uri);
IEnumerable<DockerImage> images = Enumerable.Empty<DockerImage>();
if (response.IsSuccessStatusCode)
{
using var responseString = await response.Content.ReadAsStreamAsync();
images = await JsonSerializer.DeserializeAsync<IEnumerable<DockerImage>>(responseString);
}
Console.WriteLine("Images: ");
foreach (var image in images)
{
Console.WriteLine(image.RepoTags.FirstOrDefault());
}
Console.ReadLine();
}
}
}
using Docker.DotNet;
using Docker.DotNet.Models;
using System;
using System.Linq;
using System.Threading.Tasks;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
DockerClient client = new DockerClientConfiguration(
new Uri("http://172.18.115.143:2375")).CreateClient();
var images = await client.Images.ListImagesAsync(new ImagesListParameters { });
Console.WriteLine("Images: ");
foreach (var image in images)
{
Console.WriteLine(image.RepoTags.FirstOrDefault());
}
Console.ReadLine();
}
}
}
Starting a container using the HttpClient library requires us to build the URI. If the URI changes in the future, we need to refactor our code as well. In the example shown for Docker.DotNet, all we need to do is call the method and pass in the container name or id.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
var uri = "http://172.29.11.209:2375/containers/c9a76d52f57a/start";
var client = new HttpClient();
var response = await client.PostAsync(uri, new StringContent("{}", Encoding.UTF8, "application/json"));
Console.WriteLine("IsSuccessful: " + response.IsSuccessStatusCode);
Console.ReadLine();
}
}
}
using Docker.DotNet;
using Docker.DotNet.Models;
using System;
using System.Threading.Tasks;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
DockerClient client = new DockerClientConfiguration(
new Uri("http://172.18.115.143:2375")).CreateClient();
var res = await client.Containers.StartContainerAsync(
"webapp1", new ContainerStartParameters {});
Console.WriteLine("IsSuccessful: " + res);
Console.ReadLine();
}
}
}
When using the HttpClient, we handle the response stream and conversion of data into a string. While in Docker.DotNet, we only need to pass in a Progress instance and listen to changes. Furthermore, it already deserializes the data received into a JSONMessage object that contains all the properties for the status.
using System;
using System.Net.Http;
using System.Threading.Tasks;
using System.Text;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
var uri = "http://172.29.11.209:2375/images/create?fromImage=alpine";
var clientHandler = new HttpClientHandler();
var client = new HttpClient(clientHandler);
client.DefaultRequestHeaders.TransferEncodingChunked = true;
var requestMessage = new HttpRequestMessage(HttpMethod.Post, uri) { Content = new StringContent("{}") };
var response = await client.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode)
{
var responseStream = await response.Content.ReadAsStreamAsync();
byte[] data = new byte[1028];
int responseBytes;
while ((responseBytes = await responseStream.ReadAsync(data, 0, data.Length)) > 0)
{
string output = Encoding.UTF8.GetString(data, 0, responseBytes);
Console.WriteLine(output);
}
}
Console.ReadLine();
}
}
}
using Docker.DotNet;
using Docker.DotNet.Models;
using System;
using System.Threading.Tasks;
namespace DockerDotNetDemo
{
class Program
{
static async Task Main(string[] args)
{
DockerClient client = new DockerClientConfiguration(
new Uri("http://172.18.115.143:2375")).CreateClient();
Progress<JSONMessage> progress = new Progress<JSONMessage>();
progress.ProgressChanged += (sender, message) =>
{
Console.WriteLine(message.Status);
};
Console.WriteLine("Pulling Image: ");
await client.Images.CreateImageAsync(
new ImagesCreateParameters
{
FromImage = "alpine",
Tag = "latest"
},
null,
progress);
Console.ReadLine();
}
}
}
As shown in the examples, Docker.DotNet helps you avoid referencing the URL endpoints when performing different operations. If you have a large project integrating with the Docker Engine API, you will inevitably be referencing a lot of them. And you would need to modify the URLs or the request and response parameters when the API is updated. Also, you don't need to serialize/deserialize the request and response body, which makes using the API easier.
So which do you think is better to use with Docker Engine API, HttpClient or Docker.DotNet? I like the faster and more streamlined approach with Docker.DotNet. But you never really know unless you try, so I suggest you do that and see for yourself. Buena suerte!
If you like this article, check out my .Net integration tutorials on Elasticsearch and RabbitMQ.
.NET architect
.NET architect
Rafael (Rafa to close friends) can get quite engrossed in the tech world and especially likes reading about innovations in containerization, .NET technology (of course), microservices architecture, and Python. Surprisingly though, he can be pretty handy in the kitchen and can actually cook you some yummy paella if you’re nice and say, “Por favor!” To occupy his time when he’s not at work, he goes scuba diving, plays music or soccer, or adds snapshots to his collection of wildlife photography.