← 全部文章
DockerDockerfile构建

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,之后的 COPYRUNCMD 都以此为当前目录。目录不存在会自动创建。 别用 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 等,加速构建、缩小镜像。

至此你已能打包自己的应用。下一篇解决一个绕不开的问题:容器删了数据就没了,怎么持久化?

相关水晶