1. 为现有 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
验证分三层,逐层排除问题:
- 容器是否存活:
docker ps应显示node-demo状态为Up。 - 应用是否响应:
curl http://localhost:8080/healthz应返回{"status":"ok"}。 - 日志是否正常:
docker logs node-demo应出现listening on 3000。
如果
docker ps 看不到容器,先用 docker ps -a 查看退出状态,再用 docker logs node-demo 定位。常见原因是 npm ci 失败(缺少 lock 文件)或 server.js 未复制进镜像。常见失败点与对应检查
| 现象 | 可能原因 | 检查动作 |
|---|---|---|
构建报 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、日志无异常栈。四项全部通过,才算完成本案例的容器化目标。