为什么 NestJS Hot Reload 在 Docker 中不起作用以及如何正确修复

发布: (2025年12月23日 GMT+8 05:51)
11 min read
原文: Dev.to

Source: Dev.to

为什么 NestJS 热重载在 Docker 中不起作用以及如何正确修复

在本篇文章中,我将解释 NestJS 项目在 Docker 环境下热重载(Hot Reload)失效的常见原因,并提供一种可靠的解决方案,使你在容器中也能享受到即时代码更新的便利。


目录

  1. 问题复现
  2. 常见错误原因
  3. 正确的解决方案
  4. 完整示例(Dockerfile + docker‑compose)
  5. 结论

问题复现

# 项目根目录
$ 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.jsonstart: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 不会监视你的文件。它只能看到容器内部实际存在的内容。更改是否能够反映,完全取决于:

  1. 文件如何进入容器
  2. NestJS 如何启动
  3. 文件系统事件是否正确传播

将开发模式的期望与生产模式的设置混在一起,会导致热重载失效。

Source:

NestJS 在没有 Docker 的情况下热重载是如何工作的

当你在本地运行 NestJS 并使用以下命令时:

npm run start:dev

Nest CLI 会使用文件监视器。文件变化时,进程会自动重启。

  • 不涉及 Webpack。
  • 不使用 HMR(热模块替换)。
  • 仅是进程重启,由原生文件系统事件驱动。

引入 Docker 后会有什么变化

在容器内部,文件监视依赖于三件事:

  1. File injection – 您是挂载源代码还是复制快照?
  2. Startup command – 您是否以 watch 模式运行 Nest?
  3. Event propagation – 文件系统事件会传递到容器吗?

如果其中任何一项配置不当,热重载就会失效。

可靠的开发唯一可行的设置

以下所有条件必须同时满足:

  1. 通过卷挂载源码
  2. NestJS 以监听模式运行(start:dev
  3. 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 支持两种重新加载策略:

  1. 进程重启 – 默认的 Nest CLI 监视器(与 start:dev 一起使用)。
  2. 热模块替换(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,可能根本不需要它。

如果这篇文章为你节省了时间,也很可能为其他人节省数小时的挫败感。

Back to Blog

相关文章

阅读更多 »

PSX:项目结构检查器

PSX – Project Structure eXtractor 是一个命令行工具,能够验证你的项目布局并自动修复。可以把它看作是整个仓库的 linter……