为什么 NestJS Hot Reload 在 Docker 中不起作用以及如何正确修复
Source: Dev.to
为什么 NestJS 热重载在 Docker 中不起作用以及如何正确修复
在本篇文章中,我将解释 NestJS 项目在 Docker 环境下热重载(Hot Reload)失效的常见原因,并提供一种可靠的解决方案,使你在容器中也能享受到即时代码更新的便利。
目录
问题复现
# 项目根目录
$ npm run start:dev
# 输出类似:
[Nest] 12345 - 2023/09/01, 12:34:56 PM LOG [NestFactory] Starting Nest application...
当我们把同样的项目放进 Docker 并运行 npm run start:dev 时,文件修改后并不会触发重新编译,服务器仍然保持原来的状态。
常见错误原因
| 原因 | 说明 | 影响 |
|---|---|---|
| 文件系统事件不被传播 | Docker 默认使用 overlay2,对宿主机的 inotify 事件不做转发。 | nodemon/ts-node-dev 监听不到文件变化。 |
watchOptions 未开启轮询 | 仅依赖 fs.watch,在容器内部失效。 | 热重载失效。 |
| 未映射源码卷 | 容器内部使用的是复制进来的代码,而不是挂载的本地目录。 | 本地修改不影响容器内代码。 |
NODE_ENV=production | 生产模式下 Nest 默认关闭热重载。 | 即使配置了监听,也不会启动。 |
正确的解决方案
1. 使用 polling 方式监听文件变化
在 package.json 的 start:dev 脚本中加入 --poll 参数(适用于 ts-node-dev):
{
"scripts": {
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts --poll"
}
}
如果你使用 nodemon,则在 nodemon.json 中添加:
{
"watch": ["src"],
"ext": "ts",
"ignore": ["src/**/*.spec.ts"],
"exec": "ts-node -r tsconfig-paths/register src/main.ts",
"legacyWatch": true
}
legacyWatch 会强制使用轮询。
2. 在 docker-compose.yml 中挂载源码卷
services:
api:
build: .
volumes:
- .:/usr/src/app:cached # 将宿主机源码挂载到容器
command: npm run start:dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
注意:
cached(或delegated)可以提升性能,但不影响文件监控。
3. 在 Dockerfile 中安装 ts-node-dev(或 nodemon)
FROM node:18-alpine
WORKDIR /usr/src/app
# 只复制 package.json 与 lock 文件,先安装依赖
COPY package*.json ./
RUN npm ci
# 复制其余源码
COPY . .
# 全局安装 ts-node-dev(可选)
RUN npm i -g ts-node-dev
EXPOSE 3000
CMD ["npm", "run", "start:dev"]
4. 确保 nest-cli.json 使用 watch 选项
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"watchAssets": true
}
}
完整示例(Dockerfile + docker‑compose)
项目结构
my-nest-app/
├─ src/
│ └─ main.ts
├─ package.json
├─ tsconfig.json
├─ nest-cli.json
├─ Dockerfile
└─ docker-compose.yml
Dockerfile(已包含上述要点)
FROM node:18-alpine
WORKDIR /usr/src/app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm i -g ts-node-dev
EXPOSE 3000
CMD ["npm", "run", "start:dev"]
docker-compose.yml
version: "3.9"
services:
api:
build: .
volumes:
- .:/usr/src/app:cached
command: npm run start:dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
package.json(关键脚本)
{
"scripts": {
"start": "node dist/main.js",
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts --poll"
},
"devDependencies": {
"ts-node-dev": "^2.0.0",
"@nestjs/cli": "^9.0.0",
"typescript": "^5.0.0"
}
}
只要运行
docker compose up --build,随后在本地编辑src/**/*.ts,容器内的 Nest 应用就会自动重新编译并热重载。
结论
- 根本原因:Docker 默认不转发
inotify事件,导致基于文件系统事件的热重载失效。 - 最佳实践:使用轮询(
--poll/legacyWatch),并确保源码通过卷挂载到容器内部。 - 完整方案:在
Dockerfile中安装ts-node-dev(或nodemon),在docker‑compose.yml中挂载源码卷,使用NODE_ENV=development,并在nest-cli.json开启watchAssets。
按照本文提供的步骤配置后,你的 NestJS 项目将在 Docker 中实现 即改即生 的开发体验,省去每次手动重启容器的烦恼。祝编码愉快 🚀
核心误解
Docker 不会监视你的文件。它只能看到容器内部实际存在的内容。更改是否能够反映,完全取决于:
- 文件如何进入容器
- NestJS 如何启动
- 文件系统事件是否正确传播
将开发模式的期望与生产模式的设置混在一起,会导致热重载失效。
Source:
NestJS 在没有 Docker 的情况下热重载是如何工作的
当你在本地运行 NestJS 并使用以下命令时:
npm run start:dev
Nest CLI 会使用文件监视器。文件变化时,进程会自动重启。
- 不涉及 Webpack。
- 不使用 HMR(热模块替换)。
- 仅是进程重启,由原生文件系统事件驱动。
引入 Docker 后会有什么变化
在容器内部,文件监视依赖于三件事:
- File injection – 您是挂载源代码还是复制快照?
- Startup command – 您是否以 watch 模式运行 Nest?
- Event propagation – 文件系统事件会传递到容器吗?
如果其中任何一项配置不当,热重载就会失效。
可靠的开发唯一可行的设置
以下所有条件必须同时满足:
- 通过卷挂载源码
- NestJS 以监听模式运行(
start:dev) - Docker 内部的文件监视正常工作
卷是不可协商的
仅仅使用 Dockerfile 将代码复制进去的方式:
COPY . .
会创建一个静态快照——这对生产环境很合适,但对开发毫无帮助,因为 Docker 永远看不到本地的编辑。
相反,应该使用 docker‑compose.yml(或 docker run)来挂载项目目录:
services:
app:
build: .
volumes:
- .:/usr/src/app
- /usr/src/app/node_modules # 排除主机的 node_modules
.:/usr/src/app 这行是热重载的关键。
排除 node_modules
挂载主机的 node_modules 可能导致原生模块不匹配。像上面那样排除它,确保容器使用自己的依赖。
以监听模式运行 Nest
npm run start:dev # ✅ 正确
# ❌ 错误的替代方案:
node dist/main.js
nest start
运行编译后的 JavaScript 会关闭监视器,热重载因此不可用。
macOS 和 Windows 文件监视问题
Docker Desktop 在 VM 中运行 Linux 容器。原生文件系统事件通常 不会从 macOS/Windows 主机传播 到容器,导致 NestJS 错过更改。
解决方案:启用轮询
设置以下环境变量,使基于 Node 的监视器轮询更改而不是依赖事件:
CHOKIDAR_USEPOLLING=true
WATCHPACK_POLLING=true
将它们添加到 docker‑compose.yml(或 Dockerfile)的 environment: 下。轮询是一个众所周知、可靠的变通办法。
Webpack 混淆解释
NestJS 支持两种重新加载策略:
- 进程重启 – 默认的 Nest CLI 监视器(与
start:dev一起使用)。 - 热模块替换(HMR) – 需要 Webpack。
只有在你刻意启用 HMR 时才需要 Webpack。以下标识表明 HMR 正在运行:
// main.ts
if (module.hot) {
module.hot.accept();
}
// nest-cli.json
{
"compilerOptions": { ... },
"webpack": true // <-- 启用 HMR
}
如果你从未打算使用 HMR,根本不需要 Webpack。仅仅为了“修复” Docker 重载问题而安装它通常是误导;缺失的部分往往是上面描述的轮询配置。
何时使用 Webpack HMR 合理
- 你的应用启动时间较长。
- 你想在不重启 Node 进程的情况下实现即时重载。
- 你在 Nest 配置中明确启用了 HMR。
对于大多数后端 API,文件变动时简单的进程重启已经足够,而且复杂度要低得多。
开发与生产的思维模型
| 方面 | 开发 | 生产 |
|---|---|---|
| 卷 | 使用主机到容器的挂载 | 不使用卷;在构建期间复制文件 |
| 启动命令 | npm run start:dev(监听模式) | 运行编译后的 JS(node dist/main.js) |
| 文件监听 | 在 macOS/Windows 上启用轮询 | 不需要 |
| 重新构建 | 代码更改时永不重新构建镜像 | 每次更改都重新构建镜像 |
| Webpack | 可选,仅用于 HMR | 不需要 |
混合使用这些环境(例如,在生产中使用卷或对每次更改都重新构建镜像)会导致工作流出现问题。
最后思考
- Docker 没有坏。
- NestJS 没有坏。
- 热重载不是魔法;它只是文件监视。
理解 文件如何进入容器 和 NestJS 如何监视它们 能让行为可预测。如果你发现自己为了看到 console‑log 更新而重新构建 Docker 镜像,请重新检查卷、启动命令和轮询设置。若你仅仅为了让文件监视工作而安装了 Webpack,可能根本不需要它。
如果这篇文章为你节省了时间,也很可能为其他人节省数小时的挫败感。