首页 / Docker 入门教程 / Dockerfile 常用指令详解

Docker 入门教程

Dockerfile 常用指令详解

本教程共 25 篇 · 第 12 篇 · 更新于 2026-07-26 · 约 10 分钟阅读

DockerDocker 入门教程Dockerfile镜像构建CMDENTRYPOINTCOPYADD

12. Dockerfile 常用指令详解

本节目标:把 Dockerfile 里最常用的指令逐个吃透,重点搞清 CMD 与 ENTRYPOINT、COPY 与 ADD 这几对容易混淆的指令。

上一章我们写了一个能跑的 Dockerfile,但里面的指令只是囫囵吞枣地用了。这一章把常用指令逐个拆开讲,告诉你每条指令干啥、什么时候用、有啥坑。

Dockerfile 的指令不少,但真正高频的就那么十几条。掌握了这些,90% 的场景都能应付。

FROM:一切的基础

FROM 是 Dockerfile 的第一条指令(除了 ARG),指定基础镜像。后面所有操作都基于它。

FROM ubuntu:24.04

选基础镜像有几个原则:

  • 优先用官方镜像,安全可靠、维护及时。
  • 体积敏感的场景用 Alpine 版本,比如 node:22-alpinepython: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

设了之后,后面的 RUNCMDENTRYPOINTCOPY 都在这个目录下执行。比手写 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

ADDCOPY 多了两个能力:

  1. 源是 URL 时,会自动下载。
  2. 源是本地 .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 -pEXPOSE 更多是文档作用,配合 -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 切过去。切了之后,CMDENTRYPOINTRUN 都以这个用户跑。

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"]
Warning

exec 形式里必须是合法 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 让构建又快又省。