I have recently been creating a new .NET Core application, which I've been running inside of a Docker container. I've been creating this using VSCode so had to spend some time linking the IDE to the container so that I could use the debugging features. I wanted to document the process here for anyone else who wants to work with this setup.
Setting up the application
For this example, I’m going to use the sample .Net Core application that Microsoft use on their documentation site here. You can follow that document till the end of the Create .NET Core app section and you’ll then have the application ready to be loaded into a container. Don’t follow the next sections covering Publishing .NET Core app and Create the Dockerfile as I’m going to doing it slight differently in the following sections below.
Setting up the Dockerfile
So first up we’re going to create the Dockerfile used to build our container. Now in the Microsoft docs example above, they show performing a manual publish of the .NET Core project, then packaging the output into the container. I don’t like this approach though as it means you need to install the .NET Core SDK on your host machine, and one of the joys of working with Docker is that it allows you to keep your host machine clean. No instead we’re going to create a multi-stage Dockerfile to first build our solution for us, and then to package it into the container image.
If you take a look at the Dockerfile you can see it performs a few actions, in the first stage we
- Pull in the .NET Core SDK image.
- Copy the project file into the stage.
- Perform a dotnet restore to load the projects dependencies.
- Copy in the files from the projects.
- Perform a dotnet publish to a directory called out.
At this point we've got a published version of the application and we're ready to build the final stage which is going to be used for our image. Note that it is instead built against the .NET Core Runtime image instead of the SDK image. This is because we've already built the application in the previous stage so we don’t need to include the extra features the SDK image introduces, all we need are the Runtime features - allowing us to build a much smaller image. The final stage performs the following steps:
- Pull in the .NET Core Runtime image.
- Copy the built application files from the previous stage.
- Updates the image then installs some extra packages (We’ll discuss this more in the next section).
- Sets the EntryPoint for the image.
At this point we can use this Dockerfile to build the image, using the following command
docker build -t myapp .
Here we're executing the docker build command, we're passing in the -t switch to give the image a name of myapp and finally, the . path tells the command to execute at the current location.
Now we have a built image, we can create a container based on this image using the following command.
docker run myapp
So now we have a running docker container for our image, but how can we attach a debugger to this and the step through the code? Well for that we’re going to need to configure VSCode.
So in order to connect to the Docker container we need to install a couple of packages into the image at build time, this is what we performed in step 3 of the final stage above that I said I would come back to. What we’re actually doing here is installing a couple of dependencies that allow us to install VSDbg, which is the Visual Studio Remote Debugger. Installing this allows VSCode to connect to the container and debug the code running there.
In order for VSCode to be able to connect to the container we need to edit its launch.json file which you will find in your .vscode folder. You can see the one I have used here:
Two of the key parts are:
- sourceFileMap: This section is used to map the application in the container to the location of your source on the host machine.
- pipeArgs: This line tells VSCode how to connect to the container, in this case it’s going to run an exec command in interactive mode against a container named myapp.
So the last part that I want to do is to wrap the creation of this container up into a docker-compose action. This makes it very simple to control things like the Container Name, which means I wont have to update my launch.json everytime I want to connect to a new container.
Setting up Docker-Compose
Docker-Compose files are used to control how docker containers are created. They give you a way to store and version control the settings that are used to generate containers. In this case it will be a very simple docker-compose file as the only element we need to control here is the container name. You can see the docker-compose.yml contents I used here:
You can see above that I only have a single service in my docker-compose.yml file. We specify the name to be the same name that was set in the launch.json file earlier and then we don’t actually point to an image, in this case we’re giving the build command. We can execute this docker-compose file with one simple command:
This will instruct docker to go and build my image, then create a container based on the newly built image with the container name myapp.
Once the container is running we can then simply hit F5 in vscode, choose the process and begin debugging our application, as easy as that!