Docker镜像到底是什么?从Dockerfile到tar包的完整链路

57 阅读6分钟

前言

你有没有遇到过这种情况:

在服务器上部署应用,运维老哥甩过来一个 .tar 文件,说"这是Docker镜像,你自己加载"。

你一脸懵逼:这玩意儿不是应该是一堆文件吗?怎么就成tar包了?

然后你百度了一圈,看了Docker文档,翻了Stack Overflow,最后发现...

Docker镜像本质上就是个tar包。

今天咱们就来彻底搞懂这个东西——从Dockerfile到tar包的完整链路。


一、Docker镜像的本质:tar包

先说结论,别被那堆概念吓到了:

Docker镜像 = 一个或多个tar包的集合

你可以把它想象成俄罗斯套娃,每个tar包都是一层,叠在一起就成了镜像。

# 导出镜像为tar包
docker save -o nginx.tar nginx:latest

# 看看里面是啥
tar -tf nginx.tar | head -20

你会看到一堆json文件和layer.tar文件,这些json文件记录了镜像的元数据(比如镜像ID、创建时间、环境变量等),而layer.tar就是每一层的文件系统内容。

所以为什么是tar包?

因为tar格式简单、通用、跨平台。Docker需要一个标准格式来打包和分发镜像,tar就是最合适的选择。


二、Dockerfile到镜像的构建过程

1. Dockerfile是啥

Dockerfile就是个文本文件,里面写了一堆指令,告诉Docker怎么构建镜像:

FROM debian:buster
RUN apt-get update && apt-get install -y nginx
COPY index.html /var/www/html/
CMD ["nginx", "-g", "daemon off;"]

别觉得高深,这玩意儿说白了就是个构建脚本

2. 构建过程发生了什么

当你运行 docker build -t my-nginx . 时,Docker会干这几件事:

  1. 解析Dockerfile:读取每一行指令
  2. 逐层构建:每个指令都会生成一个新的镜像层
  3. 提交每一层:每一层都会被提交成一个独立的镜像
  4. 打标签:最后给整个镜像打个标签

重点来了:每一层都是一个只读的文件系统

这些层的文件系统内容会被打包成tar文件,然后Docker用Union File System(联合文件系统)把它们叠在一起。


三、镜像的分层结构

Docker镜像之所以轻量,是因为它采用了分层设计。

1. 分层的好处

假设你有两个镜像:

  • my-app:1.0 基于 debian:buster,装了Python
  • my-app:2.0 也基于 debian:buster,装了Python和Node.js

这两个镜像会共享 debian:buster 这一层,不需要重复存储。

这就是为什么Docker镜像这么小的原因——共享资源

2. Copy-on-Write策略

你可能会问:如果多个容器共享同一个基础镜像,一个容器改了文件,其他容器也会受影响吗?

答案是:不会

因为Docker用了Copy-on-Write(写时复制)策略:

  1. 读取文件:从上往下在各层中查找,找到就直接读
  2. 修改文件:先把文件从镜像层复制到容器层(可写层),然后修改
  3. 删除文件:在容器层记录一个"删除标记"

所有修改都发生在容器层,镜像层始终保持只读。


四、从镜像到tar包:docker save原理

当你运行 docker save -o nginx.tar nginx:latest 时,Docker会干这些事:

  1. 获取镜像的元数据:包括镜像ID、层信息、环境变量等
  2. 打包每一层的tar文件:把每个layer的文件系统内容打包
  3. 生成manifest.json:记录镜像的配置信息
  4. 生成repositories文件:记录镜像的仓库和标签信息

最后生成的tar包结构大概长这样:

nginx.tar
├── manifest.json          # 镜像的元数据
├── repositories           # 镜像的仓库信息
└── [layer-sha256].tar     # 每一层的文件系统内容
    ├── bin/
    ├── etc/
    └── ...

你可以解压这个tar包看看里面到底有啥:

mkdir nginx-extracted
tar -xf nginx.tar -C nginx-extracted/

五、从tar包到镜像:docker load原理

docker load 就是 docker save 的逆过程:

  1. 读取manifest.json:获取镜像的元数据
  2. 解压每一层的tar文件:恢复文件系统内容
  3. 重建镜像的元数据:包括镜像ID、标签等
  4. 注册到Docker daemon:让Docker知道这个镜像的存在
docker load -i nginx.tar

加载完成后,你就可以像使用普通镜像一样使用它了:

docker run -d -p 80:80 nginx:latest

六、实战技巧

1. 如何减小镜像体积

多阶段构建:用多个FROM指令,只保留需要的文件

# 构建阶段
FROM golang:1.19 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp

# 运行阶段
FROM alpine:latest
COPY --from=builder /app/myapp /usr/local/bin/myapp
CMD ["myapp"]

清理缓存:在同一个RUN指令中安装和清理

RUN apt-get update && \
    apt-get install -y python3 && \
    rm -rf /var/lib/apt/lists/*

2. 镜像的迁移

# 导出镜像
docker save -o myapp.tar myapp:1.0

# 在另一台机器上加载
docker load -i myapp.tar

注意docker savedocker export 是两码事:

  • docker save:保存完整的镜像(包括所有层和元数据)
  • docker export:导出容器的文件系统(不包含历史和元数据)

迁移镜像一定要用 docker save


七、踩坑指南

1. 镜像加载失败

docker load -i myapp.tar
# Error: open /var/lib/docker/tmp/docker-import-xxx/repositories: no such file or directory

原因:tar包损坏或格式不对

解决:重新导出,确保tar包完整

2. 镜像体积过大

你的应用才几十MB,镜像却有几百MB?

原因:基础镜像太大,或者没有清理缓存

解决:用 alpine 做基础镜像,或者用多阶段构建

3. 镜像层太多

每执行一个RUN/COPY/ADD指令就会增加一层,层数过多会影响性能。

解决:合并指令

# ❌ 不好的写法:5层
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip install flask
RUN pip install requests

# ✅ 好的写法:1层
RUN apt-get update && \
    apt-get install -y python3 python3-pip && \
    pip install flask requests

八、总结

记住这张图:

Dockerfile
    ↓
docker build
    ↓
镜像(多层tar文件)
    ↓
docker save
    ↓
单个tar包(包含所有层)
    ↓
docker load
    ↓
恢复为镜像
    ↓
docker run
    ↓
容器(在镜像层之上加一个可写层)

核心要点:

  1. Docker镜像本质是tar包的集合
  2. 分层设计实现资源共享
  3. Copy-on-Write保证隔离性
  4. docker save/load 用于镜像迁移

下次再遇到docker save/load的命令,你就知道它们到底在干啥了。


参考资料


你在项目里是怎么管理Docker镜像的?有没有遇到过镜像体积过大的问题?

评论区聊聊,看看有没有更骚的操作。