awesome-compose 实战指南:react-express-mysql 后端 Node.js 容器化开发的“好默认值”实践

原创2026-09-29 23:43:411,674 阅读
文章标签:示例工程

awesome-compose 实战指南:react-express-mysql 后端 Node.js 容器化开发的“好默认值”实践

导读

本文以 react-express-mysql/backend/README.md 为骨架,深入剖析该示例如何用 Docker 与 Docker Compose 为 Node.js 应用打造一套兼顾本地开发体验与生产部署质量的“good defaults”(好默认值)方案。你将掌握:如何用一行命令启动本地开发环境、如何在容器内热重载与调试、如何把依赖安装与源码拷贝分层以加速构建、如何利用内置健康检查与优雅退出机制让容器“生产可用”,并看到这份 README 背后对应的 Dockerfile、compose.yaml 等仓库源码证据。

一、示例定位:一个“好默认值”的 Node.js Docker 样板

react-express-mysql 是 awesome-compose 仓库中一个典型的“前端 React + 后端 Express + MySQL 数据库”全栈示例。本文聚焦其后端 backend 目录:README 明确自述这是一个 "good defaults" 示例——展示在 Docker 中运行 Node.js 时,从本地开发到生产交付都应该遵循的最佳实践集合,并欢迎社区提交 Issue 与 PR。

该示例的核心主张可以浓缩为一句话:让本地开发环境尽可能接近测试与生产环境,同时不牺牲开发者的便利工具。下文分别从“本地开发特性”和“生产导向特性”两条主线展开。

二、本地开发特性:在容器里享受接近原生的开发体验

README 列出的本地开发能力,每一条都能在仓库源码中找到对应实现,我们逐一结合佐证:

  1. 开发镜像尽量贴近生产镜像:Docker Compose 构建的本地开发镜像与生产镜像几乎一致,仅附加开发所需的少量特性。对应地,Dockerfile 采用多阶段/多 target 设计:基础阶段 development 直接用于本地,并派生出 dev-envs 阶段(额外安装 git、创建 vscode 用户、注入 Docker CLI 工具),生产构建则可以选用不含开发附加物的基础阶段。

  2. 宿主机无需安装 node/npm:镜像把 node_modules 安装在应用根目录之外,避免本地源码通过 bind-mount 覆盖掉容器内已装好的依赖。构建时只执行一次 npm install,只有 package.json 变化才会触发重装。这一点在 compose.yaml 中有直接体现:

volumes:
  - ./backend/src:/code/src:ro
  - ./backend/package.json:/code/package.json
  - ./backend/package-lock.json:/code/package-lock.json
  - back-notused:/opt/app/node_modules

宿主机的 src、package.json、package-lock.json 被挂载进容器,而依赖目录通过命名卷 back-notused:/opt/app/node_modules 隔离,避免宿主机的 node_modules 干扰容器内安装结果。

  1. 一行命令启动:执行 docker-compose up 即可完成镜像构建与本地开发服务器的启动。

  2. 本地改代码、容器跑代码:Compose 将宿主机源码正确 bind-mount 进 Linux 容器,实现“本地编辑、容器运行”的实时联动。

  3. 容器内使用 nodemon 热重载:Compose 在开发模式用 nodemon 运行应用,宿主机文件变更时容器内 Node 进程自动重启。对应 compose.yaml 中的:

command: npm run start-watch

而 start-watch 定义在 package.json:

"start-watch": "nodemon src/index.js --inspect=0.0.0.0:9229",
  1. 从宿主机向容器调试:开放 inspect 端口 9229,可使用 Chrome DevTools 或 VS Code 进行宿主机侧调试;nodemon 在 Compose 下默认开启 --inspect。同时镜像还额外暴露测试调试端口 9230(见 Dockerfile 中的 EXPOSE $PORT 9229 9230),并在 compose.yaml 中映射:
ports:
  - 80:80
  - 9229:9229
  - 9230:9230
  1. 提供 VS Code 调试配置与测试任务:README 注明 .vscode 目录内含调试与测试任务(由社区贡献),支持“Docker Test (Attach 9230 --inspect)”这类断点调试测试流程。

  2. 镜像更小、重建更快:COPY 依赖清单并先执行 npm install,再 COPY 源代码,充分利用 Docker 构建缓存——源码变化时只需重建最后几层。这在 Dockerfile 中清晰可见:

