Docker 核心技术深度解析

本文深入探讨 Docker 的核心技术,包括 Namespace、Cgroups 和 UnionFS,并涵盖 Dockerfile 最佳实践、网络、存储以及与 Kubernetes 的关系。

1. 引言:容器化与微服务

1.1. 系统架构演进:从单体到微服务

传统分层架构 (Monolithic)

  • 结构: 表示层 -> 业务逻辑层 -> 数据访问层 -> 数据库
  • 优点: 简单应用易于开发、测试、部署。
  • 缺点 (复杂系统): 开发维护困难、部署慢、技术栈单一、扩展性差。
graph LR
    A[Presentation Layer] --> B(Business Logic Layer)
    B --> C(Data Access Layer)
    C --> D{Database}

微服务架构 (Microservices)

  • 结构: 将大型应用拆分为小型、独立的服务,通过 API 网关或直接通信。
  • 优点: 易于理解和维护、独立部署、技术选型灵活、易于扩展、促进 CI/CD。
  • 缺点: 分布式系统复杂性(通信、事务、监控)、运维挑战。
graph LR
    subgraph Microservices
        A[Service 1] -->|API| B(API Gateway)
        C[Service 2] -->|API| B
        D[Service 3] -->|API| B
    end
    B --> E{External Services/UI}
  • 微服务改造原则: 按业务能力、领域边界、性能需求等进行拆分。
  • 微服务间通讯: 点对点 vs API 网关。

1.2. 为何需要容器化?

微服务架构带来了部署和管理的复杂性。每个服务可能依赖不同的库、环境。容器化技术(如 Docker)提供了一种标准化的方式来打包、分发和运行应用,解决了环境一致性、快速部署和资源隔离等问题,是实现微服务的关键技术之一。

2. Docker 基础

2.1. Docker 是什么?

  • Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用以及依赖包到一个可移植的镜像中,然后发布到任何流行的 Linux 或 Windows 机器上,也可以实现虚拟化。
  • 核心概念:
    • 镜像 (Image): 一个只读的模板,包含创建 Docker 容器的说明。基于 UnionFS,镜像由多个层 (Layer) 组成。
    • 容器 (Container): 镜像的运行实例。容器是可写的,在镜像层之上增加了一个可写层。容器之间、容器与宿主机之间通过 Namespace 进行隔离。
    • 仓库 (Repository): 集中存放镜像文件的地方 (如 Docker Hub)。

2.2. Docker vs. 虚拟机

graph TD
    subgraph "虚拟机 (VM)"
        App1 --> GuestOS1
        App2 --> GuestOS2
        GuestOS1 --> Hypervisor
        GuestOS2 --> Hypervisor
        Hypervisor --> HostOS
        HostOS --> Hardware
    end
    subgraph "容器 (Container)"
        AppA --> ContainerEngine[Docker Engine]
        AppB --> ContainerEngine
        ContainerEngine --> HostOS_C[Host OS]
        HostOS_C --> Hardware_C[Hardware]
    end
特性 容器 (Docker) 虚拟机 (VM)
隔离级别 进程级 (共享内核) 操作系统级 (独立内核)
启动速度 秒级 分钟级
资源占用 少 (MB 级镜像, 低内存开销) 多 (GB 级镜像, 高内存开销)
性能 接近原生 有损耗
密度 高 (单机可运行成百上千个) 低 (单机运行数十个)

2.3. 为什么要用 Docker?

  • 环境一致性: 打包应用及其所有依赖,消除 “在我机器上可以运行” 的问题。
  • 快速交付部署: 加速开发、测试、部署流程 (CI/CD)。
  • 资源利用率高: 更轻量,启动更快,系统开销小。
  • 弹性伸缩: 快速创建和销毁容器实例。
  • 易于迁移: 跨云、跨环境迁移方便。

