In my previous post, I described how we can leverage the new EventCounter diagnostics API to add custom event counters and listen for built in counters. In this article, I will walk through how we can leverage the dotnet-counters tool with a running docker image.

Creating our diagnostics image

In order to connect to a docker image, we will create a diagnostics image which will host the same .Net SDK version as our application and will have the dotnet-counters tool pre-installed:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1
RUN mkdir /root/.dotnet/tools
ENV PATH="/root/.dotnet/tools:${PATH}"
RUN  dotnet tool install dotnet-counters --global
WORKDIR /diagnostics
ENTRYPOINT [ "/bin/bash" ]

A common mistake when creating docker images that contain .Net tools which are installed globally is not remembering to add the tool path, in this case /root/.dotnet/tools/ to the PATH so that it can be globally executed. Luckily, the .Net CLI will remind you in the build logs should you forget to do this.

Note: You can see the other tools that are available here.

Now that we have our docker image ready, we can build with the following command:

docker build -f diagnostics.Dockerfile -t dotnetdiag:3.1 .

Setting up the host image

For the purposes of this article, we will setup our application using a brand new application within a dockerfile, created by dotnet new:

FROM mcr.microsoft.com/dotnet/core/sdk:3.1
WORKDIR /app
EXPOSE 5000
EXPOSE 5001
RUN dotnet new webapp -n BlogApp
WORKDIR /app/BlogApp
ENTRYPOINT dotnet run -c Release

And we will build our application with the following command line:

docker build -f app.Dockerfile --name app -t dotnetapp:latest .

Once you have you're application built we are ready to start our docker image with debugging enabled.

Connecting from the diagnostics image to the host image

Normally we would start our applications with a command line similar to this:

docker run --rm --name app dotnetapp:latest

However, in order to be able to connect to the running application we need to mount a volume to the temporary directory on the application container. We can do this by appending -v dotnetdiag:/tmp, which instructs docker to mount a named volume dotnetdiag to the path /tmp. Docker will create the named volume during startup if it does not exist.

We mount the volume because as the .Net runtime starts up, it places a load of temporary files into the /tmp directory such as the following:

[email protected]:/# ls /tmp
CoreFxPipe_root.b5he0_wwfcD_lH7g471Brpw4X   VBCSCompiler                                 
jiksomfd.ri0                                NuGetScratch
hn2K8eq8bHUcTVSgvuckPlSK9tw9_ORiMDm_Vn4ylfI system-commandline-sentinel-files

Note the inclusion of the file beginning with CoreFxPipe_root, which is the EventPipe that we will connect to.

Once the application is running, we are now able to start connecting to our application. Normally we would run the following command line to start the diagnostics image: docker run --rm -it --pid=container:app --net=container:app -v dotnetdiag:/tmp --cap-add ALL --privileged dotnetdiag:3.1. Before we execute this command, we need to modify it by add arguments for:

  • Mounting to the same volume as the running application
  • Be able to inspect the process list of the running application,
  • Be able to share the same networking as the running application,
  • Elevate execution for the new container

Without completing the steps listed above we will be unable to connect to the running application. For mounting the volume we can use the exact same argument as before (-v dotnetdiag:/tmp).

In order to get the process id, we need to join the same process namespace through the use of the –pid argument. The --pid offers two modes, container or host. For this article, we will connect to a specific container by name, though you can also connect to the container by id as well.

Like the process argument, we also need to join the same networking space as the running container. So we will use –net which can also be run in multiple modes. For this article, we will connect to the application via the container name.

Lastly, by default, Docker containers restrict a lot of what you can do with running processes, like run docker in docker. So we need to tell docker to run in privileged mode and what capabilities we require to have from our diagnostics container. For this we will use the --cap-add and the --privileged arguments. Click here to read more about runtime privilege and docker capabilities.

After putting it all together, here is the full command line that we will run:

docker run --rm -it --pid=container:app --net=container:app -v dotnetdiag:/tmp --cap-add ALL --privileged dotnetdiag:3.1

Now you should have an empty command line to run, so if we execute dotnet-counters ps you should see something similar to the following:

[email protected]:/diagnostics# dotnet-counters ps
       103 BlogApp    /app/BlogApp/bin/Release/netcoreapp3.1/BlogApp
         6 dotnet     /usr/share/dotnet/dotnet
        42 dotnet     /usr/share/dotnet/dotnet
        61 dotnet     /usr/share/dotnet/dotnet
       247 dotnet-counters /root/.dotnet/tools/dotnet-counters

Assuming that your application is running under process id 103 then we would execute the following command to view the counters:

[email protected]:/diagnostics# dotnet-counters monitor -p 103 System.Runtime Microsoft.AspNetCore.Hosting

Recap

In order to diagnose a running docker image from another docker image, you need to:

  • Mount the /tmp on the application image prior to starting the application
  • Create a diagnostic image with your diagnostics tools
  • Run your diagnostics image with the following arguments:
    • -v for mounting to the same volume as the application image
    • --pid for joining the same process space
    • --net for joining the same network
    • --privileged for requesting additional permissions to cross container boundaries, and
    • --cap-add ALL for adding the ability to list processes etc.

Happy diagnostics!