コンテナで動くGoアプリをデバッグする方法
初めに
最近開発環境を整備していて、コンテナでGoアプリケーションをリモートでデバッグするのにちょっと面倒だったので、自分のメモも兼ねてやり方を残しておきます。
要件
プロジェクトによりますが、今の現場では主要エディタがVSCodeです。
もちろんVimユーザー(うち一人はぼく)がいるので、ターミナルでもデバッグできるように対応する必要があります。
また、コード変更を反映させるために都度イメージをビルドしてコンテナを再作成するは開発スピードが落ちるため、コンテナ再起動だけで反映されるようにする必要があります。
デバッガのしくみ
Goにはdelveというデバッガがあります。
delveはClient-Serverモデルになっていて、次の2つのプロトコルで通信が可能になります。
JSON-RPCはdelveのCLIに使われています。
DAPはMicrosoftが策定しているデバッグ用プロトコルで、実際にVSCodeのGo拡張はこれを使っています。
dlv-dapについて
もともとdelveはDAPに対応していなかったため、VSCodeはDAPとJSON-RPCを変換してくれるdlv-dapでプロトコルの変換をしていました。
今はdelveが標準でDAPを対応しているため不要になりましたが、まだ実験的な段階のため不安定の場合は従来の構成を使うとよいと思います。

delveの詳細なしくみは公式ドキュメントに記載されているこちらのスライドを参照してください。
アーキテクチャレベルで詳しく書かれています。
プロジェクト構成
簡単な例ですが、次のような構成とします。
skanehira@godzilla github.com/skanehira/go-remote-debug $ tree .
.
├── Dockerfile
├── LICENSE
├── README.md
├── docker-compose.yaml
├── go.mod
├── main.go
└── start.sh
Goのバージョンは次のとおりです。
skanehira@godzilla github.com/skanehira/go-remote-debug $ go version
go version go1.17.2 darwin/arm64
リモートデバッグ
デバッガの概要で説明したとおり、実際にVSCodeでコンテナにあるアプリケーションをデバッグするにはDAPプロトコルを使う必要があります。
しかしVimユーザーもいるため、必要に応じて使うプロトコルを切り替えられるしくみを用意する必要があります。
次のようなスクリプトを用意して、それを含めたベースのコンテナイメージを作ります。
それぞれのファイルの中身は次のようになっています。
start.sh
#!/bin/bash
echo -e "mode: ${DEBUG}\nport: ${DEBUG_PORT}"
if [ "${DEBUG}" = "dap" ];then
go build -gcflags='all=-N -l' -o app
dlv dap -l 0.0.0.0:${DEBUG_PORT} --log --check-go-version=false
elif [ "${DEBUG}" = "rpc" ]; then
dlv debug --continue --check-go-version=false --accept-multiclient --headless -l 0.0.0.0:${DEBUG_PORT} main.go
else
go run main.go
fi
Dockerfile
FROM golang:1.17.3
COPY start.sh /usr/local/bin/start.sh
RUN chmod 775 /usr/local/bin/start.sh && go install github.com/go-delve/delve/cmd/dlv@latest
CMD ["start.sh"]
docker-compose.yaml
version: '3.8'
services:
app:
build:
context: .
# dlvをヘッドレスで起動する場合
# SIGINTで終了できないため強制終了させる
stop_signal: SIGKILL
ports:
- "8080:8080"
- "${DEBUG_PORT:-9998}:${DEBUG_PORT:-9998}"
volumes:
- $PWD:/dev/go-remote-debug
- type: volume
source: go_module_cache
target: /go/pkg/mod
working_dir: /dev/go-remote-debug
environment:
- "DEBUG=${DEBUG:-}"
- "DEBUG_PORT=${DEBUG_PORT:-9998}"
volumes:
go_module_cache:
main.go
package main
import (
"log"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello Gorilla"))
})
log.Println("start http server :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
デバッガのポートとプロトコルを環境変数で切り替えられるようにしています。DEBUG=dapの場合はDAP、DEBUG=rpcの場合はJSON-RPCでサーバが起動します。
なお、ポートはコンテナ作成後に変えられないので、変更するときはコンテナの作り直しが必要です。
skanehira@godzilla github.com/skanehira/go-remote-debug $ DEBUG=dap docker compose up
[+] Running 1/0
⠿ Container go-remote-debug-app-1 Created 0.0s
Attaching to go-remote-debug-app-1
go-remote-debug-app-1 | mode: dap
go-remote-debug-app-1 | port: 9998
go-remote-debug-app-1 | 2021-11-26T15:12:56Z warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted)
go-remote-debug-app-1 | DAP server listening at: [::]:9998
VSCodeの場合、launch.jsonは次のように設定する必要があります。
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug API Container",
"type": "go",
"debugAdapter": "dlv-dap",
"request": "launch",
"port": 9998,
"host": "localhost",
"mode": "exec",
"program": "/dev/go-remote-debug/app",
"substitutePath": [
{
"from": "${workspaceFolder}",
"to": "/dev/go-remote-debug"
}
]
}
]
}
ポイントとなる項目は次のとおりです。
| 項目 | 概要 |
|---|---|
port |
デバッガのポート |
program |
コンテナ側にある実行ファイルのフルパス |
exec |
事前ビルドされたprogramを実行する |
substitutePath |
VSCode側のパスをコンテナ側のパスに置換 |
VSCodeでデバッグを開始するとコンテナ側のアプリケーションが起動します。
あとはブレークポイントを設置するなり好きにデバッグできます。

CLIでデバッグする場合はdlv connect host:portでデバッグが可能になります。
コンテナにアタッチまたローカルから接続のどちらも利用可能です。
次のデモはコンテナにアタッチしてからdlv connectしてデバッグする例です。

最後に
DAPでのデバッグはちょっとだけ手間ですが、これで要件は満たせるかなと思います。
同じことで困った方の助けになれればと思います。
記事で使用したコードはこちらにおいておきますので、ご自由に参照ください。

Discussion