自动化部署之 Jenkins 与 Docker 的结合

957 阅读12分钟

在很久之前,我们已经学习了 Jenkins 是如何使用自由组合的流水线任务来创建一个项目的部署任务,忘记了之前的知识可以点击下面的链接重新回顾下 Jenkins 下创建一个自由风格的项目:

Jenkins - 打造强大的前端自动化工作流 - 掘金

这段时间完成了一个项目的 Node 后台服务,并且项目的上线是使用 Jenkins 和 Docker 容器的技术进行了自动化部署的处理。这里就对这段时间的学习和实践进行一个阶段性的总结,也是对之前前端工程化自动化部署部分的进行扩展。

一、Docker 基础知识与操作

首先是 Docker 的基础知识和命令的学习,这里就介绍最简单的一些操作命令,有明确想要深入的学习的童鞋可以搜索更多相关的文章进行查阅与学习。

1.1 基础概念

最基础的三个概念便是镜像容器仓库

镜像(Image):

  • Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。

容器(Container):

  • 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。

仓库(Repository):

  • 仓库可看成一个代码控制中心,用来保存镜像。

是不是看起来和 git 的相关概念有点像呢,仓库存放项目(镜像),将项目(镜像)拉取下来之后运行项目(生成容器运行)。

1.2 基础操作命令

镜像相关:

从仓库拉取/更新镜像本地:docker pull [image-name][:image-tag]
  • :image-tag: 如果不填写镜像标签默认获取当前最新版本(latest)的镜像

查看镜像列表:docker images
  • -a: 列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层)
  • -q: 只显示镜像ID
  • --digests: 显示镜像的摘要信息
  • --format: 指定返回值的模板文件
  • --no-trunc: 显示完整的镜像信息

删除一个或多个镜像:docker rmi -f image [image...]
  • -f: 强制删除
  • image: 镜像名称或者镜像ID,多个镜像使用空格隔开

容器相关:

镜像启动容器:docker run [options] image
  • image: 镜像名称或者镜像ID,多个镜像使用空格隔开
  • -d: 后台运行容器,并返回容器ID;
  • -p: 指定端口映射,格式为:主机(宿主)端口:容器端口
  • -i: 以交互模式运行容器,通常与 -t 同时使用;
  • -t: 为容器重新分配一个伪输入终端,通常与 -i 同时使用;
  • -e username="ritchie": 设置环境变量;
  • --name="mongo-server": 为容器指定一个名称;
  • --net="bridge": 指定容器的网络连接类型,支持 bridge/host/none/container: 四种类型;
  • --link=[]: 添加链接到另一个容器;
  • --volume , -v: 将容器内目录和主机的目录进行挂载绑定

启动、停止、重启容器:docker start/stop/restart [container-id/container-name]
  • docker start: 启动一个或多个已经被停止的容器
  • docker stop: 停止一个或多个运行中的容器
  • docker restart: 重启一个或多个容器
查看容器列表信息:docker ps [options]
  • -a: 显示所有的容器,包括未运行的
  • -f: 根据条件过滤显示的内容
  • -l: 显示最近创建的容器。
  • -n: 列出最近创建的n个容器
  • -q: 静默模式,只显示容器编号
  • -s: 显示总的文件大小

删除容器:docker rm -f [container-id/container-name]
  • -f: 通过 SIGKILL 信号强制删除一个运行中的容器

构建镜像相关:

想要构建一个自己项目专属的镜像需要两样操作,一个是编写 Dockerfile 构建配置文件,另一个则是运行构建命令。

构建镜像配置文件:Dockerfile

这里就介绍 Dockerfile 常用的一些基础配置命令。

Dockerfile 指令说明
FROM指定基础镜像,用于后续的指令构建。
MAINTAINER指定Dockerfile的作者/维护者。(已弃用,推荐使用LABEL指令)
LABEL添加镜像的元数据,使用键值对的形式。
RUN在构建过程中在镜像中执行命令。
CMD指定容器创建时的默认命令。(可以被覆盖)
ENTRYPOINT设置容器创建时的主要命令。(不可被覆盖)
EXPOSE声明容器运行时监听的特定网络端口。
ENV在容器内部设置环境变量。
ADD将文件、目录或远程URL复制到镜像中。
COPY将文件或目录复制到镜像中。
VOLUME为容器创建挂载点或声明卷。
WORKDIR设置后续指令的工作目录。
USER指定后续指令的用户上下文。
ARG定义在构建过程中传递给构建器的变量,可使用 "docker build" 命令设置。
ONBUILD当该镜像被用作另一个构建过程的基础时,添加触发器。
STOPSIGNAL设置发送给容器以退出的系统调用信号。
HEALTHCHECK定义周期性检查容器健康状态的命令。
SHELL覆盖Docker中默认的shell,用于RUN、CMD和ENTRYPOINT指令。

