☰
90DaysOfDevOps:深入剖析 Docker 镜像结构 —— 从分层原理到 Dockerfile 构建实战
2026/10/5 7:36:50 网站建设 项目流程
  • 文档/教程

【免费下载链接】90DaysOfDevOps

This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Principles, Processes, Tooling and Use Cases surrounding this vast topic.

项目地址:https://gitcode.com/gh_mirrors/90/90DaysOfDevOps
点击查看免费下载

镜像(Image)是 Docker 一切操作的起点,也是容器编排、CI/CD 与云原生交付的基础单元。本文以 90DaysOfDevOps 第 45 天的内容为骨架(对应 2022/ja/Days/day45.md),系统讲解 Docker 镜像的分层结构、Dockerfile 的指令体系,以及从编写 Dockerfile、docker build构建、运行容器到推送到 DockerHub 的完整实战链路,并结合本仓库 Containers 目录下的真实示例文件进行源码级印证。读完本文,你将能够独立设计并构建一个可复现、可分享的自定义 Docker 镜像。

从镜像到容器:先回顾 Docker 镜像是什么

在动手构建之前,先明确镜像的定义:Docker 镜像是一个只读模板(read-only template),其中包含一组用于创建可在 Docker 平台上运行的容器的指令。它提供了一种便捷的方式,将应用程序和预配置的服务器环境打包在一起,既可以供个人私有使用,也可以公开分享给其他 Docker 用户。对于任何 Docker 初学者而言,镜像就是入门的起点。

第 44 天(2022/ja/Days/day44.md)我们体验过如何借助 Docker Desktop 与 DockerHub 部署和运行经过验证的官方镜像,例如docker run -it ubuntu bash拉起的 Ubuntu 容器、docker run hello-world这种"运行即退出"的示例容器。但那是"消费镜像",而不是"制造镜像"。

如果只是拉取一个 Ubuntu 镜像,手动在容器里安装软件,问题马上就会出现:一旦容器被关闭或销毁,所有软件更新与安装都会随之消失,不存在一份可重复使用的记录。这种方式适合演示 Docker 的能力,却无法满足"在多个环境中以同一套软件配置稳定交付镜像"的需求。这正是 Dockerfile 存在的意义。

什么是 Dockerfile:镜像的"施工图纸"

Dockerfile 是一个文本文件,其中包含你平时手动执行、用于构建 Docker 镜像的命令。Docker 会读取 Dockerfile 中的指令,自动完成镜像构建。也就是说,Dockerfile 把"人工操作"变成了"声明式脚本"。

理解 Dockerfile,首先要理解镜像的分层模型:

  • 构成 Docker 镜像的每一个文件单元被称为一个层(layer);
  • 这些层以"堆叠"的方式逐级构建,形成一系列镜像,每一层都依赖其正下方的那一层;
  • 层的顺序直接决定镜像生命周期管理的效率。

下图直观展示了"只读镜像层 + 容器可写层"的整体结构:一个镜像由多层只读 Layer 堆叠而成,多个容器可以共享同一镜像,但各自拥有独立的薄可写层(Thin Writeable Layer):

层的顺序为何如此关键

原则很简单:把最常变化的层尽量放在栈的顶部。原因是:当你修改镜像中的某一层时,Docker 不仅会重建该层,还会重建所有基于它构建的上层。因此:

  • 变更发生在底层 → 需要重建其上所有层,工作量最大;
  • 变更发生在顶层 → 只需重建最少的层,整个镜像的重新构建开销最小。

在实际工程中,这一原则直接体现为 Dockerfile 的书写顺序:稳定的基础镜像和系统级依赖放前面,频繁变动的应用源码放最后,从而最大化利用构建缓存、最小化重建代价。

容器层:镜像与运行中容器的唯一差异

每当 Docker 从镜像启动一个容器,它都会额外添加一个可写层(writeable layer),即容器层(container layer)。容器运行期间的所有变更(写入文件、修改配置、安装软件)都保存在这一层。因此:

  • 容器层是"正在运行的容器"与"源镜像"之间唯一的差异;
  • 任意数量的同类容器可以共享访问同一个底层镜像,同时各自维护独立的运行状态。

回到第 44 天的 Ubuntu 例子:对同一个 Ubuntu 镜像反复执行docker run,在第一个容器里安装 pinta(约 200MB 的图像编辑器),在第二个容器里安装 figlet——两个容器用途、大小完全不同,但它们共享同一个底层镜像,却互不共享状态;删除容器后,这些状态也随之消失。

父镜像(Parent Image)与 Manifest

上面的 Ubuntu 镜像,以及 DockerHub 和其他第三方仓库中大量现成的镜像,通常被称为父镜像(parent image)。它是所有其他层构建的地基,为容器环境提供基础积木。

除了各层文件本身,Docker 镜像还包含一个额外的清单文件(manifest):它本质上是镜像的 JSON 格式描述,包含镜像标签(tag)、数字签名,以及针对不同宿主机平台如何配置容器的细节信息。这也是同一个镜像能够在多架构平台上运行的技术基础——拉取时 Docker 会根据宿主平台选择对应的 manifest 条目。

