第 5 章 · 02 镜像构建与容器生命周期 镜像的分层模型告诉我们"每一层来自一条指令",本节就把这些指令逐个讲透。Dockerfile 是构建镜像的配方文件,其中最微妙的是 RUN/CMD/ENTRYPOINT 三兄弟的分工与 Exec form/Shell form 的写法陷阱。后半节走出构建,看看容器从 run 到 stop 的一生,以及 docker run 背后那条 daemon → containerd → runc 的链路。 学习目标 掌握 Dockerfile 关键指令的作用与建层规则 说清 RUN/CMD/ENTRYPOINT 的区别与拼接逻辑 避开 Exec form 与 Shell form 混用陷阱,理解 ADD 与 COPY 的取舍 会用构建上下文、.
镜像的分层模型告诉我们"每一层来自一条指令",本节就把这些指令逐个讲透。Dockerfile 是构建镜像的配方文件,其中最微妙的是 RUN/CMD/ENTRYPOINT 三兄弟的分工与 Exec form/Shell form 的写法陷阱。后半节走出构建,看看容器从 run 到 stop 的一生,以及 docker run 背后那条 daemon → containerd → runc 的链路。
Dockerfile 是容器引擎可自动读取的文本文件,包含构建镜像的全部指令。按"是否创建新层"可分为两类:创建新层的指令(FROM、COPY、RUN 等)与只写元数据、不建层的指令(ENTRYPOINT、ENV、EXPOSE 等)。
| 指令 | 作用 | 建层 |
|---|---|---|
| FROM | 指定基础镜像(几乎总是第一条,可前置 ARG) | 是 |
| RUN | 构建时执行命令,结果写入新层 | 是 |
| COPY | 从构建上下文复制文件/目录进镜像 | 是 |
| ADD | COPY 的增强版:支持 URL 源与 tar 自动解压 | 是 |
| WORKDIR | 设置工作目录,影响其后所有指令 | 否 |
| EXPOSE | 声明暴露端口(仅文档化元数据,不真正发布) | 否 |
| ENTRYPOINT | 指定容器启动时运行的固定命令 | 否 |
| CMD | 指定容器启动时的默认命令/参数 | 否 |
| ENV | 设置环境变量 | 否 |
| USER | 设置运行镜像时使用的用户(可含组) | 否 |
| ARG | 构建参数(仅构建期可见) | 否 |
| HEALTHCHECK | 定义容器健康检查命令 | 否 |
两个最佳实践先立起来:FROM 必须显式指定 tag(否则永远拉 latest,镜像随时间漂移导致不可复现);EXPOSE 只是"说明书",真正发布端口靠运行时 -p 参数。
这是容器面试的第一高频题,核心是分清"构建期"与"运行期":
| 指令 | 执行时机 | 行为 |
|---|---|---|
| RUN | 构建时 | 执行命令并写入镜像,成为新的一层 |
| CMD | 运行时 | 容器启动的默认命令,可被 run 命令行参数覆盖 |
| ENTRYPOINT | 运行时 | 容器启动的固定命令,run 参数不覆盖它 |
CMD 与 ENTRYPOINT 的关系是拼接:容器启动时,引擎把 ENTRYPOINT 与 CMD 拼在一起执行——ENTRYPOINT 固定命令,CMD 提供默认参数。docker run -it image ls 中的 ls 会覆盖 CMD(追加替换默认参数),但不会覆盖 ENTRYPOINT。规则还有三条:
docker run image 某命令 传的命令会替换 CMD 部分而非 ENTRYPOINT。典型设计:ENTRYPOINT 固定主程序(如 httpd),CMD 提供默认参数;需要覆盖启动参数时用 --entrypoint 显式指定。
每条指令都有两种写法:
# Exec form(推荐) ENTRYPOINT ["cmd", "param0", "param1"] CMD ["param0"] # Shell form ENTRYPOINT cmd param0 param1 CMD param0
区别在 Shell form 会把命令包装进 /bin/sh -c,因此会多创建一个 shell 进程;Exec form 直接以参数数组执行,不经过 shell(信号传递更干净,是官方推荐形式)。单独用任何一种都没问题,混用会出事故:
ENTRYPOINT ["ls"] CMD /tmp
实际执行的是 ls /bin/sh -c /tmp——Exec form 下 CMD 的参数被原样追加,而 Shell form 的 CMD 变成了三个参数。结果 ls 把 "/bin/sh"、"-c"、"/tmp" 当参数列出三个不存在的文件。判断技巧:写出拼接后的完整命令行再核对。
COPY 与 ADD 都从构建上下文复制文件进镜像。区别是 ADD 额外支持两种来源:直接使用 URL 下载;从源位置自动解压 tar 文件到目标。听起来方便,但自动解压/远程拉取的隐式行为容易造成意外结果(比如本想复制一个 tar 却被解开了)。常规复制一律用 COPY,只有明确需要"下载"或"解压"语义时才用 ADD。
构建上下文(build context)是"位于指定 PATH 或 URL 中的文件集合"。默认情况下目录下所有文件都会被打包进上下文——包括 node_modules、.git 这类巨物,既拖慢传输又可能污染镜像。.dockerignore 的作用就是排除不需要的文件/目录(语义类似 .gitignore)。构建命令在 Dockerfile 所在目录执行:
docker image build -t some_app:latest .
| 场景 | 命令 |
|---|---|
| 运行容器(前台) | docker container run ubuntu |
| 后台运行 | docker container run -d httpd |
| 附加终端运行 | docker container run -it ubuntu /bin/bash |
| 列出运行中容器 | docker container ls(docker ps) |
| 列出全部(含已退出) | docker ps -a |
| 进入运行中容器 | docker container exec -it bash |
| 停止容器 | docker container stop |
| 删除容器 | docker container rm |
| 删除全部已停止容器 | docker rm $(docker ps -a -q) |
| 一键清理 | docker system prune |
两个高频陷阱先排掉。为什么 docker run ubuntu 之后 ps 是空的:容器为"运行服务/应用"而设计,任务干完立即退出——ubuntu 没有前台任务,瞬间退出;用 docker ps -a 能看到已退出的它,想保持运行就加 -it /bin/bash 或让它执行 sleep。为什么不能直接删运行中的容器:必须 stop 再 rm,或者用 -f。
restart 的行为也要说清:docker restart 不是"杀掉主进程重跑",而是创建一个同 ID 的新容器、复用原容器的文件系统与状态。
一条 docker run 命令背后是完整的分层架构:
这解释了几个经典判断题:杀掉 daemon 不会杀掉运行中的容器(运行时在 containerd/runc 手里);运行 12 个容器不会有 12 个常驻 runc(创建后即退出);daemon 做的是比 containerd 更高层的活(管理网络、卷、镜像,而非直接操作容器)。
Docker 技术栈分三层:Runtime(runc/containerd 负责启停容器)、Daemon(实现 Docker API,负责镜像/认证/安全/网络)、Orchestrator(编排)。containerd 由 Docker 捐给 CNCF,Docker 和 Kubernetes 都拿它当容器运行时。
容器崩溃了怎么办?重启策略(restart policy)在特定事件后自动重启容器,是生产环境的第一道自愈防线:
| 策略 | 行为 |
|---|---|
| no | 任何情况都不重启(默认) |
| always | 容器停止时始终重启(手动 stop 除外) |
| unless-stopped | 除非被手动停止,否则始终重启 |
| on-failure | 仅因错误退出(退出码非 0)时重启 |
本节走完了"写配方 → 构建 → 运行"的主线:Dockerfile 指令中 RUN 管构建、CMD/ENTRYPOINT 管运行、COPY 管文件;Exec/Shell 两种形式不能混用;容器生命周期命令构成了日常操作的主干;docker run 链路揭示了三层运行时架构与 shim 的保命作用。镜像构建好了、容器跑起来了,还差最后一块拼图:容器之间如何通信、数据如何持久——下一节讲网络与存储。
第 5 章第 3 节《网络存储与生产实践》将解决容器化落地三问:容器怎么互相访问(CNM/CNI 与端口映射)、数据怎么不丢(volume/bind mount)、多容器应用怎么编排(Compose),最后给出安全五条铁律与多阶段构建。