Docker 容器文件系统怎么工作?分层和 Copy-on-Write 机制
容器里写了一个文件,它到底存在哪里?删掉容器后文件去哪了?为什么镜像层是只读的但容器可以写入?理解 Docker 文件系统的工作原理,这些问题就都清楚了。
从镜像到容器:分层文件系统
Docker 镜像不是一个大文件——它是多层只读文件系统的堆叠。每个 Dockerfile 指令产生一层:
dockerfileFROM ubuntu:22.04 # 层 1:操作系统基础(~70MB) RUN apt-get update # 层 2:包索引更新 RUN apt-get install -y python3 # 层 3:Python 运行时 COPY . /app # 层 4:应用代码 CMD ["python3", "/app/main.py"] # 不产生新层(只是元数据)
shell┌────────────────────────┐ │ 可写层(容器运行时添加) │ ← 容器启动后才有 ├────────────────────────┤ │ 层 4:应用代码 │ ← COPY 产生 ├────────────────────────┤ │ 层 3:Python 运行时 │ ← RUN 产生 ├────────────────────────┤ │ 层 2:包索引 │ ← RUN 产生 ├────────────────────────┤ │ 层 1:Ubuntu 基础 │ ← FROM 产生 └────────────────────────┘
关键:每个只读层都是不可变的。删除了上一层的文件,并不会真的删除——只是在新层里标记为"已删除"(whiteout 文件)。
Copy-on-Write:容器写入的核心机制
容器启动时,Docker 在所有只读层上面加一层可写层。容器内的写操作都走 CoW:
修改已有文件
shell1. 容器要修改 /etc/config.yml(存在于镜像层 2) 2. Docker 从层 2 把 config.yml 复制到可写层 3. 修改发生在可写层的副本上 4. 后续读取这个文件时,可写层的版本"遮盖"了镜像层的原始版本
新建文件
直接写在可写层,不涉及复制。
删除文件
在可写层创建一个 whiteout 文件(标记为已删除),不真的从只读层删除。
overlay2 的实现
overlay2 是 Docker 默认的存储驱动,基于 Linux 内核的 OverlayFS:
shell容器看到的是 merged 视图: ┌──────────────┐ │ merged │ = upperdir + lowerdir 合并后的视图 ├──────────────┤ │ upperdir │ = 可写层(容器修改的文件) ├──────────────┤ │ lowerdir │ = 镜像的所有只读层 └──────────────┘
在宿主机上的实际位置
bash# Docker 的存储根目录 ls /var/lib/docker/overlay2/ # 每个镜像层一个目录 /var/lib/docker/overlay2/<layer-id>/ diff/ # 这一层的文件内容 link # 指向层的短链接 lower # 指向下层层的链接 merged/ # 合并视图(容器运行时才出现) work/ # OverlayFS 工作目录
查看容器的 overlay2 层
bash# 找到容器的 overlay2 路径 docker inspect myapp --format '{{.GraphDriver.Data.MergedDir}}' # /var/lib/docker/overlay2/abc123/merged # 查看可写层的内容 ls /var/lib/docker/overlay2/abc123/diff/
diff/ 目录里就是容器内所有修改过的文件——和 docker diff 命令看到的一致。
为什么容器删除后数据会丢
shell1. docker run → 创建可写层 + 启动容器 2. 容器运行中 → 数据写在可写层 3. docker rm → 删除可写层 + 容器元数据
可写层随容器一起删除,里面的数据也消失了。这就是为什么需要 Volume:
yamlservices: postgres: image: postgres:16 volumes: - pg_data:/var/lib/postgresql/data # 数据存在 Volume,不在可写层
Volume 的数据存在 /var/lib/docker/volumes/pg_data/,容器删除不影响它。
镜像层的共享和节省空间
多个镜像共享相同的基础层:
shell镜像 A(Node.js 应用) 镜像 B(Python 应用) ┌──────────────┐ ┌──────────────┐ │ 层 3:App A │ │ 层 3:App B │ ← 不同 ├──────────────┤ ├──────────────┤ │ 层 2:Node.js│ │ 层 2:Python │ ← 不同 ├──────────────┤ ├──────────────┤ │ 层 1:Ubuntu │ │ 层 1:Ubuntu │ ← 共享!只存一份 └──────────────┘ └──────────────┘
bash# 查看 Docker 磁盘占用 docker system df -v # SHARED SIZE 列显示被多个镜像共享的大小 # UNIQUE SIZE 列显示该镜像独有的层大小
镜像构建优化:减少层和大小
合并 RUN 指令
dockerfile# 差:每条 RUN 一层 RUN apt-get update RUN apt-get install -y python3 RUN apt-get install -y pip # 好:合并成一层 RUN apt-get update && \ apt-get install -y python3 pip && \ rm -rf /var/lib/apt/lists/*
合并的好处:
- 层数减少
rm -rf /var/lib/apt/lists/在同一层执行——删除操作真的释放了空间,而不是在下一层标记为已删除
多阶段构建
dockerfileFROM node:20 AS builder WORKDIR /app COPY . . RUN npm ci && npm run build FROM node:20-alpine COPY /app/dist ./dist COPY /app/node_modules ./node_modules CMD ["node", "dist/index.js"]
最终镜像只有 dist/ 和 node_modules/——没有源码、没有构建工具,体积缩小 60-80%。
.dockerignore 减少构建上下文
shell# .dockerignore .git node_modules .env *.md test/
.dockerignore 排除的文件不会进入构建上下文——既加快构建,又防止敏感文件进镜像。
常见文件系统问题
镜像越来越大
bash# 查看每层大小 docker history myapp:latest # 找出最大的层 docker history --format "{{.Size}}\t{{.CreatedBy}}" myapp:latest
常见的膨胀原因:apt-get update 的缓存、npm 缓存、临时文件。在同一层里清理掉。
容器写入慢
可写层写大量小文件时性能差——特别是 Mac/Windows 上 Docker Desktop 的文件共享。解决方案:大量写入用 Volume。
inode 耗尽
大量小文件会消耗 inode。df -i 查看剩余 inode。overlay2 比 overlay 的 inode 使用更少——这也是推荐 overlay2 的原因之一。