Cross Platform Development with Docker and dotnet Core

Cross platform development with dotnet is here and it’s awesome.  However, heterogeneous development environments bring their own complications.  Modern dotnet development is often done with traditional tooling such as Visual Studio for windows,  but the landscape is changing quickly.  We recently had a project where some developers were using VS2017 on Windows, others used VS for Mac and still others used VSCode.

Adding docker support to a project in Visual Studio is as simple as a right click, but we found it a little more challenging to set up debugging in VSCode.  Beyond that it was a chore to get all of these things to play well together.  Once we did it was easy to maintain, but figuring out the “how” took some trial and error.

There is a lot of detail in this post, but bottom line is that our goal is to be able to debug through our simple dotnet core app, regardless of what tool or what platform we are on.  All of the code in this post can be found here: https://github.com/jtoussaint/dotnet-containers

Being able to debug across platforms.

Using Visual Studio

Debugging docker in Visual Studio is mostly OOTB. We started by creating a new aspnet core app and added docker support.  Visual Studio generates a Docker file and a docker compose project.  This was a good starting point, but we wanted to make a couple changes.
  1. We wanted to move the docker compose project into a “compose” folder.  We did this mainly so we could type dotnet build from the root folder and not be asked what project we wanted to build.
  2. We changed the base image to use alpine linux to reduce the footprint of our containers in QA and PROD.

Docker Compose

Visual Studio creates a file named docker-compose.override.yml to configure settings that need to be different when debugging. The tooling uses this file when starting a debug session.  We only made a couple changes here.
  1. We changed the docker file that we were pointing at.
  2. We added in some environment variables and a port mapping.
services:
  web:
    image: web:debug
    build:
      dockerfile: ../Website/Dockerfile.vs.debug
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - BASE_PATH=/app
    ports:
      - "5002:80"

Docker file

We started with the docker file generated by Visual Studio. It compiles the app and pushes the published content to the /app folder. This file is very similar to the original Dockerfile that is used in our CI/CD process. The only difference is our base image.  At time of this posting remote debugging on alpine linux does not work without some extra gymnastics.  We still wanted to use alpine in QA/PROD so we setup a different docker file for debugging locally.

FROM microsoft/dotnet:2.1.4-aspnetcore-runtime AS base
WORKDIR /app
EXPOSE 80

FROM microsoft/dotnet:2.1.401-sdk AS build
WORKDIR /src
COPY . .
RUN dotnet restore Website/Website.csproj

WORKDIR /src/Website
RUN dotnet build Website.csproj -c Release -o /app

FROM build AS publish
RUN dotnet publish Website.csproj -c Release -o /app

FROM base AS final
WORKDIR /app
COPY --from=publish /app .
ENTRYPOINT ["dotnet", "Website.dll"]

Putting it all together

At this point you can simply open up the solution file in Visual Studio (mac or windows) and press the debug button.  You do need to have docker installed and configured with linux container support.

Using VSCode

The VSCode setup has three components. The steps are the same on windows and mac, although you need to use the scripts in the appropriate folder mac-cli vs win-cli.

 

Docker Compose

There is a configuration override for VSCode in docker-compose.  It builds on the settings defined in docker-compose.yml and docker-compose.override.yml. Notice how this compose file only specifies values that are specific to running and debugging with VSCode? Other values such as environment variables and port mappings can be found in the docker-compose.override.yml file, providing a single place to configure these values between Visual Studio and VSCode

 

services:
  web:
    build:
      dockerfile: ../Website/Dockerfile.vscode.debug
    volumes: 
      - ../Website/bin/pub/:/app
.

This compose file is used with build, start and stop scripts in the mac-cli and win-cli folders.  As an example of our support scripts, this is what the mac-cli/start-containers.sh script looks like. It runs a docker compose up using all three configuration files and runs in detached mode so the container will continue to run in the background.

#!/bin/bash


pushd ../compose


docker-compose -f docker-compose.yml \
-f docker-compose.override.yml \
-f docker-compose.vscode.debug.yml \
--project-name dotnet-containers \
up -d


popd

Notice the project name of dotnet-containers in the script above? This name is used as a prefix for the container name which can be seen by doing a docker ps after running the start script. We will use that name in a later step.

docker ps output

Docker file

The docker file for VSCode is fairly straight forward and is comprised of four steps.
  1. It is based on the aspnet core runtime image and creates an /app directory.
  2. It downloads the latest linux remote debugger and installs it.
  3. It exposes port 80
  4. It performs a `tail -f /dev/null` to keep the container running indefinitely.
FROM microsoft/dotnet:2.1.4-aspnetcore-runtime
RUN mkdir app

#Install debugger
RUN apt-get update
RUN apt-get install curl -y unzip
RUN curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg

EXPOSE 80/tcp

#Keep the debugger container on
ENTRYPOINT ["tail", "-f", "/dev/null"]

VSCode Task

The final step is the glue that ties everything together and enables remote debugging from within VSCode. The json excerpt below is from the .vscode/launch.json file and it has the following key settings.
  1. The program value is set to app/Website.dll. This is because my web app is named “Website” and we are mounting to an “app” folder within the container.
  2. We define a source file map from our ${workspaceRoot}/Website folder to the /app folder within the container.
  3. We invoke the docker command line telling it to connect to our container and start the remote debugger. Note that container name is coming from a combination of the compose project name and the service name within the compose file.
{
    "name": "website",
    "type": "coreclr",
    "request": "launch",    
    "preLaunchTask": "publish",   
    "program": "/app/Website.dll",
    "sourceFileMap": {
        "/app": "${workspaceRoot}/Website"
    },       
    "pipeTransport": {
        "pipeProgram": "docker",
        "pipeCwd": "${workspaceRoot}",
        "pipeArgs": [
            "exec -i dotnet-containers_web_1"
        ],
        "debuggerPath": "/vsdbg/vsdbg",
        "quoteArgs": false
    }
},

Putting it all together

Debugging in VSCode is a little more involved that Visual Studio, but not by much.  The instructions below are for a mac, but you can follow the same steps on windows using the appropriate scripts.  You will need to have docker installed and configured for linux containers.

  1. Go to the mac-cli folder and run the build-containers.sh script.  This will build your containers locally.  Note, you will need to repeat this step if you change the Dockerfile.vscode.debug file.
  2. Run the start-containers.sh script to start your container locally.  Note, if you were previously debugging in Visual Studio you will need to stop those containers by hand.  Both containers will try and bind to port 5002 which will cause a conflict.
  3. Open up VSCode and start debugging.

Leave a Reply

avatar
  Subscribe  
Notify of