镜像原理与管理
4.1 镜像分层机制
为什么分层
graph TB
subgraph 优化前["优化前:每层都装依赖"]
A1["FROM ubuntu"] --> A2["RUN apt install python3"]
A2 --> A3["COPY app1 /app1"]
A3 --> A4["RUN pip install flask"]
end
subgraph 优化后["优化后:依赖层共享"]
B1["FROM ubuntu"] --> B2["RUN apt install python3"]
B2 --> B3["RUN pip install flask"]
B3 --> B4["COPY app1 /app1"]
B3 --> B5["COPY app2 /app2"]
end
- 多个镜像共享相同的基础层,节省磁盘空间
- 构建时只重建变化的层,加速构建
- 传输时只传输缺失的层,加速推送/拉取
只读层 + 可写层
# 镜像层是只读的,容器启动时在最上层加一个可写层
docker run -it ubuntu bash
# 在容器内创建文件
touch /new_file
# 退出后这个文件在可写层,原始镜像不变
# 查看容器的可写层变化
docker diff <container_id>
# C = 新增文件, A = 修改文件, D = 删除文件
查看层信息
# 查看镜像所有层
docker history nginx:alpine
# 查看层大小和指令
docker history --no-trunc nginx:alpine
# 查看层 ID
docker image inspect nginx:alpine --format '{{.RootFS.Layers}}'
# 层的存储位置
ls /var/lib/docker/overlay2/
层的共享与回收
# 查看镜像实际磁盘占用
docker system df -v
# 删除未使用的层
docker image prune
# 删除所有未使用的镜像
docker image prune -a
4.2 构建镜像
构建上下文
# docker build 命令最后的 . 是构建上下文路径
docker build -t myapp:latest .
# Docker 会将当前目录的所有文件发送给 daemon
# .dockerignore 中的文件会被排除
# 指定其他目录作为上下文
docker build -t myapp:latest -f /path/to/Dockerfile /path/to/context
# 使用 stdin 传入 Dockerfile(无上下文)
docker build -t myapp:latest - < Dockerfile
构建缓存
# BuildKit 缓存机制
# 当某层的指令和输入没变化时,直接使用缓存
# 一旦某层变化,后续所有层都要重建
# 禁用缓存
docker build --no-cache -t myapp:latest .
# 查看构建缓存
docker builder du
docker builder prune # 清理缓存
构建参数
# ARG:构建时变量(只在 Dockerfile 中有效)
docker build --build-arg VERSION=1.0 --build-arg ENV=prod -t myapp .
# 多平台构建
docker buildx create --use
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .
# 本地加载构建结果
docker buildx build --platform linux/amd64 -t myapp:latest --load .
从容器创建镜像
# 从运行中的容器创建镜像
docker commit <container_id> myimage:snapshot
# 带注释的提交
docker commit -m "Added config" -a "admin" <container_id> myimage:v2
4.3 标签策略
语义化版本
# 推荐:精确版本号
docker tag myapp:latest myapp:1.2.3
docker tag myapp:latest myapp:1.2
docker tag myapp:latest myapp:1
# latest 的陷阱
# latest 不是"最新版",只是默认标签
# 别人可能推送了不同的 latest
docker pull myapp:latest # 不确定拿到的是哪个版本
docker pull myapp:1.2.3 # 确定版本
标签最佳实践
# 生产环境:用精确版本号
docker pull myapp:1.2.3
# 开发环境:用 git hash
docker tag myapp:latest myapp:abc1234
# 多架构标签
docker buildx build --platform linux/amd64,linux/arm64 \
-t myapp:1.2.3 --push .
清理旧标签
# 查看所有标签
docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}"
# 删除特定标签(只删除标签,不影响其他标签引用的层)
docker rmi myapp:old-tag
4.4 多阶段构建详解
为什么需要多阶段
# 问题:构建镜像包含编译工具,体积大
FROM golang:1.22
COPY . .
RUN go build -o server .
# 这个镜像包含整个 Go 工具链(~800MB)
# 解决:多阶段构建
# 构建阶段
FROM golang:1.22 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o server .
# 运行阶段
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
# 最终镜像只有 ~15MB
多语言示例
# Python 多阶段构建
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12-slim
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["python", "app.py"]
# Node.js 多阶段构建
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
# Java 多阶段构建
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/target/*.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
4.5 缓存优化
分层顺序优化
# 错误顺序:每次改代码都要重新装依赖
COPY . .
RUN pip install -r requirements.txt
# 正确顺序:依赖不变时用缓存
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
BuildKit 缓存挂载
# syntax=docker/dockerfile:1
# 依赖安装使用缓存挂载,不随层变化失效
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y python3
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Go 模块缓存
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build -o server .
.dockerignore
.git
.gitignore
node_modules
__pycache__
*.pyc
.env
*.md
Dockerfile
docker-compose.yml
.vscode
.idea
4.6 推送到 Registry
Docker Hub
docker login -u myusername
docker tag myapp:latest myusername/myapp:1.0
docker push myusername/myapp:1.0
GitHub Container Registry
docker login ghcr.io -u myusername
docker tag myapp:latest ghcr.io/myusername/myapp:1.0
docker push ghcr.io/myusername/myapp:1.0
私有 Registry
docker login 192.168.1.100:5000
docker tag myapp:latest 192.168.1.100:5000/myapp:1.0
docker push 192.168.1.100:5000/myapp:1.0
Registry 管理
# 登出
docker logout ghcr.io
# 查看镜像摘要(用于不可变引用)
docker inspect --format '{{index .RepoDigests 0}}' myapp:1.0
镜像签名与安全扫描
# 使用 Docker Content Trust 签名
export DOCKER_CONTENT_TRUST=1
docker push myusername/myapp:1.0
# 扫描镜像漏洞
docker scout cves myapp:1.0
4.7 镜像导入导出
save/load(镜像级别)
# 导出镜像(包含所有层)
docker save -o nginx.tar nginx:latest
docker save nginx:latest | gzip > nginx.tar.gz # 压缩
# 导入镜像
docker load -i nginx.tar
docker load < nginx.tar.gz
# 适用场景:离线传输、备份
export/import(容器文件系统)
# 导出容器文件系统(单层)
docker export mycontainer > container.tar
# 导入为新镜像
docker import container.tar myimage:latest
# 区别:export 丢失元数据(CMD/ENTRYPOINT/ENV 等)
# 适用场景:从运行中的容器创建快照
镜像迁移实战
# 在 A 机器导出
docker save myapp:1.0 mydb:5.7 | gzip > images.tar.gz
# 传输到 B 机器
scp images.tar.gz user@b-machine:/tmp/
# 在 B 机器导入
docker load < images.tar.gz
# 验证导入结果
docker images | grep -E "myapp|mydb"
4.8 镜像安全最佳实践
使用最小基础镜像
FROM alpine:3.19 # ~5MB
FROM debian:bookworm-slim # ~80MB
FROM ubuntu:24.04 # ~77MB
非 root 运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
只读文件系统
docker run --read-only --tmpfs /tmp myapp:1.0
docker run --read-only \
-v /app/data:/data:rw \
-v /tmp:/tmp:rw \
myapp:1.0
定期更新基础镜像
docker pull python:3.12-slim
docker history python:3.12-slim