看到上面的配置是不是感觉到迷茫,不知道应该如何下手进行配置项目镜像构建配置的编写呢?

Node.js 项目的 docker 镜像构建基本就是几个步骤的原则:

  1. 配置指定基础镜像FROM
  2. 配置构建镜像时候执行的命令RUN(例如 npm install 安装构建依赖,npm run build 构建命令等)
  3. 根据实际设置挂载卷、设置环境变量、执行其他操作等...
  4. 根据实际需要对容器的端口号进行抛出监听EXPOSE
  5. 配置创建启动容器时候执行的命令CMDENTRYPOINT

这里展示一个简单的 Node.js 项目的 Dockerfile 配置

FROM node:latest

# 创建 app 目录
WORKDIR /app

# 安装 app 依赖
# 使用通配符确保 package.json 与 package-lock.json 复制到需要的地方。(npm 版本 5 以上) COPY package*.json ./

RUN npm install
# 如果你需要构建生产环境下的代码,请使用:
# RUN npm install --only=production

# 打包 app 源码
COPY src /app

CMD [ "node", "server.js" ]

EXPOSE 8080
构建镜像命令:docker build -t xxx ./
  • --tag, -t: 镜像的名字及标签,通常 name:tag 或者 name 格式;可以在一次构建中为一个镜像设置多个标签。
  • ./: 是指当前目录寻找 Dockerfile 名字的构建配置文件,也可以使用 -f 参数来指定要使用的Dockerfile 路径

学习了一些基础的操作后,接下来牛刀小试,试下来真实的 Node.js 项目镜像的构建。

二、Node.js 项目的 Docker 构建

2.1 项目镜像的构建

构建镜像的 Dockerfile 配置编写

在 node.js 项目当中相关镜像构建 Dockerfile 配置其实也比较简单,主要步骤就是配置相关基础镜像和环境变量、接着是 node.js 项目自身的代码打包编译构建,最后就是处理镜像启动容器时候需要运行的相关的命令。

这里先给出项目的 Dockerfile 配置,基本的配置在前面的章节当中已经提及到了,这里就不累赘复述,因为里面有些优化 docker 镜像构建的操作,建议结合着后面的镜像构建优化操作配合食用。

# 第一阶段,命名为 build-stage
FROM node:20-alpine as build-stage

# 指定工作目录
WORKDIR /app

COPY package.json ./

# npm 源,选用国内镜像源以提高下载速度
RUN npm config set registry https://registry.npmmirror.com

# npm 安装依赖
RUN npm install

COPY . ./

# 打包项目 - 如果
RUN npm run build



# 第二阶段,命名为 prod-stage
FROM node:20-alpine as prod-stage

WORKDIR /app

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

RUN npm config set registry https://registry.npmmirror.com

COPY --from=build-stage /app/dist /app/dist

CMD npm run run:prod

EXPOSE 3000

镜像构建的优化

这里简单讲下镜像构建当中几个可以进行优化的地方。

1. 选用 alpine 版的 Node.js 基础镜像

Alpine Linux 是一个轻量级的 Linux 发行版,它具有小巧、安全、高效的特点,非常适合作为 Docker 容器的基础镜像。因此用该版本 linux 作为底层镜像构建出来的 Node.js 服务镜像则能够有效的减少体积。

2. 设置 docker 构建的忽略目录和文件

在 Dockerfile 同级目录下创建.dockerignore文件,编写格式和 git 的.gitignore文件相似。

里面设置的文件和目录就会在执行 docker build 构建项目镜像时候被省略掉。像 node_module 这种占用空间如同黑洞的依赖包目录我们就可以利用 dockerignore 进行构建忽略,减少构建出来的服务镜像体积。

3. 设置 npm 下载的镜像地址

因为某些大家都懂的原因,有时候不通过科学上网,访问外面的资源链接有时候会很慢,因此这里我们可以设置 npm 安装资源的访问地址为国内的镜像资源地址,从而加速 npm 依赖包的安装速度。

全局 npm 安装设置镜像地址:npm config set registry https://registry.npmmirror.com

单次 npm 安装设置镜像地址:npm install 包名 --registry=https://registry.npmmirror.com

4. 使用 Docker 文件缓存优化 npm 依赖包安装

