1. 为现有 Node.js 应用编写最小可用 Dockerfile

为现有 Node.js 应用编写最小可用 Dockerfile 学习卡片

从本地可运行到容器可运行:先固定应用契约

容器化的第一步不是写 Dockerfile,而是确认应用在本地以什么方式启动、监听哪个端口、依赖哪些文件。以一个最小 Express 应用为例,项目结构如下:

node-demo/
├── package.json
├── package-lock.json
└── server.js

package.json 中声明启动脚本与依赖:

{
  "name": "node-demo",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "express": "^4.19.2"
  }
}

server.js 读取环境变量 PORT,默认 3000,并暴露一个健康检查路径:

const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => res.send('hello from container'));
app.get('/healthz', (req, res) => res.json({ status: 'ok' }));

app.listen(port, '0.0.0.0', () => {
  console.log(`listening on ${port}`);
});
监听地址必须是 0.0.0.0 而不是 127.0.0.1。容器内的回环地址只对容器自身可见,宿主机端口映射无法转发到它,这是新手最常见的“容器起来了但访问不到”的原因。

最小可用 Dockerfile

最小可用意味着:能装依赖、能复制代码、能声明端口、能以非 root 用户启动。下面这份 Dockerfile 使用 node:20-alpine 作为基础镜像,先复制依赖清单再安装,最后复制源码:

FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY server.js ./

ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

USER node

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

关键点说明:

  • COPY package*.json 先于 COPY server.js,让依赖层在源码变更时仍能命中缓存。
  • npm ci 依据 lock 文件做确定性安装,比 npm install 更适合构建镜像。
  • USER node 使用基础镜像自带的非 root 用户,降低容器内进程权限。
  • EXPOSE 3000 只是文档声明,真正对外暴露靠运行时 -p 映射。

构建与运行验证

在项目根目录执行构建,并给镜像打上明确标签:

docker build -t node-demo:1.0 .

启动容器,把宿主机 8080 映射到容器 3000:

docker run -d --name node-demo -p 8080:3000 node-demo:1.0

验证分三层,逐层排除问题:

  1. 容器是否存活:docker ps 应显示 node-demo 状态为 Up。
  2. 应用是否响应:curl http://localhost:8080/healthz 应返回 {"status":"ok"}。
  3. 日志是否正常:docker logs node-demo 应出现 listening on 3000。
如果 docker ps 看不到容器,先用 docker ps -a 查看退出状态,再用 docker logs node-demo 定位。常见原因是 npm ci 失败(缺少 lock 文件)或 server.js 未复制进镜像。
构建上下文 → 镜像层 → 容器运行 → 访问验证 构建上下文 package.json package-lock.json server.js Dockerfile docker build -t node-demo:1.0 . 镜像层(自下而上) FROM node:20-alpine RUN npm ci --omit=dev COPY server.js ./ USER node / CMD 容器运行 docker run -d --name node-demo \ -p 8080:3000 node-demo:1.0 容器内监听 0.0.0.0:3000 宿主机端口 8080 → 3000 三层验证与判读 ① 容器存活 docker ps 期望:STATUS = Up 异常:Exited → 查 logs ② 应用响应 curl :8080/healthz 期望:{"status":"ok"} 异常:拒绝连接 → 查端口 ③ 启动日志 docker logs node-demo 期望:listening on 3000 异常:报错栈 → 查依赖/路径

常见失败点与对应检查

现象可能原因检查动作
构建报 npm ci 失败缺少 package-lock.json 或未复制确认 lock 文件存在且被 COPY 包含
容器立即退出server.js 未复制或路径错误docker logs 查看 Cannot find module
容器 Up 但访问被拒应用监听 127.0.0.1改为 0.0.0.0 后重建镜像
端口映射无效-p 写成容器端口在前确认格式为 -p 宿主机:容器
最小可用 Dockerfile 的验收标准:docker build 成功、docker run 后容器保持 Up、curl 健康检查返回预期 JSON、日志无异常栈。四项全部通过,才算完成本案例的容器化目标。