创建 Docker 镜像的两种方式

创建镜像有两条路径,本仓库 Containers 目录下的文件正是围绕第二种方式准备的:

方式一:docker commit(临时快照法)

这是"即兴"做法,承接第 44 天的流程:选取基础镜像 → 启动容器 → 安装所需软件与依赖 → 然后执行:

docker commit <容器名>

执行后,本地docker images列表以及 Docker Desktop 的 Images 标签页中就会出现这份镜像的本地副本。

这个方法非常简单直接,但作者明确不推荐用于正式场景:它难以进行生命周期管理,且伴随大量手动配置/重配置。它最适合测试、故障排查、验证依赖关系等临时需求,是构建镜像最快、最简单的途径。

方式二:Dockerfile(声明式构建)

这是本仓库推荐的正式方式:通过 Dockerfile 获得干净、紧凑、可重复的镜像创建过程,生命周期管理容易得多,也能轻松接入持续集成(CI)与持续交付(CD)流水线。代价是比docker commit稍复杂,但更符合真实世界、企业级的容器部署实践。

一个典型的 Dockerfile 构建是"三步走":创建 Dockerfile → 编写组装镜像所需的指令 → 执行构建。下面的流程示意图清晰展示了 Dockerfile → 镜像 → 容器这条核心工作链路:

Dockerfile 核心指令速查

下表汇总了本文以及日常实战中最常用的 Dockerfile 指令,是编写 Dockerfile 时最直接的参考:

指令用途
FROM指定父镜像(parent image),是每个 Dockerfile 的第一条有效指令。
WORKDIR为后续指令设置工作目录;路径不存在时会自动创建。
RUN安装容器所需的应用程序和软件包,是执行apt、yum、pip等安装命令的载体。
COPY从特定位置将文件或目录复制进镜像。
ADD功能同 COPY,但额外支持远程 URL 和自动解压压缩文件。
ENTRYPOINT容器启动时始终会执行的命令;若未指定,默认是/bin/sh -c。
CMD传给 ENTRYPOINT 的参数;若未设置 ENTRYPOINT(默认为/bin/sh -c),CMD 将成为容器启动时执行的命令。
EXPOSE声明容器应用对外提供服务的端口,用于定义访问容器应用的入口。
LABEL为镜像添加元数据(如维护者、版本、用途说明)。

补充几个原文未展开、但实战中高频使用的指令要点:

  • CMD 与 ENTRYPOINT 的区别:ENTRYPOINT 固定入口,不易被覆盖;CMD 提供默认参数或默认命令,docker run时追加的参数可覆盖 CMD,从而灵活扩展容器行为;
  • EXPOSE 只是"声明"而非"发布":真正让端口对外可达需要docker run -p 宿主机端口:容器端口,或 Docker Compose 中的ports映射(见下文仓库示例);
  • USER 指令:切换运行身份(如非 root 用户),是容器安全加固的常用手段,本仓库的 Dockerfile 中就有实际使用。

动手实践:构建第一个 Docker 镜像

准备目录与 .dockerignore

在仓库的 Containers 目录中,我们创建一个工作目录,并在其中创建.dockerignore文件——它的作用与上一节讲到的.gitignore类似:列出那些在 Docker 构建过程中产生、但你不希望进入最终镜像的文件。

记住容器世界的核心信条:一切从紧凑出发,尽可能快、尽可能无臃肿(no bloat)。.dockerignore就是控制镜像体积的第一道闸门。

编写一个极简 Dockerfile

原文示例(同样可在上述目录中找到)如下:

# 使用官方 Ubuntu 18.04 作为基础镜像 FROM ubuntu:18.04 # 安装 nginx 和 curl RUN apt-get update && apt-get upgrade -y RUN apt-get install -y nginx curl RUN rm -rf /var/lib/apt/lists/*

