docker使用技巧
总结的一些docker使用技巧
目的
一般docker常用于搭建各种网络服务,但是也可以用于编译、开发环境搭建、命令行工具等
编译环境
各种语言需要的编译环境不尽相同,甚至可能彼此冲突。
以我用过的语言来说,
- java没什么问题,环境简单,但是有少数库编译会出问题;
- node版本多变,兼容性差,不少三方库可能还会依赖c库;
- c不常用,但是其对环境依赖最强,甚至没有包管理功能,我都不知道怎么控制依赖的版本,要是不兼容怎么办;
- rust和c近似,有包管理,但是依旧有不少库是c的包装
- python几乎完全不懂,环境搭建不能保证百分百成功
- dart和flutter环境依赖不强,但是flutter更新可能会有版本兼容和依赖适配问题
基于上述的种种理由,利用docker实现一个彼此隔离,可重现的编译环境非常有用,这里是我总结的一些库的编译脚本
此外,对于c、rust、go、dart等支持静态编译的语言来说,可以使用docker做到更完善纯粹的环境,比如使用scratch镜像,剔除所有用不到的文件,只需要保留glibc和可执行文件,如果有涉及https的,再保留一份ssl证书文件,例如我自己写的git-lfs-server。如果是rust还能更加极端,直接使用musl编译,真正实现一个文件处处运行
类似dart和go不支持彻底的静态编译,需要保留glibc和其他可能使用到的依赖,这里给出一个精简方案
dart可以参考这个,有哪些依赖可以通过ldd查看
还有一种方案
这种方案更为繁琐。首先在静态编译完成后,执行以下脚本获取依赖
1 | |
然后再通过以下脚本运行程序
1 | |
原理就是通过指定LD_LIBRARY_PATH修改查找依赖的位置
从编译完成的镜像中获取产物有两种方案
- 方案一
运行起来后执行cp命令,基本格式如下
1 | |
其中bash可能需要根据镜像更换成其他shell,或者换成tail -f /dev/null,总之就是要让镜像处于一直运行的状态
- 方案二
将镜像输出为tar文件,然后解压,从中获取文件,样例如下
1 | |
这里是解压最后一层layer,具体需要哪一层可查看manifest.json
没有tac命令可用tail -r代替
不同版本的docker导出的tar目录结构略有不同,但是好在不影响manifest.json,基本格式如下
1 | |
这里的命令过于繁琐,建议使用jq代替,例如以下样例
1 | |
开发环境
使用docker搭建开发环境有一定局限性,只适用于网络应用或者命令行工具,对于依赖硬件或者GUI的目前无法使用
建议是搭配vscode的远程功能使用,如果环境部署到远程服务器,使用ssh通信,如果是本机的,可以不要ssh,同时镜像也可以使用alpine,体积更小,当然开发环境一般不差这点
远程的镜像之前常用的都是ubuntu:20.04,但是最近有几次用这个镜像出了些奇奇怪怪的状况,所以换成debian:12.2了,注意这个镜像的软件源位置不一样,参考上面给出的网址
还有一种方案是使用webide,直接在浏览器上使用,但是要注意可能有性能问题
这里是我自用的开发环境脚本
命令行工具
这里一般是用别人开发的软件居多了,因为别人的软件可能用各种语言开发,总不能什么环境都装吧,只能上docker了
缺点就是命令会变得很繁琐,而且由于文件读写映射兼容,可能还需要修改源代码
如果是clone源代码的,在dockerfile里最好指定tag或者commit,避免二次构建因为版本更新而失败
建议
基础镜像
ubuntu:20.04起步,22.04包管理器好像换了,不是很建议使用。如果有问题,就换成debian,debian还有基于日期的tag,更容易维持版本
工具类建议使用alpine,如果可以还能使用busybox,甚至scratch,都能有效减小体积,但是可能会出各种运行问题,比如有些软件在scratch下不能响应Ctrl+C,需要在程序里自己监听信号处理
shell
开发环境直接装oh-my-zsh,alpine只有sh,非常难用
同时记得加入以下命令以便支持中文
1 | |
时区
基本上所有官方镜像都有时区问题,需要在Dockerfile中处理
例如
1 | |
1 | |
具体哪个有效,自己尝试吧
减小体积
除了换镜像外,还能通过多阶段构建,只保留构建产物,配合静态编译效果更好
将RUN命令进行合并,减少镜像层级,下载的文件记得删除,例如以下例子
1 | |
加速构建
在测试阶段,将RUN命令尽可能分开,以便调整后续命令时能够用上之前的缓存,不用从头开始
不同语言有不同的包管理方式,对于减少依赖下载次数,给出几个例子
- 单文件类
依赖存储于一个文件,例如node,简单的rust和java项目
node最为简单,可以直接 RUN npm i axios,或者提前COPY package.json,例如:
1 | |
- 多文件类
rust和java的多模块项目,依赖文件可能位于不同的文件夹中,如果依旧使用上述方案的话,命令会较为繁琐,比如使用多个COPY命令,或者通过shell脚本修改目录结构等
这里最简单的方案还是使用 buildkit 的RUN挂载功能
例如:RUN --mount=type=cache,mode=0777,target=/root/.gradle/,id=gradle ./gradlew :spring-boot-project:spring-boot:build -x test
这里要注意缓存的目录,尽可能把后续命令中涉及的目录都归到一个父目录下
另外在mac上遇到了必须指定mode参数才生效的情况