COPY package.json /code/package.json
COPY package-lock.json /code/package-lock.json
RUN npm ci

# copy in our source code last, as it changes the most
COPY . /code

注释点明了关键原则:源码是变更最频繁的层,所以放在最后拷贝。

  1. bind-mount 挂载 package.json:允许不重建镜像就实时安装新依赖,例如 docker-compose exec node npm install --save <package name>。由于 compose.yaml 已把 ./backend/package.json 挂载进容器,容器内执行 npm install --save 会同步写回宿主机文件,供下次构建使用。

三、生产导向特性:容器天然“可运维”

README 强调,除了开发便利,这套样板在生产运维上也做了充分的“默认值”设计:

  1. 内置 Docker 健康检查:使用 Dockerfile HEALTHCHECK 配合 /healthz 路由,让 Docker 判断容器是否健康。Dockerfile 中的定义:
HEALTHCHECK --interval=30s \
  CMD node healthcheck.js

每 30 秒检查一次。配套的 healthcheck.js 向 http://localhost:${PORT}/healthz 发起 2 秒超时的 HTTP 请求,返回 200 即退出码 0(健康),否则退出码 1(不健康)。而 /healthz 路由在 src/server.js 中实现,注释提示:应在该路由中做真正的业务健康判断,只有健康时才返回 200。README 也说明示例中该路由始终返回 200,实际项目中应替换为真实的健康检查逻辑。

  1. 正确使用 NODE_ENV:Dockerfile 默认 NODE_ENV=production,Compose 在本地开发时覆盖为 development:
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV

compose.yaml 通过 build args 与运行时环境变量双重覆盖:

build:
  args:
  - NODE_ENV=development
  ...
environment:
  - NODE_ENV=development
  1. 开发依赖不进生产镜像:由于 NODE_ENV=production 是默认值,容器默认不会安装 devDependencies;而 Compose 构建时显式传入 NODE_ENV=development,因此本地开发会带上一整套开发依赖(mocha、nodemon、chai 等,见 package.json 的 devDependencies)。

  2. 优雅处理 SIGTERM/SIGINT:默认以 node src/index.js 而非 npm 启动,因为 npm 不会正确透传 SIGTERM/SIGINT(表现为 docker run 前台运行时无法用 Ctrl-C 优雅退出)。为此 src/index.js 显式捕获两个信号并调用 shutdown():

process.on("SIGINT", function onSigint() { ... shutdown(); });
process.on("SIGTERM", function onSigterm() { ... shutdown(); });

function shutdown() {
  server.close(function onServerClosed(err) {
    if (err) { console.error(err); process.exit(1); }
    process.exit(0);
  });
}

