- 文档/教程
【免费下载链接】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.
镜像(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 天的核心脉络:
- Docker 镜像是只读模板,由多层只读 Layer 构成,容器启动时叠加一层可写容器层,同一镜像可被多容器共享;
- 层的排列顺序决定构建与生命周期管理的效率,高频变化的内容应置于栈顶;
- 镜像由父镜像 + 各层文件 + JSON 格式的 manifest 清单组成,manifest 携带标签、签名与平台配置;
- 两种构建方式中,
docker commit适合快速验证,Dockerfile 才是可重复、可审计、适合 CI/CD 的企业级方案; - 一个规范的构建流程是:准备
.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.
相关推荐
90DaysOfDevOps 第 45 天:深入剖析 Docker 镜像的结构与 Dockerfile 实战
90DaysOfDevOps 第 45 天:深入剖析 Docker 镜像的结构与 Dockerfile 实战 本文是 90DaysOfDevOps 系列第 45
文档/教程90DaysOfDevOps 实战:Docker 镜像解剖与 Dockerfile 构建指南(Day 45)
90DaysOfDevOps 实战:Docker 镜像解剖与 Dockerfile 构建指南(Day 45) 本篇是 90DaysOfDevOps 开源学习计划
文档/教程docker-stacks镜像构建原理:从Dockerfile到优化交付
docker stacks镜像构建原理:从Dockerfile到优化交付 一、镜像构建基础:分层与继承架构 docker stacks采用分层构建策略,所有镜像
云原生开发工具数据科学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考