1 使用精简的alpine镜像
如果不做特殊指定,一般docker内运行的Linux系统均为全功能的镜像,占据着大量的磁盘空间,而一般开发者部署的应用不会超过几百兆,甚至前端应用只有几十KB。
因而我们需要一种精简过后的Linux系统,只需要应用基本的环境即可。这就是alpine的作用。
alpine
美 [ˈælˌpaɪn] 英 [ˈælpaɪn]
adj.高山的;(尤指中欧)阿尔卑斯山的
n.高山植物
网络:阿尔派;阿尔派恩;阿尔派公司
这里取的意思为高山植物
,意为只需要很少的资源就能存活的植物。对于alpine系统,其实就是裁减掉不必要的功能,只需要必要的功能的系统就可以满足应用的运行环境问题。
假设某一应用需要node(version 18),可通过构建后的应用镜像启动,观察其所占空间大小。
构建文件如下:
FROM node:18
#.......以下省略
构建后,镜像信息如下:
大小占用1.45G!
点进去看看构建详情,发现Linux版本为debian,其为Linux发型版本中的其中一个,另外熟悉的还有CentOS,Ubuntu,RedHat等。
而采用alpine的构建文件如下:
FROM node:18-alpine3.14
#以下命令省略
构建后,发现少了900M!
点进去看看:
简直太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
的话,耗时多少。
再次构建:
如果读者亲自实践下,可以看到,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 /app/dist /app
COPY /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 .
构建:
然后 desktop 里看下构建出来的镜像:
体积整整小了250M!!! Amazing!!!
3 使用ARG灵活构建
上一章节,我们介绍了ARG、ENV的使用、优缺点以及各自的作用范围。通过ARG给定的变量,如果在构建时传入,后续通过查看镜像docker history
可以轻松检查ARG定义的对应变量值。
ARG
是构建时的参数,ENV
是运行时的变量。通过构建时传入参数,然后在构建后的镜像启动后能够使用该参数,可通过ARG传值,然后赋给ENV定义的变量得以实现。
假设现有如下js文件,打印参数aaa
、bbb
的值。
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 已经被替换为具体的值了:
run起来:
docker run --name fourth-container arg-test
结果:
[zyxelva]$ docker run --name fourth-container arg-test
3
4
可以看到容器内拿到的环境变量就是ENV
设置的。
灵活使用 ARG,可以增加 dockerfile 的灵活性。
4 CMD结合ENTRYPOINT
CMD
与ENTRYPOINT
非常相似,在上一章节中有所提及。这里举例子证实一下:
写个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 更加灵活、性能更好。
参考: