前言
🍃 你好啊,我是你的人类朋友!✨
本文主要来一起阅读一个高效的 Node.js 应用 Dockerfile。
在开始分析这个 Dockerfile 之前,先问大家一个问题:为什么这个 Dockerfile 要分两个阶段来构建,而不是直接复制所有文件然后安装依赖? 读完本文后,你就能找到答案!
😎 小贴士:如果你不懂啥是两段构建,问题不大,后面有解释,可以继续看。
下面展示的是一个用于部署 Node.js 应用的 Dockerfile,让我们先看看完整代码:
dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
# 只复制 package.json
COPY package.json ./
# 安装依赖
RUN npm install
# 运行时阶段
FROM node:18-alpine
WORKDIR /app
# 从构建阶段复制 node_modules
COPY --from=builder /app/node_modules ./node_modules
# 复制源代码:注意,这边默认小伙伴们配置了 .dockerignore 文件,所以这一步不会复制 node_modules 目录。建议大家都补上 .dockerignore 文件,好处多多!
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
如果你是个新手,这个时候有点头晕是正常的,莫慌,让我们进入正文!😁
正文
🤔 啥是单段构建与多段构建?
单段构建:整个应用在一个 Docker 镜像中完成构建和运行,适合简单应用。
多段构建:将构建过程分为多个阶段,每个阶段负责不同的任务,最终只将必要的文件复制到最终镜像中。这样做可以减小镜像大小,提高安全性。
看不懂?问题不大,下面使用 dockerfile 来作比较:
✍️ 单段构建示例
dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "index.js"]
我们来总结一下,他有什么特征?
-
整个 Dockerfile 只有一个 FROM 指令
-
所有操作(安装依赖、编译代码、运行应用)都在同一个镜像中完成
-
最终生成的镜像包含了构建过程中的所有文件和工具
还看不懂的话,直接记住:单段构建:只有 1 个 FROM
✍️ 多段构建示例
dockerfile
# ❗孩子们,我是第一个FROM!!!
FROM node:18-alpine AS builder
WORKDIR /app
# 只复制 package.json
COPY package.json ./
# 安装依赖
RUN npm install
# ❗孩子们,我是第二个FROM!!
# 运行时阶段
FROM node:18-alpine
WORKDIR /app
# 从构建阶段复制 node_modules
COPY --from=builder /app/node_modules ./node_modules
# 复制源代码
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
我们来总结一下,他有什么特征?
-
整个 Dockerfile 有两个 FROM 指令
-
所有操作(安装依赖、编译代码、运行应用)都在不同的镜像中完成
-
最终生成的镜像只包含运行时必要的文件和工具
还看不懂的话,直接记住:多段构建:有 2 个 FROM
套公式做题就是快!bro 😏
逐行解读 Dockerfile
我们现在知道啥是单段构建与多段构建
下面就来详细康康这个所谓的多段构建,其每一个阶段都在做啥!【当然,其实每一步这边都会解释!】
第一阶段:构建阶段
dockerfile
FROM node:18-alpine AS builder
- 使用 Node.js 18 的 Alpine Linux 版本作为基础镜像
- Alpine 版本非常轻量,适合生产环境
AS builder给这个阶段命名为 "builder",方便后续引用
dockerfile
WORKDIR /app
- 设置工作目录为
/app,后续命令都在这个目录下执行。
补充知识:啥是 WORKDIR(工作目录)?
我不解释什么是工作目录,你只需要知道,设置 WORKDIR /app 就相当于你先进入容器的 /app 文件夹,之后的所有操作都在这个 /app 文件夹里完成。
其实很好理解,如果没有设置工作目录,操作会在根目录 / 进行,文件会放得乱七八糟的。
dockerfile
COPY package.json ./
- 只复制
package.json文件到当前目录 - 这是关键步骤:先只复制依赖定义文件
dockerfile
RUN npm install
- 安装项目依赖包
- 由于只复制了
package.json,Docker 会缓存这一层,如果package.json没变化,后续构建会直接使用缓存。构建是啥意思?构建就是把你的源代码和配置打包成一个可以运行的 Docker 镜像的过程。
第二阶段:运行时阶段
dockerfile
FROM node:18-alpine
- 开始新的构建阶段,再次使用相同的基础镜像
- 这样确保运行环境与构建环境一致
dockerfile
WORKDIR /app
- 同样设置工作目录为
/app
dockerfile
COPY --from=builder /app/node_modules ./node_modules
--from=builder从之前的构建阶段复制文件- 只复制已安装的
node_modules目录到当前镜像 - 这样避免了在最终镜像中包含构建工具和缓存文件
dockerfile
COPY . .
- 复制所有源代码到镜像中
- 由于依赖已经安装好,这里不会触发依赖重新安装
dockerfile
EXPOSE 3000
- 声明容器运行时监听的端口是 3000
- 这只是文档说明,实际端口映射需要在运行容器时设置
dockerfile
CMD ["node", "index.js"]
- 设置容器启动时执行的命令
- 使用数组格式,直接运行
node index.js
🤔【疑问】 你可能会有的疑问:
看到这你可能一脸懵逼,第一个阶段安装依赖,第二个阶段用第一阶段的依赖,就能够节省资源?这就是所谓的多段构建吗?
✍️【回答】是的,这就是多段构建的精髓所在。
第一个阶段(builder)负责安装依赖,生成 node_modules 文件夹。第二个阶段只从这个阶段复制 node_modules 文件夹,而不复制其他构建相关的文件。
那么这样做为什么就能节省资源呢 😎:
最终镜像只包含运行需要的文件(你的代码+node_modules)
不包含 npm 安装过程中产生的缓存文件和临时文件
不包含构建工具和开发依赖
简单来说: 第一个阶段准备材料,第二个阶段只拿需要的材料来运行,把垃圾留在原地。
🤔【疑问】你老是说什么缓存文件、临时文件之类的,为啥我自己pnpm i或者npm i的时候没看到啥缓存文件、临时文件?✍️【回答】当你运行
npm install时,npm 其实会在后台下载包到缓存目录(通常在用户主目录的 .npm 文件夹中),然后从缓存复制到项目的 node_modules。虽然你在项目里看不到这些缓存文件,但它们确实存在于系统其他地方。而多阶段构建的优势就是连这些隐藏的系统级缓存文件都不会带到最终镜像中,从而减小了镜像的大小,进而提高了镜像的加载速度。😎
最后
OK 了兄弟们,现在全体目光向我看齐,看我看我,我来总结下!
回到开头的问题:为什么要分两个阶段构建?
答案主要有三点:
- 减小镜像大小:最终镜像只包含运行所需的文件,不包含构建过程中的【中间文件】和【缓存】
- 提高构建速度 :利用 Docker 缓存机制,当
package.json不变时,直接使用缓存的依赖层,也就是npm install这一层。如果package.json改变了,才会重新安装依赖。 - 增强安全性:最终镜像不包含构建工具,减少了攻击面。哈意思?就是说,因为最终镜像不包含构建工具,所以就不能通过攻击工具来攻击应用,这方面算作了解吧!
这就是本文的全部内容了,祝你今天开心~
☀️