3. Docker 镜像管理:分层、提交与版本控制

镜像管理,说白了就是 Docker 的“交付物”管理。你写再多 Dockerfile,最终产出的还是一个个镜像。我刚开始接触 Docker 时,花了不少时间在镜像优化上,踩过的坑不少。

这一章我们聊三个核心话题:镜像到底怎么存的、怎么通过 commit 制作镜像、以及怎么做好版本管理。

Docker 镜像管理核心体系 分层存储(UnionFS) docker commit 导出 / 导入 标签与版本管理 可写容器层 镜像层 3(修改) 镜像层 2(依赖) 镜像层 1(基础) 容器修改 → docker commit → 生成新镜像层 docker save / load 离线传输或备份 latest / v1.0.0 / commit-sha 语义化版本 + 不可变标签 最佳实践:版本化 + 自动化

3.1 镜像分层原理

Docker 镜像,说白了就是个文件系统分层(UnionFS)的用法。每一层都是只读的,层与层之间按堆叠方式合并。

你想想看,一个 Ubuntu 基础镜像可能 200MB,你装个 Nginx 后镜像只大了 20MB?原因就是这 20MB 是独立的一层,基础镜像层被所有基于它的镜像共享了。

关键概念:

  • 每个 Dockerfile 指令(RUN、COPY、ADD)都会产生一个新层
  • 层是只读的,容器启动时在最上层加一个可写层
  • 删除文件只是在该层打标记,不会真正释放底层空间

看个例子:

FROM ubuntu:22.04          # 基础镜像层
RUN apt update              # 层 1
RUN apt install -y nginx   # 层 2
COPY index.html /var/www   # 层 3

为什么会这样设计?说白了就是为了节省空间和加速分发。我在项目中遇到过,同一个基础镜像被 20 多个微服务共用,磁盘占用比预期小很多,都是分层的功劳。

3.2 docker commit 的用法与陷阱

docker commit 可以把一个容器的当前状态保存为新镜像。听起来很方便,但我个人习惯是:能不用就不用,除非你是调试临时用一下。

用法很简单:

# 先启动容器并做些修改
docker run -it --name my-app ubuntu:22.04 bash
# 在容器里安装一些东西
apt update && apt install -y curl vim

# 退出容器后提交
docker commit my-app my-app:v1.0.0

我曾经踩过的坑:

  • commit 出的镜像没有构建记录,你不知道每层做了什么
  • 层会包含容器运行时产生的临时文件(比如 /tmp 下的东西)
  • 无法复用缓存,每次 commit 都是全新构建

我建议只在两种场景下用 commit:

  1. 临时保存调试环境,方便复现问题
  2. 对第三方镜像做紧急补丁,来不及写 Dockerfile

生产环境,还是老老实实用 Dockerfile 吧。

3.3 镜像导出与导入

镜像怎么从一台机器搬到另一台?或者离线环境怎么办?嗯,这里要用到 docker save 和 docker load。

命令用途输出格式
docker save -o image.tar image:tag将镜像保存为 tar 包tar(包含所有层)
docker load -i image.tar从 tar 包恢复镜像恢复元数据和层
docker export -o container.tar container_name导出容器文件系统tar(不含元数据)
docker import container.tar new_image:tag导入容器快照为新镜像单层镜像

我在项目中遇到过一次惊险的事:生产环境网络断了,私有仓库拉不了镜像。还好我提前导出了一份 tar 包,用 U 盘拷进去 docker load 就恢复了。

正确用法:

# 导出镜像(推荐)
docker save -o nginx-1.25.tar nginx:1.25

# 在目标机器导入
docker load -i nginx-1.25.tar

一个小技巧: 导出时尽量用 gzip 压缩,体积能小 50% 以上:

docker save nginx:1.25 | gzip > nginx-1.25.tar.gz
# 导入时
gunzip -c nginx-1.25.tar.gz | docker load

3.4 标签与版本管理

标签(tag)是镜像的“别名”。同一个镜像可以有多个标签,但一个标签只能指向一个镜像。

我见过很多团队这么干:

docker tag my-app:latest my-app:1.0.0
docker tag my-app:latest my-app:stable

看似没毛病,但问题来了:

  • latest 是可变标签,下次 Build 就不一样了
  • 1.0.0 应该是不可变的,但如果手动重新打标签,就变味了
  • 没有统一规则,大家乱打标签,根本不知道哪个是哪个

我个人的标签策略:

  1. 语义化版本: 像 v1.2.3 这样,主版本.次版本.修订号
  2. Git SHA 前缀: 比如 v1.2.3-abc1234,保证唯一性
  3. 环境标签: staging、production 用固定 tag,但只保留最近 5 个版本
  4. 禁止覆盖: 一旦打了 v1.0.0 就不允许再给它打别的镜像

举个例子:

# CI/CD 流程中的打标签方式
docker build -t my-app:1.2.3 .
docker tag my-app:1.2.3 my-app:1.2.3-$(git rev-parse --short HEAD)
docker tag my-app:1.2.3 my-app:latest   # 仅用于快速拉取开发版
docker tag my-app:1.2.3 my-app:stable   # 经过测试后才打

警告: 永远不要在 latest 标签上建立任何依赖。这玩意就是个马甲,随时可能被换掉。生产环境用精确定义版本号或 SHA 摘要。

3.5 镜像清理与存储优化

镜像多了,磁盘就会炸。我经历过把 200GB 的磁盘撑满,最后全部重建的惨剧。

常用的清理命令:

# 删除所有悬空镜像(没有标签的)
docker image prune

# 删除未使用的镜像(更激进)
docker image prune -a

# 查看磁盘占用
docker system df

建议自动化: 在 CI/CD 流水线末尾加一步清理操作:

# 保留最近 3 个版本的镜像,其余全部清理
docker image ls --format '{{.Repository}}:{{.Tag}}' | sort -r | tail -n +4 | xargs docker rmi

嗯,这一章内容基本覆盖了镜像管理的关键点。记住一点:镜像就像代码一样,要有版本、要有记录、要能回滚。别让“这个镜像是什么版本”成为你们团队的日常问题。