【Docker学习】4.与Dockerfile优化有关的5个小技巧


1 使用精简的alpine镜像

如果不做特殊指定,一般docker内运行的Linux系统均为全功能的镜像,占据着大量的磁盘空间,而一般开发者部署的应用不会超过几百兆,甚至前端应用只有几十KB。

因而我们需要一种精简过后的Linux系统,只需要应用基本的环境即可。这就是alpine的作用。

alpine
美 [ˈælˌpaɪn] 英 [ˈælpaɪn]
adj.高山的;(尤指中欧)阿尔卑斯山的
n.高山植物
网络:阿尔派;阿尔派恩;阿尔派公司

这里取的意思为高山植物,意为只需要很少的资源就能存活的植物。对于alpine系统,其实就是裁减掉不必要的功能,只需要必要的功能的系统就可以满足应用的运行环境问题。

假设某一应用需要node(version 18),可通过构建后的应用镜像启动,观察其所占空间大小。

构建文件如下:

FROM node:18
#.......以下省略

构建后,镜像信息如下:

原生Linux构建的镜像

大小占用1.45G!

点进去看看构建详情,发现Linux版本为debian,其为Linux发型版本中的其中一个,另外熟悉的还有CentOS,Ubuntu,RedHat等。

原生Linux构建的镜像详情

而采用alpine的构建文件如下:

FROM node:18-alpine3.14
#以下命令省略

构建后,发现少了900M!

alpine构建的镜像

点进去看看:

alpine构建的镜像详情

简直太Amazing啦!

2 使用多阶段构建

先给出一下dockerfile:

FROM node:18-alpine3.14
WORKDIR /app
COPY package.json
RUN npm config set registry https://registry.npmmirror.com
RUN npm install
COPY ..
RUN npm run build
EXPOSE 3000
CMD ["node", "./dist/mani.js"]

有的同学可能会说:为什么先复制package.json进去,安装依赖之后再复制其他文件,直接全部复制进去不就行了?

这里就要说说docker的分层存储原理了。

Dockerfile内的每一行指令都是一层,会被docker缓存起来,如果下次build时某一层未发生变化,就不会执行该层的指令,只会从变化的那一层开始构建。
比如,如果上述文件中的package.json未发生变动,则下面的npm install就不会执行。如果从一开始就直接一个命令全部复制进去,则只要包内的任一文件发生变动,都必须从头开始,没有充分发挥出docker的缓存功能。

还是以上面的前端项目的构建过程来进行证明,看看docker的缓存到底能使构建过程有多快,加快了多少倍。

构建命令:

docker build -t dockerfile-test:second .

分层的构建速度:

分层的构建速度

修改README.MD文件:

修改文件

重新build:

重新构建

通过图中CACHED字眼,我们证明了没有变化的layer,docker直接通过缓存跳过,没有经过npm install,花费了25秒。请暂时记住这个时间。

现在,我们来更改下package.json,看看经过npm install的话,耗时多少。

修改package.json

再次构建:

修改package.json后构建

如果读者亲自实践下,可以看到,docker在构建到npm install那里时停留了很长时间,由于package.json发生了更改,导致docker构建从COPY package.json开始了。

整个耗时是之前25的三倍!!!

另外,通过分层构建这种技术,我们还可以对构建的镜像就行瘦身,因为构建后的镜像,我们只需要./dist及其运行时需要的相关依赖即可,并不需要那些比如源码、构建时的依赖等这些东西的。

修改后的dockerfile文件如下:

FROM node:18-alpine3.14 as build-stage

WORKDIR /app

COPY package.json .

RUN npm install

COPY . .

RUN npm run build

# production stage
FROM node:18-alpine3.14 as production-stage

COPY --from=build-stage /app/dist /app
COPY --from=build-stage /app/package.json /app/package.json

WORKDIR /app

RUN npm install --production

EXPOSE 3000

CMD ["node", "/app/main.js"]

其中,FROM node:18-alpine3.14 as build-stage中,as后面跟着的是本次构建的阶段的别名。

COPY --from=xxx可以从上个阶段复制文件过来。

然后 npm install 的时候添加--production,这样只会安装dependencies的依赖。docker build之后,只会留下最后一个阶段的镜像。

也就是说,最终构建出来的镜像里是没有源码的,有的只是dist的文件运行时依赖。这样镜像就会小很多。

本次构建,我们打上标签third-f是指定 Dockerfile 的名字。

docker build -t dockerfile-test:third -f 222.Dockerfile .

构建:

优化后的dockerfile构建过程

然后 desktop 里看下构建出来的镜像:

优化后的dockerfile构建镜像大小

体积整整小了250M!!! Amazing!!!

3 使用ARG灵活构建

