Skip to content

Local and Remote Debugging with Docker

Aleksandr Arofikin edited this page Mar 2, 2023 · 4 revisions

Docker Examples

Some examples below are in lua, if you want to set up nvim-dap in a .vim file, you have to wrap the code blocks like this:

lua <<EOF
-- lua code goes here
EOF

See :help lua-commands

Otherwise you can add a lua/ dir inside the dir that hosts your init.vim and configure all of this in raw lua like this:

cd ~/.config/nvim
mkdir lua
cd lua
touch debuggers.lua

then you can require this lua file inside your init.vim like this:

lua require"debuggers"

Contents

Shared

There are some basic steps that remain the same across both remote and local Docker Debugging:

  1. Expose a port in your Docker container responsible for debugging.
  2. Modify your containers security settings to allow ptrace to run correctly.
  3. Install the program/package responsible for starting the debug server (and maybe adapter) in your container.
  4. Install the program/package responsible for launching the debug adapter locally (if necessary).
  5. Start/Attach the debug server (and potentially adapter) inside your container to your code.
  6. Launch the debug adapter locally (if not done by the debug server in the container).
  7. Connect nvim-dap to either the debug adapter you spawned locally or the adapter spun up by your debug server in the container.

For remote debugging you are probably best off creating an SSH tunnel between your remote Docker instance and your local machine for the port you designate for debugging: Running something like:

ssh -L localhost:${your_debug_port}:${remote_docker_host}:${your_debug_port} ${remote_docker_host} tail -f /dev/null

from inside your container should do the trick.

Docker Security Settings

For debugging in Docker (both remotely and locally) you will need to allow a couple of security settings see here for more information. These settings can either be achieved:

  • through the command line as docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined
  • in a Docker compose file via:
security_opt:
  - seccomp:unconfined
cap_add:
  - SYS_PTRACE

Go

  • Install vscode-go. This lets you spin up a debug adapter locally and is not necessary inside the Docker container.
    • git clone https://github.com/golang/vscode-go
    • cd vscode-go
    • npm run compile
  • Install delve on both your local machine and the Docker container.
    • go get github.com/go-delve/delve/cmd/dlv
    • or via package manager (pacman -S delve)
    • or in Docker RUN go get github.com/go-delve/delve/cmd/dlv
  • Attach delve to your code in the container:
dlv --listen :${your_debug_port} --headless --accept-multiclient --api-version 2 attach $(pgrep -fn ${the_service_name})

This command could be replaced by:

dlv --listen :${your_debug_port} --headless --accept-multiclient --api-version 2 exec ${your_compiled_go_program}

If your container does not start the go process itself already.

  • Launch the debug adapter locally via nvim-dap:
local M = {}
M.launch_go_debugger = function(args)
    local dap = require "dap"
    -- The adapter has not been started yet.
    -- Spin it up.
    goLaunchAdapter = {
        type = "executable";
        command = "node";
        args = {os.getenv("HOME") .. "/vscode-go/dist/debugAdapter.js"};
    }

    goLaunchConfig = {
        type = "go";
        request = "attach";
        mode = "remote";
        name = "Remote Attached Debugger";
        dlvToolPath = os.getenv('HOME') .. "/go/bin/dlv";  -- Or wherever your local delve lives.
        remotePath = ${where_your_local_copy_of_the_code_in_your_container_lives};
        port = ${your_exposed_container_port};
        cwd = vim.fn.getcwd();
   }
   -- If you want you can even have nvim be responsible for the `delve` launch step above:
   --  vim.fn.system({"${some_script_that_starts_dlv_in_your_container}", ${script_args})
   local session = dap.launch(goLaunchAdapter, goLaunchConfig);
    if session == nil then
        io.write("Error launching adapter");
    end
    dap.repl.open()
end

Alternative Golang setup with docker compose

go-delve.Dockerfile

FROM golang:1.20
RUN go install github.com/go-delve/delve/cmd/dlv@latest
ENTRYPOINT /go/bin/dlv

docker-compose.yaml

services:
  debug:
    build:
      context: .
      dockerfile: ./docker/go-delve.Dockerfile
    entrypoint: dlv
    working_dir: /opt/app
    restart: unless-stopped
    ports:
      - 9004:9004
    command:
      - "debug"
      - "--headless"
      - "--listen=:9004"
      - "--api-version=2"
      - "--accept-multiclient"
      - "--log"
      #- "--log-output=debugger,rpc,dap"
      - "./cmd/myapp"
      - "--"
      - "-c" # myapp arguments
      - "./config.yaml"
    volumes:
      - ./:/opt/app

nvim dap configuration

local dap = {
  adapters = {
    go = {
      type = "server",
      port = 9004,
    }
  },
  configurations = {
    go = {
      {
         type = "go",
         name = "delve container debug",
         request = "attach",
         mode = "remote",
         substitutepath = {{
           from = "${workspaceFolder}",
           to = "/opt/app",
         }},   
      }
    },
  }
}

Python

Python Docker debugging is a little simpler since the Python debug package debugpy can run both the server and adapter inside your container.

  • Install Debugpy both locally and in your Docker container:
    • This does not need to be in a separate venv if there is a different one you would prefer to install to feel free.
python -m venv path/to/virtualenvs/debugpy
path/to/virtualenvs/debugpy/bin/python -m pip install debugpy
  • Attach a debugpy server to your code in the container and launch a debug adapter.
path/to/virtualenvs/debugpy/bin/python -m debugpy --listen 0.0.0.0:${your_debug_port} --pid $(pgrep -nf ${your_running_program})

See here for some more examples. Unlike with our Go example the debugpy command will also take care of launching our debug adapter so our nvim-dap code needs to be an attach configuration, not a launch one.

  • Attach nvim-dap to the running debugpy DAP adapter
local M = {}
M.attach_python_debugger = function(args)
    local dap = require "dap"
    local host = args[1] -- This should be configured for remote debugging if your SSH tunnel is setup.
    -- You can even make nvim responsible for starting the debugpy server/adapter:
    --  vim.fn.system({"${some_script_that_starts_debugpy_in_your_container}", ${script_args}})
    pythonAttachAdapter = {
        type = "server";
        host = host;
        port = tonumber(${your_debug_port});
    }
    pythonAttachConfig = {
        type = "python";
        request = "attach";
        connect = {
            port = tonumber(${your_debug_port});
            host = host;
        };
        mode = "remote";
        name = "Remote Attached Debugger";
        cwd = vim.fn.getcwd();
        pathMappings = {
            {
                localRoot = vim.fn.getcwd(); -- Wherever your Python code lives locally.
                remoteRoot = "/usr/src/app"; -- Wherever your Python code lives in the container.
            };
        };
    }
    local session = dap.attach(host, tonumber(${your_debug_port}), pythonAttachConfig)
    if session == nil then
        io.write("Error launching adapter");
    end
    dap.repl.open()
end