Docker / 命令文档

Dockerfile 指令详解(面试向)

$ docker 8 个章节 Linux 命令详解

概述

本篇是「Docker 面试核心」系列第三篇,逐条讲 Dockerfile 高频指令:FROM、RUN、COPY、ADD、WORKDIR、ENV、ARG、EXPOSE、CMD、ENTRYPOINT、USER。CMD 与 ENTRYPOINT 的配合、ARG 与 ENV 的区别是每年的必考题。

本篇所有构建与运行输出均在 Docker 26.1.4 测试机实测。示例 Dockerfile:

FROM alpine:3.19
ARG VERSION=1.0
ENV APP_NAME=kb-app VERSION=${VERSION}
WORKDIR /app
COPY hello.sh /app/
RUN chmod +x /app/hello.sh && echo "built ${VERSION}" > /app/build.info
USER nobody
EXPOSE 8080
ENTRYPOINT ["/app/hello.sh"]
CMD ["default-arg"]

FROM 与 RUN

FROM 指定基础镜像(一切层的起点),RUN 在构建期执行命令并生成新层。

FROM alpine:3.19          # 建议带版本 Tag,不用 latest(不可复现)
RUN chmod +x /app/hello.sh && echo "built" > /app/build.info   # 每条 RUN = 一层

面试要点:

  • 一条 RUN = 一个镜像层,层只增不减:把 apt update && apt install && rm -rf /var/lib/apt/lists/* 写进同一条 RUN 才能减小体积
  • 多条 RUN 之间用 && 或 heredoc 合并是瘦身的基本功;RUN cd xxx 不改后续层的工作目录,应该用 WORKDIR
  • FROM 可多次出现实现多阶段构建(见镜像篇);FROM scratch 表示从零开始(Go 静态编译常用)

COPY 与 ADD

COPY 只做「从构建上下文复制文件」,行为可预测;ADD 额外支持自动解压本地 tar 与远程 URL,语义杂、不推荐。

COPY hello.sh /app/       # 推荐:单纯复制
ADD app.tar.gz /opt/      # 会自动解压到 /opt(唯一比 COPY 好用的场景)

面试要点:

  • 两者都只能取构建上下文内的文件(上下文外路径直接报错)
  • COPY 支持多源与 --chown;源路径改变会使其后所有层缓存失效
  • 面试标准答案:优先 COPY,只有「需要自动解 tar」时才用 ADD;远程文件下载应该用 RUN curl/wget(还能与清理合层)

WORKDIR、ENV 与 ARG

WORKDIR 设定后续指令的工作目录(自动创建);ENV 定义运行时也在的环境变量;ARG 只在构建期有效。

# 构建期:--build-arg 覆盖 ARG 默认值,ENV 借 ARG 固化
$ docker build -t kb-test:2.0 --build-arg VERSION=2.0 /tmp/kb_build

# 运行期:ENV 在容器内可见,ARG 不可见(已固化进 ENV 的 VERSION 例外)
$ docker inspect -f "{{range .Config.Env}}{{println .}}{{end}}" kb-test:2.0
APP_NAME=kb-app
VERSION=2.0
PATH=/usr/local/sbin:...

$ docker inspect -f "User={{.Config.User}} WorkDir={{.Config.WorkingDir}}" kb-test:2.0
User=nobody WorkDir=/app

面试要点:

  • ARG vs ENV 标准答案:ARG 构建期可被 --build-arg 覆盖、不进入容器环境;ENV 写进镜像、运行时可见且可被 docker run -e 覆盖
  • ARG 变化的层及其后所有层缓存失效 —— 把 ARG 放在尽量靠后的位置保护缓存
  • ENV 的最佳用途:路径、JVM 参数这类「运行时需要」的配置;敏感信息(密码)既不该进 ARG 也不该进 ENV(build history 可见)

EXPOSE 与 USER

EXPOSE 只是元数据声明(文档 + docker run -P 随机映射的依据),不做任何端口转发;USER 指定运行身份。

$ docker inspect -f "{{.Config.ExposedPorts}}" kb-test:2.0
map[8080/tcp:{}]
$ docker inspect -f "{{.Config.User}}" kb-test:2.0
nobody

面试要点:

  • EXPOSE 不等于 -p:真正把端口发布到宿主机的是 docker run -p;EXPOSE 是给人和 -P 看的声明
  • 生产镜像应避免以 root 运行:Dockerfile 末尾 USER app + 文件属主配合 chown;root 容器被攻破即宿主机风险
  • 验证身份:docker exec <容器> whoami;alpine 里用 id -u

CMD 与 ENTRYPOINT(必考)

ENTRYPOINT 是固定的可执行入口,CMD 是默认参数;两者都有时拼成 ENTRYPOINT + CMD。run 命令行传参时 CMD 被替换、ENTRYPOINT 仍在。

实测两种行为:

# 1) ENTRYPOINT + CMD:不传参 → CMD 作为默认参数
$ docker run --rm kb-test:1.0
APP=kb-app VER=1.0 args=default-arg

# 2) 传参 → CMD 被覆盖,追加到 ENTRYPOINT 后
$ docker run --rm kb-test:1.0 "cli-arg"
APP=kb-app VER=1.0 args=cli-arg

# 3) CMD-only 镜像:传参是整体替换 CMD
$ docker run --rm kb-cmd
默认输出
$ docker run --rm kb-cmd echo "被替换的输出"
被替换的输出

# 4) --entrypoint 运行时替换入口
$ docker run --rm --entrypoint cat kb-test:2.0 /app/build.info
built 2.0

面试要点:

  • 推荐写法:ENTRYPOINT ["exec-form"] 固定程序 + CMD ["默认参数"] 给用户覆盖空间
  • exec form(JSON 数组)不走 shell,信号直达进程(优雅退出正常);shell form(CMD app)实际是 /bin/sh -c app,SIGTERM 打在 sh 上,应用收不到,docker stop 只能等超时强杀
  • shell form 想正确传信号要在脚本里 exec 真实程序(exec form 语义前移)

完整示例与构建缓存

构建缓存逐层命中:没有输入变化的层全部 CACHED;COPY 的源文件或 ARG 的值一变,从那一层开始全部重建。

# 无改动重建:除 FROM 外全部 CACHED
$ docker build -t kb-test:1.0 /tmp/kb_build 2>&1 | grep -c CACHED
3

# --build-arg VERSION=2.0 使引用它的 RUN 层失效,其后层全部重建
$ docker build -t kb-test:2.0 --build-arg VERSION=2.0 /tmp/kb_build 2>&1 | tail -3
#6 CACHED
#7 CACHED
#8 [4/4] RUN chmod +x /app/hello.sh && echo "built 2.0" > /app/build.info

面试要点:

  • 缓存失效规则:某层的输入(父层、指令、COPY 的文件、ARG 值)变了,它和它之后所有层失效 —— 所以「不常变的放前面,常变的放后面」
  • 依赖安装层单独成层并先 COPY 依赖清单(如 package.json/requirements.txt)再 COPY 代码,代码改动不触发依赖重装
  • --no-cache 或同层指令变化可强制重建;CI 里可用 --cache-from 复用远端缓存

高频面试题速答

问题一句话答案
CMD 与 ENTRYPOINT 区别ENTRYPOINT 定程序、CMD 给默认参数;run 传参覆盖 CMD 不动 ENTRYPOINT,两者都有时拼接执行
ARG 与 ENV 区别ARG 构建期变量可被 --build-arg 覆盖且不进容器;ENV 固化进镜像、运行时可见可被 -e 覆盖
COPY 与 ADD 区别COPY 单纯复制行为可预测;ADD 多了自动解 tar 和远程 URL,优先 COPY
EXPOSE 会开放端口吗不会,只是元数据声明;真正发布端口是 docker run -p
shell form 有什么坑以 /bin/sh -c 运行,信号被 sh 拦截导致 stop 超时强杀,用 exec form 或脚本内 exec
为什么镜像越构建越大每条 RUN 是独立只读层,删文件不缩小下层,须合并清理到同一层
构建缓存何时失效父层变化、指令变化、COPY 源文件变化、ARG 值变化,从该层起全部重建
如何减小镜像体积小基础镜像 + 多阶段构建 + 合层清理包管理器缓存 + .dockerignore

同分类 · 其他命令文档