零、这篇文章讲什么
前几篇用的都是官方现成镜像(nginx、mysql),那是别人写好的 Dockerfile 构建的。这一篇轮到自己写:把我们之前跑在宿主机 1314 端口的 Node 服务,打包成自己的镜像 ,走完 Dockerfile → build → run → 验证 的完整链路。
读完你会得到四样东西:
- 一份逐行讲透的 Dockerfile
- "层与缓存"这个必考概念
- 一次真实的排错实录:国内镜像源 EOF(我踩过的坑)
- 镜像存哪、为什么
docker rmi会拒绝(删除的正确顺序)
前置知识:第 01 篇的 image / container 概念。会跑
docker build、docker run即可。
一、为什么需要 Dockerfile
第 01 篇说 image 是"光盘"。那么光盘是怎么造出来的? 答案是 Dockerfile------一份"如何构建镜像"的说明书。
arduino
源码(index.js / package.json)
│ docker build -t node-demo .
▼
Dockerfile 逐条指令 → 镜像(node-demo)
│ docker run
▼
容器(node-app)
在项目根目录新建一个文件,名字就叫 Dockerfile(无扩展名),内容:
dockerfile
FROM node:22 # 基础镜像:node 22 + linux + npm,即"运行环境"
WORKDIR /app # 容器内的工作目录(不存在会自动创建)
COPY . . # 把当前目录的源码拷进容器的 /app
EXPOSE 1314 # 声明容器内端口(只是声明,不负责映射)
CMD ["node", "index.js"] # 容器启动时执行的命令
逐行解释:
| 指令 | 干什么 | 类比 |
|---|---|---|
FROM node:22 |
基于官方 node 镜像起步 | 买一个"node 环境"的半成品 |
WORKDIR /app |
设定容器内工作目录 | 进到厨房的"案板"位置 |
COPY . . |
拷源码进容器 | 把食材摆上案板 |
EXPOSE 1314 |
声明应用监听端口 | 告诉别人"我的菜是热乎的" |
CMD [...] |
容器启动执行的命令 | 端上桌的动作 |
我的 index.js 只用 Node 内置模块、没有第三方依赖,所以不用
npm install。如果项目有依赖,正确姿势是先COPY package.json ./再RUN npm install最后COPY . .------原因见下一节。
二、层与缓存(必考概念)
Dockerfile 里每条指令 = 构建出的一层,层之间像 git 提交一样做增量缓存:
- 某条指令的输入没变,这一层直接复用缓存,不用重跑
- 所以把"经常变的"放后面:源码天天改,依赖很少变
dockerfile
# 错误的顺序:源码一变,依赖跟着全部重装
COPY . .
RUN npm install
# 正确的顺序:先锁依赖,再进源码
COPY package.json ./
RUN npm install # 只有 package.json 变了才重装
COPY . .
这就是"缓存友好"的 Dockerfile。项目大了之后,这个顺序能让每次构建从分钟级降到秒级。
三、build → run → 验证
1. 构建镜像
bash
docker build -t node-demo .
-t node-demo 给镜像起名,末尾的 . 是构建上下文(当前目录)。预期看到一段分步日志,最后:
ruby
=> [1/3] FROM docker.io/library/node:22
=> [2/3] WORKDIR /app
=> [3/3] COPY . .
=> naming to docker.io/library/node-demo:latest done ← 镜像造好了
2. 运行容器
bash
docker run -d --name node-app -p 1314:1314 node-demo
docker ps
-p 1314:1314:宿主机 1314 → 容器内 1314。docker ps 能看到端口列 0.0.0.0:1314->1314/tcp。
3. 验证
bash
curl http://localhost:1314 # 应返回 hello world
docker logs node-app # 应看到 node service run on 1314
docker exec -it node-app bash # 进容器,ls /app 能看到你的 index.js
到这里,你的第一个镜像链路就完整了:源码 → build → image → run → container → 验证。
四、排错实录:国内镜像源 EOF
第一次 docker build,我卡在第一步,报错:
vbscript
ERROR: failed to solve: node:22: failed to resolve source metadata
for docker.io/library/node:22: failed to do request: Head
"https://docker.mirrors.ustc.edu.cn/v2/library/node/manifests/22": EOF
报错末尾的 EOF 是线索------构建根本没开始,是拉基础镜像失败。原因:我机器配了镜像加速器,但中科大(ustc)和网易(163)这两个源已经失效了,而我的网络又直连不了 Docker Hub。
排查和修复三步:
bash
# 1. 测各镜像源连通性(curl 返回 401 表示服务端可达,000 表示不通)
curl -sS -o /dev/null -w "%{http_code}\n" https://docker.mirrors.ustc.edu.cn/v2/ # 000 失效
curl -sS -o /dev/null -w "%{http_code}\n" https://docker.1ms.run/v2/ # 401 可用
json
// 2. 改 daemon.json 里的 registry-mirrors,换成可用的源
// Windows 路径:C:\Users\<你>\.docker\daemon.json
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.m.daocloud.io"
]
}
bash
# 3. 重启 Docker Desktop 让配置生效,再重新 build
docker desktop restart
这也是个运维实战经验:国内 Docker 加速器经常悄悄失效 ,遇到
EOF/failed to resolve source metadata报错,第一反应就是"检查镜像源"。
五、镜像存哪 & 为什么 rmi 会拒绝
镜像不在你的项目文件夹里
docker images 能看到 node-demo,但项目目录里什么都没有------因为镜像不是普通文件,是 Docker 引擎管理的库存。你的项目目录只有"配方"(Dockerfile)和"原料"(源码),成品在 Docker 自己的存储区里:
- Windows 上就是 WSL2 虚拟机的虚拟磁盘(
ext4.vhdx),Windows 资源管理器看不到 docker images/docker ps是你看到 Docker 内部世界的唯一窗口
想导出成普通文件,用快照:
bash
docker save -o node-demo.tar node-demo # 镜像 → tar 文件,可以传给别人
docker load -i node-demo.tar # 别人收到后导入
为什么 docker rmi 会拒绝
我删除时踩了个经典的坑:
vbnet
docker rmi node-demo
Error: conflict: unable to delete node-demo:latest (must be forced)
- container da9adcc6d068 is using its referenced image 9b1a8ed86bdf
因为容器 node-app 正用着这个镜像在跑。 Docker 的保护机制拒绝删除被引用的镜像。正确顺序是先删容器、再删镜像:
bash
docker rm -f node-app # ① 先删容器(镜像的下游)
docker rmi node-demo # ② 没有引用者了,能删
顺序记忆法:谁引用谁,先删被引用的下游。容器引用镜像 → 先删容器,再删镜像。 报错提示的 -f 强制删除不是日常做法,会留下悬空引用。
六、小结
| 知识点 | 一句话 |
|---|---|
| Dockerfile | 一条指令一层,构建镜像的说明书 |
| 层与缓存 | 把"经常变的"放后面,命中缓存秒级构建 |
| build/run | -t 起名构建,-p 端口映射运行,logs 看日志 |
| 镜像源排错 | EOF 先查加速器,curl 测 401 即通 |
| 镜像删除 | 先删容器,再删镜像,别用 -f |
下一篇 05 两个容器协作 做这次练习的升级版------把 node 和 nginx 都容器化,放进同一个 docker 网络,让 nginx 用容器名反代到 node,完成真正的"两个容器协作"。