3. Docker 镜像管理:分层、提交与版本控制
镜像管理,说白了就是 Docker 的“交付物”管理。你写再多 Dockerfile,最终产出的还是一个个镜像。我刚开始接触 Docker 时,花了不少时间在镜像优化上,踩过的坑不少。
这一章我们聊三个核心话题:镜像到底怎么存的、怎么通过 commit 制作镜像、以及怎么做好版本管理。
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:
- 临时保存调试环境,方便复现问题
- 对第三方镜像做紧急补丁,来不及写 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应该是不可变的,但如果手动重新打标签,就变味了- 没有统一规则,大家乱打标签,根本不知道哪个是哪个
我个人的标签策略:
- 语义化版本: 像 v1.2.3 这样,主版本.次版本.修订号
- Git SHA 前缀: 比如 v1.2.3-abc1234,保证唯一性
- 环境标签: staging、production 用固定 tag,但只保留最近 5 个版本
- 禁止覆盖: 一旦打了 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
嗯,这一章内容基本覆盖了镜像管理的关键点。记住一点:镜像就像代码一样,要有版本、要有记录、要能回滚。别让“这个镜像是什么版本”成为你们团队的日常问题。