VSCode调试Docker容器Go程序

一般来说开发过程大都是在本地编写代码、编译、调试运行等,但是有一些情况是编写的代码无法在本地直接执行,需要系统相关环境(比如Linux环境),这个时候我们可以通过VSCode的远程开发功能ssh连接到服务器并在上面编写代码、编译、调试运行(无论本地机器是Windows/Linux/macOS),这两种方式比较常见。

不过有时候编译后的程序需要和其他程序共同工作才能完成(微服务化),这个时候可以将其打包进docker,然后通过docker-compose一键运行,那么这个时候如何调试docker中的程序呢?这就是本文的主要解决的问题,使用VSCode以及dlv调试docker容器内的程序。

打包docker镜像

本文使用VSCode来演示这一过程,当然对于Goland也是支持的。

下面的代码展示了在容器内app-svc对redis的读写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package main

import (
"context"
"encoding/json"
"flag"
"log"
"net"
"os"
"strconv"

"github.com/gin-gonic/gin"
"github.com/redis/go-redis/v9"
)

var (
configPath string
DB *redis.Client
AppConfig = new(Config)
)

type Config struct {
DbSvc string `json:"dbSvc"`
DbSvcPort int `json:"dbSvcPort"`
AppSvc string `json:"appSvc"`
AppSvcPort int `json:"appSvcPort"`
}

func loadConfig(name string) error {
data, err := os.ReadFile(name)
if err != nil {
return err
}
if err = json.Unmarshal(data, AppConfig); err != nil {
return err
}
return nil
}

func initDb(ctx context.Context) error {
DB = redis.NewClient(&redis.Options{
Addr: net.JoinHostPort(AppConfig.DbSvc, strconv.Itoa(AppConfig.DbSvcPort)),
})
if err := DB.Ping(context.Background()).Err(); err != nil {
return err
}
return nil
}

func init() {
flag.StringVar(&configPath, "conf", "config.json", "configuration json")
}

func main() {
flag.Parse()

if err := loadConfig(configPath); err != nil {
panic(err)
}

if err := initDb(context.Background()); err != nil {
panic(err)
}

r := gin.Default()
r.GET("/", func(ctx *gin.Context) {
log.Println(ctx.Request.Header)
ctx.String(200, "hello world")
})
r.POST("/get/:key", func(ctx *gin.Context) {
key := ctx.Param("key")
value := DB.Get(context.Background(), key).Val()
log.Println("[get]", key, value)

ctx.JSON(200, gin.H{
"key": key,
"value": value,
})
})
r.POST("/set/:key/:value", func(ctx *gin.Context) {
key, value := ctx.Param("key"), ctx.Param("value")
log.Println("[set]", key, value)
DB.Set(context.Background(), key, value, 0).Err()
ctx.String(200, "ok")
})
r.Run(":" + strconv.Itoa(AppConfig.AppSvcPort))
}

然后将其打包成docker镜像,需要注意调试时使用到dlv工具,大致过程为在容器内启动dlv服务,然后配置VSCode并连接到该服务,从而实现远程调试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM golang:1.20.3 as builder

WORKDIR /build
ENV GOPROXY=https://goproxy.cn,direct
COPY go.mod .
COPY go.sum .
RUN go mod download
RUN go install github.com/go-delve/delve/cmd/dlv@latest
COPY . .
RUN go build -o app .

FROM ubuntu:latest
WORKDIR /usr/local/bin
COPY --from=builder /build/app /usr/local/bin/
COPY --from=builder /go/bin/dlv /usr/local/bin/
COPY --from=builder /build/config.json /etc/config/config.json
CMD ["./app", "-conf", "/etc/config/config.json"]

构建docker镜像:docker build -t app-svc:v1 -f Dockerfile .

为了一键启动docker,可以使用docker-compose来管理容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
version: '3'
services:
redis:
image: "redis"
container_name: redis

app-svc:
image: app-svc:v1
container_name: app-svc
ports:
- "10086:8888"
- "12345:2345"
volumes:
- "./app:/usr/local/bin/app"
- "./config.json:/etc/config/config.json"
command: ["dlv", "--headless=true", "--listen=:2345", "--log", "--api-version=2", "exec", "./app", "--", "-conf", "/etc/config/config.json"]
depends_on:
- redis
security_opt:
- apparmor:unconfined
1
2
# 启动redis和app-svc
docker-compose down && docker-compose up -d

需要特别注意:

  • 10086:8888:app-svc暴露的端口,用于本地测试
  • 12345:2345:dlv服务暴露的端口,给vscode连接的

这里还将app二进制程序以及config.json配置文件挂载在容器内,是为了减少每次修改代码、编译后build镜像的次数。

command 字段表示在启动容器时相应的启动dlv服务,并且由dlv启动go程序,通过 docker-compose logs app-svc 发现容器此时输出 warning layer=rpc Listening for remote connections (connections are not authenticated nor encrypted) 表示dlv服务需要等待客户端(VSCode)的连接才开始执行go程序(进入调试状态)。

当然也可以将 command 设置为 sh,并添加 tty: true,这样app-svc服务启动时就是进入shell交互模式,之后就可以手动启动dlv服务,这样做的一个好处是可以直接查看容器输出的内容

1
docker-compose exec app-svc ./dlv --headless=true --listen=:2345 --log --api-version=2 exec ./app -- -conf /etc/config/config.json

可以看看这篇文章:https://blog.jetbrains.com/go/2019/02/06/debugging-with-goland-getting-started/#debugging-a-running-application-on-a-remote-machine

至此app-svc和redis容器均已启动,下一步则是配置VSCode以连接到容器内的dlv服务,这也是为什么需要docker暴露端口

配置VSCode

配置VSCode比较简单,创建一个 .vscode/launch.json 文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Docker",
"type": "go",
"request": "attach",
"mode": "remote",
"host": "127.0.0.1",
"port": 12345,
"showLog": true,
"remotePath": "${workspaceFolder}"
}
]
}

hostport 则是容器中dlv服务暴露的端口,这里通过docker-compose暴露出来。remotePath 则是执行go程序的源代码路径,这里是将本机上的go程序挂载到容器内,因此 remotePath 需要指定源代码路径为本机,${workspaceFolder} 表示当前工作目录。

VSCode,启动!

至此可以愉快在在VSCode调试docker中的go程序了:先启动容器,打断点,然后点击【调试与运行】绿色小三角就行啦。

当发生下面几种情况时需要 手动重启docker容器 重新调试:

  • 手动退出VSCode调试界面
  • go程序退出而导致容器结束
  • 在本地上修改源码/配置并重新编译go程序(挂载文件而非目录)
1
2
3
4
# 只重启需要的app-svc服务
docker-compose restart app-svc
# 重建所有服务
docker-compose down && docker-compose up -d

需要注意的一点是,在本地编译的go程序时需要指定 GOOS,比如 GOOS=linux go build -o app .,因为编译后的go二进制文件在启动/重启docker容器时会被复制到执行目录下然后执行。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!