2.4. 安装 Docker (以 Ubuntu 为例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 1. 卸载旧版本 (如果存在)
# sudo apt-get remove docker docker-engine docker.io containerd runc

# 2. 更新 apt 包索引并安装依赖
sudo apt-get update
sudo apt-get install -y \
apt-transport-https \
ca-certificates \
curl \
gnupg \
lsb-release

# 3. 添加 Docker 官方 GPG 密钥
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

# 4. 设置稳定版仓库
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 5. 安装 Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io

# 6. (可选) 配置用户组,避免每次使用 sudo
# sudo groupadd docker
# sudo usermod -aG docker $USER
# newgrp docker # 需要重新登录或执行 newgrp 生效

# 7. 验证安装
sudo docker run hello-world

2.5. 常用 Docker 命令

  • 镜像操作:
    • docker images: 列出本地镜像。
    • docker pull <image_name>:<tag>: 从仓库拉取镜像。
    • docker build -t <repository>/<image_name>:<tag> .: 根据 Dockerfile 构建镜像。
    • docker rmi <image_id_or_name>: 删除本地镜像。
    • docker tag <source_image> <target_image>: 给镜像打标签。
    • docker push <repository>/<image_name>:<tag>: 推送镜像到仓库。
    • docker save -o <output_file.tar> <image_name>: 将镜像保存为 tar 文件。
    • docker load -i <input_file.tar>: 从 tar 文件加载镜像。
  • 容器操作:
    • docker run [OPTIONS] IMAGE [COMMAND] [ARG...]: 创建并启动一个新容器。
      • -d: 后台运行。
      • -it: 交互式运行 (通常结合 /bin/bash 等)。
      • --name <container_name>: 指定容器名称。
      • -p <host_port>:<container_port>: 端口映射。
      • -v <host_path>:<container_path>: 卷挂载 (数据持久化)。
      • --rm: 容器退出时自动删除。
      • --network <network_mode>: 指定网络模式 (bridge, host, none, container:<name|id>, 自定义网络)。
      • --env <key>=<value>--env-file <file>: 设置环境变量。
      • --memory <limit>: 限制内存。
      • --cpus <limit>: 限制 CPU 核心数。
    • docker ps: 列出正在运行的容器。
    • docker ps -a: 列出所有容器 (包括已停止的)。
    • docker stop <container_id_or_name>: 优雅地停止容器 (发送 SIGTERM,超时后 SIGKILL)。
    • docker kill <container_id_or_name>: 强制停止容器 (发送 SIGKILL)。
    • docker start <container_id_or_name>: 启动已停止的容器。
    • docker restart <container_id_or_name>: 重启容器。
    • docker rm <container_id_or_name>: 删除已停止的容器。
    • docker logs [-f] <container_id_or_name>: 查看容器日志 (-f 持续跟踪)。
    • docker inspect <container_id_or_name>: 查看容器/镜像的详细信息 (JSON 格式)。
    • docker exec -it <container_id_or_name> <command>: 在运行中的容器内执行命令 (常用 docker exec -it <id> /bin/bash)。
    • docker attach <container_id_or_name>: 连接到正在运行的容器的标准输入、输出和错误流 (不推荐用于执行命令,退出时可能导致容器主进程停止)。
    • docker cp <host_path> <container_id>:<container_path>: 从主机复制文件到容器。
    • docker cp <container_id>:<container_path> <host_path>: 从容器复制文件到主机。

3. Docker 核心技术详解

Docker 的实现依赖于 Linux 内核的几项关键技术:Namespace (资源隔离)、Cgroups (资源限制) 和 UnionFS (镜像分层)。

3.1. Namespace (命名空间) - 实现隔离

Namespace 是 Linux 内核提供的用于隔离内核资源的方式。通过将全局系统资源包装在一个抽象层中,使得 Namespace 内的进程看起来拥有它们自己独立的全局资源实例。

  • 原理: 内核通过 struct nsproxy 结构体将不同类型的 Namespace 关联到进程 (struct task_struct)。

  • 主要类型:

    • PID (Process ID): 隔离进程 ID。容器内的进程拥有独立的 PID 空间,PID 1 是容器的 init 进程。
    • Net (Network): 隔离网络设备、IP 地址、端口、路由表等。每个容器拥有独立的网络栈。
    • IPC (InterProcess Communication): 隔离 System V IPC 和 POSIX 消息队列。
    • Mnt (Mount): 隔离文件系统挂载点。容器拥有独立的文件系统视图。
    • UTS (Unix Timesharing System): 隔离主机名和域名。允许每个容器拥有自己的 hostname。
    • User (User ID): 隔离用户和用户组 ID。允许容器内的 root 用户映射到宿主机上的非 root 用户,提高安全性。
    • Cgroup: 隔离 Cgroup 根目录。
  • 操作命令:

    • lsns: 列出系统中的 Namespace。
    • unshare: 创建并运行一个带有新 Namespace 的程序。
    • nsenter: 进入指定的 Namespace 并执行程序。
  • 示例 (进入容器网络 Namespace):

    1
    2
    3
    4
    # 1. 获取容器的 PID
    PID=$(docker inspect --format '{{.State.Pid}}' <container_name_or_id>)
    # 2. 进入该容器的网络 Namespace 查看 IP 地址
    sudo nsenter --target $PID --net ip addr

3.2. Cgroups (控制组) - 实现资源限制

Cgroups (Control Groups) 是 Linux 内核提供的机制,用于限制、核算和隔离一组进程所使用的物理资源 (CPU、内存、磁盘 I/O、网络等)。

  • 核心概念:

    • Task: 系统中的一个进程。
    • Cgroup: 按某种标准划分的进程组。
    • Hierarchy: Cgroup 组成的树状结构,子 Cgroup 继承父 Cgroup 的属性。
    • Subsystem/Controller: 具体的资源控制器 (如 cpu, memory, blkio)。一个 Subsystem 只能附加到一个 Hierarchy。
  • 版本:

    • v1: 早期版本,每个 Subsystem 需要挂载到不同的 Hierarchy (除了联合挂载),管理较混乱。
    • v2: 改进版本,所有 Controller 挂载到统一的 Hierarchy (/sys/fs/cgroup),接口更清晰统一。现代 Linux 发行版多使用 v2。
  • Docker Cgroup Driver:

    • Docker 通过 Cgroup Driver 与内核 Cgroups 交互。
    • cgroupfs: Docker 直接读写 cgroup 文件系统。简单直接,但如果系统 init system (如 systemd) 也在管理 cgroups,可能导致冲突。
    • systemd: Docker 通过 systemd 的 API 来管理 cgroups。推荐在 systemd 作为 init system 的系统中使用此驱动,以确保 cgroup 管理的一致性。可以通过 docker info | grep -i cgroup 查看和配置。
  • 常用子系统 (Controller):

    • cpu:
      • cpu.shares (v1) / cpu.weight (v2): 相对权重。CPU 繁忙时,按比例分配 CPU 时间。默认 1024 (v1) 或 100 (v2)。仅在 CPU 竞争时生效。
      • cpu.cfs_period_us, cpu.cfs_quota_us (v1) / cpu.max (v2): 绝对限制。限制在一个周期 (period, 通常 100ms) 内可使用的 CPU 时间 (quota)。例如,quota=50000, period=100000 表示最多使用 50% 的单核 CPU 时间。-1 表示不限制。这是硬限制。
      • cpu.stat (v1) / cpu.stat (v2): 统计 CPU 使用时间、节流次数 (nr_throttled) 和节流时间 (throttled_time)。
    • memory:
      • memory.limit_in_bytes (v1) / memory.max (v2): 硬限制。进程使用的内存(包括文件缓存)不能超过此值,否则可能触发 OOM Killer。
      • memory.soft_limit_in_bytes (v1) / memory.low (v2): 软限制。系统内存紧张时,优先回收超过软限制的 Cgroup 的内存。
      • memory.usage_in_bytes (v1) / memory.current (v2): 当前内存使用量。
      • memory.oom_control (v1) / memory.oom.group (v2): 控制 OOM Killer 行为。oom_kill_disable=1 (v1) 或 memory.oom.group=1 (v2) 表示当该 Cgroup 发生 OOM 时,杀死 Cgroup 内的进程,而不是禁用 OOM Killer。
    • blkio (v1) / io (v2): 限制块设备的 I/O。
    • pids: 限制 Cgroup 内的进程数量。
    • cpuset: 绑定进程到指定的 CPU 核心和内存节点。
    • devices: 控制对设备文件的访问。
  • CFS 调度器: Linux 内核默认的 CPU 调度器,通过虚拟运行时间 (vruntime) 保证进程公平地共享 CPU。Cgroups 的 cpu.shares 正是影响 vruntime 的计算,从而影响调度优先级。

  • 练习 (限制 CPU):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 1. 创建测试 cgroup (假设使用 cgroup v1)
    sudo mkdir /sys/fs/cgroup/cpu/cpudemo
    # 2. 启动一个耗费 CPU 的进程 (例如: while true; do :; done &)
    # 获取其 PID
    PID=<your_busy_process_pid>
    # 3. 将进程移入 cgroup
    echo $PID | sudo tee /sys/fs/cgroup/cpu/cpudemo/cgroup.procs
    # 4. 查看当前 CPU 使用率 (如使用 top)
    # 5. 限制 CPU 使用率为 20%
    sudo sh -c 'echo 100000 > /sys/fs/cgroup/cpu/cpudemo/cpu.cfs_period_us'
    sudo sh -c 'echo 20000 > /sys/fs/cgroup/cpu/cpudemo/cpu.cfs_quota_us'
    # 6. 再次查看 CPU 使用率,应被限制在 20% 左右
    # 7. 清理
    # sudo kill $PID
    # sudo rmdir /sys/fs/cgroup/cpu/cpudemo
  • 练习 (限制 Memory):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # 1. 创建测试 cgroup (假设使用 cgroup v1)
    sudo mkdir /sys/fs/cgroup/memory/memorydemo
    # 2. 启动一个消耗内存的程序 (例如一个简单的 C 程序不断 malloc)
    # 获取其 PID
    PID=<your_memory_eater_pid>
    # 3. 将进程移入 cgroup
    echo $PID | sudo tee /sys/fs/cgroup/memory/memorydemo/cgroup.procs
    # 4. 查看内存使用情况 (如使用 top 或 free -m)
    # 5. 限制内存为 100MB
    echo 104857600 | sudo tee /sys/fs/cgroup/memory/memorydemo/memory.limit_in_bytes
    # 6. (可选) 启用 OOM Killer (默认通常是启用的)
    # echo 0 | sudo tee /sys/fs/cgroup/memory/memorydemo/memory.oom_control
    # 7. 观察进程因超出内存限制而被 OOM Killer 终止
    # 8. 清理
    # sudo rmdir /sys/fs/cgroup/memory/memorydemo

3.3. Union File System (联合文件系统) - 实现镜像分层

UnionFS 是一种分层、轻量级的文件系统,它允许将多个目录(称为分支或层)的内容叠加在一起,形成一个单一的、一致的文件系统视图。对只读分支的修改会发生在最上层的可写分支中。

  • Docker 中的应用: Docker 镜像正是基于 UnionFS 实现的。
    • 镜像层 (Image Layers): Dockerfile 中的每条指令(主要是 RUN, COPY, ADD)通常会创建一个新的只读层。这些层堆叠在一起。
    • 容器层 (Container Layer): 当基于镜像启动容器时,Docker 会在只读镜像层之上添加一个可写的容器层。
  • 写时复制 (Copy-on-Write, CoW):
    • 当容器需要修改一个存在于下层只读镜像中的文件时,该文件首先会被复制到最上层的可写容器层,然后修改操作在这个副本上进行。原始镜像层的文件保持不变。
    • 优点:节省存储空间(多个容器共享只读镜像层),容器启动快(只需创建可写层)。
    • 缺点:首次写入时有性能开销,层数过多可能影响性能。
  • 存储驱动 (Storage Driver): Docker 使用存储驱动来实现 UnionFS 的功能。不同的驱动有不同的实现方式和性能特点。
    • overlay2: 当前推荐的默认驱动。性能好,稳定性高,被 Linux 内核主线支持。使用 lowerdir (只读层) 和 upperdir (可写层) 以及 workdir (内部使用)。
    • aufs: Docker 最早使用的驱动,稳定但未并入 Linux 主线内核,仅在部分发行版(如早期 Ubuntu)可用。
    • devicemapper: 基于 LVM 的块级存储驱动。性能较好,但配置复杂,空间管理不如 overlay2 灵活。
    • btrfs, zfs: 基于相应文件系统的写时复制特性,提供高级功能(如快照),但可能需要特定的文件系统格式化和配置。
    • 可以通过 docker info | grep -i storage 查看当前使用的驱动。
graph TD
    subgraph "运行中的容器"
        WritableLayer["容器可写层 (Container Layer)"]
        ReadOnlyLayer1["镜像层 N (Image Layer N)"]
        ReadOnlyLayer2["..."]
        ReadOnlyLayer3["镜像层 1 (Image Layer 1)"]
        BaseImage["基础镜像层 (Base Image Layer)"]
        WritableLayer -- CoW --> ReadOnlyLayer1
        ReadOnlyLayer1 --> ReadOnlyLayer2
        ReadOnlyLayer2 --> ReadOnlyLayer3
        ReadOnlyLayer3 --> BaseImage
    end
    style WritableLayer fill:#f9d,stroke:#333,stroke-width:2px
  • OverlayFS 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    # 准备目录
    mkdir lower upper merged work
    # 创建文件
    echo "File in Lower" > lower/lower.txt
    echo "File in Both (Lower)" > lower/both.txt
    echo "File in Upper" > upper/upper.txt
    echo "File in Both (Upper)" > upper/both.txt

    # 挂载 OverlayFS (需要 root 权限)
    sudo mount -t overlay overlay -o lowerdir=`pwd`/lower,upperdir=`pwd`/upper,workdir=`pwd`/work `pwd`/merged

    # 查看合并后的视图
    ls merged
    # lower.txt upper.txt both.txt

    # 查看文件内容 (upper 层覆盖 lower 层)
    cat merged/both.txt
    # File in Both (Upper)

    # 在 merged 视图中修改/删除文件 (实际发生在 upper 层)
    echo "Modified in merged" > merged/lower.txt
    rm merged/upper.txt

    # 查看 upper 层的变化
    ls upper
    # lower.txt both.txt (upper.txt 被标记为删除,通常通过 whiteout 文件实现)
    cat upper/lower.txt
    # Modified in merged

    # 卸载
    sudo umount merged

    # 清理
    rm -rf lower upper merged work

4. Docker 网络

Docker 提供了多种网络模式来连接容器。

4.1. Bridge 模式 (默认)

  • 原理: Docker 安装时会创建一个名为 docker0 的虚拟网桥。每当创建一个使用 bridge 模式的容器时,Docker 会:
    1. 创建一对 veth pair (虚拟以太网设备对)。
    2. 一端连接到 docker0 网桥。
    3. 另一端放入容器的网络 Namespace,并命名为 eth0
    4. docker0 网桥所在的子网(默认为 172.17.0.0/16)分配一个 IP 地址给容器的 eth0
  • 容器间通信: 同一宿主机上的容器可以通过 docker0 网桥直接通信(使用容器 IP)。
  • 访问外部网络: docker0 网桥通过宿主机的 iptables 规则进行 NAT (网络地址转换),使得容器可以访问外部网络。
  • 外部访问容器: 需要通过 -p-P 参数进行端口映射,Docker 会配置相应的 iptables DNAT 规则将宿主机端口的流量转发到容器端口。
graph LR
    subgraph Host (e.g., 192.168.1.100)
        ContainerA[Container A (172.17.0.2)] -- veth_a_peer --> Docker0[docker0 Bridge (172.17.0.1/16)]
        ContainerB[Container B (172.17.0.3)] -- veth_b_peer --> Docker0
        Docker0 -- NAT (iptables) --> HostNIC[Host NIC (eth0)]
        HostNIC <--> ExternalNetwork{External Network}
        PortMapping[Port Mapping (e.g., 8080:80 for Container A)] -- iptables DNAT --> ContainerA
        HostNIC -- PortMapping
    end
    style ContainerA fill:#lightblue
    style ContainerB fill:#lightblue

4.2. Host 模式 (--network=host)

  • 原理: 容器不再拥有独立的网络 Namespace,而是直接共享宿主机的网络栈。
  • 优点: 网络性能最高(没有 veth 和网桥的开销),容器可以直接使用宿主机的所有网络接口和端口。
  • 缺点: 牺牲了网络隔离性,容器端口可能与宿主机或其他 host 模式容器冲突,安全性较低。

4.3. None 模式 (--network=none)

  • 原理: 容器拥有独立的网络 Namespace,但没有任何网络配置(没有网卡、IP、路由)。只有一个 lo (loopback) 设备。
  • 用途: 用于需要完全自定义网络配置的场景,或者不需要网络的容器。

4.4. Container 模式 (--network=container:<name|id>)

  • 原理: 新创建的容器共享另一个已存在容器的网络 Namespace。它们共享相同的 IP 地址、端口空间和网络接口。
  • 用途: 常用于需要紧密协作的容器组,例如一个应用容器和一个监控/代理容器 (Sidecar 模式)。

4.5. Overlay 模式 (多主机网络)

  • 原理: 用于连接跨越多个 Docker 主机的容器。它在现有宿主机网络之上创建一个覆盖网络 (Overlay Network),通常使用 VXLAN 等隧道技术将不同主机上容器的网络流量封装起来进行传输。
  • 实现: Docker Swarm 模式内置了 overlay 网络驱动。Kubernetes 则使用 CNI (Container Network Interface) 插件 (如 Flannel, Calico, Weave) 来实现跨主机网络,这些插件也常使用 overlay 或 BGP 等技术。
  • Libnetwork: Docker 的网络库,提供了可插拔的网络驱动模型,支持 bridge, host, overlay 等。
graph LR
    subgraph Host 1
        ContainerA[Container A (10.0.1.2)] --> VTEP1[VTEP 1]
        VTEP1 -- VXLAN Tunnel --> Underlay[Underlay Network (e.g., 192.168.1.0/24)]
    end
    subgraph Host 2
         VTEP2[VTEP 2] --> ContainerB[Container B (10.0.1.3)]
         Underlay -- VXLAN Tunnel --> VTEP2
    end
    style ContainerA fill:#lightblue
    style ContainerB fill:#lightblue

4.6. Underlay 模式

  • 原理: 不创建覆盖网络,而是直接将容器连接到宿主机所在的物理网络(或底层网络)。容器获得与宿主机同网段的可路由 IP 地址。
  • 实现: 通常需要网络管理员的配合,配置物理网络设备或使用特定的网络插件(如 Calico 的 IP-in-IP 或 BGP 模式)。
  • 优点: 网络性能好,没有封装开销。
  • 缺点: 消耗底层网络 IP 地址,配置相对复杂,依赖底层网络架构。

5. Dockerfile 与镜像构建

Dockerfile 是一个文本文件,包含用于自动化构建 Docker 镜像的指令。

5.1. Dockerfile 常用指令

  • FROM <image>[:<tag>] [AS <name>]: 指定基础镜像。必须是第一条非注释指令。AS <name> 用于多阶段构建。
  • LABEL <key>=<value> ...: 添加元数据到镜像。
  • ENV <key>=<value> ...: 设置环境变量。这些变量在构建过程中和容器运行时都可用。
  • ARG <name>[=<default_value>]: 定义构建时参数,只在构建过程中可用。可以通过 docker build --build-arg <name>=<value> 传递。
  • RUN <command>: 在镜像构建过程中执行命令(shell 格式或 exec 格式)。每条 RUN 指令会创建一个新的镜像层。
  • COPY [--chown=<user>:<group>] <src>... <dest>: 从构建上下文复制文件或目录到镜像文件系统。推荐优先使用 COPY
  • ADD [--chown=<user>:<group>] <src>... <dest>: 功能类似 COPY,但增加了额外特性:
    • src 可以是 URL。
    • 如果 src 是本地可识别的压缩包 (tar, gzip, bzip2, xz),会自动解压到 dest此自动解压行为可能导致不确定性,通常不推荐使用 ADD 来处理压缩包,建议使用 RUN tar ...
  • WORKDIR /path/to/workdir: 设置后续 RUN, CMD, ENTRYPOINT, COPY, ADD 指令的工作目录。
  • EXPOSE <port> [<port>/<protocol>...]: 声明容器运行时监听的端口。这只是元数据,实际端口映射需要在 docker run -p 中指定。
  • VOLUME ["/path/to/volume"]: 创建一个挂载点,用于持久化数据或共享数据。容器运行时会自动创建卷。Dockerfile 中此指令后的修改对该目录无效。
  • USER <user>[:<group>]: 指定运行后续 RUN, CMD, ENTRYPOINT 指令时使用的用户名或 UID(以及可选的组名或 GID)。最佳实践:避免使用 root 用户运行容器。
  • ENTRYPOINT ["executable", "param1", "param2"] (exec 格式, 推荐) 或 ENTRYPOINT command param1 param2 (shell 格式): 配置容器启动时运行的命令。docker run 提供的参数会追加到 exec 格式的 ENTRYPOINT 之后,或者覆盖 shell 格式的 ENTRYPOINT
  • CMD ["executable","param1","param2"] (exec 格式, 推荐), CMD ["param1","param2"] (作为 ENTRYPOINT 的默认参数), 或 CMD command param1 param2 (shell 格式): 提供容器启动的默认命令或参数。
    • 如果 Dockerfile 同时有 ENTRYPOINTCMDCMD 的内容会作为 ENTRYPOINT 的默认参数。
    • 如果 docker run 指定了命令,会覆盖 CMD
    • 如果只有 CMD,它定义了容器启动时执行的命令。
  • ONBUILD <INSTRUCTION>: 定义当基于此镜像构建新的镜像时,会自动执行的指令。
  • STOPSIGNAL <signal>: 设置停止容器时发送的系统调用信号 (默认 SIGTERM)。
  • HEALTHCHECK [OPTIONS] CMD <command>HEALTHCHECK NONE: 定义如何检查容器的健康状态。
  • SHELL ["executable", "parameters"]: 指定 RUN, CMD, ENTRYPOINT shell 格式指令使用的默认 shell。

5.2. 构建上下文 (Build Context)

  • docker build 命令最后指定的路径 (. 表示当前目录) 是构建上下文。
  • Docker daemon 在构建开始时会将整个构建上下文(除了 .dockerignore 中排除的文件/目录)发送过去。
  • 注意: 保持构建上下文尽可能小,只包含构建所需的文件,使用 .dockerignore 排除不必要的文件(如 .git, node_modules, 临时文件等)。

5.3. 构建缓存 (Build Cache)

  • Docker 会缓存镜像层。如果 Dockerfile 的某一行指令及其依赖(如 COPY 的源文件内容)没有改变,Docker 会重用之前构建的缓存层,加快构建速度。
  • 一旦某一层缓存失效(指令改变或依赖文件改变),其后的所有层缓存都会失效,需要重新构建。
  • 优化策略: 将变化频率低的指令(如安装基础依赖)放在 Dockerfile 前面,变化频率高的指令(如 COPY 应用程序代码)放在后面,以最大化利用缓存。

5.4. 多阶段构建 (Multi-stage Builds)

  • 目的: 减小最终镜像体积,只包含运行时必要的依赖,去除构建时工具和中间产物。
  • 方法: 在一个 Dockerfile 中使用多个 FROM 指令。每个 FROM 开始一个新的构建阶段,可以给阶段命名 (AS <name>)。使用 COPY --from=<stage_name_or_index> <src> <dest> 从之前的阶段复制需要的文件到当前阶段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# ---- Build Stage ----
FROM golang:1.19-alpine AS builder
WORKDIR /app
# 使用 .dockerignore 排除不必要文件
COPY . .
# 下载依赖、编译应用
RUN go mod download
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/myapp .

# ---- Final Stage ----
FROM alpine:latest
# 考虑使用 scratch 或 distroless 基础镜像进一步减小体积
# FROM scratch
# FROM gcr.io/distroless/static-debian11

WORKDIR /root/
# 只从 builder 阶段复制编译好的二进制文件
COPY --from=builder /app/myapp .
# (可选) 如果需要 CA 证书
# COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 设置非 root 用户运行
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 8080
# 使用 exec 格式
ENTRYPOINT ["./myapp"]

5.5. Dockerfile 最佳实践

  • 基础镜像:
    • 选择官方、经过验证的基础镜像。
    • 使用具体标签(如 ubuntu:22.04),避免使用 latest(可能导致构建不确定性)。
    • 优先选择体积小的基础镜像(如 alpine, distroless)。Alpine 使用 musl libc,可能与 glibc 应用存在兼容性问题;Distroless 镜像不包含 shell 和包管理器,更安全但调试困难。
  • 最小化层数:
    • 合并多个 RUN 指令,使用 && 连接命令,并在最后清理缓存 (apt-get clean, rm -rf /var/lib/apt/lists/*)。
    • 合理规划指令顺序,利用缓存。
  • 指令优化:
    • 优先使用 COPY 而不是 ADD
    • COPY/ADD 时,尽量只复制需要的文件,而不是整个目录。
    • 使用 .dockerignore 排除不需要发送到 daemon 的文件。
    • 使用多阶段构建分离构建环境和运行环境。
  • 安全:
    • 不要在容器内使用 root 用户运行应用。 使用 USER 指令切换到非 root 用户。需要在 RUN 指令中创建用户和组,并调整文件权限。
    • 最小化安装包,只安装必要的依赖。
    • 定期扫描镜像漏洞 (可以使用 Trivy, Clair 等工具)。
  • 可维护性:
    • LABEL 添加维护者、版本等元数据。
    • ARG 用于传递构建时变量,ENV 用于设置运行时环境变量。
    • 保持 Dockerfile 简洁、清晰,添加注释。
  • CMDENTRYPOINT:
    • 推荐使用 exec 格式 (["executable", "param1"]) 而不是 shell 格式 (command param1),以正确处理信号。
    • 使用 ENTRYPOINT 定义容器主命令,CMD 提供默认参数。

6. Docker 镜像管理

6.1. 镜像标签与版本管理

  • 使用 docker tag <source_image> <repository>/<image_name>:<tag> 为镜像打标签。
  • 标签通常用于表示版本号(如 v1.0.0, 2.1-alpine)。
  • 结合 Git tag,可以将代码版本与镜像版本对应起来,实现可追溯的版本管理。

6.2. 镜像仓库 (Registry)

  • 公共仓库: Docker Hub 是最常用的公共镜像仓库。
  • 私有仓库:
    • 可以使用 Docker 官方提供的 registry 镜像快速搭建私有仓库 (docker run -d -p 5000:5000 --restart=always --name registry registry:2)。
    • 企业级私有仓库方案:Harbor, Nexus Repository, JFrog Artifactory 等,提供 UI、权限管理、安全扫描等功能。
  • 推送与拉取:
    • docker login <registry_address>: 登录仓库。
    • docker push <image_name_with_registry_prefix>: 推送镜像。
    • docker pull <image_name_with_registry_prefix>: 拉取镜像。

6.3. 镜像安全

  • 使用官方或可信的基础镜像。
  • 最小化镜像内容。
  • 使用非 root 用户运行。
  • 定期进行漏洞扫描。
  • 考虑使用镜像签名 (如 Docker Content Trust) 验证镜像来源和完整性。

7. Docker 与 Kubernetes

  • Docker (或其他符合 OCI 标准的容器运行时,如 containerd, CRI-O) 负责单个容器的生命周期管理(构建、运行、停止)。
  • Kubernetes (K8s) 是一个容器编排平台,负责大规模容器集群的自动化部署、扩展、管理和网络。
  • 关系: Kubernetes 以 Docker 容器(或 OCI 容器)作为其调度的基本单元(通常封装在 Pod 中)。Kubernetes 利用底层的容器运行时来实际创建和管理容器,并在此之上提供了服务发现、负载均衡、自动伸缩、滚动更新、存储编排等高级功能。
  • 虽然 Kubernetes 正在逐步移除对 Docker Engine (dockershim) 的直接依赖,转而使用标准的 CRI (Container Runtime Interface) 与 containerd 或 CRI-O 等运行时交互,但 Docker 构建的 OCI 兼容镜像仍然是 Kubernetes 生态系统中最常用的应用打包格式。

8. 总结

Docker 通过 Namespace、Cgroups 和 UnionFS 等 Linux 内核技术,实现了轻量级的应用隔离和资源限制,彻底改变了软件的开发、分发和部署方式。理解其核心原理、掌握 Dockerfile 最佳实践、熟悉网络和存储配置,对于现代云原生应用的开发和运维至关重要。结合 Kubernetes 等编排工具,Docker 为构建弹性、可扩展、高可用的分布式系统奠定了坚实的基础。


Docker 核心技术深度解析
https://mfzzf.github.io/2025/03/16/docker/
作者
Mzzf
发布于
2025年3月16日
许可协议