DK-04 Dockerfile
04 · Dockerfile(Dockerfile)
📅 预计 60 分钟 | ⭐ = 重要知识点 | 📌 中英术语见文末
🛠 本讲示例基于 Linux 环境,命令可直接运行
4.0 先给直觉:做镜像的"菜谱"
前两讲我们直接 pull 现成镜像。那镜像怎么来的?答案是用 Dockerfile 做出来的。
把 Dockerfile 想成一张菜谱:每一步都是一条指令(步骤),按从上到下的顺序执行,一步步把材料加工好,最后"烤"出一个镜像。别人拿到你的菜谱,在任意机器上都能做出一样的菜——这就是"构建一次,到处运行"。
Dockerfile(菜谱)
↓ docker build -t 镜像名 .
镜像(成品)
↓ docker run
容器(上桌的那一份)
💡 记忆口诀:Dockerfile 从 FROM 起锅,每行指令是一步,RUN 在"制作时"执行,CMD/ENTRYPOINT 决定"上桌时"干什么。
4.1 常用指令 ⭐
FROM:起锅的基础镜像
Dockerfile 必须以 FROM 开头,它指定本镜像基于哪个基础镜像:
FROM node:18-alpine
FROM python:3.11-slim
FROM ubuntu:22.04
为什么必须第一行?因为镜像的每一层都叠在上一层之上,没有地基就无从建楼。基础镜像选 alpine(超小 Linux 发行版)能显著减小体积,生产常用。
RUN:制作时执行的命令
RUN apt-get update && apt-get install -y curl
RUN 在构建镜像时执行,其效果被固化进镜像层。注意两点:
- 多个命令用
&&连成一条,避免每个命令各生成一层、镜像变臃肿。 -y之类的非交互参数记得带上,否则构建时可能卡在交互确认。
COPY 与 ADD:把文件拷进镜像
COPY package.json /app/package.json
ADD app.tar.gz /app/ # ADD 额外支持 URL 和自动解压
COPY只做"复制",语义单纯,优先用它。ADD多了自动解压压缩包、支持远程 URL 的能力,用不到这些能力时,别用它。
WORKDIR:设置工作目录
WORKDIR /app
之后的 RUN/COPY/CMD 都会在这个目录下执行。反复 cd 不如直接 WORKDIR,它还会自动创建不存在的目录。
ENV 与 ARG:注入变量
ENV NODE_ENV=production # 环境变量,运行时容器里也生效
ARG VERSION=1.0 # 构建参数,只在构建时有效
ENV会进入最终镜像,容器运行时可以用。ARG只在构建过程有效,构建时用docker build --build-arg VERSION=1.1覆盖。
EXPOSE:声明端口(只是文档!)
EXPOSE 3000
EXPOSE 只是告诉别人"我的服务监听 3000 端口",它不会真正映射端口。真正的映射由运行时 docker run -p 3000:3000 或 -P 完成。
USER:切换运行用户
USER node
出于安全,生产镜像避免用 root 跑应用,改用普通用户(第 6 讲展开)。
构建上下文与 .dockerignore ⭐
docker build -t myapp:1.0 . 最后的 . 是构建上下文(build context)目录。Docker 会把这个目录下的文件打包发给守护进程,Dockerfile 里的 COPY . . 就是从这里取文件的。
上下文目录不该装无关文件——node_modules、.git、缓存动不动几百 MB,全打包进去又慢又占空间。用 .dockerignore 文件排除(语法类似 .gitignore):
node_modules
.git
*.log
.env
写一份 .dockerignore,构建上下文立刻瘦下来,COPY 也不会误把密钥拷进镜像。
一个完整示例 ⭐
FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "index.js"]
构建并运行:
docker build -t myapp:1.0 . # . 是构建上下文目录
docker run -d -p 3000:3000 myapp:1.0
构建时每执行一条指令会生成一层,终端会显示 Step 1/7 : FROM node:18-alpine 之类的步骤编号。缓存命中的步骤会显示 Using cache——这正是"变化少的指令放前面"的收益来源。
⚠️ 常见错误
FROM不在第一行:基础镜像必须在最开始指定,前面有其它指令会报错。- 每条
RUN各写一行:产生大量无用层、镜像膨胀,用&&合并。 - 把
EXPOSE当端口映射:它只是声明,真正对外开放要靠docker run -p。
4.2 CMD 与 ENTRYPOINT:上桌时干什么 ⭐
两种写法:exec 形式 vs shell 形式
CMD ["node", "index.js"] # exec 形式(数组),推荐
CMD node index.js # shell 形式(字符串),会自动包一层 /bin/sh -c
ENTRYPOINT ["docker-entrypoint.sh"] # 同样两种写法
ENTRYPOINT + CMD 的经典组合:先初始化再启动
真实场景里,容器启动前常要做初始化(等数据库、生成配置、替换模板),这套逻辑写进一个启动脚本,用 ENTRYPOINT 固定,CMD 给默认参数:
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["node", "index.js"]
脚本里做初始化,最后 exec 启动真正的进程,保证信号能正确传给主进程。这种"入口脚本 + 默认命令"模式在官方镜像里非常普遍(如 nginx、MySQL 镜像都这么干)。
核心区别:能不能被覆盖
- CMD:提供默认启动命令,运行时可以被
docker run后面的参数覆盖。 - ENTRYPOINT:固定入口,
docker run后面的参数会拼在它后面,而不是替换它。
最经典的配合是:ENTRYPOINT 定"谁来做",CMD 定"默认做什么":
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
运行时 docker run mynginx -h,实际执行的是 nginx -h(-h 追加到了 ENTRYPOINT 后面,CMD 被忽略)。
再看一个反例,理解"CMD 可被覆盖":
CMD ["node", "index.js"]
# docker run myapp npm test → 实际执行 npm test,CMD 被覆盖
⚠️ 常见错误
- 两种形式混着写:
CMD ["node index.js"]会被当成单个参数,找不到"node index.js"这个命令。exec 形式必须拆成["node", "index.js"]。 - 以为
CMD和RUN一样在构建时执行:RUN在构建时跑,CMD是容器启动时跑,时机完全不同。 - 需要固定入口却用
CMD:如果入口必须不可覆盖,用ENTRYPOINT;需要允许灵活追加参数,用CMD。
4.3 多阶段构建:瘦身利器 ⭐
为什么需要
很多应用"编译用的工具链"非常重。比如 Go 编译需要完整的 Go 工具链(GB 级),但编译出来的二进制在运行时只需要一个极小的系统。如果只用一个阶段,这些工具链全会被塞进镜像。
多阶段构建:在 Dockerfile 里写多个 FROM,前面的阶段只用来"加工",最后只把产物拷进一个干净的小基础镜像。
示例:Go 程序
# 阶段一:builder,负责编译
FROM golang:1.21 AS builder
WORKDIR /src
COPY . .
RUN go build -o app main.go
# 阶段二:final,只要产物
FROM alpine:3.19
COPY --from=builder /src/app /app
CMD ["/app"]
关键点:
AS builder给第一阶段起名。COPY --from=builder /src/app /app:从builder阶段把编译产物拷过来。- 最终镜像只包含
alpine+ 一个二进制,体积可能从 800MB 降到 20MB 左右。
好处
- 镜像小:传输快、启动快、占用磁盘少。
- 更安全:不再包含编译器、源码、调试信息等无关内容,攻击面变小。
- 层数少:最终镜像只有两三层。
Python 版本示例
脚本语言同样适用多阶段。下面把"装依赖 → 跑测试 → 只拷依赖产物"分成两段:
# 阶段一:装依赖并验证
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --prefix=/install -r requirements.txt
# 阶段二:运行时镜像,只保留已安装的依赖
FROM python:3.11-slim
COPY --from=builder /install /usr/local
COPY app.py .
CMD ["python", "app.py"]
依赖被装进 /install,最终镜像复制过去即可,构建阶段的源码和中间文件一概不带。
构建优化小贴士
COPY package.json ./
RUN npm install # 先拷依赖清单再装依赖
COPY . . # 最后拷全部源码
把变化少的指令放前面,能充分利用层缓存:只要 package.json 没变,npm install 那一层就命中缓存,重建秒级完成。配合 .dockerignore 排除 node_modules、.git 等,避免把无关文件拷进上下文。
⚠️ 常见错误
- 忘了
AS命名或--from写错阶段名:COPY --from=xxx找不到阶段会直接失败。 - 多阶段只写不用:写了两个阶段却没
COPY --from,等于白写,两个阶段都会进最终镜像。 - 把
node_modules拷进上下文:没有.dockerignore,构建上下文可能数百 MB,构建又慢又大。
📌 双语术语表(本讲)
| 中文 | English | 记忆点 |
|---|---|---|
| 基础镜像 | base image | FROM 指定 |
| 构建上下文 | build context | docker build 的 . |
| 工作目录 | workdir | 之后指令的默认目录 |
| 环境变量 | environment | ENV 运行时生效 |
| 构建参数 | build arg | ARG 构建时生效 |
| 暴露端口 | expose | 声明端口,非映射 |
| 启动命令 | cmd | 可被覆盖的默认命令 |
| 入口命令 | entrypoint | 固定入口,追加参数 |
| 多阶段构建 | multi-stage build | 产物瘦身 |
| 层缓存 | layer cache | 变化少的指令放前面 |
| 构建上下文 | build context | docker build 的 . |
| 忽略文件 | .dockerignore | 缩小上下文、防泄密 |
| 入口脚本 | entrypoint script | 初始化后 exec 主进程 |
⭐ 本讲考点清单
- Dockerfile 必须以
FROM开头 RUN构建时执行,CMD容器启动时执行COPYvsADD:COPY 语义单纯优先;ADD 自动解压WORKDIR设置工作目录,自动创建ENV进镜像运行时可用;ARG仅构建时有效EXPOSE只是声明,-p/-P才是映射CMD可被覆盖;ENTRYPOINT固定入口、参数追加- exec 形式
["node", "index.js"]vs shell 形式 - 多阶段构建:
AS命名 →COPY --from拷产物 → 最终镜像只含产物 - 变化少的指令放前面利用层缓存;
.dockerignore减小上下文 - 构建上下文 =
docker build的.;.dockerignore排除 node_modules、.git、.env - ENTRYPOINT + CMD 组合:入口脚本做初始化,CMD 给默认参数
- 多阶段构建的 COPY --from 可拷任意中间阶段产物(Go / Python 通用)