Orchestrating Visual Studio Code : Part 3 : Debugging Docker Containers

When working with multiple microservices, it is important to be able to run a debugger inside the docker container. In this article, we will learn how to load vsdbg - the Visual Studio Debugger - inside of a docker image and attach to it using VS Code's launchers.

The Files

The following files will be created in this section:

.
├── Dockerfile
├── docker-compose.yml
├── scripts
    ├── project-tasks.ps1
    └── project-tasks.sh
Source

The Dockerfile

We are using a multi-stage dockerfile to load a base image containing the Visual Studio Code debugger with the following steps:

  • Create a base image layer from aspnetcore2.0.
  • Download the VSCode debugger vsdbg.
  • Create a build image layer from aspnetcore-build.
  • Copy the contents of the web project WebProject.
  • Publish the web project into /publish folder.
  • Create a production layer from the base layer.
  • Copy the /publish folder contents into the production layer.

The ENTRYPOINT for the dockerfile serves dual purposes:

  • Run the application normally.
  • If the ENABLE_DEBUGGING environmental variable is set, sleep the application and listen for debug commands
# #############################################################################
# Development docker image with support for Visual Studio debugging integration
#

# #############################################################################
# BASE IMAGE
FROM microsoft/aspnetcore:2.0 AS base
WORKDIR /app
EXPOSE 80

