02. Docker 镜像深入与 Dockerfile
2.1 Docker 镜像原理
2.1.1 镜像的本质:分层文件系统
Docker 镜像采用 Union File System (联合文件系统) 技术,支持将不同的目录挂载到同一个虚拟文件系统下。
分层结构示例:
Ubuntu 镜像的层次结构:
┌─────────────────────────────┐
│ 可写容器层 (Container Layer) │ ← 容器启动时添加
├─────────────────────────────┤
│ 应用层 (your-app) │ ← 你的应用
├─────────────────────────────┤
│ 依赖层 (dependencies) │ ← 应用依赖
├─────────────────────────────┤
│ 运行时层 (python/node) │ ← 运行时环境
├─────────────────────────────┤
│ 系统层 (ubuntu:22.04) │ ← 基础操作系统
└─────────────────────────────┘
↓ (所有层只读)
分层的优势:
- 共享资源:多个镜像可以共享相同的基础层
- 快速分发:只需传输差异层
- 节省空间:相同的层只存储一份
- 快速构建:利用缓存机制
实例说明:
bash
# 假设你有两个镜像
镜像A: Ubuntu(200MB) + Python(100MB) + App1(50MB) = 350MB
镜像B: Ubuntu(200MB) + Python(100MB) + App2(50MB) = 350MB
# 实际占用空间
Ubuntu层(200MB) + Python层(100MB) + App1(50MB) + App2(50MB) = 400MB
# 节省了 300MB!
2.1.2 查看镜像分层
bash
# 拉取一个镜像
docker pull nginx:alpine
# 查看镜像分层历史
docker history nginx:alpine
# 输出示例:
# IMAGE CREATED SIZE COMMENT
# <missing> 2 weeks ago 7.3MB CMD ["nginx" "-g" "daemon off;"]
# <missing> 2 weeks ago 0B EXPOSE 80
# <missing> 2 weeks ago 42MB RUN /bin/sh -c apk add nginx
# <missing> 3 weeks ago 7.8MB /bin/sh -c #(nop) ADD file...
查看详细信息:
bash
# 查看镜像的详细 JSON 信息
docker inspect nginx:alpine
# 查看镜像的层信息
docker inspect nginx:alpine | grep -A 10 "Layers"
2.1.3 镜像存储驱动
Docker 支持多种存储驱动:
| 存储驱动 | 适用系统 | 特点 |
|---|---|---|
| overlay2 | 现代Linux | 推荐,性能最好 |
| aufs | Ubuntu/Debian | 旧版,稳定 |
| devicemapper | CentOS/RHEL | 较慢 |
| btrfs | 支持Btrfs的系统 | 高级特性 |
| zfs | 支持ZFS的系统 | 企业级 |
查看当前存储驱动:
bash
docker info | grep "Storage Driver"
# Storage Driver: overlay2
2.2 镜像管理
2.2.1 镜像命名规范
Docker 镜像的完整格式:
[registry-url/]repository[:tag][@digest]
组成部分:
registry-url:仓库地址(默认 docker.io)repository:镜像名称(可包含组织/用户名)tag:版本标签(默认 latest)digest:镜像摘要(SHA256哈希)
示例:
bash
nginx # 完整:docker.io/library/nginx:latest
nginx:1.24 # 完整:docker.io/library/nginx:1.24
myregistry.com/myapp:v1.0 # 私有仓库镜像
ubuntu@sha256:abc123... # 使用摘要引用(不可变)
2.2.2 拉取镜像
bash
# 拉取最新版本
docker pull nginx
# 拉取指定版本
docker pull nginx:1.24-alpine
# 从私有仓库拉取
docker pull myregistry.com:5000/myapp:v1.0
# 拉取所有标签
docker pull -a nginx
# 查看拉取进度(详细模式)
docker pull --quiet=false nginx
拉取时发生了什么?
1. 连接到 Registry
2. 检查镜像的 manifest (清单)
3. 下载各个层 (layer)
4. 验证每个层的校验和
5. 解压并存储到本地
2.2.3 查看本地镜像
bash
# 列出所有本地镜像
docker images
# 或使用
docker image ls
# 查看所有镜像(包括中间层)
docker images -a
# 只显示镜像 ID
docker images -q
# 格式化输出
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"
# 过滤镜像
docker images nginx # 查看所有 nginx 镜像
docker images --filter "dangling=true" # 查看悬挂镜像
输出说明:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest a6bd71f48f68 2 weeks ago 187MB
ubuntu 22.04 27941809078c 3 weeks ago 77.8MB
- REPOSITORY: 镜像仓库名
- TAG: 镜像标签
- IMAGE ID: 镜像唯一标识
- CREATED: 镜像创建时间
- SIZE: 镜像大小
2.2.4 搜索镜像
bash
# 在 Docker Hub 搜索镜像
docker search nginx
# 限制搜索结果数量
docker search --limit 5 nginx
# 只显示官方镜像
docker search --filter "is-official=true" nginx
# 只显示星级超过 100 的镜像
docker search --filter "stars=100" nginx
2.2.5 删除镜像
bash
# 通过镜像名删除
docker rmi nginx:latest
# 通过镜像 ID 删除
docker rmi a6bd71f48f68
# 删除多个镜像
docker rmi nginx:1.24 nginx:1.23
# 强制删除(即使有容器在使用)
docker rmi -f nginx
# 删除所有悬挂镜像(dangling images)
docker image prune
# 删除所有未使用的镜像
docker image prune -a
# 删除所有镜像
docker rmi $(docker images -q)
什么是悬挂镜像?
- 没有标签的镜像(
<none>:<none>) - 通常是构建新版本后的旧镜像
- 不再被任何容器使用
2.2.6 镜像标签管理
bash
# 给镜像打标签(创建副本引用)
docker tag nginx:latest mynginx:v1.0
# 为推送到私有仓库准备
docker tag myapp:latest myregistry.com:5000/myapp:v1.0
# 标记为多个版本
docker tag myapp:latest myapp:v1.0
docker tag myapp:latest myapp:stable
2.2.7 镜像的导入导出
导出镜像(保存为 tar 文件):
bash
# 导出单个镜像
docker save -o nginx.tar nginx:latest
# 导出多个镜像
docker save -o images.tar nginx:latest ubuntu:22.04
# 使用管道压缩
docker save nginx:latest | gzip > nginx.tar.gz
导入镜像:
bash
# 从 tar 文件加载镜像
docker load -i nginx.tar
# 从压缩文件加载
docker load < nginx.tar.gz
使用场景:
- 离线环境部署
- 镜像备份
- 跨主机传输镜像
2.2.8 镜像详细信息
bash
# 查看镜像详细信息(JSON 格式)
docker inspect nginx:latest
# 查看特定字段
docker inspect --format='{{.Architecture}}' nginx
docker inspect --format='{{.Os}}' nginx
docker inspect --format='{{.Size}}' nginx
2.3 Dockerfile 基础
2.3.1 什么是 Dockerfile?
Dockerfile 是一个文本文件,包含一系列指令,用于自动化构建 Docker 镜像。
优势:
- 版本控制:可以放入 Git 管理
- 可重复:任何人都能重现相同的镜像
- 透明:清楚地知道镜像包含什么
- 自动化:集成到 CI/CD 流程
2.3.2 基本结构
dockerfile
# 注释以 # 开头
FROM ubuntu:22.04 # 基础镜像
LABEL maintainer="you@example.com" # 元数据
RUN apt-get update && \ # 执行命令
apt-get install -y python3
COPY app.py /app/ # 复制文件
WORKDIR /app # 设置工作目录
ENV APP_ENV=production # 环境变量
EXPOSE 8080 # 暴露端口
CMD ["python3", "app.py"] # 容器启动命令
2.3.3 核心指令详解
FROM - 指定基础镜像
dockerfile
# 从官方镜像开始
FROM ubuntu:22.04
# 从特定版本
FROM python:3.11-slim
# 从 scratch(空白镜像)
FROM scratch
# 多阶段构建
FROM node:18 AS builder
FROM nginx:alpine AS production
最佳实践:
- 使用官方镜像
- 使用具体版本标签(不要用
latest) - 优先使用 Alpine/Slim 版本(更小)
RUN - 执行命令
dockerfile
# Shell 形式(推荐用于复杂命令)
RUN apt-get update && apt-get install -y curl
# Exec 形式(不会启动 shell)
RUN ["apt-get", "update"]
# 合并多个命令以减少层数
RUN apt-get update && \
apt-get install -y \
python3 \
python3-pip \
git && \
rm -rf /var/lib/apt/lists/*
# 使用 && 链接命令,使用 \ 换行
最佳实践:
- 合并 RUN 指令减少层数
- 清理缓存文件(如 apt lists)
- 使用
&&确保前一个命令成功
COPY vs ADD
COPY(推荐):
dockerfile
# 复制单个文件
COPY app.py /app/
# 复制目录
COPY ./src /app/src
# 复制多个文件
COPY requirements.txt package.json /app/
# 使用通配符
COPY *.py /app/
# 保留文件属性
COPY --chown=user:group files* /app/
ADD(特殊用途):
dockerfile
# ADD 支持 URL
ADD https://example.com/file.tar.gz /tmp/
# ADD 自动解压 tar 文件
ADD archive.tar.gz /app/
何时使用哪个?
- 一般情况使用
COPY(更透明) - 需要下载 URL 或自动解压时使用
ADD
WORKDIR - 设置工作目录
dockerfile
# 设置工作目录(自动创建)
WORKDIR /app
# 相对路径(相对于上一个 WORKDIR)
WORKDIR /app
WORKDIR src # 实际是 /app/src
# 使用环境变量
ENV APP_HOME=/app
WORKDIR ${APP_HOME}
最佳实践:
- 使用绝对路径
- 不要使用
RUN cd /app(不持久)
ENV - 设置环境变量
dockerfile
# 设置单个变量
ENV NODE_ENV=production
# 设置多个变量
ENV APP_HOME=/app \
APP_PORT=8080 \
APP_VERSION=1.0
# 在后续指令中使用
ENV APP_DIR=/app
WORKDIR ${APP_DIR}
COPY . ${APP_DIR}
常用环境变量:
dockerfile
ENV PYTHONUNBUFFERED=1 # Python 不缓冲输出
ENV DEBIAN_FRONTEND=noninteractive # apt-get 非交互模式
ENV PATH=/app/bin:$PATH # 添加到 PATH
EXPOSE - 声明端口
dockerfile
# 声明容器监听的端口
EXPOSE 80
# 声明多个端口
EXPOSE 8080 8443
# 指定协议
EXPOSE 80/tcp
EXPOSE 53/udp
注意:
EXPOSE只是声明,不会真正发布端口- 需要
docker run -p来实际映射端口
CMD vs ENTRYPOINT
CMD - 容器默认命令:
dockerfile
# Shell 形式
CMD python app.py
# Exec 形式(推荐)
CMD ["python", "app.py"]
# 作为 ENTRYPOINT 的参数
CMD ["--port", "8080"]
ENTRYPOINT - 容器入口点:
dockerfile
# 定义可执行容器
ENTRYPOINT ["python", "app.py"]
# 结合 CMD 使用
ENTRYPOINT ["python", "app.py"]
CMD ["--port", "8080"]
区别对比:
dockerfile
# 只有 CMD
CMD ["echo", "hello"]
# 运行:docker run myimage → 输出 "hello"
# 运行:docker run myimage ls → 执行 ls(覆盖 CMD)
# 只有 ENTRYPOINT
ENTRYPOINT ["echo", "hello"]
# 运行:docker run myimage → 输出 "hello"
# 运行:docker run myimage world → 输出 "hello world"
# ENTRYPOINT + CMD
ENTRYPOINT ["echo"]
CMD ["hello"]
# 运行:docker run myimage → 输出 "hello"
# 运行:docker run myimage world → 输出 "world"
最佳实践:
- 需要灵活运行不同命令:使用
CMD - 容器始终运行同一程序:使用
ENTRYPOINT - 组合使用:
ENTRYPOINT定义程序,CMD定义默认参数
USER - 切换用户
dockerfile
# 创建用户并切换
RUN useradd -m -u 1000 appuser
USER appuser
# 切换到特定用户和组
USER appuser:appgroup
# 使用 UID
USER 1000
安全最佳实践:
dockerfile
FROM python:3.11-slim
# 创建非 root 用户
RUN useradd -m -u 1000 appuser && \
mkdir -p /app && \
chown -R appuser:appuser /app
# 切换到非 root 用户
USER appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
CMD ["python", "app.py"]
VOLUME - 声明挂载点
dockerfile
# 声明数据卷挂载点
VOLUME /data
# 声明多个挂载点
VOLUME ["/data", "/logs"]
用途:
- 持久化数据
- 共享数据
- 提高 I/O 性能
ARG - 构建参数
dockerfile
# 定义构建参数
ARG VERSION=1.0
ARG PYTHON_VERSION=3.11
# 使用构建参数
FROM python:${PYTHON_VERSION}-slim
RUN echo "Building version ${VERSION}"
# 设置默认值
ARG APP_PORT=8080
EXPOSE ${APP_PORT}
构建时传递参数:
bash
docker build --build-arg VERSION=2.0 --build-arg PYTHON_VERSION=3.12 .
ARG vs ENV:
ARG:只在构建时可用ENV:构建和运行时都可用
LABEL - 添加元数据
dockerfile
# 添加元数据
LABEL version="1.0"
LABEL description="My awesome app"
LABEL maintainer="you@example.com"
# 多个标签
LABEL version="1.0" \
description="My app" \
maintainer="you@example.com"
2.4 构建镜像
2.4.1 基本构建
创建 Dockerfile:
dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
构建镜像:
bash
# 基本构建(自动生成 ID)
docker build .
# 指定标签
docker build -t myapp:v1.0 .
# 指定 Dockerfile 路径
docker build -f Dockerfile.prod -t myapp:prod .
# 指定构建上下文
docker build -t myapp /path/to/context
# 不使用缓存
docker build --no-cache -t myapp .
# 查看构建过程
docker build --progress=plain -t myapp .
2.4.2 构建上下文
什么是构建上下文?
docker build命令最后的路径(如.)- 该目录下的所有文件都会发送给 Docker daemon
COPY和ADD指令使用的相对路径基准
优化构建上下文:
.dockerignore 文件:
dockerignore
# 忽略文件和目录
node_modules
npm-debug.log
.git
.gitignore
*.md
.env
.vscode
__pycache__
*.pyc
*.pyo
*.pyd
.pytest_cache
.coverage
# 使用通配符
**/*.log
**/.DS_Store
# 例外规则
!important.log
2.4.3 构建缓存机制
Docker 构建使用缓存来加速:
dockerfile
FROM python:3.11-slim
# ✓ 这层会被缓存
WORKDIR /app
# ✓ 依赖很少变化,使用缓存
COPY requirements.txt .
RUN pip install -r requirements.txt
# ✗ 代码经常变化,这层之后的都会重新构建
COPY . .
CMD ["python", "app.py"]
缓存失效规则:
- Dockerfile 指令改变
- 复制的文件内容改变
- 父层缓存失效
优化技巧:
- 把不常变化的层放在前面
- 依赖安装和代码复制分开
- 合理使用
.dockerignore
2.4.4 多阶段构建
问题场景:
- 编译型语言需要编译工具
- 构建后的镜像包含不必要的工具
- 镜像体积过大
多阶段构建解决方案:
dockerfile
# 阶段 1:构建阶段
FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 阶段 2:运行阶段
FROM nginx:alpine
# 只复制构建产物
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
另一个示例(Go 应用):
dockerfile
# 构建阶段
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o myapp
# 运行阶段(使用最小镜像)
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
# 从构建阶段复制二进制文件
COPY --from=builder /app/myapp .
CMD ["./myapp"]
优势:
- 构建镜像:1.2GB → 运行镜像:15MB
- 只包含运行时需要的文件
- 更安全(没有构建工具)
2.5 实战案例
案例 1:Flask Web 应用
项目结构:
my-flask-app/
├── app.py
├── requirements.txt
└── Dockerfile
python
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello from Docker!'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
requirements.txt:
Flask==3.0.0
Dockerfile:
dockerfile
FROM python:3.11-slim
WORKDIR /app
# 先复制依赖文件(利用缓存)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# 再复制应用代码
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
构建和运行:
bash
# 构建镜像
docker build -t flask-app:v1.0 .
# 运行容器
docker run -d -p 5000:5000 --name my-flask-app flask-app:v1.0
# 测试
curl http://localhost:5000
案例 2:Node.js 应用(多阶段构建)
Dockerfile:
dockerfile
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# 运行阶段
FROM node:18-alpine
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
WORKDIR /app
# 从构建阶段复制依赖
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
# 复制应用代码
COPY --chown=nodejs:nodejs . .
USER nodejs
EXPOSE 3000
CMD ["node", "server.js"]
案例 3:优化的 Nginx 静态站点
Dockerfile:
dockerfile
FROM nginx:alpine
# 删除默认配置
RUN rm /etc/nginx/conf.d/default.conf
# 复制自定义配置
COPY nginx.conf /etc/nginx/conf.d/
# 复制静态文件
COPY dist/ /usr/share/nginx/html/
# 使用非 root 用户运行(需要自定义配置)
USER nginx
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost:8080/ || exit 1
CMD ["nginx", "-g", "daemon off;"]
2.6 最佳实践总结
镜像体积优化
dockerfile
# 不推荐的做法
FROM ubuntu:22.04
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
COPY . /app
# 镜像大小:~500MB
# 推荐的做法
FROM python:3.11-alpine
RUN apk add --no-cache python3
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . /app
# 镜像大小:~50MB
安全性
dockerfile
# ✓ 使用非 root 用户
RUN useradd -m appuser
USER appuser
# ✓ 使用具体版本
FROM python:3.11-slim # 而不是 python:latest
# ✓ 不要在镜像中存储机密信息
# 使用 docker secrets 或环境变量
构建效率
dockerfile
# ✓ 把不常变化的层放前面
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . . # 代码经常变,放后面
# ✓ 合并 RUN 命令
RUN apt-get update && \
apt-get install -y package1 package2 && \
rm -rf /var/lib/apt/lists/*
# ✓ 使用 .dockerignore