Dockerfile 常用指令详解
本教程共 25 篇 · 第 12 篇 · 更新于 2026-07-26 · 约 10 分钟阅读
12. Dockerfile 常用指令详解
本节目标:把 Dockerfile 里最常用的指令逐个吃透,重点搞清 CMD 与 ENTRYPOINT、COPY 与 ADD 这几对容易混淆的指令。
上一章我们写了一个能跑的 Dockerfile,但里面的指令只是囫囵吞枣地用了。这一章把常用指令逐个拆开讲,告诉你每条指令干啥、什么时候用、有啥坑。
Dockerfile 的指令不少,但真正高频的就那么十几条。掌握了这些,90% 的场景都能应付。
FROM:一切的基础
FROM 是 Dockerfile 的第一条指令(除了 ARG),指定基础镜像。后面所有操作都基于它。
FROM ubuntu:24.04
选基础镜像有几个原则:
- 优先用官方镜像,安全可靠、维护及时。
- 体积敏感的场景用 Alpine 版本,比如
node:22-alpine、python:3.13-alpine。 - 生产环境别用
latest标签,它指向会变,今天构建的和明天构建的可能不一样。钉死具体版本,比如python:3.13.1。
Tip一个 Dockerfile 里可以有多个
FROM,这就是多阶段构建,第 14 章专门讲。这一章先只看单阶段。
RUN:在镜像里执行命令
RUN 用来在构建时执行命令,比如装软件包、创建目录、编译代码。它产生的结果会被固化到镜像层里。
RUN 有两种写法:
- shell 形式:
RUN apt-get update && apt-get install -y curl - exec 形式:
RUN ["apt-get", "install", "-y", "curl"]
shell 形式更直观,命令在 /bin/sh -c 下执行,能用管道、变量。exec 形式不经过 shell,直接执行程序,适合不需要 shell 特性的场景,也更安全。
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
git \
&& rm -rf /var/lib/apt/lists/*
这条命令有几个细节值得学:用 && 把多条命令串起来,避免每条命令单独一层;装完包后顺手清理 apt 缓存,减小镜像体积。
Warning别写多条
RUN装一堆包,每个RUN都是一层,层数越多镜像越大。把相关操作合并到一个RUN里,用&&连接。
WORKDIR:设定工作目录
WORKDIR 设定后续指令的工作目录。如果目录不存在,Docker 会自动创建。
WORKDIR /app
设了之后,后面的 RUN、CMD、ENTRYPOINT、COPY 都在这个目录下执行。比手写 RUN cd /app && ... 干净多了,而且 RUN cd 在不同层之间是不生效的—每个 RUN 都是独立的进程。
WORKDIR 可以写多次,后面会覆盖前面:
WORKDIR /app
WORKDIR src
最终工作目录是 /app/src,是相对拼接的。
COPY 与 ADD:把文件塞进镜像
这两条指令都用来把宿主机的文件拷进镜像,但行为有区别。绝大多数情况用 COPY 就够了。
COPY
COPY 语法很简单:
COPY <宿主机路径> <容器内路径>
COPY package.json /app/
COPY . /app
它只做一件事:把文件或目录从上下文里拷到镜像里。简单、可预测、推荐首选。
ADD
ADD 比 COPY 多了两个能力:
- 源是 URL 时,会自动下载。
- 源是本地
.tar压缩包时,会自动解压到目标目录。
ADD https://example.com/bigfile.tar.gz /tmp/
ADD rootfs.tar.gz /
听起来挺方便,但这俩能力也是坑。自动下载的文件不会校验完整性,自动解压的行为有时候不符合预期。官方明确建议:只是拷文件就用 COPY,需要解压 tar 包时才考虑 ADD。
Note记住一句话:
COPY是「老老实实拷贝」,ADD是「会变魔术的拷贝」。能用COPY就别用ADD,行为更可控。
一句话总结这两对区别:
| 指令 | 行为 | 推荐场景 |
|---|---|---|
COPY | 纯拷贝文件/目录 | 日常拷文件,首选 |
ADD | 拷贝 + 自动下载 + 自动解压 tar | 需要解压本地 tar 包时 |
ENV 与 ARG:环境变量
这俩都用来设变量,但作用范围不一样。
ENV:运行时环境变量
ENV 设的变量会持久存在于镜像里,容器跑起来也能读到。
ENV APP_PORT=8080
ENV PATH=/usr/local/app/bin:$PATH
容器启动后 echo $APP_PORT 能看到 8080。适合设那些运行时需要的配置,比如版本号、路径、默认端口。
ARG:构建时变量
ARG 只在构建阶段有效,容器跑起来后这些变量就不存在了。
ARG VERSION=1.0
RUN echo "Building version $VERSION"
ARG 常用来在构建时传参:
docker build --build-arg VERSION=2.0 -t myapp .
Warning别用
ARG传敏感信息(密码、密钥)。构建历史里能看到ARG的值,镜像的docker history也会暴露。敏感信息用构建时的 secret 机制或多阶段构建隔离。
EXPOSE:声明端口
EXPOSE 只是声明容器打算监听哪个端口,方便看镜像的人理解。它不会真的把端口对外开放。
EXPOSE 8080
EXPOSE 80 443
真正对外开端口,还得靠 docker run -p。EXPOSE 更多是文档作用,配合 -P(大写)能自动把这些端口随机映射到宿主机。
VOLUME:声明匿名卷
VOLUME 在镜像里声明某个路径是数据卷,容器跑起来时 Docker 会自动给它挂一个匿名卷。
VOLUME /data
这样做的好处是容器删了,数据还在匿名卷里。但匿名卷不好管理,名字是一串随机哈希。实际用的时候,更推荐用 docker run -v 具名卷名:/data 显式挂载,第 16 章会讲。
USER:切换运行用户
默认情况下,容器里的进程是以 root 跑的。这在生产环境是个安全隐患—万一应用有漏洞,攻击者直接拿到 root。
USER 指令让后续指令以指定用户身份执行:
RUN useradd -m app
USER app
CMD ["node", "app.js"]
通常先 RUN useradd 建好用户,再 USER 切过去。切了之后,CMD、ENTRYPOINT、RUN 都以这个用户跑。
Tip如果基础镜像里没有你要的用户,得先用
RUN useradd创建,然后再USER。顺序反了会报错。注意有些目录切了用户后可能没权限写,比如/app如果是 root 建的,普通用户写不了,需要RUN chown调整权限。
CMD 与 ENTRYPOINT:容器启动命令
这是 Dockerfile 里最容易混淆的一对,也是面试常考题。把它俩讲清楚是这一章的重点。
先说结论:CMD 是「默认命令」,ENTRYPOINT 是「固定入口」。一个容易被覆盖,一个不容易被覆盖。
两种书写形式
这俩指令都有多种写法,但最该掌握的是 exec 形式和 shell 形式:
- exec 形式:
CMD ["node", "app.js"],参数是 JSON 数组。 - shell 形式:
CMD node app.js,等价于["/bin/sh", "-c", "node app.js"]。
Warningexec 形式里必须是合法 JSON:双引号、逗号分隔、不能有单引号。写成
CMD ['node', 'app.js']会被当成 shell 形式,Docker 不会报错但行为就变了。这是非常隐蔽的坑。
推荐用 exec 形式。 原因有二:一是 exec 形式直接启动进程,进程 PID 是 1,能正确接收信号(比如 docker stop 发的 SIGTERM);二是 shell 形式实际跑的是 /bin/sh -c "你的命令",你的应用成了 sh 的子进程,信号传递会有问题,docker stop 可能要等超时才能强杀。
CMD 单独用
只有 CMD 没有 ENTRYPOINT 时,CMD 就是容器启动命令。docker run 后面追加的参数会整个替换掉 CMD:
CMD ["nginx", "-g", "daemon off;"]
docker run myimage echo "hello" # 实际执行 echo "hello",CMD 被覆盖
ENTRYPOINT 单独用
只有 ENTRYPOINT 时,ENTRYPOINT 是固定的启动命令,docker run 后面的参数会作为 ENTRYPOINT 的追加参数:
ENTRYPOINT ["nginx"]
docker run myimage -g "daemon off;" # 实际执行 nginx -g "daemon off;"
两者配合:最佳实践
最推荐的做法是 ENTRYPOINT + CMD 配合。ENTRYPOINT 定死要跑的程序,CMD 给默认参数,用户还能通过 docker run 覆盖参数:
ENTRYPOINT ["node", "app.js"]
CMD ["--port", "3000"]
实际执行的是 node app.js --port 3000。如果运行时传参:
docker run myimage --port 8080
CMD 被覆盖,实际执行 node app.js --port 8080。程序不变,参数可调,非常灵活。
shell 形式的陷阱
如果 ENTRYPOINT 用 shell 形式:
ENTRYPOINT node app.js
它会忽略 CMD,而且由于套了 /bin/sh -c,信号传递失效。所以 ENTRYPOINT 几乎总是用 exec 形式。
总结一张表:
| 组合 | docker run 追加参数的行为 | 实际启动 |
|---|---|---|
只有 CMD | 整个替换 CMD | 追加参数 |
只有 ENTRYPOINT(exec 形式) | 作为 ENTRYPOINT 的参数追加 | 程序 + 追加参数 |
ENTRYPOINT + CMD | 替换 CMD 部分,保留 ENTRYPOINT | 程序 + 新参数 |
ENTRYPOINT(shell 形式) | 忽略 CMD 和追加参数 | /bin/sh -c 套娃,不推荐 |
指令速查表
把这一章的指令汇总一下,方便查阅:
| 指令 | 作用 | 常见用法 |
|---|---|---|
FROM | 指定基础镜像 | FROM python:3.13 |
RUN | 构建时执行命令 | RUN apt-get install -y curl |
WORKDIR | 设定工作目录 | WORKDIR /app |
COPY | 拷贝文件进镜像 | COPY . . |
ADD | 拷贝 + 下载 + 解压 | ADD app.tar.gz /opt/ |
ENV | 设运行时环境变量 | ENV NODE_ENV=production |
ARG | 设构建时变量 | ARG VERSION=1.0 |
EXPOSE | 声明端口 | EXPOSE 8080 |
VOLUME | 声明匿名卷 | VOLUME /data |
USER | 切换运行用户 | USER app |
CMD | 默认启动命令 | CMD ["node", "app.js"] |
ENTRYPOINT | 固定入口命令 | ENTRYPOINT ["docker-entrypoint.sh"] |
还有几条不那么常用但值得知道的:LABEL 给镜像加元数据标签,HEALTHCHECK 定义健康检查命令,STOPSIGNAL 指定停止信号,ONBUILD 在被作为基础镜像时触发。用到时查官方 reference 即可。
下一章我们聊构建缓存—为什么有时候构建飞快、有时候又慢得要命,以及怎么写 Dockerfile 让构建又快又省。