# vscode debugging support
WORKDIR /vsdbg
RUN apt-get update \
    && apt-get install -y --no-install-recommends \
        unzip \
    && rm -rf /var/lib/apt/lists/* \
    && curl -sSL https://aka.ms/getvsdbgsh | bash /dev/stdin -v latest -l /vsdbg


# #############################################################################
# BUILDER IMAGE
FROM microsoft/aspnetcore-build:2.0 AS builder
ENV NUGET_XMLDOC_MODE skip

# publish
COPY . /app
WORKDIR /app/src
RUN dotnet publish -f netcoreapp2.0 -r debian.8-x64 -c Debug -o /publish -v quiet


# #############################################################################
# PRODUCTION IMAGE
FROM base AS production
WORKDIR /app
COPY --from=builder /publish .

# Kick off a container just to wait debugger to attach and run the app
ENTRYPOINT ["/bin/bash", "-c", "if [ \"$REMOTE_DEBUGGING\" = \"enabled\" ]; then sleep infinity; else dotnet WebProject.dll; fi"]

# #############################################################################
Dockerfile

The docker-compose file

The docker-compose file contains a single service for creating the docker container.
The REMOTE_DEBUGGING variable is set via a VSCode pre-launch task.

version: '3'

services:

  docker-debug-webapp:
    container_name: docker-debug-webapp
    image: docker-debug-webapp
    build: 
      context: .
      dockerfile: Dockerfile
    environment:
      - ASPNETCORE_ENVIRONMENT=development
      - REMOTE_DEBUGGING=${REMOTE_DEBUGGING}
    ports:
      - "5000:80"
    networks:
      - dev-network
    tty: true
    stdin_open: true

networks:
  dev-network:
    driver: bridge
docker-compose.yml

The setup scripts

The setup scripts project-tasks.sh and project-tasks.ps1 are used to build and compose the container before launching the debugger.

.
├── scripts
│   ├── project-tasks.ps1
│   └── project-tasks.sh

We will be using the bash project-tasks.sh script for this exercise. If you are on windows, a PowerShell project-tasks.ps script is provided in the GitHub source.

The compose() helper method

The compose() method is called by the task to stage your container. It supports multiple docker.{environment}.yml files. The default is docker-compose.yml.

compose () {

    echo -e "${GREEN}"
    echo -e "++++++++++++++++++++++++++++++++++++++++++++++++"
    echo -e "+ Composing docker images                       "
    echo -e "++++++++++++++++++++++++++++++++++++++++++++++++"
    echo -e "${RESTORE}"

    if [[ -z $ENVIRONMENT ]]; then
        ENVIRONMENT="debug"
    fi

    composeFileName="docker-compose.yml"
    if [[ $ENVIRONMENT != "debug" ]]; then
        composeFileName="docker-compose.$ENVIRONMENT.yml"
    fi

    if [[ ! -f $composeFileName ]]; then
        echo -e "${RED} $ENVIRONMENT is not a valid parameter. File '$composeFileName' does not exist. ${RESTORE}\n"
    else

        echo -e "${YELLOW} Building the image $imageName ($ENVIRONMENT). ${RESTORE}\n"
        docker-compose -f "$composeFileName" build

        echo -e "${YELLOW} Creating the container $imageName ${RESTORE}\n"
        docker-compose -f $composeFileName kill
        docker-compose -f $composeFileName up -d
    fi
}

...

case "$1" in
    "composeForDebug")
        export REMOTE_DEBUGGING="enabled"
        compose
        ;;
    *)
        showUsage
        ;;
esac
project-tasks.sh

Note the export REMOTE_DEBUGGING environmental variable. It is being set so that docker-compose can pass it to the Dockerfile's ENTRYPOINT. This will pause the container so that it can listen for vsdbg debugging commands from the IDE. It also allows us to use the compose() method to run our containers without debugging enabled.


The VSCode Task

The Visual Studio Code tasks.json is used to link the setup scripts.
Links to the Windows PowerShell and OSX/Linux Bash scripts. It is used to create our docker image and container:

  • Creates the image.
  • Kills any running instances.
  • Starts the container.

Add the following to your .vscode/tasks.json:

{
    "label": "compose-for-debug",
    "type": "shell",
    "osx": {
        "command": "bash ./scripts/project-tasks.sh composeForDebug"
    },
    "presentation": {
        "echo": true,
        "reveal": "always",
        "focus": true,
        "panel": "dedicated"
    },
    "problemMatcher": [],
    "windows": {
        "command": ".\\scripts\\project-tasks.ps1 -ComposeForDebug"
    }
}
tasks.json

The VSCode Launcher

The Visual Studio Code launch.json orchestrates the application with the following settings:

  • Configures apreLaunchTask called compose-for-debug to build and compose the docker container.
  • Sets the REMOTE_DEBUGGING env variable to allow the container debugger vsdbg to listen for commands.
  • Configures the pipeTransport with the path /vsdgb/vsdbgto the debugger within the container and the command docker and args exec -i that should be executed to begin debugging.
  • Sets the sourceFileMap to allow breakpoints on the local filesystem to line-up with the compiled source in the container.
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Docker Launch",
            "type": "coreclr",
            "request": "launch",
            "preLaunchTask": "compose-for-debug",
            "cwd": "/app",
            "program": "/app/WebApp.dll",
            "env": {
                "ASPNETCORE_ENVIRONMENT": "development",
                "REMOTE_DEBUGGING": "true"
            },
            "sourceFileMap": {
                "/app": "${workspaceRoot}"
            },
            "launchBrowser": {
                "enabled": true,
                "args": "http://localhost:5000/api/values",
                "windows": {
                    "command": "cmd.exe",
                    "args": "/C start http://localhost:5000/api/values"
                },
                "osx": {
                    "command": "open"
                }
            },
            "pipeTransport": {
                "debuggerPath": "/vsdbg/vsdbg",
                "pipeProgram": "docker",
                "pipeCwd": "${workspaceRoot}",
                "pipeArgs": [
                    "exec -i docker-debug-webapp"
                ],
                "quoteArgs": false
            }
        }
    ]
}
launch.json

Debugging Your App

At this point, you should be able to run the debug task from within VSCode. Press F1, select Run Tasks, and select compose-for-debug. You application will start with a debugger attached.


Composing Without Debug

I've included an additional task compose that allows you to run your docker container without the debugger attached. This is useful in microservice environments where you want to run several services to work against.

In a later post, I'll be showing how you can orchestrate a large number of microservices using VSCode workspaces and the compose task.

The Source Code

You can find the source code for this article in the following repository under the part-2-tasks-and-launchers branch:

https://github.com/christophla/blog-orchestrating-vscode/tree/part-3-debugging-docker

Next Post : Running Docker Containers

In the next post we will learn how to run stand-along docker container without the debugger attached. This is useful when orchestrating several dependent microservices in a large application.


Let's run some containers!

Previous Post : Tasks and Launchers

https://christophertown.com/orchestrating-vscode-tasks-and-launchers