上一章节,我们介绍了ARG、ENV的使用、优缺点以及各自的作用范围。通过ARG给定的变量,如果在构建时传入,后续通过查看镜像docker history可以轻松检查ARG定义的对应变量值。

ARG是构建时的参数,ENV是运行时的变量。通过构建时传入参数,然后在构建后的镜像启动后能够使用该参数,可通过ARG传值,然后赋给ENV定义的变量得以实现。

假设现有如下js文件,打印参数aaabbb的值。

console.log(process.env.aaa);
console.log(process.env.bbb);

运行一下:

export aaa=1 bbb=2
node ./test.js

结果:

[zyxelva]$ export aaa=1 bbb=2
[zyxelva]$ node ./test.js 
1
2

然后我们写个dockerfile,文件名是333.Dockerfile

FROM node:18-alpine3.14

ARG aaa
ARG bbb

WORKDIR /app
#拷贝test.js至目录app下
COPY ./test.js .

ENV aaa=${aaa} \
    bbb=${bbb}

CMD ["node", "/app/test.js"]

使用ARG声明构建参数,使用${xxx}来取,然后用ENV声明环境变量。

构建的时候传入构建参数:

docker build --build-arg aaa=3 --build-arg bbb=4 -t arg-test -f 333.Dockerfile .

通过--build-arg xxx=yyy传入 ARG 参数的值。

开始构建

点击查看镜像详情,可以看到 ARG 已经被替换为具体的值了:

查看镜像详情
查看镜像详情2

run起来:

docker run  --name fourth-container arg-test

结果:

[zyxelva]$ docker run  --name fourth-container arg-test
3
4

可以看到容器内拿到的环境变量就是ENV设置的。

灵活使用 ARG,可以增加 dockerfile 的灵活性。

4 CMD结合ENTRYPOINT

CMDENTRYPOINT非常相似,在上一章节中有所提及。这里举例子证实一下:
写个444.Dockerfile

FROM node:18-alpine3.14

CMD ["echo", "光光", "到此一游"]

然后build:

docker build -t cmd-test -f 444.Dockerfile .

run一下:

[zyxelva]$ docker run cmd-test
光光 到此一游

CMD是可以被重写的,这是与ENTRYPOINT最大的区别。

[zyxelva]$ docker run cmd-test echo "东东"
东东

若换成命令ENTRYPOINT,我们再试试,看看结果。

FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光", "到此一游"]

build一下:

docker build -t cmd-test -f 444.Dockerfile .

run起来:

[zyxelva]$ docker run cmd-test echo "东东"
光光 到此一游 echo 东东

可以看到,现在dockerfile里ENTRYPOINT的命令依然执行了。

docker run 传入的参数作为了 echo 的额外参数。

一般还是 CMD 用的多点,可以灵活修改启动命令。

其实 ENTRYPOINT 和 CMD 是可以结合使用的。

比如这样:

FROM node:18-alpine3.14

ENTRYPOINT ["echo", "光光"]

CMD ["到此一游"]

docker build:

docker build -t cmd-test -f 444.Dockerfile .

docker run:

[zyxelva]$ docker run cmd-test
光光 到此一游
[zyxelva]$ docker run cmd-test 66666
光光 66666

当没传参数的时候,执行的是ENTRYPOINT + CMD组合的命令,而传入参数的时候,只有CMD部分会被覆盖。

这就起到了默认值的作用。

所以,用ENTRYPOINT + CMD的方式更加灵活。

5 COPY vs ADD

这里不再赘述,主要区别还是针对打包压缩的文件处理方式,ADD就可以自动解压。一般情况下,还是用 COPY 居多。

6 总结

Dockerfile 有挺多技巧:

  • 使用 alpine 的镜像,而不是默认的 linux 镜像,可以极大减小镜像体积,比如node:18-alpine3.14这种
  • 使用多阶段构建,比如一个阶段来执行 build,一个阶段把文件复制过去,跑起服务来,最后只保留最后一个阶段的镜像。这样使镜像内只保留运行需要的文件以及 dependencies。
  • 使用 ARG 增加构建灵活性,ARG 可以在 docker build 时通过 –build-arg xxx=yyy 传入,在 dockerfile 中生效,可以使构建过程更灵活。如果是想定义运行时可以访问的变量,可以通过 ENV 定义环境变量,值使用 ARG 传入。
  • CMD 和 ENTRYPOINT 都可以指定容器跑起来之后运行的命令,CMD 可以被覆盖,而 ENTRYPOINT 不可以,两者结合使用可以实现参数默认值的功能。
  • ADD 和 COPY 都可以复制文件到容器内,但是 ADD 处理 tar.gz 的时候,还会做一下解压。
    灵活使用这些技巧,可以让你的 Dockerfile 更加灵活、性能更好。

参考:


文章作者: Kezade
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Kezade !
评论
  目录