指令解读:

  • FROM ubuntu:18.04:指定父镜像与标签,18.04 是当时 LTS 版本的 Ubuntu;
  • RUN apt-get update && apt-get upgrade -y:先同步软件源索引,再升级已有软件包;
  • RUN apt-get install -y nginx curl:安装 nginx Web 服务器与 curl 客户端;
  • RUN rm -rf /var/lib/apt/lists/*:清理 apt 缓存列表,这是减小镜像体积的经典手法——每个 RUN 指令产生一个层,层越少、层内无用文件越少,镜像就越小。

值得对照的是,仓库中 Containers/Dockerfile 的实际文件做了进一步演进:它注释掉了 nginx/curl 的安装行,新增了RUN groupadd -g 1000 basicuser && useradd -r -u 1000 -g basicuser basicuser和USER basicuser两条指令——创建一个固定 UID/GID 的普通用户并将运行身份切换到它。这正是前面提到的 USER 指令实战:以非 root 身份运行容器,降低安全风险。对比文档示例与仓库当前版本,可以清晰看到"能跑"到"跑得安全"的工程演进过程。

执行构建

在终端中进入上述目录,执行:

docker build -t 90daysofdevops:0.1 .

其中-t用于设置镜像名称与标签(tag),.表示构建上下文为当前目录。构建过程会逐条执行 Dockerfile 指令、生成对应镜像层,最终产出一个名为90daysofdevops、标签为0.1的本地镜像:

运行镜像并验证

构建完成后,既可以在 Docker Desktop 中启动容器,也可以用命令行直接运行:

docker run -it 90daysofdevops:0.1 bash

进入容器后执行curl,会发现此前通过 RUN 指令安装的 curl 已经可用——这验证了 Dockerfile 中每条指令都被如实"固化"进了镜像,而非停留在某个易失的容器状态里。这正是 Dockerfile 方法与docker commit的本质区别:可复现、可交付。

在 Docker Desktop 中管理镜像:Inspect、Pull 与 Push

镜像构建完成后,Docker Desktop 的 UI 提供了针对该镜像的额外管理能力:

  • Inspect(检查):查看镜像的详细配置,你能在其中看到 Dockerfile 中的指令与期望在容器内执行的命令,即镜像的构建历史与元数据;
  • Pull(拉取):此时执行会失败并报错,因为该镜像尚未托管在任何远端仓库中;
  • Push to hub(推送到 DockerHub):将镜像推送到 DockerHub 仓库,推送成功后 Pull 选项即可正常使用。

关于推送有一点必须注意:如果此前构建命令只写了docker build -t 90daysofdevops:0.1 .,直接推送是行不通的。推送到 DockerHub 要求镜像名必须携带你的 DockerHub 用户名,正确写法为:

docker build -t {{username}}/{{imagename}}:{{version}}

即镜像的完整引用格式为<DockerHub用户名>/<镜像名>:<版本标签>,这与第 44 天从 DockerHub 拉取docker/getting-started、hello-world、ubuntu等官方镜像时的命名约定完全一致——官方镜像可以省略用户名,而个人仓库必须显式携带。

推送完成后,登录 DockerHub 即可在个人仓库中看到刚刚推送的镜像,此后在任何安装了 Docker 的环境里都能通过docker pull {{username}}/{{imagename}}:{{version}}重新拉取使用。

仓库配套示例:从单镜像到多服务编排

本文讲解的 Dockerfile 只是起点。在 Containers 目录下还准备了两个多服务编排示例,可以帮助你理解"父镜像 + 端口映射 + 数据卷"在实际项目中的组合方式:

  • my_wordpress/docker-compose.yaml:以mysql:5.7和wordpress:latest两个官方镜像为父镜像,通过ports: "8000:80"发布 WordPress 端口,并用命名卷db_data、wordpress_data持久化数据库与站点文件——数据卷对应第 44 天 Docker Desktop 中提到的 Volumes 概念;
  • elasticsearch-logstash-kibana/docker-compose.yml:组合 elasticsearch、logstash、kibana 三个官方镜像(ELK 日志分析栈),通过ports发布 9200/9300/5601 等端口,用healthcheck定义健康检查,并用networks: driver: bridge组建服务间通信网络——这些正是 EXPOSE、端口映射与容器网络的工程化落地。

这两个示例印证了本文的核心结论:Dockerfile 定义"镜像怎么做",而镜像一旦构建好,就可以像乐高积木一样被任意组合、共享与再发布。你在 Containers 目录中可以同时看到"镜像构造层"(Dockerfile)与"镜像消费层"(docker-compose)两类文件的完整配套。

小结

回顾第 45 天的核心脉络:

  1. Docker 镜像是只读模板,由多层只读 Layer 构成,容器启动时叠加一层可写容器层,同一镜像可被多容器共享;
  2. 层的排列顺序决定构建与生命周期管理的效率,高频变化的内容应置于栈顶;
  3. 镜像由父镜像 + 各层文件 + JSON 格式的 manifest 清单组成,manifest 携带标签、签名与平台配置;
  4. 两种构建方式中,docker commit适合快速验证,Dockerfile 才是可重复、可审计、适合 CI/CD 的企业级方案;
  5. 一个规范的构建流程是:准备.dockerignore→ 编写 Dockerfile(FROM/RUN/COPY/CMD/EXPOSE/LABEL 等指令)→docker build -t <user>/<image>:<tag> .→ 运行验证 → 推送到 DockerHub。

下一节(Day 46)将在此基础之上继续深入容器生态的更多主题。如果你想一边阅读一边练习,直接参考本仓库 Containers 目录中的 Dockerfile 与两个 docker-compose 示例即可开始。

  • 文档/教程

【免费下载链接】90DaysOfDevOps

This repository started out as a learning in public project for myself and has now become a structured learning map for many in the community. We have 3 years under our belt covering all things DevOps, including Principles, Processes, Tooling and Use Cases surrounding this vast topic.

项目地址:https://gitcode.com/gh_mirrors/90/90DaysOfDevOps
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询