Implementing a Docker HEALTHCHECK using ASP.Net Core 2.2

From my archive - originally published on 21 September 2018

It’s easy enough to tell if a container is running, but more difficult to tell whether the container is ready to accept instructions. Docker supports a health check mechanism that allows you check that a container is doing its job correctly.

There are several ways of setting this up, but the HEALTHCHECK instruction allows you to bake this kind of readiness checking into an image definition. This directive specifies a shell command that returns a zero if all is well and one if the container is unhealthy. It can be picked up by an orchestrator, such as a readiness probe in Kubernetes or the system health reporting in Service Fabric.

A typical health check declaration uses curl’s --fail option to call an HTTP endpoint and regard any error response status as unhealthy. The example below augments the shell command with some switches to set an interval along with retries and a timeout:

HEALTHCHECK --interval=5s --timeout=10s --retries=3 CMD curl --fail http://localhost:80/healthcheck || exit 1
Ideally a readiness check like this should be lightweight. This is not an advanced diagnostic function but a simple status check that is repeated by the orchestrator to check that the lights are on.

Implementing health checks in ASP.Net Core

Given that a Docker HEALTHCHECK allows you to define a shell command you are free to use any mechanism for returning the result. An HTTP end-point is the most obvious approach for an ASP.Net Core application, and curl is included in the aspnetcore-runtime Docker images.

Although you are free to implement your own end-point, a health check implementation is being added into version 2.2 of Asp.Net Core via some new extension methods.

You can set up health-checking in your StartUp class by invoking extension methods for the services collection and application builder as shown below:

public void ConfigureServices(IServiceCollection services)
{
  services.AddHealthChecks();
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
  app.UseHealthChecks("/healthcheck");
}

This basic configuration sets up an end-point that will always return a "Healthy" response with a 200 OK HTTP status while the application is running.

You implement checking mechanisms via the services extension method. The example below creates a simple health check for HTTP requests that checks that the API root is accepting requests.

services.AddHealthChecks().AddAsyncCheck("Http"async () =>    
{
    using (HttpClient client = new HttpClient())
    {
        try           
        {
            var response = await client.GetAsync("http://localhost:80");

            if (!response.IsSuccessStatusCode)                
            {
                throw new Exception("Url not responding with 200 OK");                
            }            
        }            
        catch (Exception)            
        {                
            return await Task.FromResult(HealthCheckResult.Unhealthy());
        }        
    }        
    
    return await Task.FromResult(HealthCheckResult.Healthy());
});

This check is executed every time the health check endpoint is called and the result added to a HealthReport object. If the check fails, the default health check end-point will return an "Unhealthy" response with a 503 Service Unavailable HTTP status.

You can modify this output using the extension methods on the application builder. The example below dumps out the contents of the HealthReport object to provide more verbose output.

app.UseHealthChecks("/healthcheck"new HealthCheckOptions    
{        
    ResponseWriter = async (context, report) =>        
    {            
        var result = JsonConvert.SerializeObject(new
        {                    
            status = report.Status.ToString(),                    
            checks = report.Entries.Select(c => new 
            { 
            check = c.Key, result = c.Value.Status.ToString() 
            }),                
        });

      context.Response.ContentType = MediaTypeNames.Application.Json;
      await context.Response.WriteAsync(result);        
    }    
});

The resulting JSON looks like this:

{  
    "status":"Healthy",  
    "checks":[{ "check":"Http", "result":"Healthy"}]
}

Note that a Docker HEALTHCHECK does not care about any of this extra information. The basic result is unchanged, i.e. a zero is returned for a healthy service while a one is returned in response to any HTTP error code. Other health check mechanisms might find this scope for verbose output more useful.

Checking the output

You can see a health check in action by inspecting the running image. If you execute a docker ps command immediately after running a container then you’ll see the health status set to “starting”. Once the check has been executed successfully it will switch to “healthy”.

CONTAINER ID    IMAGE           COMMAND                CREATED         STATUS                            PORTS     NAMES
e603c74c840a    example:latest  "dotnet example.dll"   7 seconds ago   Up 5 seconds (health: starting)   80/tcp    unruffled_morse

CONTAINER ID    IMAGE           COMMAND                CREATED         STATUS                    PORTS     NAMES
e603c74c840a    example:latest  "dotnet example.dll"   7 seconds ago   Up 20 seconds (healthy)   80/tcp    unruffled_morse