很多情况下,其实只是业务逻辑代码变化了,但是 package.json 上面安装引入的 npm 依赖包并没有进行更新。但是前面我们都是没有进行优化处理,每一次构建新的镜像就会重新安装一次 npm 依赖包,因此也造成了构建时间和机器性能的损耗。

我们可以通过拷贝 package.json 到镜像的操作来触发 Docker 构建的缓存,只有当 package.json 文件发生变化时才会重新执行 npm 安装步骤。

5. 分阶段进行 npm 依赖构建

使用多阶段构建可以减小最终镜像的大小,并且可以避免将开发环境的依赖包包含在生产环境的镜像中。

在 Node.js 的项目当中镜像的构建可以划分成两个阶段:

  • 第一个打包构建项目阶段:处理 node.js 项目自己的打包构建(框架、ts编译等),这时候可以安装开发打包构建的 npm 依赖包
  • 第二个是 node.js 项目运行阶段:仅安装生产环境运行的依赖包,编写启动容器时候要运行的相关命令

好了,Docker 的知识学习的差不多了(你已经被强化了,可以上了),接下来就是 Jenkins 如何结合着 Docker 来进行自动化构建容器部署的流程了。其实两者的基础操作都学会了的话,将两者相结合也是一件小事情。


好了,其实这篇文章最主要还是为了在 Jenkins 上利用 Docker 进行 Node.js 服务的部署(为了这碟酱油包的饺子哈哈哈哈哈哈)。接着来看下在 Jenkins 如何构建 Docker 并且利用 Docker 来部署运行 Node.js 项目服务。

三、Jenkins 接入 Docker 构建部署项目

能够构建出来镜像,其实和完成项目的自动化构建和部署已经很接近了。其实最简化就是先停止前面的服务容器(删除)然后再启动新的容器服务这两个步骤。相信看到这里,并且有前面的基础铺垫之后,这一步已经不是什么问题了。

最简单和最核心的处理方式就是在服务器都安装好 Jenkins 和 Docker 软件后在 Jenkins 项目的构建步骤(Build Steps) 里面配置相关 Docker 操作。

首先是 Jenkins 项目构建任务中构建镜像:

相关的项目的镜像构建在上一章节已经

docker build -t xxx:${BUILD_NUMBER} ./

  • 这里使用到一个 Jenkins 的构建变量BUILD_NUMBER,这个变量是当前 jenkins 当前项目的每一次构建任务的唯一标识,在这作为构建镜像的标签使用,防止多次构建项目镜像而覆盖掉旧镜像。

接着是停止项目旧容器的运行:

docker rm -f xxx-server

  • 这里个人处理是直接将旧的容器删除了,毕竟还有旧的镜像在,恢复回滚可以使用旧的镜像重新运行容器。
  • 但是如果是需要热更新不断服务的进行部署发布,可能就不能进行停止和删除容器处理了,需要额外的一些操作(这里因为篇幅的原因,我们后面有机会单独出另外的文章进行讲述如何热更新部署)

最后是使用镜像创建运行新的服务容器:

docker run --name=xxx-server -p 3001:3000 -d xxx-server:${BUILD_NUMBER}

  • 这里为了能够访问到容器内的 Node.js 服务是需要进行一次端口的映射,将容器内部启动的 Node.js 服务 3000 端口映射到本机的 3000 端口,这样子我们就能在本机(服务器)通过 3000 端口号访问到启动的 Node.js 项目服务了。

Jenkins 构建配置的例子:

到这里,已经是成功的在服务器当中顺利的进行 node.js 项目的 jenkins 自动化构建 docker 镜像和部署运行容器了,也就是 node.js 项目在服务器当中跑起来了!

因此,这篇文章主要的内容也到这里也基本上结束了,期待下次再见面。

See You Next Time!!


Ps:展望后续 (挖坑)

节约生产服务器的资源压力

目前我们都是将构建和服务部署放置在同一个服务器当中,但是其实 jenkins 会消耗不少的服务器资源,下期我们有机会可以尝试下如何进行 jenkins 项目的构建与 docker 运行部署服务的分开处理来节约服务器资源的操作。

服务灰度和热更新部署

在上面的操作其实都是建立在项目初启动时候,因为业务还不是很大的情况下还勉强可以这样子操作。

  • 因为涉及到服务的暂停一段时间再启动,并不是热更新替换重启,因此项目初始启动时候可以说辛苦下程序员或者运维在深夜或者使用峰值低的时间段进行发布部署。

但是当后续业务不断发展(如果能够发展起来的话),这种发布模式其实就是相当危险和致命了,这时候就要引入更优的发布部署策略了(灰度发布和热更新部署),这部分拓展优化的内容就期待下一次的整理分享了。