一般来说开发过程大都是在本地编写代码、编译、调试运行等,但是有一些情况是编写的代码无法在本地直接执行,需要系统相关环境(比如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 mainimport ( "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 builderWORKDIR /build ENV GOPROXY=https://goproxy.cn,directCOPY 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:latestWORKDIR /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
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服务,这样做的一个好处是可以直接查看容器输出的内容
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
文件
{ "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}" } ] }
host
和 port
则是容器中dlv服务暴露的端口,这里通过docker-compose暴露出来。remotePath
则是执行go程序的源代码路径,这里是将本机上的go程序挂载到容器内,因此 remotePath
需要指定源代码路径为本机,${workspaceFolder}
表示当前工作目录。
VSCode,启动!
至此可以愉快在在VSCode调试docker中的go程序了:先启动容器,打断点,然后点击【调试与运行】绿色小三角就行啦。
当发生下面几种情况时需要 手动重启docker容器 重新调试:
手动退出VSCode调试界面
go程序退出而导致容器结束
在本地上修改源码/配置并重新编译go程序(挂载文件而非目录)
docker-compose restart app-svc docker-compose down && docker-compose up -d
需要注意的一点是,在本地编译的go程序时需要指定 GOOS
,比如 GOOS=linux go build -o app .
,因为编译后的go二进制文件在启动/重启docker容器时会被复制到执行目录下然后执行。