Browse Source

Initial commit

chodak166 4 weeks ago
commit
cfed8c150d
  1. 102
      README.md
  2. 26
      common/README.md
  3. 20
      common/project/CMakeLists.txt
  4. 88
      common/project/src/calculator.cpp
  5. 34
      common/project/src/calculator.h
  6. 19
      common/project/src/main.cpp
  7. 92
      devstation/Dockerfile
  8. 87
      devstation/README.md
  9. 69
      devstation/nvim-config/lua/plugins/dap.lua
  10. 20
      devstation/nvim-config/lua/plugins/lsp.lua
  11. 9
      devstation/nvim-config/lua/plugins/theme.lua
  12. 32
      devstation/scripts/precompile-treesitter.lua
  13. 63
      docker-compose.yml
  14. 54
      remote/Dockerfile
  15. 207
      remote/README.md
  16. 38
      remote/entrypoint.sh

102
README.md

@ -0,0 +1,102 @@
# DAP Remote Debugging Example
A minimal example showing how to debug a C++ program running in a **remote**
container from any DAP-compatible client — Neovim, VSCode, Emacs, and others.
```
┌──────────────────────┐ ┌──────────────────────┐
│ YOUR EDITOR │ DAP over TCP │ REMOTE │
│ (Neovim, VSCode,…) │ ◀──────────────────── ▶│ (Alpine container) │
│ │ port 13000 │ │
│ • source code │ │ • codelldb DAP │
│ • breakpoints │ │ server │
│ • variable inspection │ │ • debugapp (C++17) │
│ │ │ • socat forwarder │
└──────────────────────┘ └──────────────────────┘
```
The remote container runs [codelldb](https://github.com/vadimcn/codelldb) as a
DAP (Debug Adapter Protocol) server. Your editor connects to it over TCP,
sends a `launch` request, and codelldb starts the program **on the remote**.
This avoids gdbserver's entry-point assembly issue entirely and keeps source
paths clean.
## Quick start (full setup)
The included `docker-compose.yml` spins up both the remote container and a
Neovim-based devstation:
```bash
docker compose build
docker compose up -d remote # start the codelldb DAP server
docker compose run --rm devstation # interactive Neovim session
```
Inside Neovim, open `src/calculator.cpp`, press `<leader>db` on a line to set
a breakpoint, then `<leader>dc` and select **"Remote launch (codelldb remote:13000)"**.
See **[devstation/README.md](devstation/README.md)** for the full Neovim keybinding
reference.
## Using your own editor
You don't need the devstation container. Build and run the remote alone, then
connect from whatever editor you prefer:
```bash
docker build -t dap-debug-remote -f remote/Dockerfile .
docker run -d --name remote \
-p 13000:13000 \
--cap-add SYS_PTRACE \
--security-opt seccomp=unconfined \
dap-debug-remote
```
Then follow the guide for your editor in
**[remote/README.md](remote/README.md)** — it covers VSCode, Neovim, Emacs,
and generic DAP clients.
## Repository layout
```
.
├── docker-compose.yml # Two-service compose (remote + devstation)
├── README.md # This file
├── common/
│ └── project/ # C++ demo project (shared by both containers)
│ ├── CMakeLists.txt
│ └── src/
├── remote/
│ ├── Dockerfile # Alpine + codelldb + socat + project build
│ ├── entrypoint.sh # codelldb DAP server + socat forwarder loop
│ └── README.md # Standalone usage + VSCode/other editor guides
└── devstation/
├── Dockerfile # Alpine + Neovim (LazyVim) + nvim-dap
├── README.md # Neovim keybindings and setup details
├── nvim-config/
│ └── lua/plugins/ # LazyVim plugin specs (theme, lsp, dap)
└── scripts/
└── precompile-treesitter.lua
```
## How it works
1. **codelldb** runs on the remote as a DAP server (`--port 13001
--multi-session`). It binds to `127.0.0.1` only, so **socat** forwards
external connections from `0.0.0.0:13000` to codelldb's listener.
2. Your editor connects to port 13000 and sends a standard DAP `launch`
request. codelldb starts the program on the remote, captures stdout/stderr,
and streams output events back.
3. The project source lives at `/project` on the remote (and on the devstation
if you use it), so debug-info paths match and editors open source files
correctly when stopped at a breakpoint.
4. The remote entrypoint restarts codelldb after each session, so you can
reconnect without restarting the container.
5. `SYS_PTRACE` + `seccomp:unconfined` on the remote allow codelldb to debug
the child process.
> **Note:** codelldb is a glibc binary but runs on Alpine (musl) via `gcompat`
> plus a tiny shim library providing the missing `__res_init` symbol.

26
common/README.md

@ -0,0 +1,26 @@
# Demo project
A small C++17 calculator used as the debug target for this example. The same
source code is copied into both the **remote** container (where it is compiled
and debugged) and the **devstation** container (where you edit and browse it).
## Build
```bash
cmake -DCMAKE_BUILD_TYPE=Debug -S . -B build
cmake --build build -j
```
This produces `build/debugapp` compiled with `-g3 -O0` (full debug info, zero
optimisation) and generates `compile_commands.json` for clangd.
## Good debugging targets
| Feature | Where to try it |
|---|---|
| **Breakpoints** | Any method in `Calculator` — e.g. `Calculator::divide` |
| **Conditional breakpoint** | In `runDemo()` at the `for` loop, set condition `i == 3` |
| **Variable inspection** | `m_accumulator`, `m_history`, `m_callCount` |
| **Watch expressions** | `m_history.size()`, `sum + diff` |
| **Call stack** | Step into `formatResult()` to see the stack frames |
| **Step over / into / out** | The `runDemo()` loop exercises all three |

20
common/project/CMakeLists.txt

@ -0,0 +1,20 @@
cmake_minimum_required(VERSION 3.16)
project(dap_debug_example CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
# Full debug info, zero optimisation ideal for stepping and inspection.
set(CMAKE_CXX_FLAGS_DEBUG "-g3 -O0")
add_executable(debugapp
src/main.cpp
src/calculator.cpp
)
target_include_directories(debugapp PRIVATE src)
target_compile_options(debugapp PRIVATE -g3 -O0 -Wall -Wextra)
# Generate compile_commands.json for clangd/LSP integration.
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

88
common/project/src/calculator.cpp

@ -0,0 +1,88 @@
#include "calculator.h"
#include <iostream>
#include <iomanip>
#include <sstream>
Calculator::Calculator()
: m_accumulator(0), m_callCount(0) {}
int Calculator::add(int a, int b) {
m_callCount++;
int result = a + b;
m_history.push_back(result);
return result;
}
int Calculator::subtract(int a, int b) {
m_callCount++;
int result = a - b;
m_history.push_back(result);
return result;
}
int Calculator::multiply(int a, int b) {
m_callCount++;
int result = a * b;
m_history.push_back(result);
return result;
}
double Calculator::divide(int a, int b) {
m_callCount++;
if (b == 0) {
log("Warning: division by zero!");
return 0.0;
}
double result = static_cast<double>(a) / static_cast<double>(b);
m_history.push_back(static_cast<int>(result));
return result;
}
void Calculator::accumulate(int value) {
m_accumulator += value;
m_history.push_back(value);
}
int Calculator::getAccumulator() const {
return m_accumulator;
}
void Calculator::log(const std::string& msg) {
std::cerr << "[Calculator] " << msg << std::endl;
}
void Calculator::runDemo() {
log("Starting demo...");
for (int i = 1; i <= 5; ++i) {
// Good place for a conditional breakpoint, e.g.: i == 3
int sum = add(i, i * 2);
accumulate(sum);
int diff = subtract(sum, i);
int product = multiply(diff, i);
if (product > 0) {
double quotient = divide(product, i);
std::string formatted = formatResult("divide", product, i, quotient);
std::cout << "Iteration " << i << ": "
<< "sum=" << sum << ", "
<< "diff=" << diff << ", "
<< "product=" << product << ", "
<< formatted
<< std::endl;
}
}
log("Demo complete. Accumulator = " + std::to_string(m_accumulator));
log("Total calls = " + std::to_string(m_callCount));
log("History size = " + std::to_string(m_history.size()));
}
std::string formatResult(const std::string& op, int a, int b, double result) {
std::ostringstream oss;
oss << op << "(" << a << ", " << b << ") = "
<< std::fixed << std::setprecision(2) << result;
return oss.str();
}

34
common/project/src/calculator.h

@ -0,0 +1,34 @@
#pragma once
#include <string>
#include <vector>
/// A small calculator class designed to demonstrate DAP debugging features:
/// stepping, variable inspection, call stack navigation, conditional
/// breakpoints, and watch expressions.
class Calculator {
public:
Calculator();
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(int a, int b);
void accumulate(int value);
int getAccumulator() const;
int getCallCount() const { return m_callCount; }
/// Runs a short interactive demo loop — the main target for debugging.
void runDemo();
private:
int m_accumulator;
int m_callCount;
std::vector<int> m_history;
void log(const std::string& msg);
};
/// Free helper function — good for demonstrating the "step out" action.
std::string formatResult(const std::string& op, int a, int b, double result);

19
common/project/src/main.cpp

@ -0,0 +1,19 @@
#include "calculator.h"
#include <iostream>
int main() {
std::cout << "=== DAP Debugging Example ===" << std::endl;
std::cout << "Set breakpoints in calculator.cpp, then press <leader>dc." << std::endl;
std::cout << std::endl;
Calculator calc;
calc.runDemo();
std::cout << std::endl;
std::cout << "Final accumulator value: " << calc.getAccumulator() << std::endl;
std::cout << "Total function calls: " << calc.getCallCount() << std::endl;
std::cout << "Program completed successfully." << std::endl;
return 0;
}

92
devstation/Dockerfile

@ -0,0 +1,92 @@
# syntax=docker/dockerfile:1
#
# Developer station: Neovim (LazyVim) with nvim-dap configured to connect
# to a codelldb DAP server running on the "remote" container.
#
# Builds the newest Neovim from source (Alpine uses musl, so prebuilt
# glibc binaries don't run here) and layers on LazyVim, Catppuccin, and
# the nvim-dap plugin stack. codelldb itself lives on the remote container.
#
# Build (run from repository root):
# docker build -t dap-debug-dev -f devstation/Dockerfile .
#
# Pin a specific Neovim release:
# docker build --build-arg NVIM_REF=v0.12.3 -t dap-debug-dev -f devstation/Dockerfile .
###############################################################################
# Stage 1 - build the newest Neovim from source
###############################################################################
FROM alpine:latest AS nvim-builder
ARG NVIM_REF=stable
RUN apk add --no-cache \
build-base cmake coreutils curl gettext-tiny-dev git \
linux-headers ninja unzip \
&& git clone --depth 1 --branch "${NVIM_REF}" https://github.com/neovim/neovim.git /tmp/neovim \
&& make -C /tmp/neovim \
CMAKE_BUILD_TYPE=Release \
CMAKE_EXTRA_FLAGS="-DCMAKE_INSTALL_PREFIX=/opt/nvim" \
&& make -C /tmp/neovim install \
&& strip /opt/nvim/bin/nvim
###############################################################################
# Stage 2 - runtime image with LazyVim and DAP configuration
###############################################################################
FROM alpine:latest
ENV HOME=/home/dev \
TERM=xterm-256color \
PATH="/opt/nvim/bin:${PATH}"
RUN addgroup -g 1000 dev \
&& adduser -D -u 1000 -G dev -h /home/dev -s /bin/ash dev
# LazyVim runtime dependencies + C++ toolchain for reading/browsing code.
# clang-extra-tools provides clangd for LSP code navigation.
RUN apk add --no-cache \
git curl ca-certificates \
ripgrep fd fzf \
build-base cmake ccmake \
gdb strace \
clang-extra-tools \
bash unzip less \
ncurses-terminfo \
tree-sitter-cli
# Neovim built in stage 1.
COPY --from=nvim-builder /opt/nvim /opt/nvim
# C++ demo project — source for editing, browsing, and setting breakpoints.
COPY --chown=dev:dev common/project/ /project/
USER dev
WORKDIR /project
RUN mkdir -p build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Debug .. \
&& make -j"$(nproc)" \
&& ln -sf /project/build/compile_commands.json /project/compile_commands.json
# ---------------------------------------------------------------------------
# LazyVim configuration
# ---------------------------------------------------------------------------
# LazyVim starter configuration (https://github.com/LazyVim/starter).
RUN git clone --depth 1 https://github.com/LazyVim/starter "${HOME}/.config/nvim" \
&& rm -rf "${HOME}/.config/nvim/.git"
# Plugin specs — theme, LSP, and DAP configuration.
# See devstation/nvim-config/lua/plugins/ for the source files.
COPY --chown=dev:dev devstation/nvim-config/lua/plugins/ "${HOME}/.config/nvim/lua/plugins/"
# Pre-install plugins so the editor is ready to use on the first launch.
RUN timeout 600 nvim --headless "+Lazy! sync" +qa || true
# Pre-compile LazyVim's treesitter parsers.
COPY --chown=dev:dev devstation/scripts/precompile-treesitter.lua /tmp/precompile-treesitter.lua
RUN timeout 900 nvim --headless -c "luafile /tmp/precompile-treesitter.lua" \
&& rm -f /tmp/precompile-treesitter.lua
CMD ["nvim"]

87
devstation/README.md

@ -0,0 +1,87 @@
# Devstation container
An Alpine container with Neovim (LazyVim) pre-configured for remote C++ DAP
debugging. It connects to the codelldb DAP server running on the **remote**
container.
## What's inside
| Component | Purpose |
|---|---|
| Neovim (built from source) | Editor — Alpine uses musl, so prebuilt glibc binaries don't run |
| [LazyVim](https://lazyvim.org) | Neovim distribution (pre-installed) |
| Catppuccin (Mocha) | Colour scheme |
| nvim-dap + dap-ui + virtual-text | DAP client, UI panels, inline variable text |
| clangd (via `clang-extra-tools`) | LSP for C/C++ code navigation |
| ripgrep, fd, fzf | Fuzzy finder and search backends |
## Quick start
```bash
# From the repository root:
docker compose build
docker compose up -d remote
docker compose run --rm devstation
```
Neovim launches automatically in `/project`.
## Debugging
1. Open a source file: `:e src/calculator.cpp`
2. Set a breakpoint: `<leader>db`
3. Start debugging: `<leader>dc`
4. Select **"Remote launch (codelldb remote:13000)"**
5. The program runs in the remote container and stops at your breakpoint.
6. dap-ui opens automatically with scopes, watch, call stack, and breakpoints.
## Key bindings
All debug keybindings are under the `<leader>d` prefix:
| Key | Action |
|---|---|
| `<leader>db` | Toggle breakpoint |
| `<leader>dB` | Conditional breakpoint |
| `<leader>dc` | Continue / start debug session |
| `<leader>dC` | Run to cursor |
| `<leader>di` | Step into |
| `<leader>dO` | Step over |
| `<leader>do` | Step out |
| `<leader>dp` | Pause |
| `<leader>dt` | Terminate session |
| `<leader>dr` | Toggle REPL |
| `<leader>du` | Toggle DAP UI |
| `<leader>de` | Evaluate expression (normal or visual mode) |
| `<leader>dw` | Hover (inspect variable under cursor) |
| `<leader>dj` / `<leader>dk` | Navigate call stack (down / up) |
| `<leader>dl` | Run last session |
| `<leader>dg` | Go to line (skip execution) |
| `<leader>ds` | Session info |
## Plugin configuration
LazyVim plugin specs live as regular files under
[`devstation/nvim-config/lua/plugins/`](nvim-config/lua/plugins/) and are
COPY'd into the image at build time:
| File | Contents |
|---|---|
| `theme.lua` | Catppuccin Mocha theme |
| `lsp.lua` | Disables Mason auto-install (glibc binaries crash on musl); uses system clangd |
| `dap.lua` | nvim-dap adapter (connects to `remote:13000`) + debug config + all `<leader>d` keybindings |
The treesitter pre-compilation script is at
[`devstation/scripts/precompile-treesitter.lua`](scripts/precompile-treesitter.lua).
To customise the DAP adapter (e.g. change host or port), edit the
`dap.adapters.codelldb` table in `dap.lua` and rebuild.
## Notes
- Named volumes (`nvim-data`, `nvim-state`, `nvim-cache`) persist plugins and
state across container restarts. Remove them with
`docker compose down -v` to start fresh.
- Mason's prebuilt binaries are glibc-linked and crash on Alpine/musl. LSP
servers are provided by system packages instead (`clang-extra-tools`).
- Treesitter parsers are pre-compiled during the image build.

69
devstation/nvim-config/lua/plugins/dap.lua

@ -0,0 +1,69 @@
-- DAP plugin stack: core client, UI, inline virtual text.
-- Provides <leader>d keybindings and codelldb adapter configuration.
return {
{
"mfussenegger/nvim-dap",
dependencies = {
"rcarriga/nvim-dap-ui",
"theHamsta/nvim-dap-virtual-text",
"nvim-neotest/nvim-nio",
},
keys = {
{ "<leader>d", "", desc = "+debug", mode = { "n", "v" } },
{ "<leader>db", function() require("dap").toggle_breakpoint() end, desc = "Toggle Breakpoint" },
{ "<leader>dB", function() require("dap").set_breakpoint(vim.fn.input("Breakpoint condition: ")) end, desc = "Conditional Breakpoint" },
{ "<leader>dc", function() require("dap").continue() end, desc = "Continue / Start" },
{ "<leader>dC", function() require("dap").run_to_cursor() end, desc = "Run to Cursor" },
{ "<leader>dg", function() require("dap").goto_() end, desc = "Go to Line (skip execution)" },
{ "<leader>di", function() require("dap").step_into() end, desc = "Step Into" },
{ "<leader>dj", function() require("dap").down() end, desc = "Frame Down" },
{ "<leader>dk", function() require("dap").up() end, desc = "Frame Up" },
{ "<leader>dl", function() require("dap").run_last() end, desc = "Run Last" },
{ "<leader>do", function() require("dap").step_out() end, desc = "Step Out" },
{ "<leader>dO", function() require("dap").step_over() end, desc = "Step Over" },
{ "<leader>dp", function() require("dap").pause() end, desc = "Pause" },
{ "<leader>dr", function() require("dap").repl.toggle() end, desc = "Toggle REPL" },
{ "<leader>ds", function() require("dap").session() end, desc = "Session Info" },
{ "<leader>dt", function() require("dap").terminate() end, desc = "Terminate" },
{ "<leader>dw", function() require("dap.ui.widgets").hover() end, desc = "Hover Widget" },
{ "<leader>du", function() require("dapui").toggle() end, desc = "Toggle DAP UI" },
{ "<leader>de", function() require("dapui").eval() end, desc = "Evaluate Expression", mode = { "n", "v" } },
},
config = function()
local dap = require("dap")
local dapui = require("dapui")
dapui.setup()
dap.listeners.after.event_initialized["dapui_config"] = function() dapui.open() end
dap.listeners.before.event_terminated["dapui_config"] = function() dapui.close() end
dap.listeners.before.event_exited["dapui_config"] = function() dapui.close() end
require("nvim-dap-virtual-text").setup()
-- Adapter: connect to codelldb running on the "remote" container.
-- codelldb listens as a DAP server on port 13000.
dap.adapters.codelldb = {
type = "server",
host = "remote",
port = 13000,
}
-- Debug configurations.
dap.configurations.cpp = {
{
-- Remote launch: codelldb on the remote starts the program.
-- Source paths in debug info (/project/src/...) match the
-- devstation's filesystem, so nvim-dap opens source files
-- correctly when stopped at a breakpoint.
name = "Remote launch (codelldb remote:13000)",
type = "codelldb",
request = "launch",
program = "/project/build/debugapp",
cwd = "/project",
stopOnEntry = false,
},
}
dap.configurations.c = dap.configurations.cpp
end,
},
}

20
devstation/nvim-config/lua/plugins/lsp.lua

@ -0,0 +1,20 @@
return {
{
"mason-org/mason.nvim",
opts = {
PATH = "append",
ensure_installed = {},
},
},
{
"neovim/nvim-lspconfig",
opts = {
servers = {
-- Prevent Mason from auto-installing these (glibc binaries crash
-- on Alpine/musl). clangd is provided by the system package.
lua_ls = { mason = false },
clangd = { mason = false },
},
},
},
}

9
devstation/nvim-config/lua/plugins/theme.lua

@ -0,0 +1,9 @@
return {
{
"catppuccin/nvim",
name = "catppuccin",
priority = 1000,
opts = { flavour = "mocha" },
},
{ "LazyVim/LazyVim", opts = { colorscheme = "catppuccin" } },
}

32
devstation/scripts/precompile-treesitter.lua

@ -0,0 +1,32 @@
local lazy = require("lazy")
local Plugin = require("lazy.core.plugin")
local dir, ts
for _, p in ipairs(lazy.plugins()) do
if p.name == "nvim-treesitter" then
dir, ts = p.dir, p
break
end
end
vim.opt.runtimepath:append(dir)
local ensure = Plugin.values(ts, "opts", false).ensure_installed or {}
local TS = require("nvim-treesitter")
local installed = TS.get_installed("parsers")
local missing = vim.tbl_filter(function(l)
return not vim.list_contains(installed, l)
end, ensure)
if #missing > 0 then
print("[precompile] installing treesitter parsers: " .. table.concat(missing, ", "))
local ok, err = pcall(function()
TS.install(missing, { summary = true }):wait()
end)
if not ok then
print("[precompile] warning: " .. tostring(err))
end
else
print("[precompile] all ensure_installed parsers already present")
end
vim.cmd("qa")

63
docker-compose.yml

@ -0,0 +1,63 @@
# DAP Debugging Example
#
# Two-container setup for remote C++ debugging with Neovim DAP:
#
# "remote" – Alpine container running codelldb as a DAP server.
# "devstation" – Alpine container with Neovim + LazyVim + nvim-dap.
#
# Usage:
# docker compose build
# docker compose up -d remote # start codelldb DAP server
# docker compose run --rm devstation # interactive Neovim session
#
# Plugins, caches, and state persist in named volumes across runs, so
# Neovim does not re-download plugins on every container start.
#
# Inside Neovim, open a source file (e.g. src/calculator.cpp), set a
# breakpoint with <leader>db, then press <leader>dc and select
# "Remote launch (codelldb remote:13000)".
services:
remote:
build:
context: .
dockerfile: remote/Dockerfile
ports:
- "13000:13000"
networks:
- debug-net
restart: unless-stopped
# SYS_PTRACE + seccomp:unconfined let codelldb control the child
# process (set breakpoints, read memory, single-step, etc.).
cap_add:
- SYS_PTRACE
security_opt:
- seccomp:unconfined
devstation:
build:
context: .
dockerfile: devstation/Dockerfile
stdin_open: true
tty: true
working_dir: /project
networks:
- debug-net
depends_on:
- remote
# Named volumes so Neovim plugins, caches, and state survive
# container removal. Without these, every `docker compose run`
# starts from scratch and re-downloads everything.
volumes:
- nvim-data:/home/dev/.local/share/nvim
- nvim-state:/home/dev/.local/state/nvim
- nvim-cache:/home/dev/.cache/nvim
networks:
debug-net:
driver: bridge
volumes:
nvim-data:
nvim-state:
nvim-cache:

54
remote/Dockerfile

@ -0,0 +1,54 @@
# syntax=docker/dockerfile:1
#
# Remote machine: runs codelldb as a DAP server. The developer station's
# nvim-dap connects to it over TCP and sends a standard DAP "launch"
# request — codelldb starts the program locally, so there is no
# gdbserver entry-point assembly issue and source paths resolve cleanly.
#
# Build (run from repository root):
# docker build -t dap-debug-remote -f remote/Dockerfile .
FROM alpine:latest
ARG CODELLDB_VERSION=1.12.2
RUN apk add --no-cache \
build-base cmake gdb \
curl unzip gcompat socat
WORKDIR /project
COPY common/project/ /project/
# Build with full debug info for a meaningful debugging experience.
RUN mkdir -p build \
&& cd build \
&& cmake -DCMAKE_BUILD_TYPE=Debug .. \
&& make -j"$(nproc)"
# codelldb DAP adapter — a glibc binary; gcompat provides the compatibility
# layer so it runs on musl/Alpine.
RUN mkdir -p /opt/codelldb \
&& curl -fsSL "https://github.com/vadimcn/codelldb/releases/download/v${CODELLDB_VERSION}/codelldb-linux-x64.vsix" \
-o /tmp/codelldb.vsix \
&& cd /opt/codelldb && unzip -q /tmp/codelldb.vsix \
&& rm /tmp/codelldb.vsix \
&& chmod +x /opt/codelldb/extension/adapter/codelldb
# codelldb needs __res_init / __res_ninit (glibc DNS resolver init) which
# gcompat does not provide. A tiny no-op shim fills the gap, then a wrapper
# script applies LD_PRELOAD transparently.
RUN printf 'int __res_init(void) { return 0; }\nint __res_ninit(void *s) { (void)s; return 0; }\n' \
> /tmp/resolv_shim.c \
&& gcc -shared -fPIC -o /usr/lib/libresolv_compat.so /tmp/resolv_shim.c \
&& rm /tmp/resolv_shim.c \
&& mv /opt/codelldb/extension/adapter/codelldb \
/opt/codelldb/extension/adapter/codelldb.bin \
&& printf '#!/bin/sh\nLD_PRELOAD=/usr/lib/libresolv_compat.so exec /opt/codelldb/extension/adapter/codelldb.bin "$@"\n' \
> /opt/codelldb/extension/adapter/codelldb \
&& chmod +x /opt/codelldb/extension/adapter/codelldb
COPY remote/entrypoint.sh /entrypoint.sh
EXPOSE 13000
ENTRYPOINT ["/entrypoint.sh"]

207
remote/README.md

@ -0,0 +1,207 @@
# Remote debugging container
An Alpine container that builds a C++ demo project and runs
[codelldb](https://github.com/vadimcn/codelldb) as a DAP (Debug Adapter
Protocol) server on **port 13000**. Any DAP-compatible editor can connect to
it, set breakpoints, and debug the program running inside the container.
```
YOUR EDITOR ──DAP over TCP:13000──▶ REMOTE CONTAINER
├── codelldb (DAP server, 127.0.0.1:13001)
├── socat (forwarder, 0.0.0.0:13000 ─▶ 13001)
└── /project/build/debugapp (C++17, -g3 -O0)
```
## Build and run standalone
You don't need docker-compose or the devstation container — the remote works
on its own.
```bash
# Build (run from repository root)
docker build -t dap-debug-remote -f remote/Dockerfile .
# Run
docker run -d --name remote \
-p 13000:13000 \
--cap-add SYS_PTRACE \
--security-opt seccomp:unconfined \
dap-debug-remote
```
`SYS_PTRACE` and `seccomp:unconfined` are required so codelldb can control the
child process (set breakpoints, read memory, single-step).
To use a different external port, set `DAP_PORT`:
```bash
docker run -d --name remote -p 8080:8080 -e DAP_PORT=8080 \
--cap-add SYS_PTRACE --security-opt seccomp:unconfined \
dap-debug-remote
```
### Connecting to a non-local host
`debugServer` and most editors connect to `localhost`. If the remote container
runs on another machine, tunnel the port over SSH:
```bash
ssh -L 13000:localhost:13000 user@remote-host
```
---
## Connect from VSCode
### Prerequisites
Install the **CodeLLDB** extension (`vadimcn.codelldb`) from the VSCode
marketplace. This registers the `"lldb"` debug type, which VSCode needs even
when connecting to an external DAP server.
### Configure launch.json
Open `common/project/` as your workspace folder in VSCode (so source paths
match), then create `.vscode/launch.json`:
```jsonc
{
"version": "0.2.0",
"configurations": [
{
"name": "Remote launch (codelldb :13000)",
"type": "lldb",
"request": "launch",
"program": "/project/build/debugapp",
"cwd": "/project",
"stopOnEntry": false,
"sourceMap": {
"/project": "${workspaceFolder}"
},
"debugServer": 13000
}
]
}
```
| Field | Why |
|---|---|
| `type: "lldb"` | Tells VSCode to use the CodeLLDB extension's session handler |
| `request: "launch"` | codelldb on the remote starts the program for you |
| `program` / `cwd` | Paths **inside the container** (not local) |
| `sourceMap` | Maps container paths (`/project`) to your local workspace folder so VSCode opens the right source files |
| `debugServer: 13000` | **Key field** — tells VSCode to connect to an already-running DAP server on this port instead of launching codelldb locally |
Set breakpoints in `src/calculator.cpp`, press **F5** (or Run ▸ Start
Debugging), and select the configuration. The program runs in the container
and stops at your breakpoints.
### Without the CodeLLDB extension
If you don't want to install CodeLLDB, use the **webfreak.debug** extension
(`debug`) which provides a generic `"type": "cppdbg"` adapter. However, the
CodeLLDB approach above is recommended because it speaks the same DAP dialect
as the server.
---
## Connect from Neovim (your own installation)
Add this to your nvim-dap configuration:
```lua
local dap = require("dap")
-- Connect to codelldb running on the remote container.
dap.adapters.codelldb = {
type = "server",
host = "localhost", -- or the remote host IP
port = 13000,
}
dap.configurations.cpp = {
{
name = "Remote launch (codelldb :13000)",
type = "codelldb",
request = "launch",
program = "/project/build/debugapp",
cwd = "/project",
stopOnEntry = false,
},
}
dap.configurations.c = dap.configurations.cpp
```
Then `<leader>dc` (or `:DapContinue`) and select the configuration.
> Using the included **devstation** container? The adapter configuration is
> already baked in — see [devstation/README.md](../devstation/README.md).
---
## Connect from Emacs (dap-mode)
```elisp
(require 'dap-codelldb)
;; Tell dap-mode to connect to the remote DAP server instead of launching
;; codelldb locally.
(dap-register-debug-template
"Remote launch (codelldb :13000)"
(list :type "codelldb"
:request "launch"
:program "/project/build/debugapp"
:cwd "/project"
:stopOnEntry nil
:dap-server-host "localhost"
:dap-server-port 13000))
```
Run `M-x dap-debug` and select the template. See the
[dap-mode wiki](https://github.com/emacs-lsp/dap-mode#codelldb) for details.
---
## Connect from any DAP client
The remote exposes a standard DAP server on port 13000. Any tool that can
act as a DAP client can connect:
- **CLI testing** — send a raw DAP `initialize` request to verify the server
is alive:
```bash
echo '{"command":"initialize","arguments":{"adapterID":"test"},"type":"request","seq":1}' | nc localhost 13000
```
- **Custom tooling** — implement the
[DAP client side](https://microsoft.github.io/debug-adapter-protocol/) of
the protocol and connect to `localhost:13000`.
---
## Environment variables
| Variable | Default | Description |
|---|---|---|
| `DAP_PORT` | `13000` | External port that socat listens on (mapped to `0.0.0.0`) |
codelldb's internal port (13001) is fixed and not configurable from outside.
## Troubleshooting
**"Connection refused"**
Make sure the container is running (`docker ps`) and the port is mapped
(`-p 13000:13000`). Check logs: `docker logs remote`.
**Breakpoints don't hit**
Ensure you are setting breakpoints in the source files and that the
configuration uses `request: "launch"` (not `"attach"`). With `launch`,
codelldb starts the program itself.
**Source files not found in VSCode**
The `sourceMap` in `launch.json` must map `/project` to your local workspace
folder. If you opened the repo root instead of `common/project/`, adjust the
mapping: `"sourceMap": { "/project": "${workspaceFolder}/common/project" }`.
**Can't reconnect after ending a session**
The entrypoint restarts codelldb automatically. Wait ~1 second after
terminating a session before starting a new one.

38
remote/entrypoint.sh

@ -0,0 +1,38 @@
#!/bin/sh
#
# Remote container entrypoint.
#
# codelldb binds to 127.0.0.1 only (no --host flag), so socat forwards
# connections from 0.0.0.0:13000 to codelldb on 127.0.0.1:13001.
#
# The developer station's nvim-dap connects to port 13000 and sends DAP
# launch/attach requests. After each session ends, codelldb exits; this
# loop restarts both processes so the developer can reconnect without
# restarting the container.
set -eu
EXTERNAL_PORT="${DAP_PORT:-13000}"
INTERNAL_PORT=13001
ADAPTER="/opt/codelldb/extension/adapter/codelldb"
while true; do
echo "[remote] codelldb DAP server on :${EXTERNAL_PORT} (forwarded to 127.0.0.1:${INTERNAL_PORT})..."
# socat forwards external TCP to codelldb's localhost listener.
socat TCP-LISTEN:"${EXTERNAL_PORT}",reuseaddr,fork \
TCP:127.0.0.1:"${INTERNAL_PORT}" &
SOCAT_PID=$!
# Give socat a moment to bind before codelldb starts accepting.
sleep 0.2
# codelldb handles one --multi-session instance; exits when client disconnects.
"$ADAPTER" --port "$INTERNAL_PORT" --multi-session
kill "$SOCAT_PID" 2>/dev/null || true
wait "$SOCAT_PID" 2>/dev/null || true
echo "[remote] DAP session ended. Restarting in 1 second..."
sleep 1
done
Loading…
Cancel
Save