Docker 系列(四):编写 Dockerfile 打造自己的镜像
2026年7月25日 · 14 分钟
Dockerfile:镜像的「配方」
前面我们都在用别人的镜像。现在学会把自己的应用打包成镜像——靠的就是 Dockerfile。 它是一个纯文本文件,用一系列指令描述「如何一步步构建出镜像」。
先看一个完整例子(打包一个 Node.js 应用),再逐行拆解:
# 1. 基础镜像
FROM node:22-alpine
# 2. 设置工作目录
WORKDIR /app
# 3. 先只拷贝依赖清单(利用缓存,关键!)
COPY package.json package-lock.json ./
# 4. 安装依赖
RUN npm ci
# 5. 再拷贝其余源码
COPY . .
# 6. 构建
RUN npm run build
# 7. 声明环境变量
ENV NODE_ENV=production
# 8. 声明容器监听的端口(仅文档作用)
EXPOSE 3000
# 9. 容器启动时执行的命令
CMD ["npm", "start"]
核心指令逐个讲
FROM —— 从哪个基础镜像开始
FROM node:22-alpine
每个 Dockerfile 必须以 FROM 开头,指定基础镜像。选基础镜像的原则:够用就好、越小越好。
能用 -alpine 或 -slim 就别用完整版。
WORKDIR —— 设置工作目录
WORKDIR /app
相当于 cd /app,之后的 COPY、RUN、CMD 都以此为当前目录。目录不存在会自动创建。
别用 RUN cd xxx,因为每条 RUN 是独立的层,cd 不会保留到下一条。
COPY —— 把文件拷进镜像
COPY package.json ./ # 拷单个文件
COPY . . # 拷贝整个构建上下文
还有个
ADD,功能类似但会自动解压 tar、支持 URL。官方建议优先用 COPY,语义更清晰, 除非你确实需要 ADD 的解压能力。
RUN —— 构建时执行命令
RUN npm ci
RUN apt-get update && apt-get install -y curl # 多条命令用 && 连接,减少层数
RUN 在构建镜像时执行,每条 RUN 会生成一个新层。把相关命令用 && 串在一条
RUN 里,既减少层数,也避免缓存导致的 apt 陈旧问题。
ENV —— 设置环境变量
ENV NODE_ENV=production
ENV PORT=3000
这些变量在构建时和容器运行时都有效。
EXPOSE —— 声明端口
EXPOSE 3000
注意:EXPOSE 只是「文档声明」,告诉别人这个容器会用哪个端口,并不会真正映射端口。
真正对外暴露要靠 docker run -p。
CMD 与 ENTRYPOINT —— 容器启动时干什么
这是新手最容易混淆的一对,重点讲。
CMD 定义容器启动时的默认命令,但可被 docker run 后面的参数覆盖:
CMD ["npm", "start"]
docker run myapp # 执行 npm start
docker run myapp node --version # 被覆盖,改执行 node --version
ENTRYPOINT 定义「固定不变」的入口,docker run 的参数会追加到它后面:
ENTRYPOINT ["python", "app.py"]
两者配合是经典用法——ENTRYPOINT 定死程序,CMD 提供默认参数:
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
# docker run myapp -> python app.py --port 8080
# docker run myapp --port 9000 -> python app.py --port 9000
记忆口诀:ENTRYPOINT 是「要执行的程序」,CMD 是「默认参数」。只写一个时, 日常用 CMD 就够了。
关于 exec 格式与 shell 格式
CMD ["npm", "start"] # exec 格式(推荐):不经过 shell,信号能正确传递
CMD npm start # shell 格式:实际是 /bin/sh -c "npm start"
优先用 exec 格式(JSON 数组),否则你的程序会变成 shell 的子进程,收不到 docker stop
发出的停止信号,导致无法优雅退出。
构建镜像
docker build -t myapp:v1.0.0 .
-t myapp:v1.0.0:给镜像起名和打标签- 最后的
.:构建上下文,即把当前目录发给 Docker Daemon 作为构建材料
分层缓存:最重要的优化思维
还记得第三篇讲的构建缓存吗?Docker 从上到下执行指令,只要某层的输入没变,就用缓存。 一旦某层变了,它及之后的所有层缓存全部失效。
这就是为什么上面例子要先拷 package.json、装依赖,再拷源码:
COPY package.json package-lock.json ./ # 依赖清单没变 -> 缓存
RUN npm ci # -> 直接用缓存,不重装!
COPY . . # 源码变了 -> 从这里才重新执行
如果反过来写 COPY . . 在前,那你每改一行代码,都会触发重新 npm install,构建
慢到怀疑人生。把「不常变的」放前面,「常变的」放后面,是优化构建速度的第一原则。
.dockerignore —— 别把垃圾拷进去
和 .gitignore 类似,构建时会忽略这些文件,既加快构建又减小镜像:
node_modules
.git
.next
*.log
.env*.local
Dockerfile
node_modules 尤其重要——它应该在容器里重新安装,而不是把你本机(可能是 Windows/Mac)
的版本拷进 Linux 容器。
本篇小结
- Dockerfile 是构建镜像的配方,核心指令:
FROM / WORKDIR / COPY / RUN / ENV / EXPOSE / CMD。 CMD是可覆盖的默认命令,ENTRYPOINT是固定入口,二者可配合;优先用 exec(JSON)格式。- 分层缓存是优化关键:把不常变的(依赖)放前面,常变的(源码)放后面。
- 用
.dockerignore排除node_modules、.git等,加速构建、缩小镜像。
至此你已能打包自己的应用。下一篇解决一个绕不开的问题:容器删了数据就没了,怎么持久化?