前言
本司新项目周期短,大约 1-2 周就会产出一个新的前端项目,在项目的不断孵化和迭代之中,碰到有以下的问题需要解决:
- 多人混合开发,项目人员频繁流动从而导致的开发环境不一致的问题。比如 node 版本不一致,某项目依赖于特定的 node 版本,但因该开发人员本机 node 版本与指定的版本不一致导致安装失败。并且也需要统一构建环境和本地的 node 版本一致。
- 新孵化的项目假如都使用的老项目
Dockerfile的复制,那么假如某天最原始那份需要做优化改动,那么其他全部复制了该份Dockerfile文件的项目就应该把这个改动全部patch过来,这是不理想的,理想应该是Dockerfile应该区分共享的和私有的。 - 部分同学对 docker 不是很熟悉,对 docker 的操作比较生涩,希望有更加简单的方式使其能快速上手做关于 docker 的操作。
- 如何对基础镜像进行选择和如何减少镜像的体积,以及 docker 的相关优化方案。
问题一
- 如何保证使用同一版本的 node 进行依赖安装和构建:
// package.json
"build": "docker run -v $(pwd):/workspace -w /workspace node:14.17.0-stretch bash -c \"npm run build:prod\"",
"install": "docker run -v $(pwd):/workspace -w /workspace node:14.17.0-stretch bash -c \"npm install\"",
- 在
package.json文件内新增两条指令build和install,分别用于使用特定版本的 node 去打包和安装依赖。 - 通过
-v选项,将当前前端项目根目录$(pwd)(依赖执行路径),与 docker 容器内的/workspace路径形成数据卷共享关系,即在容器内可以通过在路径/workspace内访问到当前前端的根目录文件。 - 通过
-w /workspace将容器的工作空间设置为/workspace路径。 - 启动
node:14.17.0-stretch这个镜像,并在这个镜像的工作空间路径内执行npm run build:prod命令。
为什么使用 node:14.17.0-stretch 这个版本的镜像首先是因为 14.17.0 是目前官方推荐的稳定版本,且多项目测试都很好的兼容了,关于 stretch 是什么并且为什么选择将在下面进行说明。
问题二
- 如何将
Dockerfile分为公有和私有
- 使用多阶段构建。即思路是我们首先将公有的配置先使用一个
Dockerfile保存起来,然后形成一个基础的镜像,其他项目再基于该镜像去做改造和传参,例如:
# 基础镜像 base-frontend-image
FROM nginx:stable-alpine
# 使用 ONBUILD 使该段命令在被其他 Dockerfile 引入且构建的时候执行
ONBUILD WORKDIR /etc/nginx/html
# 这一步先设置 ARG 是为了方便后面阶段 Dockerfile 传参,为什么不用 ENV 而用 ARG 将在下段代码演示中说明
ONBUILD ARG ARG_PORT
ONBUILD ENV PORT $ARG_PORT
# 将 dist 构建出来的静态文件复制入镜像中
ONBUILD COPY ./dist .
# 将 nginx 配置复制入镜像中
ONBUILD COPY ./docker/nginx/* /etc/nginx/conf.d
// 将后阶段传入的 ARG_PORT 参数去替换掉 nginx 内的变量
ONBUILD RUN envsubst '\$PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# 业务镜像
# 配置该业务占用 7788 端口,其实该配置目前暂无太大意义,仅是为了表明该项目和部署时候的端口映射关系,理想是希望宿主端口和容器暴露的端口一致。
# 为什么使用 ARG 是因为 ENV 不能在 FROM 之前执行,其余 ARG 和 ENV 的差别再此不做赘述
ARG ARG_PORT=7788
FROM remote-docker-registry.com/base-frontend-image
# nginx default.conf.template 配置
server {
# 通过
# RUN envsubst '\$PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf
# 命令,将 $PORT 注入 default.conf.template 内,并生成 default.conf
listen $PORT;
# <domain> 这里只是为了后续方便区分域名,目前可以省略。当前也可以通过变量传参进来,看个人项目需求
server_name ~^(?<domain>(.+))$;
location / {
root /etc/nginx/html/;
index index.html;
}
}
- 使用基础镜像的
Dockerfile构建出base-frontend-image,然后其他的业务项目再基于该镜像去配置对应的参数即可。因为base-frontend-image没有配置版本默认选取的是 latest,所以只要我们远端更新了该镜像,那么本地构建的时候就会去拉新的镜像。
问题三
- 使不熟悉 docker 命令的同学在了解了基本的 docker 概念之后就能上手操作 docker 参考该篇文章:Docker 图形化工具 Portainer
该工具非常棒,安装和使用都十分的简单,基本解决了该问题。但是大家最好还是不要过于依赖该工具,能手动敲命令还是最快的。
问题四
使用 docker 时候的优化方案
- 尽量在
Dockerfile里面使用RUN的时候,尽量将多个RUN命令整合在在一起,比如:
RUN mkdir xxx
RUN mv xx xxx
那么可以修改为:
RUN mkdir xxx && mv xx xxx
# 或者使用换行分隔符
RUN mkdir xxx \
mv xx xxx
为什么需要将多个 RUN 合并成一个是因为在 Dockerfile 里面,每执行一次 RUN 都会在该镜像上再添加多一层该镜像的复制 + 该 RUN 命令做的改动。比如一个初始镜像 20 MB,在执行了 5 个 RUN 之后,本地去看这个镜像的大小,发现超过了 100 MB 的。这也就是因为该镜像除本身之外,还有 5 层的镜像层在上面。但是其实因为每一层镜像跟上一层都是继承关系,引用是相同的,所以实际占用的硬盘大小没那么大,你可以通过 docker system df 去查看硬盘占用。但是每一层不仅是有改动的部分,还有因为这个改动 docker 所要去对相关文件的做的保存。
所以每一层虽然实际上没 20 MB 那么大,但是也应该尽量合并 RUN 以减少镜像体积,加快上传和下载的速度。
- 使用
.dockerignore文件去忽略构建时传输给 docker 构建引擎的文件 在我们 docker build 的时候,会根据当前Dockerfile形成上下文目录,然后 docker 会将该目录下所有的文件传输到 docker 构建引擎中使用,所以当你确定不需要传输给 docker 构建引擎使用的文件时,你可以通过.dockerignore文件去告诉 docker 忽略掉这些目录,从而加快构建的速度,例如:
/src
/public
/node_modules
- 使用符合场景的基础镜像
当我们在 dockerhub 挑选镜像的时候,往往有以下几种 tag 的镜像:
buster、stretch、jessie、slim、alpine。
buster、stretch、jessie包含比较完成的操作系统工具,比如包下载工具、完整 shell 工具、git 工具。slim非常轻量的容器环境,仅包含基础的包下载工具、shell 基础工具。alpine及其轻量的容器环境,仅包含该镜像特点要求的环境,比如 nginx-alpine,仅包含可以运行 nginx 的工具,其余无关的工具都不会安装在里面。
这些都是语义的,主要取决于镜像的作者在构建的时候都安装了哪些工具包
在我们构建的时候,使用的是 stretch ,因为在构建 node 项目的时候可能需要各种工具包,比如我项目里面有用到 git 工具包,那么我们希望在本地构建的时候,尽可能的满足复杂的 node 构建需求,体积是次要的,因为本地的 node 镜像,拉完一次之后就不用再拉了。
在我们提交业务 docker 镜像的时候,那么我们应该尽可能地减少镜像的体积,因为在日常的开发中会涉及频繁的上传和下载,所以选用了 nginx:stable-alpine 这个镜像。
但是因为 alpine / slim 太轻量了,会与平常我们使用的 Ubuntu / Centos 有很大的区别,很多的工具包是没有的,当你不确定自己可以处理这些问题的时候,并且对体积不是很敏感的时候,不建议使用。