深色模式
Dockerfile
概述
Dockerfile 不是“把部署命令抄进一个文件里”这么简单。它真正做的事,是把镜像构建过程写成一份可重复执行的说明书。镜像能不能稳定构建、能不能合理利用缓存、最终体积是不是失控,很多时候都在这里分出高下。
这一篇不做指令大全,而是先把一个能工作的构建链路讲清楚,再拆常用指令和日常写法。
先看一个最小示例
下面这个例子足够覆盖 Dockerfile 最常见的骨架:
dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "start"]这份文件表达的是一条很清楚的构建路线:
- 以
node:22-alpine作为基础镜像 - 工作目录切到
/app - 先复制依赖清单并安装依赖
- 再复制项目代码
- 声明容器内应用监听的端口
- 容器启动时执行
npm run start
真正关键的,不是会不会写这些指令名,而是知道为什么复制依赖清单和复制源码要分开写。答案和构建缓存有关。
构建上下文是什么
执行 docker build 时,Docker 不会直接读取宿主机任意位置的文件,它只会看到构建上下文里的内容。通常这个上下文就是当前目录:
sh
docker build -t my-app .最后那个 .,就是构建上下文。COPY 和 ADD 默认只能从这个范围里取文件。
这也是为什么 Dockerfile 明明写对了,构建时却提示文件不存在,问题往往不是语法,而是文件根本没进入构建上下文。
先掌握这几个常用指令
FROM
FROM 指定基础镜像,也是 Dockerfile 的起点。
dockerfile
FROM nginx:1.27-alpine基础镜像选型会直接影响:
- 最终镜像体积
- 可用的软件包生态
- 安全更新方式
- 容器里默认带了什么
所以基础镜像不是越小越好,而是要和应用类型匹配。很多场景下,官方镜像通常是最稳的起点。
WORKDIR
WORKDIR 用来设置后续指令的工作目录:
dockerfile
WORKDIR /app后面的 COPY . .、RUN npm ci、CMD ["npm", "run", "start"],都会在这个目录语境下执行。比起反复写绝对路径,WORKDIR 更清楚,也更不容易出错。
COPY
COPY 把构建上下文里的文件复制进镜像:
dockerfile
COPY package.json package-lock.json ./
COPY . .它最常见的用途,就是先复制依赖清单,再复制业务代码。这个拆分动作很重要,因为依赖清单不变时,安装依赖那一层就可以复用缓存。
RUN
RUN 在镜像构建阶段执行命令:
dockerfile
RUN npm ci
RUN apt-get update && apt-get install -y curl它和 CMD 的区别很容易混:
RUN发生在构建镜像时CMD发生在启动容器时
把这两个阶段混在一起,Dockerfile 基本就会越写越乱。
CMD
CMD 指定容器启动后的默认命令:
dockerfile
CMD ["npm", "run", "start"]它更像“默认启动方式”。如果运行容器时显式给了别的命令,CMD 可以被覆盖。
ENTRYPOINT
ENTRYPOINT 用来定义更固定的启动入口。常见组合是:
ENTRYPOINT指定主程序CMD提供默认参数
如果只是普通应用启动,单独用 CMD 往往已经够用。只有在确实需要把容器当成某个固定可执行工具时,再考虑 ENTRYPOINT。
ENV 和 EXPOSE
ENV 用来写环境变量:
dockerfile
ENV NODE_ENV=productionEXPOSE 用来声明容器内部打算使用的端口:
dockerfile
EXPOSE 3000EXPOSE 本身不会自动把端口映射到宿主机,它更像是镜像层面的说明。真正的端口映射仍然要在 docker run -p 或 Compose 里配置。
层缓存为什么很重要
Dockerfile 每条会生成镜像层的指令,都会影响构建缓存。顺序写得合理,构建会快很多;顺序写得随意,每次改一点代码都可能整镜像重来。
例如这两种写法,效果差很多:
dockerfile
COPY . .
RUN npm cidockerfile
COPY package.json package-lock.json ./
RUN npm ci
COPY . .第二种写法更好,因为只要依赖文件没变,npm ci 这层通常就能命中缓存。源码改动不该让依赖安装白跑一遍。
.dockerignore 不是可有可无
构建上下文太大,常常不是小问题,而是最直接的慢构建来源之一。.dockerignore 的作用,就是把根本不该进构建上下文的东西挡在外面。
例如:
text
node_modules
.git
dist
.env这几个对象如果原封不动塞进构建上下文,通常只有两个结果:构建变慢,或者把不该进镜像的内容也打包进去。
多阶段构建什么时候值得用
多阶段构建的核心价值,是把“编译环境”和“运行环境”拆开。
例如:
dockerfile
FROM golang:1.24 AS builder
WORKDIR /src
COPY . .
RUN go build -o app .
FROM debian:stable-slim
WORKDIR /app
COPY --from=builder /src/app .
CMD ["./app"]这个写法的好处是,构建阶段需要 Go 编译器,运行阶段却不需要。最后得到的镜像更小,也更干净。
只要项目存在“先构建产物,再运行产物”的过程,多阶段构建就基本值得考虑。
一些日常写法上的习惯
- 优先用精简但可信的基础镜像
- 把高频变化的内容放后面,尽量保住前面的缓存层
- 把安装依赖和复制源码分开
- 配好
.dockerignore - 能用多阶段构建时,不要把整套构建工具链带进运行镜像
Dockerfile 写到最后,真正决定质量的通常不是炫技,而是有没有把构建过程拆得清楚。