shutdown() 调用 server.close() 先停止接收新连接,再退出进程,实现真正优雅的关闭。源码注释还附上了 npm 信号问题的已知 Issue 链接(npm/npm#4603 等,仓库 README 原文引用了这些链接)。若坚持用 npm start,README 与 Dockerfile 建议配合 docker run --init,但作者认为直接跑 node 仍是更优雅的选择。

  1. 提供 docker-stack.yml 示例:README 说明生产/编排场景(Docker Swarm)应使用 docker-stack.yml,而 compose.yaml 只服务于本地开发。

四、使用前提与设计假设

README 列出了适用这套方案的前提与假设,明确“边界”本身也是最佳实践的一部分:

  • 已安装 Docker 与 Docker Compose(macOS/Windows 用 Docker Desktop,Linux 用官方脚本 + Compose);
  • 希望用 Docker 做本地开发,宿主机永远不用装 node/npm,同时让开发镜像与生产镜像尽量一致;
  • 不希望牺牲开发工作流的质量:本地编辑器、node 调试/inspect、本地代码仓库照常使用,只是 Node 服务器跑在容器里;
  • 只把 docker-compose 用于本地开发(README 明确指出 Compose 本就不是生产部署工具);
  • compose.yaml 不用于 docker stack deploy,Swarm 场景请使用 docker-stack.yml。

五、快速上手:从启动到安装依赖到跑测试

1. 启动本地开发环境

如果这就是你的 Node.js 应用,只需一行命令:

docker-compose up

它会自动完成:构建启用开发模式的本地镜像(nodemon + NODE_ENV=development)→ 以该镜像启动容器并映射 80、9229、9230 端口(仅 localhost)→ 用 nodemon 启动,宿主机工作目录变更即重启 → 将宿主机当前目录挂载到容器应用目录。

需要数据库等附加服务时,只需在 Compose 文件里追加服务,up 时它们会自动加入该应用的自定义 Docker 网络。本示例的 compose.yaml 即包含 backend、db(mariadb,兼容 amd64/arm64)与 frontend 三个服务,并通过 public/private 双网络隔离:db 只暴露在 private 网络,frontend 与 backend 共享 public 网络,数据库密码通过 Docker Secrets(db/password.txt 文件注入 /run/secrets/db-password)传递。

Compose 会自动检测 package.json 或 Dockerfile 是否变化并触发重建;如需手动构建,执行:

docker-compose build

开发结束后务必清理环境:

docker-compose down

2. 应用运行中安装新依赖包

在 Compose 运行期间添加新包,无需重建镜像:

docker-compose exec node npm install --save <package name>

流程是:在运行中的容器内安装 → nodemon 检测到变更自动重启 → --save 把新依赖写回 package.json,供下次 docker-compose build 使用。

3. 执行单元测试

在容器内运行测试:

docker-compose exec node npm test

它会在 node 容器中执行 npm test。对应 package.json 中的测试脚本:

"test": "cross-env NODE_ENV=test PORT=8081 mocha --timeout 10000 --exit --inspect=0.0.0.0:9230",

测试框架为 mocha,测试样例见 test/sample.js:它用 chai + chai-http 断言 /healthz 返回 200,并断言首页 / 返回预期文本。

如需在 VS Code 中调试测试,可使用 “Docker Test (Attach 9230 --inspect)” 配置:它通过 VS Code 任务在容器内启动一个等待调试器连接的测试进程(--inspect-brk=0.0.0.0:9230,对应 test-wait-debuger 脚本),并会先终止之前遗留的调试进程。

六、源码级纵深:配置、依赖与真实调用链

1. 环境变量驱动的配置层

src/config.js 集中读取环境变量:数据库连接信息来自 DATABASE_HOST、DATABASE_PORT、DATABASE_DB、DATABASE_USER,而密码 DATABASE_PASSWORD 指向一个文件路径(Compose 中为 /run/secrets/db-password),配置层用 fs.readFileSync 读出密码内容;服务端口默认 8080(未用 Compose 时避免非 root 用户占用 80 的权限问题,容器内则无此顾虑,由 Dockerfile 设为 80)。

2. 数据库访问链路

src/database.js 用 Knex 封装连接,client 为 mysql2,连接配置即上文 config 对象。src/server.js 的首页路由直接执行 SELECT VERSION():

app.get("/", function(req, res, next) {
  database.raw('select VERSION() version')
    .then(([rows, columns]) => rows[0])
    .then((row) => res.json({ message: `Hello from MySQL ${row.version}` }))
    .catch(next);
});

因此访问首页时返回的 JSON 会包含真实的 MySQL 版本号,验证全栈链路连通。该文件注释还点出了两个实践:morgan 默认把日志写到 stdout(容器内日志应输出到 stdout 而非写文件),以及 Dockerfile 层顺序对构建缓存时间的影响。

3. 依赖清单与基础镜像

package.json 中运行时依赖仅 4 个:express(Web 框架)、knex(SQL 构建器)、morgan(HTTP 日志)、mysql2(MySQL 驱动);开发依赖为 chai/chai-http(断言与 HTTP 测试)、cross-env、mocha、nodemon。基础镜像为 node:lts,Dockerfile 顶部注释建议:超出本机范围的使用场景,务必到 Docker Hub 将镜像固定到具体版本。

七、总结

react-express-mysql/backend 这份 README 的价值不在于代码量,而在于把“Node.js 上 Docker”的成熟工程经验浓缩成一套可复制的默认配置:依赖先行分层、NODE_ENV 贯穿构建与运行、健康检查 + 信号捕获保证可运维、bind-mount 与 nodemon 保障开发体验、Compose 与 Swarm 职责分离。对照 Dockerfile、compose.yaml、src/index.js 与 healthcheck.js 逐一验证,你会发现文档中的每条主张都有源码级落点——这正是把“最佳实践”从口号变成工程默认值的典范。

该示例遵循 MIT License(Copyright (c) 2015-2018 Bret Fisher),可在遵循许可的前提下自由使用与改进。

登录后查看全文
awesome-compose