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
2
3
4
5
exe="lfs" # 构建产物名称
des="$(pwd)/lib" # 依赖拷贝目录
echo $des
deplist=$(ldd $exe | awk '{if (match($3,"/")){ printf("%s "),$3 } }')
cp $deplist $des

然后再通过以下脚本运行程序

1
2
3
4
5
6
7
8
9
dirname=`dirname $0`
tmp="${dirname#?}"
if [ "${dirname%$tmp}" != "/" ]; then
dirname=$PWD/$dirname
fi
LD_LIBRARY_PATH=$dirname/lib
export LD_LIBRARY_PATH
echo $LD_LIBRARY_PATH
$dirname/lfs "$@"

原理就是通过指定LD_LIBRARY_PATH修改查找依赖的位置


从编译完成的镜像中获取产物有两种方案

  • 方案一

运行起来后执行cp命令,基本格式如下

1
docker run -itd --rm --name temp image_name bash && docker cp temp:/out ./ && docker stop temp

其中bash可能需要根据镜像更换成其他shell,或者换成tail -f /dev/null,总之就是要让镜像处于一直运行的状态

  • 方案二

将镜像输出为tar文件,然后解压,从中获取文件,样例如下

1
2
3
4
5
6
docker save -o mini.tar $mini_image_id
tar xvf mini.tar
cat manifest.json
echo "layer:$(sed 's/","/\n/g' manifest.json | sed 's/"]}/\n/g' | tac | sed -n "2,2p")"
tar xvf $(sed 's/","/\n/g' manifest.json | sed 's/"]}/\n/g' | tac | sed -n "2,2p")
cp server ../bin/server-mini

这里是解压最后一层layer,具体需要哪一层可查看manifest.json

没有tac命令可用tail -r代替

不同版本的docker导出的tar目录结构略有不同,但是好在不影响manifest.json,基本格式如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
{
"Config": "9d4c89bd4fa6947ea1ec699a1cd675b36f944cb6f661427cc1c7f9ebbb833fba.json",
"RepoTags": [
"redis:alpine"
],
"Layers": [
"b93731aff72308a4aba32de5ee9f50dc3a2e702627b6893691c7f3f099132aca/layer.tar",
"0da7b4adee891f8e97c3619f3a4ac942076cb8ac84cd952c5b3427686bccc64f/layer.tar",
"86546f53d2382092a06e332014d30de2cb91ceb64e56a00a6602c944577bae17/layer.tar",
"c078df15cf9dce7fe6ffaa0e715fcf2f9eb0875e88e289330d73448ae8667937/layer.tar",
"f1b37bdd71e374c35fdb82c0d4a4703d07eb21260a078eecd7a003fd5b9b2da5/layer.tar",
"692214158801f0e9360e0addf367701d03263ac6a4f49d614a4000ce6595c3e5/layer.tar",
"87ad4eaa16e78a00b3baf73559ff63c31639d9bba4dbab2ce5b1cb862d179c14/layer.tar"
]
}
]

这里的命令过于繁琐,建议使用jq代替,例如以下样例

1
tar xvf $(jq --raw-output '.[0].Layers[1]' manifest.json)

开发环境

使用docker搭建开发环境有一定局限性,只适用于网络应用或者命令行工具,对于依赖硬件或者GUI的目前无法使用

建议是搭配vscode的远程功能使用,如果环境部署到远程服务器,使用ssh通信,如果是本机的,可以不要ssh,同时镜像也可以使用alpine,体积更小,当然开发环境一般不差这点

远程的镜像之前常用的都是ubuntu:20.04,但是最近有几次用这个镜像出了些奇奇怪怪的状况,所以换成debian:12.2了,注意这个镜像的软件源位置不一样,参考上面给出的网址

还有一种方案是使用webide,直接在浏览器上使用,但是要注意可能有性能问题

这里是我自用的开发环境脚本

命令行工具

这里一般是用别人开发的软件居多了,因为别人的软件可能用各种语言开发,总不能什么环境都装吧,只能上docker了

比如我自己用的就有epubcertbot

缺点就是命令会变得很繁琐,而且由于文件读写映射兼容,可能还需要修改源代码

如果是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
ENV LANG C.UTF-8

时区

基本上所有官方镜像都有时区问题,需要在Dockerfile中处理

例如

1
2
3
4
5
RUN export DEBIAN_FRONTEND=noninteractive \
&& apt-get update \
&& apt-get install -y tzdata \
&& ln -fs /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& dpkg-reconfigure --frontend noninteractive tzdata
1
ENV TZ=Asia/Shanghai

具体哪个有效,自己尝试吧

减小体积

除了换镜像外,还能通过多阶段构建,只保留构建产物,配合静态编译效果更好

将RUN命令进行合并,减少镜像层级,下载的文件记得删除,例如以下例子

1
RUN wget -q https://mirrors.huaweicloud.com/openjdk/${JAVA_VERSION}/openjdk-${JAVA_VERSION}_linux-x64_bin.tar.gz  && mkdir -p ${JDK_HOME} && tar -zxvf openjdk-${JAVA_VERSION}_linux-x64_bin.tar.gz -C ${JDK_HOME} && rm -rf openjdk-${JAVA_VERSION}_linux-x64_bin.tar.gz

加速构建

在测试阶段,将RUN命令尽可能分开,以便调整后续命令时能够用上之前的缓存,不用从头开始

不同语言有不同的包管理方式,对于减少依赖下载次数,给出几个例子

  • 单文件类

依赖存储于一个文件,例如node,简单的rust和java项目

node最为简单,可以直接 RUN npm i axios,或者提前COPY package.json,例如:

1
2
3
4
5
WORKDIR /app
COPY package.json /app/
RUN npm i
COPY . /app/
# 其他命令
  • 多文件类

rust和java的多模块项目,依赖文件可能位于不同的文件夹中,如果依旧使用上述方案的话,命令会较为繁琐,比如使用多个COPY命令,或者通过shell脚本修改目录结构等

这里最简单的方案还是使用 buildkitRUN挂载功能

例如:RUN --mount=type=cache,mode=0777,target=/root/.gradle/,id=gradle ./gradlew :spring-boot-project:spring-boot:build -x test

这里要注意缓存的目录,尽可能把后续命令中涉及的目录都归到一个父目录下

另外在mac上遇到了必须指定mode参数才生效的情况

资源


docker使用技巧
http://blog.inkroom.cn/2023/12/13/3DJZB2C.html
作者
inkbox
发布于
2023年12月13日
许可协议