Docker镜像管理与Dockerfile实战

本文是Docker系列教程的第三篇,系统讲解Docker镜像管理操作与Dockerfile编写的全部知识。从镜像底层原理(联合文件系统、Overlay2、分层结构、写时复制)讲起,覆盖镜像的搜索、拉取、查看、删除、导入导出等全部管理操作,再到Dockerfile每一条指令的详细语法与使用规范、docker build命令的全部参数、多阶段构建、镜像构建优化策略、私有镜像仓库搭建、以及八个贴近真实开发的实战案例。无论你是想深入理解镜像分层原理,还是想写出更小更安全的Dockerfile,本文都能给你系统性的指导。


第一章 Docker镜像原理深度剖析

要真正掌握Docker镜像管理,首先必须理解镜像在底层是如何存储和组织的。很多开发者只会使用docker pulldocker build,却不知道镜像为什么是分层的、容器为什么秒级启动、修改文件时发生了什么。本章从文件系统底层原理出发,彻底讲透Docker镜像的本质。

1.1 镜像是什么(只读模板的本质)

Docker镜像(Image)是一个特殊的文件系统,它包含了容器运行时所需的程序、库、资源、配置等一切内容。但镜像与普通的文件系统镜像(如ISO文件)有一个根本区别:Docker镜像是只读的(read-only)

更准确地说,Docker镜像是一个只读的模板,它是容器(container)运行的基础。你可以把镜像理解为面向对象编程中的"类"(class),而把容器理解为基于这个类创建的"实例"(instance):

镜像(Image)  <-->  类(Class)    -- 只读模板
容器(Container) <--> 实例(Instance) -- 可运行实例

镜像的只读特性保证了同一个镜像可以被多个容器同时使用而互不干扰。当你基于一个镜像启动容器时,Docker并不会复制整个镜像文件,而是在镜像的最上方添加一个可写层(writable layer),这个可写层就是容器层。容器中的所有修改都发生在这个可写层中,底层的镜像文件始终保持不变。

镜像的组成可以概括为以下几点:

  1. 分层结构: 一个镜像由多个只读层(read-only layer)堆叠而成,每一层都代表一次文件系统的变更。
  2. 联合挂载: 多个层通过联合文件系统(UnionFS)叠加在一起,对外表现为一个统一的根文件系统。
  3. 内容寻址: 每一层通过其内容的SHA256哈希值来唯一标识,相同内容的层可以被多个镜像共享。
  4. 元数据: 镜像还包含JSON格式的配置文件,记录了镜像的环境变量、暴露端口、启动命令、作者等信息。

我们可以通过以下命令来查看一个镜像的详细信息:

# 查看镜像的详细信息(包括分层结构)
docker inspect nginx:latest

# 只查看镜像的层信息
docker inspect nginx:latest --format='{{json .RootFS.Layers}}' | python -m json.tool

# 查看镜像的配置信息
docker inspect nginx:latest --format='{{json .Config}}' | python -m json.tool

执行docker inspect后,你会看到类似这样的JSON输出:

{
    "Id": "sha256:605c77e624ddb75e...",
    "RepoTags": ["nginx:latest"],
    "Created": "2024-01-01T00:00:00.000000000Z",
    "RootFS": {
        "Type": "layers",
        "Layers": [
            "sha256:2edcec3590a4...",
            "sha256:e379e8aedd4d...",
            "sha256:b8d5e1138809...",
            "sha256:f389e7c1f1b3...",
            "sha256:762b1479011d..."
        ]
    },
    "Config": {
        "Env": ["PATH=/usr/local/sbin:..."],
        "Cmd": ["nginx", "-g", "daemon off;"],
        "ExposedPorts": {"80/tcp": {}}
    }
}

从上面的输出可以看出,nginx:latest镜像由5个层组成,每一层都有一个唯一的SHA256哈希值。这些层就是镜像存储的基本单位。

1.2 联合文件系统(UnionFS)详解

联合文件系统(Union File System,简称UnionFS)是Docker镜像分层存储的底层技术基础。理解UnionFS,才能理解Docker镜像为什么能分层、为什么能共享层、为什么容器启动如此之快。

1.2.1 什么是联合文件系统

联合文件系统是一种能把多个不同物理位置的目录"联合"挂载(mount)到同一个目录树下的文件系统技术。它最核心的能力是: 将多个目录叠加在一起,使其看起来像一个目录,同时还能处理不同层之间的文件冲突。

假设有三个目录lowerupperwork,我们想把它们联合挂载到merged目录。在UnionFS中:

  • lower目录是只读的底层目录(对应镜像的只读层)
  • upper目录是可读写的上层目录(对应容器的可写层)
  • work目录是文件系统内部使用的工作目录
  • merged目录是联合挂载后的结果(用户看到的统一文件系统)

用户在merged目录中看到的文件,是lowerupper目录文件叠加的结果。如果lowerupper中都有同名文件,上层的文件会"遮盖"下层的文件。

1.2.2 联合文件系统的核心能力

联合文件系统提供了以下几种核心能力:

1. 叠加(Overlay)
多个目录中的文件可以叠加显示。例如,底层目录有/bin/lib,上层目录有/app,合并后用户能看到/bin/lib/app

2. 遮盖(Whiteout)
当上层目录中删除了底层目录中的文件时,UnionFS不会真正修改底层文件,而是创建一个"whiteout"文件来标记该文件已被删除。这样在合并视图中,该文件就不可见了。

3. 复制向上(Copy-up)
当用户尝试修改底层(只读层)的文件时,UnionFS会先把该文件从只读层复制到可写层,然后在可写层中进行修改。这就是后面要讲的"写时复制"(Copy-on-Write)机制。

1.2.3 联合文件系统的历史与演进

Docker在不同的历史时期使用过不同的联合文件系统实现:

文件系统 全称 说明
AUFS Another UnionFS Docker早期默认使用的联合文件系统,功能完善但未进入Linux内核主线
OverlayFS Overlay File System 2014年合入Linux内核主线(3.18),Docker中称为overlay驱动
Overlay2 OverlayFS v2 OverlayFS的改进版,解决了overlay驱动的一些限制,Docker 17.06+默认推荐
DeviceMapper Device Mapper 基于Linux内核的Device Mapper框架实现的块级存储驱动
Btrfs B-Tree File System Oracle开发的支持快照和子卷的现代文件系统
ZFS Z File System Sun Microsystems开发的高级文件系统,支持快照、压缩、校验

以下是这些文件系统的发展时间线:

2013年: Docker发布,默认使用AUFS
  |
2014年: OverlayFS合入Linux 3.18内核,Docker引入overlay驱动
  |
2016年: Docker 1.12引入overlay2驱动(基于内核4.0+)
  |
2017年: Docker 17.06将overlay2设为推荐默认驱动
  |
至今:   overlay2已成为Linux上Docker的默认存储驱动

1.3 Overlay2文件系统工作原理

Overlay2是当前Docker在Linux上默认且推荐的存储驱动。它基于Linux内核的OverlayFS文件系统,性能优异且稳定可靠。本节深入讲解Overlay2的工作原理。

1.3.1 Overlay2的三个核心目录

Overlay2文件系统涉及三个核心概念:

  1. LowerDir(底层目录): 只读的底层目录,可以有多个。对应镜像的多个只读层。多层之间按从上到下的顺序叠加。
  2. UpperDir(上层目录): 可读写的上层目录,只有一个。对应容器的可写层。所有对文件的修改都发生在这里。
  3. MergedDir(合并目录): 联合挂载后的目录,用户和容器进程看到的就是这个目录。它是LowerDir和UpperDir叠加后的结果。

此外还有一个WorkDir(工作目录),是OverlayFS内部使用的临时目录,用于保证文件操作的原子性。

1.3.2 Overlay2文件操作流程

下面用一个具体例子来演示Overlay2的工作过程。假设我们在Ubuntu系统上手动挂载一个OverlayFS:

# 创建所需的目录
mkdir -p /tmp/overlay-test/{lower,upper,work,merged}

# 在lower目录中创建一些文件(模拟镜像的只读层)
echo "This is from lower layer" > /tmp/overlay-test/lower/file1.txt
echo "Another file in lower" > /tmp/overlay-test/lower/file2.txt

# 在upper目录中创建一个文件(模拟容器的可写层)
echo "This is from upper layer" > /tmp/overlay-test/upper/file3.txt

# 挂载OverlayFS
mount -t overlay overlay \
  -o lowerdir=/tmp/overlay-test/lower,upperdir=/tmp/overlay-test/upper,workdir=/tmp/overlay-test/work \
  /tmp/overlay-test/merged

# 查看merged目录
ls /tmp/overlay-test/merged/
# 输出: file1.txt  file2.txt  file3.txt

# 查看file1.txt内容(来自lower层)
cat /tmp/overlay-test/merged/file1.txt
# 输出: This is from lower layer

# 查看file3.txt内容(来自upper层)
cat /tmp/overlay-test/merged/file3.txt
# 输出: This is from upper layer

现在我们来模拟写时复制(Copy-on-Write)操作:

# 修改file1.txt(这个文件原来在lower层,只读)
echo "Modified content" > /tmp/overlay-test/merged/file1.txt

# 再次查看file1.txt内容
cat /tmp/overlay-test/merged/file1.txt
# 输出: Modified content

# 检查lower层中的file1.txt是否被修改(应该没有变)
cat /tmp/overlay-test/lower/file1.txt
# 输出: This is from lower layer

# 检查upper层(修改后的文件被复制到了upper层)
cat /tmp/overlay-test/upper/file1.txt
# 输出: Modified content

从上面的实验可以看出:

  • 修改file1.txt时,OverlayFS先把文件从lower层复制到upper层,然后在upper层中修改。
  • lower层的文件始终保持不变。
  • merged目录显示的是upper层修改后的内容。

接下来演示删除操作(Whiteout机制):

# 删除file2.txt(这个文件来自lower层)
rm /tmp/overlay-test/merged/file2.txt

# merged目录中file2.txt消失了
ls /tmp/overlay-test/merged/
# 输出: file1.txt  file3.txt

# lower层的file2.txt仍然存在
ls /tmp/overlay-test/lower/
# 输出: file1.txt  file2.txt

# upper层中出现了一个whiteout文件(字符设备文件,主次设备号都是0)
ls -la /tmp/overlay-test/upper/
# 输出中可以看到一个特殊的文件:
# c--------- 1 root root ... file2.txt

whiteout文件是一个主设备号和次设备号都为0的字符设备文件。当OverlayFS在merged目录中看到upper层有对应文件的whiteout标记时,就会在merged视图中隐藏该文件,从而实现"删除"的效果,而实际上lower层的文件并未被删除。

1.3.3 查看Docker容器的Overlay2层

在实际的Docker环境中,我们可以通过docker inspect命令来查看一个运行中容器的Overlay2层结构:

# 启动一个nginx容器
docker run -d --name my-nginx nginx:latest

# 查看容器的Overlay2层信息
docker inspect my-nginx --format='{{json .GraphDriver}}' | python -m json.tool

输出类似如下:

{
    "Data": {
        "LowerDir": "/var/lib/docker/overlay2/abc123.../init:/var/lib/docker/overlay2/def456.../diff:/var/lib/docker/overlay2/ghi789.../diff:...",
        "MergedDir": "/var/lib/docker/overlay2/xyz000.../merged",
        "UpperDir": "/var/lib/docker/overlay2/xyz000.../diff",
        "WorkDir": "/var/lib/docker/overlay2/xyz000.../work"
    },
    "Name": "overlay2"
}

从输出可以看到:

  • LowerDir: 多个只读层,用冒号(:)分隔,从左到右是从上到下的顺序。其中/init结尾的是Docker初始化容器时自动添加的层(包含/dev/proc等虚拟文件系统的挂载点配置)。
  • UpperDir: 容器的可写层,所有运行时修改都存储在这里。
  • MergedDir: 联合挂载后的统一视图,这是容器进程看到的根文件系统。
  • WorkDir: OverlayFS内部使用的临时目录。

我们可以直接到宿主机上查看这些目录的内容(需要root权限):

# 查看容器的可写层中有哪些文件被修改
sudo ls -la /var/lib/docker/overlay2/xyz000.../diff/

# 如果在容器中创建一个新文件
docker exec my-nginx sh -c 'echo "hello" > /tmp/test.txt'

# 再次查看可写层,会看到新增的文件
sudo find /var/lib/docker/overlay2/xyz000.../diff/ -type f

1.4 镜像分层结构详解(每一层是什么)

理解了Overlay2之后,我们来看Docker镜像的分层结构到底是怎么形成的,每一层具体包含了什么。

1.4.1 镜像层的来源

Docker镜像的每一层都对应Dockerfile中的一条指令(部分指令会创建新层,部分不会)。当执行docker build时,Dockerfile中的每条指令(如RUNCOPYADD)都会创建一个新的只读层。

来看一个具体的Dockerfile:

FROM ubuntu:22.04                    # 基础镜像层(包含Ubuntu文件系统)

LABEL maintainer="admin@example.com"  # 不创建新层(元数据写入配置)

RUN apt-get update && \
    apt-get install -y python3         # 创建新层: 安装Python3产生的文件变更

COPY app.py /app/app.py               # 创建新层: 添加app.py文件

RUN pip3 install flask                 # 创建新层: 安装flask包

EXPOSE 5000                           # 不创建新层(元数据写入配置)

CMD ["python3", "/app/app.py"]        # 不创建新层(元数据写入配置)

构建后,这个镜像大约有以下几个层:

┌─────────────────────────────┐
│  Layer 5: CMD配置(元数据)    │  ← 不产生新层,写入镜像配置JSON
├─────────────────────────────┤
│  Layer 4: EXPOSE配置(元数据) │  ← 不产生新层,写入镜像配置JSON
├─────────────────────────────┤
│  Layer 3: pip3 install flask │  ← 新层: /usr/local/lib/python3.10/...下的flask包
├─────────────────────────────┤
│  Layer 2: COPY app.py        │  ← 新层: /app/app.py
├─────────────────────────────┤
│  Layer 1: apt-get install    │  ← 新层: /usr/bin/python3, /usr/lib/python3.10/...
├─────────────────────────────┤
│  Layer 0: ubuntu:22.04基础层  │  ← Ubuntu 22.04的完整根文件系统
└─────────────────────────────┘

我们可以用docker history命令来查看镜像的分层历史:

docker history my-python-app:latest

输出示例:

IMAGE          CREATED        CREATED BY                                      SIZE
a1b2c3d4e5f6   1 minute ago  /bin/sh -c #(nop) CMD ["python3" "/app/app.py"]  0B
b2c3d4e5f6a7   2 minutes ago /bin/sh -c #(nop) EXPOSE 5000/tcp               0B
c3d4e5f6a7b8   3 minutes ago /bin/sh -c pip3 install flask                   12.5MB
d4e5f6a7b8c9   4 minutes ago /bin/sh -c #(nop) COPY app.py /app/app.py       2.1KB
e5f6a7b8c9d0   5 minutes ago /bin/sh -c apt-get update && apt-get install... 85.3MB
f6a7b8c9d0e1   6 minutes ago /bin/sh -c #(nop) LABEL maintainer=admin@...    0B
605c77e624dd   2 weeks ago   /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>      2 weeks ago   /bin/sh -c #(nop) ADD file:... in /              77.8MB

注意以下几点:

  • CMDEXPOSELABEL等指令的SIZE为0B,说明它们不创建新层,只是修改了镜像的配置JSON。
  • RUNCOPYADD等指令会产生实际的文件系统变更,创建了新层,SIZE大于0。
  • #(nop)标记表示该指令没有产生新的文件系统层(No Operation on filesystem)。
1.4.2 哪些指令会创建新层

下表列出了Dockerfile中各指令是否创建新的文件系统层:

指令 是否创建新层 说明
FROM 是(使用基础镜像的层) FROM不直接创建层,而是引入基础镜像的所有层
RUN 执行命令产生的文件系统变更
COPY 复制文件到镜像中
ADD 复制文件或解压归档到镜像中
WORKDIR 只修改环境变量中的工作目录配置
ENV 只修改环境变量配置
LABEL 只修改镜像元数据
EXPOSE 只修改端口声明配置
USER 只修改用户配置
VOLUME 只修改卷声明配置
ARG 只定义构建参数
CMD 只修改默认启动命令配置
ENTRYPOINT 只修改入口点配置
HEALTHCHECK 只修改健康检查配置
ONBUILD 只修改触发器配置
STOPSIGNAL 只修改停止信号配置
SHELL 只修改默认shell配置

理解这一点非常重要,因为它直接关系到镜像的层数和大小。层数过多会增加镜像的元数据开销,而某些不产生文件变更的指令不会增加层数。

1.4.3 层的共享与复用

Docker镜像的分层结构带来了一个巨大的优势: 层共享。当多个镜像包含相同的层时,Docker只会在本地存储一份,从而节省磁盘空间。

例如,你从ubuntu:22.04构建了三个不同的应用镜像:

镜像A (Python应用):                  镜像B (Node应用):                  镜像C (Java应用):
┌──────────────────┐               ┌──────────────────┐               ┌──────────────────┐
│ app.py + flask   │               │ app.js + express │               │ app.jar          │
├──────────────────┤               ├──────────────────┤               ├──────────────────┤
│ python3安装       │               │ node.js安装       │               │ jdk安装           │
├──────────────────┤               ├──────────────────┤               ├──────────────────┤
│ ubuntu:22.04基础  │ ← 共享 →      │ ubuntu:22.04基础  │ ← 共享 →      │ ubuntu:22.04基础  │
└──────────────────┘               └──────────────────┘               └──────────────────┘

三个镜像共享最底层的ubuntu:22.04基础层,Docker在本地只存储一份该层的文件。可以通过以下命令验证:

# 查看本地所有镜像共享的层数量
docker system df -v

输出示例:

Images space usage:

REPOSITORY       TAG       IMAGE ID       CREATED ago      SIZE    SHARED SIZE   UNIQUE SIZE
my-python-app    latest    a1b2c3d4e5f6   10 minutes ago   175MB   77.8MB        97.2MB
my-node-app      latest    b2c3d4e5f6a7   20 minutes ago   165MB   77.8MB        87.2MB
my-java-app      latest    c3d4e5f6a7b8   30 minutes ago   320MB   77.8MB        242.2MB

SHARED SIZE列显示三个镜像都共享了77.8MB的基础层空间。

1.5 Copy-on-Write(写时复制)机制

写时复制(Copy-on-Write,简称CoW)是Docker实现高效容器运行的核心机制之一。它使得多个容器可以共享同一个镜像的只读层,只有在文件被修改时才复制该文件到可写层。

1.5.1 为什么需要写时复制

假设没有写时复制机制,当我们从一个300MB的镜像启动10个容器时,每个容器都需要复制一份300MB的文件系统,总共需要3GB的磁盘空间,而且每次启动都要等待文件复制完成,这显然是低效的。

有了写时复制机制后:

  • 10个容器共享同一份300MB的镜像只读层,总共只需要300MB(镜像层) + 各容器可写层的实际修改量。
  • 容器启动时不需要复制任何文件,因此启动速度极快(毫秒到秒级)。
  • 只有在实际修改文件时,才会复制被修改的文件。
1.5.2 写时复制的三种操作场景

场景一: 读取文件

当容器进程尝试读取一个文件时:

  1. Docker首先在容器的可写层(UpperDir)中查找该文件。
  2. 如果找到,直接返回可写层中的文件内容。
  3. 如果没找到,从上到下依次在镜像的各只读层(LowerDir)中查找。
  4. 找到后返回文件内容。

读取操作不涉及任何文件复制,开销很小。

场景二: 修改已存在的文件

当容器进程尝试修改一个存在于只读层中的文件时:

  1. Docker首先在只读层中找到该文件。
  2. 将该文件从只读层复制到可写层(Copy-up操作)。
  3. 在可写层中修改文件。

复制操作是按文件粒度进行的,不是按层粒度。也就是说,只有被修改的文件才会被复制,同一层中的其他文件不受影响。

场景三: 创建新文件

当容器进程创建一个新文件时:

  1. 直接在可写层中创建该文件。
  2. 不涉及任何只读层的操作。

可以通过以下实验来观察写时复制的行为:

# 启动一个Ubuntu容器
docker run -it --name cow-test ubuntu:22.04 /bin/bash

# 在容器中执行以下操作

# 1. 读取一个已存在的文件(不触发复制)
cat /etc/hostname

# 2. 修改一个已存在的文件(触发写时复制)
echo "modified" >> /etc/hostname

# 3. 创建一个新文件(直接在可写层创建)
echo "new file" > /tmp/newfile.txt

# 退出容器
exit

# 在宿主机上查看可写层中有哪些文件被复制/创建
docker inspect cow-test --format='{{.GraphDriver.Data.UpperDir}}'
# 输出: /var/lib/docker/overlay2/xxx/diff

sudo ls -laR /var/lib/docker/overlay2/xxx/diff/
# 你会看到/etc/hostname被复制到了可写层(并且被修改了)
# /tmp/newfile.txt也被创建在可写层
1.5.3 写时复制的性能影响

写时复制虽然节省了空间和启动时间,但也带来了一些性能开销:

  1. 首次写入开销: 第一次修改大文件时,需要将整个文件从只读层复制到可写层,这会消耗时间和额外的磁盘空间。例如修改一个100MB的日志文件,会复制100MB数据。

  2. 查找开销: 读取文件时可能需要遍历多个层才能找到目标文件。不过Overlay2对此做了优化,通过内核层面的缓存机制将查找开销降到很低。

  3. 存储放大: 如果频繁修改只读层中的文件,可写层会持续增长,可能导致存储空间的浪费。因此建议将频繁写入的数据(如日志、数据库数据)放在数据卷(volume)中,而不是容器的可写层。

# 推荐做法: 将频繁写入的数据放在volume中
docker run -d \
  --name mysql \
  -v mysql-data:/var/lib/mysql \
  mysql:8.0

# 而不是直接写入容器的可写层
# docker run -d --name mysql mysql:8.0  # 不推荐: 数据写在可写层

1.6 镜像层、容器层与可写层的关系

理解镜像层、容器层和可写层之间的关系,对于排查容器问题、管理存储空间至关重要。

1.6.1 三层关系图解
         ┌──────────────────────────────────────────┐
         │            容器运行时视图                   │
         │                                          │
         │  ┌──────────────────────────────────┐    │
         │  │        可写层(容器层)              │    │  ← 容器运行时所有修改都存储在这里
         │  │  UpperDir (overlay2)             │    │     容器删除后,这层也会被删除
         │  └──────────────────────────────────┘    │
         │                  ↕ 联合挂载               │
         │  ┌──────────────────────────────────┐    │
         │  │     镜像只读层 N (RUN pip install) │    │
         │  ├──────────────────────────────────┤    │
         │  │     镜像只读层 2 (COPY app.py)    │    │  ← 多个容器可共享这些只读层
         │  ├──────────────────────────────────┤    │
         │  │     镜像只读层 1 (RUN apt install) │    │
         │  ├──────────────────────────────────┤    │
         │  │     镜像只读层 0 (FROM ubuntu)     │    │
         │  └──────────────────────────────────┘    │
         └──────────────────────────────────────────┘
1.6.2 关键关系总结
  1. 镜像层是只读的: 一旦镜像构建完成,其所有层都是不可变的(immutable)。镜像可以被多个容器共享使用。

  2. 可写层是容器私有的: 每个容器都有自己的可写层,不同容器的可写层相互独立。容器A在可写层中的修改,容器B看不到。

  3. 可写层的生命周期与容器绑定: 当容器被删除时,其可写层也会被删除。如果想要持久化数据,必须使用数据卷(Volume)或绑定挂载(Bind Mount)。

  4. 初始化层(init layer): Docker在镜像层和可写层之间还会添加一个特殊的初始化层,用于设置容器运行时的初始化文件(如/etc/hostname/etc/hosts/etc/resolv.conf等)。这个层对用户透明。

可以通过以下命令验证多个容器共享镜像层:

# 启动两个基于同一个镜像的容器
docker run -d --name container1 nginx:latest
docker run -d --name container2 nginx:latest

# 查看两个容器的LowerDir(应该相同,说明共享镜像层)
docker inspect container1 --format='{{.GraphDriver.Data.LowerDir}}'
docker inspect container2 --format='{{.GraphDriver.Data.LowerDir}}'

# 查看两个容器的UpperDir(应该不同,说明各自有独立的可写层)
docker inspect container1 --format='{{.GraphDriver.Data.UpperDir}}'
docker inspect container2 --format='{{.GraphDriver.Data.UpperDir}}'

1.7 镜像存储驱动对比(overlay2/aufs/devicemapper/btrfs/zfs)

Docker支持多种存储驱动,不同的驱动有不同的特点和适用场景。本节对比五种主要的存储驱动。

1.7.1 存储驱动详细对比
特性 overlay2 aufs devicemapper btrfs zfs
内核支持 Linux 4.0+ 非主线(需patch) Linux 2.6.9+ Linux 2.6.34+ 需加载模块
性能 优秀 良好 中等(块级) 良好 良好
稳定性 优秀 良好 中等 良好 优秀
内存占用 中等 中等 高(ARC缓存)
写时复制 文件级 文件级 块级 块级 块级
层共享 支持 支持 不支持 支持 支持
磁盘空间效率 中等
Docker推荐 是(默认) 否(已弃用) 否(仅RHEL旧版)
适用场景 通用 旧系统兼容 RHEL/CentOS旧版 实验性 高级存储需求
1.7.2 overlay2详解

overlay2是目前Docker在Linux上的默认存储驱动,基于内核OverlayFS文件系统。它的主要优点:

  • 高性能: 直接在内核空间实现,文件操作开销小。
  • 层共享: 多个容器可以高效共享镜像层。
  • 稳定可靠: 经过大量生产环境验证,是Docker官方推荐驱动。

检查当前Docker使用的存储驱动:

docker info | grep "Storage Driver"
# 输出: Storage Driver: overlay2

如果当前不是overlay2,可以通过修改/etc/docker/daemon.json来切换:

{
    "storage-driver": "overlay2"
}

修改后重启Docker:

sudo systemctl restart docker

注意: 切换存储驱动会导致已有的镜像和容器数据丢失,切换前请务必备份。

1.7.3 aufs详解

AUFS是Docker最早使用的存储驱动,但由于未能进入Linux内核主线,逐渐被OverlayFS取代。AUFS的主要特点:

  • 支持最多127层(旧版本限制)。
  • 文件级的写时复制,性能较好。
  • 但需要特殊编译的内核,大多数现代发行版不再支持。

Docker从17.06版本开始不再将AUFS作为推荐驱动,从Docker 20.10开始在某些发行版上完全移除了AUFS支持。

1.7.4 devicemapper详解

DeviceMapper是早期RHEL/CentOS上的默认存储驱动,使用块级别的写时复制。它有两种模式:

  • loop-lvm(默认): 使用稀疏文件模拟块设备,性能较差,仅适合测试。
  • direct-lvm: 直接使用物理块设备,性能较好,适合生产环境。

配置direct-lvm模式需要在/etc/docker/daemon.json中设置:

{
    "storage-driver": "devicemapper",
    "storage-opts": [
        "dm.directlvm_device=/dev/sdb",
        "dm.thinp_percent=95",
        "dm.thinp_metapercent=1",
        "dm.thinp_autoextend_threshold=80",
        "dm.thinp_autoextend_percent=20"
    ]
}

由于配置复杂且性能不如overlay2,新版本的RHEL/CentOS也已将默认驱动改为overlay2。

1.7.5 btrfs和zfs详解

Btrfs和ZFS都是现代文件系统,本身支持子卷、快照和写时复制,因此可以直接作为Docker的存储驱动。

Btrfs:

  • 适合使用Btrfs作为根文件系统的系统(如SUSE/openSUSE)。
  • 块级写时复制,对大文件操作更高效。
  • 但Btrfs本身在Linux生态中还不够成熟。

ZFS:

  • 功能强大,支持压缩、去重、快照、校验等高级特性。
  • 适合对数据完整性要求极高的场景。
  • 但内存占用较高(ZFS ARC缓存),许可证问题(CDDL)使其无法直接合入Linux内核。
  • Docker官方不推荐使用ZFS,除非有特殊的存储需求。
1.7.6 如何选择存储驱动

选择存储驱动的原则:

  1. Linux系统(推荐): 使用overlay2,这是Docker官方推荐且默认的驱动,性能和稳定性最佳。
  2. 旧版RHEL/CentOS(内核<4.0): 如果内核版本太低不支持overlay2,使用devicemapper(配置direct-lvm模式)。
  3. 需要特殊存储特性: 如果需要数据去重、压缩等高级特性,且系统已使用Btrfs或ZFS,可考虑使用对应的驱动。

绝大多数情况下,使用默认的overlay2即可,无需手动配置。


第二章 镜像管理基础操作

理解了镜像的底层原理之后,本章我们来学习Docker镜像的日常管理操作。这些命令是每个Docker使用者必须掌握的基础技能,包括搜索、拉取、查看、打标签、删除、导入导出等。

2.1 搜索镜像(docker search详解)

在拉取镜像之前,我们通常需要先搜索Docker Hub上有哪些可用的镜像。docker search命令可以帮我们在Docker Hub上搜索公开的镜像仓库。

基本语法
docker search [OPTIONS] TERM

常用选项:

选项 说明
--filter, -f 根据条件过滤结果
--format 使用Go模板格式化输出
--limit 限制返回结果数量(最大100,默认25)
--no-trunc 显示完整信息(不截断描述)
搜索示例
# 搜索包含"nginx"的镜像
docker search nginx

输出示例:

NAME                              DESCRIPTION                                     STARS     OFFICIAL   AUTOMATED
nginx                             Official build of Nginx.                        19500     [OK]
bitnami/nginx                     Bitnami container image for Nginx               200       [OK]
nginx/nginx-ingress               NGINX and NGINX Plus Ingress Controllers fo...  85
ubuntu/nginx                      Nginx, a high-performance web server and r...  92
nginxinc/nginx-unprivileged       Unprivileged NGINX Docker images                54
...

各列含义:

  • NAME: 镜像仓库名称
  • DESCRIPTION: 镜像描述
  • STARS: 在Docker Hub上获得的星标数量(反映受欢迎程度)
  • OFFICIAL: 是否为官方镜像([OK]表示是)
  • AUTOMATED: 是否为自动构建镜像
使用过滤条件
# 只搜索官方镜像
docker search --filter is-official=true nginx

# 搜索星标数大于50的镜像
docker search --filter stars=50 nginx

# 同时过滤官方镜像和星标数
docker search --filter is-official=true --filter stars=100 nginx
使用格式化输出
# 自定义输出格式
docker search --format "table {{.Name}}\t{{.StarCount}}\t{{.IsOfficial}}" nginx

# 只输出镜像名称
docker search --format "{{.Name}}" nginx

# 输出JSON格式(需要配合jq工具)
docker search --format "{{json .}}" nginx | jq .
搜索建议
  1. 优先使用官方镜像: 官方镜像(Official Images)经过Docker团队审核,更安全、更规范。官方镜像的名称通常没有组织前缀(如nginx而不是bitnami/nginx)。

  2. 关注星标数量: 星标数量高的镜像通常更受欢迎、更活跃。

  3. 查看Docker Hub页面: docker search只显示基本信息,更多详情(如支持的tag、使用文档、更新历史)需要到Docker Hub网站查看。

2.2 拉取镜像(docker pull,指定版本、平台)

docker pull命令用于从镜像仓库拉取镜像到本地。

基本语法
docker pull [OPTIONS] NAME[:TAG|@DIGEST]

常用选项:

选项 说明
--all-tags, -a 拉取仓库中所有标签的镜像
--platform 指定目标平台(如linux/amd64, linux/arm64)
--quiet, -q 安静模式,减少输出
拉取指定版本的镜像
# 拉取最新版本(默认tag为latest)
docker pull nginx

# 等价于
docker pull nginx:latest

# 拉取指定版本
docker pull nginx:1.25.3

# 拉取指定版本(alpine变体)
docker pull nginx:1.25.3-alpine

# 拉取指定组织/用户的镜像
docker pull bitnami/nginx:latest

# 从私有仓库拉取(需要先登录)
docker pull registry.example.com:5000/myapp:v1.0

重要提示: 在生产环境中,永远不要使用latest标签latest标签会随时间变化,不同时间拉取的latest可能是不同的版本,这会导致环境不一致。应该始终指定具体的版本号。

拉取指定平台的镜像

在多平台环境下(如Apple M1/M2芯片的Mac上运行x86镜像),可以指定平台:

# 在ARM64机器上拉取AMD64版本的镜像
docker pull --platform linux/amd64 nginx:latest

# 在AMD64机器上拉取ARM64版本的镜像
docker pull --platform linux/arm64 nginx:latest

# 指定操作系统和架构
docker pull --platform linux/arm64/v8 nginx:latest
使用摘要(Digest)拉取

镜像的标签(tag)是可以被覆盖的,也就是说同一个标签在不同时间可能指向不同的镜像内容。如果需要确保拉取到特定内容的镜像,可以使用摘要(digest)来拉取:

# 使用SHA256摘要拉取(内容寻址,保证内容完全一致)
docker pull nginx@sha256:6d75c99be155c96ca6a4ff57a1d70b75d72acb89d0d66e1daccc6e2f6a55e1d6

摘要拉取保证了内容的不变性,因为SHA256哈希值是基于镜像内容计算的。这在CI/CD流水线中非常有用,可以确保每次构建使用完全相同的基础镜像。

拉取所有标签
# 拉取一个仓库中所有标签的镜像(慎用,可能非常占用空间)
docker pull -a alpine
查看拉取过程

拉取镜像时,Docker会逐层下载,并显示每一层的下载进度:

docker pull python:3.12-slim

输出示例:

3.12-slim: Pulling from library/python
0a9a5dfd008f: Pull complete     ← 第1层下载完成
8c4e823d1ca3: Pull complete     ← 第2层下载完成
5f83fc6aaa5a: Pull complete     ← 第3层下载完成
ab86b5b89d0d: Pull complete     ← 第4层下载完成
8d6f4f545e06: Pull complete     ← 第5层下载完成
Digest: sha256:5fe8e9a6f7a3...
Status: Downloaded newer image for python:3.12-slim
docker.io/library/python:3.12-slim

每一行对应镜像的一个层,如果某个层在本地已存在(被其他镜像共享),会直接显示Already exists而不会重新下载:

3.12-slim: Pulling from library/python
0a9a5dfd008f: Already exists      ← 该层已存在,无需下载
8c4e823d1ca3: Pull complete
...

2.3 查看本地镜像(docker images,过滤、格式化)

docker images命令用于列出本地存储的所有镜像。

基本语法
docker images [OPTIONS] [REPOSITORY[:TAG]]

常用选项:

选项 说明
--all, -a 显示所有镜像(包括中间层)
--digests 显示镜像摘要
--filter, -f 根据条件过滤
--format 使用Go模板格式化输出
--no-trunc 显示完整镜像ID
--quiet, -q 只显示镜像ID
基本用法
# 列出所有本地镜像
docker images

输出示例:

REPOSITORY    TAG          IMAGE ID       CREATED         SIZE
nginx         latest       605c77e624dd   2 weeks ago     141MB
nginx         1.25.3       4dc91b6f1b1e   3 weeks ago     142MB
python        3.12-slim    3d6c77e624dd   1 month ago     125MB
ubuntu        22.04        a8d6c77e624d   2 months ago    77.8MB

各列含义:

  • REPOSITORY: 镜像仓库名称
  • TAG: 镜像标签
  • IMAGE ID: 镜像唯一标识(默认显示前12位)
  • CREATED: 镜像创建时间
  • SIZE: 镜像大小
过滤镜像
# 列出指定仓库的镜像
docker images nginx

# 列出指定仓库和标签的镜像
docker images nginx:latest

# 过滤悬空镜像(dangling images,即没有标签的中间层镜像)
docker images -f dangling=true

# 过滤指定标签之前的镜像
docker images -f before=nginx:1.25.3

# 过滤指定标签之后的镜像
docker images -f since=ubuntu:22.04

# 过滤引用(reference)匹配的镜像
docker images -f reference="nginx:*"

# 过滤标签包含特定字符串的镜像
docker images -f reference="*alpine*"
格式化输出
# 自定义表格格式
docker images --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}"

# 只输出镜像ID(常用于批量操作)
docker images -q

# 输出镜像ID和仓库名(用冒号分隔,适合脚本处理)
docker images --format "{{.ID}}: {{.Repository}}"

# 输出JSON格式
docker images --format "{{json .}}" | jq .

# 显示摘要信息
docker images --digests
显示完整信息
# 显示完整的镜像ID(不截断)
docker images --no-trunc

# 显示所有镜像包括中间层(构建过程中产生的中间镜像)
docker images -a

2.4 镜像标签管理(docker tag)

docker tag命令用于给已有的镜像创建一个新的标签。它不会创建新的镜像,只是给同一个镜像添加了一个新的引用名称。

基本语法
docker tag SOURCE_IMAGE[:TAG] TARGET_IMAGE[:TAG]
使用示例
# 给本地镜像添加新标签
docker tag nginx:latest my-nginx:v1.0

# 给镜像添加多个标签
docker tag myapp:latest myapp:v1.0
docker tag myapp:latest myapp:v1.0.0
docker tag myapp:latest myapp:stable

# 为推送到私有仓库做准备(打上仓库地址前缀)
docker tag myapp:v1.0 registry.example.com:5000/myapp:v1.0

# 为推送到Docker Hub做准备(打上用户名前缀)
docker tag myapp:v1.0 myusername/myapp:v1.0
验证tag不创建新镜像
# 查看tag前后的镜像ID
docker images myapp

# 你会发现不同标签的镜像IMAGE ID是相同的
# 这说明它们指向的是同一个镜像
REPOSITORY   TAG     IMAGE ID       CREATED         SIZE
myapp        latest  a1b2c3d4e5f6   10 minutes ago  150MB
myapp        v1.0    a1b2c3d4e5f6   10 minutes ago  150MB
myapp        v1.0.0  a1b2c3d4e5f6   10 minutes ago  150MB
删除标签

删除一个标签不会影响其他标签指向的同一个镜像:

# 删除myapp:v1.0.0标签(其他标签不受影响)
docker rmi myapp:v1.0.0

# 只有当所有标签都被删除后,镜像才会被真正删除
docker rmi myapp:latest
docker rmi myapp:v1.0

2.5 删除镜像(docker rmi,强制删除、批量删除)

docker rmi命令用于删除本地镜像。

基本语法
docker rmi [OPTIONS] IMAGE [IMAGE...]

常用选项:

选项 说明
--force, -f 强制删除
--no-prune 不删除未标记的父镜像
基本用法
# 通过仓库名和标签删除
docker rmi nginx:latest

# 通过镜像ID删除
docker rmi 605c77e624dd

# 通过镜像ID的前几位删除(只要能唯一识别即可)
docker rmi 605c
删除注意事项
  1. 如果有容器正在使用该镜像,无法直接删除:
docker rmi nginx:latest
# 报错: Error response from daemon: conflict: unable to remove repository reference
# "nginx:latest" (must force) - container abc123 is using its referenced image
  1. 强制删除(即使有容器引用也删除,但不会删除正在运行的容器):
docker rmi -f nginx:latest

警告: 强制删除可能导致依赖该镜像的容器无法正常重启。建议先停止并删除相关容器,再删除镜像。

  1. 先删除容器再删除镜像(推荐做法):
# 停止使用该镜像的所有容器
docker stop $(docker ps -q --filter ancestor=nginx:latest)

# 删除这些容器
docker rm $(docker ps -aq --filter ancestor=nginx:latest)

# 然后删除镜像
docker rmi nginx:latest
批量删除镜像
# 删除所有本地镜像(危险操作,谨慎使用!)
docker rmi $(docker images -q)

# 删除所有悬空镜像(dangling images)
docker rmi $(docker images -f dangling=true -q)

# 删除指定仓库的所有镜像
docker rmi $(docker images -q nginx)

# 删除所有none标签的镜像
docker rmi $(docker images --filter "dangling=true" -q --no-trunc)

# 删除创建时间超过7天的镜像
docker image prune -a --filter "until=168h"

2.6 镜像详细信息(docker inspect)

docker inspect命令可以查看镜像(或容器)的详细JSON格式信息,包括配置、层信息、环境变量、网络设置等。

基本用法
# 查看镜像的完整信息(JSON格式)
docker inspect nginx:latest

# 查看特定字段
docker inspect nginx:latest --format='{{.Architecture}}'
# 输出: amd64

docker inspect nginx:latest --format='{{.Os}}'
# 输出: linux

docker inspect nginx:latest --format='{{.Created}}'
# 输出: 2024-01-01 00:00:00.000000000 +0000 UTC
查看镜像配置信息
# 查看镜像的环境变量
docker inspect nginx:latest --format='{{.Config.Env}}'
# 输出: [PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin NJS_VERSION=0.8.0]

# 查看镜像的启动命令
docker inspect nginx:latest --format='{{.Config.Cmd}}'
# 输出: [nginx -g daemon off;]

# 查看镜像的入口点
docker inspect nginx:latest --format='{{.Config.Entrypoint}}'
# 输出: [/docker-entrypoint.sh]

# 查看镜像暴露的端口
docker inspect nginx:latest --format='{{.Config.ExposedPorts}}'
# 输出: map[80/tcp:{}]

# 查看镜像的工作目录
docker inspect nginx:latest --format='{{.Config.WorkingDir}}'
# 输出: /

# 查看镜像的标签
docker inspect nginx:latest --format='{{.Config.Labels}}'
查看镜像层信息
# 查看镜像的层数和各层ID
docker inspect nginx:latest --format='{{json .RootFS.Layers}}' | python -m json.tool

# 查看镜像的架构和操作系统
docker inspect nginx:latest --format='Arch: {{.Architecture}}, OS: {{.Os}}'

# 查看镜像的作者
docker inspect nginx:latest --format='{{.Author}}'

# 查看镜像的注释信息
docker inspect nginx:latest --format='{{json .Comment}}'
实用技巧: 对比两个镜像的配置差异
# 将两个镜像的配置输出到文件,然后对比
docker inspect nginx:1.25.3 --format='{{json .Config}}' > /tmp/nginx-1.25.3.json
docker inspect nginx:1.24.0 --format='{{json .Config}}' > /tmp/nginx-1.24.0.json

# 使用diff对比
diff /tmp/nginx-1.25.3.json /tmp/nginx-1.24.0.json

2.7 镜像构建历史(docker history)

docker history命令用于查看镜像的构建历史,即每一层是如何创建的。这对于理解镜像的构建过程、排查构建问题非常有用。

基本语法
docker history [OPTIONS] IMAGE

常用选项:

选项 说明
--no-trunc 显示完整信息(不截断)
--quiet, -q 只显示镜像ID
--format 使用Go模板格式化输出
基本用法
# 查看nginx镜像的构建历史
docker history nginx:latest

输出示例:

IMAGE          CREATED       CREATED BY                                      SIZE      COMMENT
605c77e624dd   2 weeks ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  EXPOSE 80                    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENTRYPOINT ["/docker-entr…   0B
<missing>      2 weeks ago   /bin/sh -c #(nop) COPY file:... in /docker-en…  4.62kB
<missing>      2 weeks ago   /bin/sh -c #(nop) COPY file:... in /docker-en…  1.27kB
<missing>      2 weeks ago   /bin/sh -c set -x     && groupadd...            61.3MB
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NJS_VERSION=0.8.0        0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NJS_RELEASE=1~bookworm   0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV PKG_RELEASE=1~bookworm   0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.25.3…    0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  LABEL maintainer=NGINX Do…   0B
<missing>      2 weeks ago   /bin/sh -c #(nop)  CMD ["bash"]                 0B
<missing>      2 weeks ago   /bin/sh -c #(nop) ADD file:... in /             74.8MB

各列含义:

  • IMAGE: 层的镜像ID(<missing>表示该层在本地没有单独的镜像ID,或来自其他镜像)
  • CREATED: 创建时间
  • CREATED BY: 创建该层的命令
  • SIZE: 该层增加的大小
  • COMMENT: 注释
显示完整信息
# 显示完整的CREATED BY信息(不截断)
docker history --no-trunc nginx:latest

# 以指定格式输出
docker history --format "{{.CreatedBy}}: {{.Size}}" nginx:latest

# 只显示有大小的层(过滤掉0B的配置层)
docker history nginx:latest | grep -v "0B"
实际应用: 分析镜像体积来源
# 分析每一层的大小,找出占用空间最大的层
docker history nginx:latest --no-trunc --format "{{.Size}}\t{{.CreatedBy}}" | sort -rh | head -10

这可以帮助你找出哪些指令产生了大量的文件系统变更,从而优化Dockerfile。

2.8 镜像导入导出(docker save/load vs docker export/import区别)

Docker提供了两对镜像导入导出命令,它们有着本质的区别,初学者经常混淆。

2.8.1 docker save / docker load

docker savedocker load操作的是镜像(image),保存的是完整的镜像信息,包括所有的层和元数据。

# 将镜像保存为tar文件
docker save -o nginx.tar nginx:latest

# 也可以保存多个镜像到一个tar文件
docker save -o all-images.tar nginx:latest python:3.12 ubuntu:22.04

# 使用gzip压缩
docker save nginx:latest | gzip > nginx.tar.gz

# 从tar文件加载镜像
docker load -i nginx.tar

# 从gzip压缩的tar文件加载
docker load -i nginx.tar.gz

# 从标准输入加载
docker load < nginx.tar

docker save保存的tar文件包含了完整的镜像结构和元数据,加载后得到的镜像与原始镜像完全一致,包括镜像名称、标签、所有层等。

2.8.2 docker export / docker import

docker exportdocker import操作的是容器(container),导出的是容器的文件系统快照,不包含镜像的分层信息和元数据。

# 将容器的文件系统导出为tar文件
docker export -o container.tar my-container

# 从tar文件导入为新的镜像
docker import container.tar my-new-image:latest

# 导入时可以添加变更信息(如CMD、EXPOSE等)
docker import -c 'CMD ["nginx", "-g", "daemon off;"]' \
              -c 'EXPOSE 80' \
              container.tar my-new-image:latest

# 从URL导入
docker import http://example.com/image.tar my-image:latest
2.8.3 两者的区别对比
对比项 docker save/load docker export/import
操作对象 镜像 容器
保存内容 完整镜像(所有层+元数据) 容器文件系统快照(合并后的单层)
分层信息 保留所有层 丢失分层信息,变成单层
元数据 保留所有元数据 丢失大部分元数据(需要手动指定)
镜像大小 较大(包含所有层) 较小(合并后的单层)
典型用途 镜像备份、离线传输 容器快照、制作基础镜像
2.8.4 使用场景示例

场景一: 离线环境传输镜像(使用save/load)

# 在有网络的环境中
docker pull nginx:latest
docker save -o nginx.tar nginx:latest

# 将nginx.tar拷贝到离线环境(如U盘、scp)
scp nginx.tar user@offline-server:/tmp/

# 在离线环境中
docker load -i /tmp/nginx.tar
docker images nginx

场景二: 从运行中的容器创建新镜像(使用export/import)

# 启动一个Ubuntu容器,安装一些软件
docker run -it --name dev-env ubuntu:22.04 /bin/bash
# 在容器中安装软件
apt-get update && apt-get install -y git vim curl

# 退出容器后,导出为新的镜像
docker export dev-env | docker import - dev-env:v1.0

# 使用新镜像启动容器
docker run -it dev-env:v1.0 /bin/bash

注意: docker export/import会丢失镜像的构建历史和分层信息,导入后的镜像只有一个层。而docker commit(另一种从容器创建镜像的方法)会保留分层信息。通常建议使用docker commit而不是docker export/import来从容器创建镜像。

2.9 清理悬空镜像(docker image prune)

在镜像构建过程中,由于Dockerfile的修改或重新构建,会产生一些没有标签的中间层镜像,这些镜像被称为"悬空镜像"(dangling images)。它们占用磁盘空间但没有任何用处,需要定期清理。

基本语法
docker image prune [OPTIONS]

常用选项:

选项 说明
--all, -a 删除所有未使用的镜像(不仅仅悬空的)
--filter 提供过滤条件
--force, -f 不提示确认,直接删除
基本用法
# 清理悬空镜像(没有标签的中间层镜像)
docker image prune

# 清理所有未被容器使用的镜像(更激进)
docker image prune -a

# 不提示确认,直接清理
docker image prune -f

# 清理所有未被使用且创建超过24小时的镜像
docker image prune -a --filter "until=24h"
查看悬空镜像
# 查看悬空镜像
docker images -f dangling=true

# 查看悬空镜像的ID
docker images -f dangling=true -q
使用docker system prune清理

Docker还提供了一个更全面的清理命令docker system prune,可以一次性清理未使用的镜像、容器、网络和构建缓存:

# 清理所有未使用的资源(镜像、容器、网络)
docker system prune

# 包括未被使用的镜像也一起清理
docker system prune -a

# 包括构建缓存也一起清理
docker system prune -a --volumes

# 不提示确认
docker system prune -f
查看磁盘使用情况

在清理之前,建议先查看Docker的磁盘使用情况:

# 查看Docker磁盘使用概况
docker system df

输出示例:

TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          15        3         3.5GB     2.8GB (80%)
Containers      5         2         120MB     80MB (66%)
Local Volumes   3         2         500MB     200MB (40%)
Build Cache     25        0         800MB     800MB (100%)

各列含义:

  • TOTAL: 总数量
  • ACTIVE: 正在使用的数量
  • SIZE: 总大小
  • RECLAIMABLE: 可回收的大小(百分比)
# 查看详细信息(每个镜像/容器/卷的具体大小)
docker system df -v

第三章 Dockerfile基础指令详解

Dockerfile是构建Docker镜像的核心。它是一个纯文本文件,包含了一系列指令(instructions),每条指令对应镜像构建的一个步骤。掌握Dockerfile的每一条指令,是编写高质量Docker镜像的基础。本章详细讲解Dockerfile中的基础构建指令。

3.1 Dockerfile概述与基本结构

3.1.1 什么是Dockerfile

Dockerfile是一个文本文件,其内容包含了构建Docker镜像所需的全部指令。Docker通过逐行读取Dockerfile中的指令,按顺序执行这些指令,最终生成一个Docker镜像。

Dockerfile的基本结构如下:

# 注释以#开头

# 指定基础镜像(必须为第一条非注释指令)
FROM ubuntu:22.04

# 设置维护者信息
LABEL maintainer="admin@example.com"

# 设置工作目录
WORKDIR /app

# 复制文件到镜像中
COPY . /app

# 安装依赖
RUN apt-get update && apt-get install -y python3

# 设置环境变量
ENV PORT=5000

# 声明暴露端口
EXPOSE 5000

# 设置默认启动命令
CMD ["python3", "app.py"]
3.1.2 Dockerfile的基本规则
  1. 指令不区分大小写,但按照惯例,指令使用大写字母(如FROMRUN),以便与参数区分。
  2. Dockerfile必须以FROM指令开头(ARG可以出现在FROM之前)。
  3. 每条指令占据一行,如果一条指令太长需要换行,可以使用反斜杠\续行。
  4. 注释以#开头,必须在单独一行或行尾。
  5. 指令按顺序执行,每条指令都会创建一个新的镜像层(部分指令除外)。
3.1.3 Dockerfile的解析器指令(Parser Directives)

Dockerfile支持解析器指令,它们写在Dockerfile的最顶部(在FROM之前),格式为# directive=value:

# syntax=docker/dockerfile:1.6
# escape=`
# 使用反引号作为续行符(Windows环境友好)

FROM microsoft/windowsservercore
  • # syntax=...: 指定使用的Dockerfile语法版本(需要BuildKit支持)。
  • # escape=...: 指定续行符,默认为反斜杠\,在Windows路径中可以改为反引号`

3.2 FROM指令(基础镜像选择策略)

FROM指令指定构建镜像的基础镜像。每个Dockerfile必须以FROM指令开始(除非使用ARG在FROM之前定义变量)。

语法
FROM [--platform=<platform>] <image>[:<tag>] [AS <name>]
FROM [--platform=<platform>] <image>[@<digest>] [AS <name>]
基本用法
# 使用最新版本(不推荐在生产环境使用)
FROM ubuntu

# 使用指定版本(推荐)
FROM ubuntu:22.04

# 使用摘要确保内容不变
FROM ubuntu@sha256:35fb1c4dcf6b5e6b...

# 使用多阶段构建中的命名阶段
FROM golang:1.21 AS builder

# 指定平台
FROM --platform=linux/amd64 ubuntu:22.04
基础镜像选择策略

选择基础镜像是编写Dockerfile最重要的决策之一。以下是常见的选择策略:

镜像类型 大小 适用场景 示例
完整版(full) 100-300MB 开发环境,需要完整工具链 python:3.12, node:20
slim版 50-100MB 生产环境,精简但保留基本系统 python:3.12-slim, node:20-slim
alpine版 5-50MB 追求最小体积,使用musl libc python:3.12-alpine, node:20-alpine
distroless 2-30MB 极致安全,无shell无包管理器 gcr.io/distroless/python3
scratch 0MB 特殊场景,完全空白的镜像 FROM scratch
# 开发环境: 使用完整版,包含所有开发工具
FROM python:3.12

# 生产环境(推荐): 使用slim版,平衡大小和兼容性
FROM python:3.12-slim

# 追求极致小体积: 使用alpine版(注意musl libc兼容性问题)
FROM python:3.12-alpine

# 追求极致安全: 使用distroless(无shell,无法进入容器调试)
FROM gcr.io/distroless/python3-debian12

提示: alpine镜像使用musl libc而非glibc,可能导致某些C扩展库(如numpy、pandas)在编译时出问题。如果遇到兼容性问题,建议改用slim版本。

在FROM中使用ARG变量
# 在FROM之前定义ARG变量
ARG PYTHON_VERSION=3.12

# 在FROM中使用ARG变量
FROM python:${PYTHON_VERSION}-slim

# 后续指令中需要重新声明ARG才能使用
ARG PYTHON_VERSION
RUN echo "Python version: ${PYTHON_VERSION}"

3.3 LABEL指令(镜像元数据)

LABEL指令用于给镜像添加元数据,以键值对的形式存储。它是MAINTAINER指令的替代者(Docker 1.13起推荐使用LABEL替代MAINTAINER)。

语法
LABEL <key>=<value> <key>=<value> <key>=<value> ...
基本用法
# 单个标签
LABEL maintainer="admin@example.com"

# 多个标签(可以在一行中写多个)
LABEL version="1.0" description="My Web Application" author="Dev Team"

# 多行写法(使用反斜杠续行)
LABEL maintainer="admin@example.com" \
      version="1.0" \
      description="My Web Application" \
      build-date="2024-01-01"
推荐的标签规范

Docker官方推荐了一套标签命名规范,使用反向域名格式:

LABEL org.opencontainers.image.authors="admin@example.com"
LABEL org.opencontainers.image.title="My Web App"
LABEL org.opencontainers.image.description="A web application built with Flask"
LABEL org.opencontainers.image.version="1.0.0"
LABEL org.opencontainers.image.created="2024-01-01T00:00:00Z"
LABEL org.opencontainers.image.source="https://github.com/myorg/myapp"
LABEL org.opencontainers.image.revision="abc123def456"
LABEL org.opencontainers.image.licenses="MIT"
查看镜像标签
# 查看镜像的所有标签
docker inspect myapp:latest --format='{{json .Config.Labels}}' | python -m json.tool

# 使用docker images查看标签
docker images --format "{{.Repository}}: {{.Label \"org.opencontainers.image.version\"}}"

3.4 RUN指令(执行命令,shell形式vs exec形式)

RUN指令是Dockerfile中最常用的指令之一,它用于在构建镜像时执行命令。每条RUN指令都会在当前镜像的基础上创建一个新层并执行命令,命令执行完成后提交这个层。

两种语法形式

Shell形式(最常用):

RUN <command>

命令在shell中执行,默认使用/bin/sh -c。支持shell特性如管道、变量替换、通配符等。

Exec形式:

RUN ["executable", "param1", "param2"]

直接调用可执行文件,不经过shell。不支持shell特性。

Shell形式示例
# 使用shell形式,支持管道
RUN apt-get update && apt-get install -y python3

# 支持变量替换
ENV APP_DIR=/app
RUN mkdir -p $APP_DIR && echo "Created $APP_DIR" > $APP_DIR/readme.txt

# 支持复杂命令
RUN wget -q https://example.com/file.tar.gz && \
    tar -xzf file.tar.gz && \
    cd file && \
    make && \
    make install && \
    rm -rf file.tar.gz file
Exec形式示例
# 使用exec形式,直接调用可执行文件
RUN ["/bin/bash", "-c", "echo hello"]

# 使用不同的shell
RUN ["/bin/bash", "-c", "echo $HOME"]

# 安装软件(exec形式不支持管道等shell特性)
RUN ["apt-get", "install", "-y", "python3"]

注意: exec形式中的参数必须使用双引号,不能使用单引号。RUN ["echo", "hello"]正确,RUN ['echo', 'hello']错误。

Shell形式与Exec形式的区别
对比项 Shell形式 Exec形式
语法 RUN command RUN ["cmd", "arg"]
Shell 通过/bin/sh -c执行 直接执行,不经过shell
变量替换 支持(如$VAR) 不支持(需要使用ARG/ENV)
管道 支持(|) 不支持
重定向 支持(>, <) 不支持
通配符 支持(*, ?) 不支持
信号处理 shell(PID 1)接收信号 可执行文件(PID 1)接收信号
RUN指令最佳实践

1. 合并RUN指令减少层数

# 不推荐: 多个RUN指令,产生多个层
RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip3 install flask
RUN apt-get clean

# 推荐: 合并为一个RUN指令,只产生一个层
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        python3 \
        python3-pip && \
    pip3 install flask && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

2. 清理不必要的文件

# 安装完成后清理缓存和列表文件
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        vim && \
    rm -rf /var/lib/apt/lists/*

3. 使用–no-install-recommends减少不必要的包

# --no-install-recommends: 不安装"推荐"的包,减少镜像体积
RUN apt-get update && \
    apt-get install -y --no-install-recommends python3 && \
    rm -rf /var/lib/apt/lists/*

3.5 COPY指令(复制文件)

COPY指令用于将本地文件或目录复制到镜像中。它是Dockerfile中最常用的文件操作指令。

语法
COPY [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
COPY [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
基本用法
# 复制单个文件
COPY package.json /app/

# 复制多个文件
COPY package.json package-lock.json /app/

# 复制整个目录(复制目录内容,不包括目录名本身)
COPY src/ /app/src/

# 使用通配符
COPY *.py /app/
COPY config/*.json /app/config/

# 目标路径不存在时自动创建
COPY app.py /app/scripts/app.py

# 复制并设置文件权限
COPY --chown=appuser:appgroup app.py /app/
COPY --chmod=755 start.sh /app/

# 使用数字UID/GID
COPY --chown=1000:1000 app.py /app/

# 路径包含空格时使用JSON数组形式
COPY ["my file.txt", "/app/"]
COPY的注意事项
  1. src路径相对于构建上下文(Build Context),不能是构建上下文之外的路径:
# 正确: 文件在构建上下文内
COPY ./app.py /app/

# 错误: 文件在构建上下文之外
COPY ../app.py /app/
# 报错: Forbidden path outside the build context
  1. 如果src是目录,复制的是目录内容而非目录本身:
# 假设本地有目录结构: src/main.py, src/utils.py
COPY src/ /app/
# 镜像中的结果是: /app/main.py, /app/utils.py
# 而不是: /app/src/main.py, /app/src/utils.py
  1. 如果dest以/结尾,视为目录; 否则视为文件:
# dest以/结尾,视为目录,文件复制到该目录下
COPY app.py /app/
# 结果: /app/app.py

# dest不以/结尾,视为文件名
COPY app.py /app/main.py
# 结果: /app/main.py(重命名了)
  1. COPY保留文件的元数据(修改时间、访问时间等),但UID/GID默认设置为0(root)。使用--chown可以修改。

3.6 ADD指令(复制与自动解压)

ADD指令与COPY类似,但多了一些额外功能。它支持自动解压tar归档文件和支持URL作为源路径。

语法
ADD [--chown=<user>:<group>] [--chmod=<perms>] <src>... <dest>
ADD [--chown=<user>:<group>] [--chmod=<perms>] ["<src>",... "<dest>"]
基本用法
# 1. 基本复制(与COPY相同)
ADD app.py /app/

# 2. 自动解压tar归档文件
ADD app.tar.gz /app/
# 如果app.tar.gz包含main.py和utils.py
# 解压后镜像中: /app/main.py, /app/utils.py

# 3. 从URL下载文件(不推荐,建议使用RUN curl代替)
ADD https://example.com/file.txt /app/

# 4. 从URL下载并解压(注意: URL下载的文件不会自动解压)
# 下面这个不会解压!
ADD https://example.com/app.tar.gz /app/
ADD与COPY的区别
对比项 COPY ADD
本地文件复制 支持 支持
自动解压tar 不支持 支持(本地文件)
URL下载 不支持 支持(不推荐)
推荐程度 推荐(明确、可预测) 仅在需要自动解压时使用
Docker官方建议 优先使用 避免使用(除非需要解压)

最佳实践: Docker官方建议优先使用COPY,因为它更透明、更可预测。只有当你需要自动解压本地tar文件时,才使用ADD。不要使用ADD来下载URL文件,应该使用RUN curl ...RUN wget ...代替,因为这样可以删除下载的归档文件以减少层数。

# 不推荐: 使用ADD下载URL文件(无法删除中间的下载文件)
ADD https://example.com/app.tar.gz /tmp/
RUN tar -xzf /tmp/app.tar.gz -C /app && rm /tmp/app.tar.gz

# 推荐: 使用RUN curl下载和解压(可以合并为一条命令)
RUN curl -fsSL https://example.com/app.tar.gz | tar -xzf - -C /app/

# 推荐使用ADD的场景: 自动解压本地tar文件
ADD app.tar.gz /app/

3.7 WORKDIR指令(工作目录)

WORKDIR指令用于设置工作目录。后续的RUNCMDENTRYPOINTCOPYADD指令都会在这个工作目录下执行。

语法
WORKDIR /path/to/workdir
基本用法
# 设置工作目录
WORKDIR /app

# 后续指令在/app目录下执行
COPY app.py .          # 复制到/app/app.py
RUN pip install -r requirements.txt  # 在/app目录下执行

# WORKDIR可以多次使用,路径可以是相对路径或绝对路径
WORKDIR /app
WORKDIR src
WORKDIR scripts
# 当前工作目录为 /app/src/scripts
RUN pwd  # 输出: /app/src/scripts

# WORKDIR支持环境变量
ENV APP_HOME=/opt/app
WORKDIR $APP_HOME
COPY . .
注意事项
  1. 如果WORKDIR路径不存在,Docker会自动创建:
# /a/b/c目录不存在,Docker会自动创建整个路径
WORKDIR /a/b/c
  1. 不要使用RUN cd代替WORKDIR:
# 不推荐: 使用RUN cd(每条RUN指令都是独立的shell,cd不会影响后续指令)
RUN cd /app
RUN python3 app.py  # 这条命令不在/app目录下执行!

# 推荐: 使用WORKDIR
WORKDIR /app
RUN python3 app.py  # 在/app目录下执行
  1. 建议在Dockerfile早期设置WORKDIR,避免使用/作为工作目录,以防与宿主机路径冲突。

3.8 ENV指令(环境变量)

ENV指令用于设置环境变量。设置的环境变量在后续的构建阶段和容器运行时都可用。

语法
# 第一种形式(推荐): key和value之间用空格分隔
ENV <key> <value>

# 第二种形式: key=value形式,可以设置多个
ENV <key>=<value> ...
基本用法
# 设置单个环境变量
ENV APP_HOME=/app
ENV PYTHONPATH=/app/src

# 一次设置多个环境变量
ENV APP_HOME=/app \
    PYTHONPATH=/app/src \
    PORT=5000 \
    FLASK_ENV=production

# 使用已设置的环境变量
ENV APP_HOME=/app
WORKDIR $APP_HOME
COPY . $APP_HOME/

# 在RUN指令中使用环境变量
ENV NODE_VERSION=20.10.0
RUN echo "Node version: $NODE_VERSION"
ENV变量的作用范围

ENV设置的环境变量在以下场景中都可用:

  1. 构建时: 后续的Dockerfile指令(如RUN、COPY等)中可以使用。
  2. 运行时: 容器启动后,环境变量也存在于容器的环境中。
  3. 继承: 基于该镜像构建的新镜像也继承这些环境变量。
# 运行容器后可以查看环境变量
docker run -it myapp:latest env | grep APP_HOME
# 输出: APP_HOME=/app
ENV vs ARG的区别
对比项 ENV ARG
可用时机 构建时和运行时 仅构建时
可被覆盖 运行时可用-e覆盖 构建时可用--build-arg覆盖
在FROM前使用 不可以 可以
存在于最终镜像 否(除非通过ENV引用)
存储安全性 不安全(可通过inspect查看) 不安全(可通过history查看)
# ARG: 仅构建时可用,不保留在最终镜像中
ARG BUILD_VERSION=1.0
RUN echo "Build version: $BUILD_VERSION"  # 可以使用

# ENV: 构建时和运行时都可用,保留在最终镜像中
ENV APP_VERSION=1.0
RUN echo "App version: $APP_VERSION"  # 可以使用
# 容器运行时也可以使用: docker run -it myapp env | grep APP_VERSION

# 将ARG的值通过ENV保留在最终镜像中
ARG VERSION=1.0
ENV APP_VERSION=$VERSION

3.9 ARG指令(构建参数)

ARG指令用于定义构建时的变量。与ENV不同,ARG变量仅在构建过程中可用,不会保留在最终的镜像中。

语法
ARG <name>[=<default value>]
基本用法
# 定义构建参数(带默认值)
ARG PYTHON_VERSION=3.12-slim
FROM python:${PYTHON_VERSION}

# 定义构建参数(不带默认值,需要通过--build-arg传递)
ARG APP_VERSION
ARG BUILD_DATE

# 使用构建参数
LABEL version=${APP_VERSION}
LABEL build-date=${BUILD_DATE}

# 在RUN指令中使用
ARG NODE_ENV=production
RUN echo "Building for ${NODE_ENV} environment"
通过命令行传递构建参数
# 传递构建参数
docker build --build-arg APP_VERSION=2.0 --build-arg BUILD_DATE=2024-01-01 -t myapp .

# 查看构建参数是否生效
docker inspect myapp --format='{{json .Config.Labels}}' | python -m json.tool
预定义的ARG变量

Docker有一些预定义的ARG变量,无需声明即可在FROM指令中使用:

# 预定义的HTTP代理变量
FROM ubuntu:22.04

# 这些ARG变量是Docker内置的,无需声明
RUN echo "HTTP_PROXY: $HTTP_PROXY"
RUN echo "HTTPS_PROXY: $HTTPS_PROXY"
RUN echo "FTP_PROXY: $FTP_PROXY"
RUN echo "NO_PROXY: $NO_PROXY"

内置的预定义ARG变量包括: HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, FTP_PROXY, ftp_proxy, NO_PROXY, no_proxy, ALL_PROXY, all_proxy

ARG的安全性注意事项

警告: 不要使用ARG传递敏感信息(如密码、密钥)。ARG变量的值可以通过docker history命令查看,存在安全风险。

# 可以通过docker history看到ARG的值!
docker history myapp --no-trunc | grep ARG

对于敏感信息,应该使用BuildKit的--mount=type=secret功能(第七章会详细讲解)。

3.10 EXPOSE指令(声明端口)

EXPOSE指令用于声明容器运行时监听的端口。它只是一个文档性质的声明,并不会真正打开端口。要让外部访问容器端口,需要在docker run时使用-p参数映射端口。

语法
EXPOSE <port> [<port>/<protocol>...]
基本用法
# 声明TCP端口
EXPOSE 80

# 声明UDP端口
EXPOSE 53/udp

# 同时声明TCP和UDP端口
EXPOSE 80/tcp
EXPOSE 53/udp

# 声明多个端口
EXPOSE 80 443 8080
EXPOSE与端口映射的关系
# EXPOSE只是声明,不映射端口,外部无法访问
docker run -d myapp
# 外部无法通过 http://localhost:80 访问

# 使用-p映射端口,外部可以访问
docker run -d -p 8080:80 myapp
# 通过 http://localhost:8080 访问容器80端口

# 使用-P(大写)自动映射所有EXPOSE声明的端口
docker run -d -P myapp
# Docker会自动将EXPOSE声明的端口映射到宿主机随机端口
# 可以通过 docker port <container> 查看映射关系

# 使用--network=host时,EXPOSE声明的端口直接在宿主机上可用
docker run -d --network=host myapp
# 直接通过 http://localhost:80 访问
EXPOSE的最佳实践
# 在EXPOSE后面添加注释说明端口的用途
EXPOSE 80    # HTTP
EXPOSE 443   # HTTPS
EXPOSE 3306  # MySQL

# 或者使用LABEL
LABEL port.80.description="HTTP web server"
LABEL port.443.description="HTTPS web server"
EXPOSE 80 443

3.11 每个指令的详细语法、参数、使用示例与注意事项

本节对第三章涉及的所有指令进行汇总对比,并提供每个指令的详细注意事项。

指令汇总对比表
指令 是否创建新层 执行时机 主要用途
FROM 是(引用基础镜像层) 构建时 指定基础镜像
LABEL 构建时 添加元数据
RUN 构建时 执行命令
COPY 构建时 复制文件到镜像
ADD 构建时 复制文件/解压归档/下载URL
WORKDIR 构建时 设置工作目录
ENV 构建时+运行时 设置环境变量
ARG 构建时 定义构建参数
EXPOSE 构建时(仅声明) 声明端口
FROM指令注意事项
  1. 一个Dockerfile中可以有多个FROM指令(用于多阶段构建)。
  2. FROM指令的tag应该明确指定,避免使用latest
  3. 如果基础镜像在远程仓库中不存在,Docker会自动拉取。
RUN指令注意事项
  1. 每条RUN指令创建一个新层,应尽量合并相关命令。
  2. RUN指令执行完应该清理不必要的文件(如apt缓存)。
  3. 使用&&连接多个命令,确保前一个命令失败时后续命令不执行。
COPY/ADD指令注意事项
  1. 源路径相对于构建上下文,不能引用上下文之外的文件。
  2. 使用.dockerignore排除不需要的文件。
  3. 优先使用COPY,仅在需要自动解压时使用ADD。
WORKDIR指令注意事项
  1. 始终使用WORKDIR而非RUN cd来切换目录。
  2. 建议使用绝对路径。
  3. WORKDIR可以多次设置,支持相对路径(基于前一个WORKDIR)。
ENV指令注意事项
  1. 环境变量在镜像中持久存在,可通过docker inspect查看。
  2. 不要在ENV中存储敏感信息。
  3. 使用ENV可以提高Dockerfile的可维护性(如版本号集中管理)。
# 集中管理版本号,方便升级
ENV PYTHON_VERSION=3.12 \
    NODE_VERSION=20.10 \
    APP_PORT=5000

第四章 Dockerfile运行时指令详解

第三章讲解了构建时执行的指令,本章讲解那些影响容器运行时行为的指令。这些指令包括容器启动命令(CMD/ENTRYPOINT)、用户切换(USER)、健康检查(HEALTHCHECK)、数据卷(VOLUME)等。理解这些指令的执行时机和行为,对于编写可正确运行的容器镜像至关重要。

4.1 CMD指令(容器默认启动命令)

CMD指令用于指定容器启动时的默认命令。如果docker run时没有指定要运行的命令,就会执行CMD中指定的命令。

三种语法形式
# 1. Exec形式(推荐)
CMD ["executable", "param1", "param2"]

# 2. 参数形式(作为ENTRYPOINT的默认参数)
CMD ["param1", "param2"]

# 3. Shell形式
CMD command param1 param2
基本用法
# Exec形式(推荐)
CMD ["python3", "app.py"]
CMD ["nginx", "-g", "daemon off;"]
CMD ["./start.sh"]

# Shell形式(在/bin/sh -c中执行)
CMD python3 app.py
CMD nginx -g "daemon off;"

# 参数形式(配合ENTRYPOINT使用)
ENTRYPOINT ["python3", "app.py"]
CMD ["--help"]  # 作为ENTRYPOINT的默认参数
CMD的覆盖行为

CMD指定的命令可以被docker run时的命令行参数覆盖:

# Dockerfile中有: CMD ["python3", "app.py"]

# 正常启动: 执行CMD中的命令
docker run -it myapp
# 等价于: python3 app.py

# 覆盖CMD: 在docker run后指定命令
docker run -it myapp /bin/bash
# 等价于: /bin/bash (CMD被完全覆盖)

docker run -it myapp python3 --version
# 等价于: python3 --version (CMD被完全覆盖)
CMD的注意事项
  1. 每个Dockerfile中只有最后一条CMD会生效:
# 只有最后一条CMD会执行,前面的会被覆盖
CMD ["echo", "hello"]
CMD ["echo", "world"]
# 容器启动时执行: echo world
  1. 推荐使用Exec形式而非Shell形式:
# 不推荐: Shell形式,命令在/bin/sh -c子进程中执行
# 进程树: sh(PID 1) -> python3(PID 子进程)
# python3收不到SIGTERM信号,优雅关闭可能失败
CMD python3 app.py

# 推荐: Exec形式,python3直接作为PID 1运行
# 进程树: python3(PID 1)
# python3能直接收到SIGTERM信号
CMD ["python3", "app.py"]
  1. Shell形式不会正确传递信号: Shell形式下,命令在/bin/sh -c的子进程中执行,当Docker发送SIGTERM信号时,信号被shell接收但不会转发给子进程,导致容器无法优雅关闭。

4.2 ENTRYPOINT指令(容器入口点)

ENTRYPOINT指令用于配置容器启动时的入口程序。与CMD不同,ENTRYPOINT指定的命令不会被docker run的命令行参数覆盖(除非使用--entrypoint选项),命令行参数会作为ENTRYPOINT的参数追加。

两种语法形式
# 1. Exec形式(推荐)
ENTRYPOINT ["executable", "param1", "param2"]

# 2. Shell形式
ENTRYPOINT command param1 param2
基本用法
# Exec形式(推荐)
ENTRYPOINT ["python3", "app.py"]
ENTRYPOINT ["/app/start.sh"]
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

# Shell形式(不推荐,原因同CMD)
ENTRYPOINT python3 app.py
ENTRYPOINT的追加参数行为
# Dockerfile中有: ENTRYPOINT ["python3", "app.py"]

# 正常启动
docker run -it myapp
# 执行: python3 app.py

# 追加参数: 命令行参数会追加到ENTRYPOINT后面
docker run -it myapp --debug --port=8080
# 执行: python3 app.py --debug --port=8080

# 覆盖ENTRYPOINT: 需要使用--entrypoint选项
docker run -it --entrypoint /bin/bash myapp
# 执行: /bin/bash (ENTRYPOINT被覆盖)
ENTRYPOINT的注意事项
  1. 每个Dockerfile中只有最后一条ENTRYPOINT会生效:
# 只有最后一条ENTRYPOINT会执行
ENTRYPOINT ["echo", "hello"]
ENTRYPOINT ["echo", "world"]
# 容器启动时执行: echo world
  1. 推荐使用Exec形式: 与CMD同理,Shell形式会导致信号传递问题。

  2. ENTRYPOINT脚本模式: 很多官方镜像使用一个entrypoint脚本来做初始化工作:

# entrypoint.sh示例
COPY entrypoint.sh /usr/local/bin/
ENTRYPOINT ["entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]

entrypoint.sh脚本内容:

#!/bin/bash
set -e

# 初始化逻辑
echo "Initializing container..."

# 执行传入的命令(CMD的内容会作为参数传入)
exec "$@"

exec "$@"这行非常关键,它使用exec替换当前进程,确保传入的命令成为PID 1,能正确接收信号。

4.3 CMD与ENTRYPOINT的配合使用

CMD和ENTRYPOINT可以配合使用,这是Dockerfile中一个非常重要且容易混淆的配置。当两者同时存在时,ENTRYPOINT指定要运行的程序,CMD提供默认参数。

配合使用规则
ENTRYPOINT CMD 实际执行
["python3", "app.py"] ["--help"] python3 app.py --help
["python3", "app.py"] 未设置 python3 app.py
["python3", "app.py"] python3 --version (shell形式) python3 --version (CMD覆盖了ENTRYPOINT)
未设置 ["python3", "app.py"] python3 app.py

注意: 当CMD使用Shell形式时,ENTRYPOINT会被忽略。两者配合使用时,都必须使用Exec形式(JSON数组形式)。

实用模式: 可配置的入口点
# Dockerfile
ENTRYPOINT ["python3", "/app/app.py"]
CMD ["--host=0.0.0.0", "--port=5000"]
# 使用默认参数启动
docker run -it myapp
# 执行: python3 /app/app.py --host=0.0.0.0 --port=5000

# 覆盖默认参数(指定不同的端口)
docker run -it myapp --host=0.0.0.0 --port=8080
# 执行: python3 /app/app.py --host=0.0.0.0 --port=8080

# 追加额外参数
docker run -it myapp --host=0.0.0.0 --port=5000 --debug
# 执行: python3 /app/app.py --host=0.0.0.0 --port=5000 --debug
实用模式: 初始化脚本 + 默认命令

这是官方镜像中最常见的模式:

# Dockerfile
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["docker-entrypoint.sh"]
CMD ["postgres"]

docker-entrypoint.sh:

#!/bin/bash
set -e

# 如果第一个参数是postgres(或特定的命令),执行初始化
if [ "$1" = 'postgres' ]; then
    echo "Initializing PostgreSQL..."
    # ... 初始化逻辑 ...
fi

# 执行传入的命令
exec "$@"
# 使用默认命令启动
docker run -d myapp
# 执行: docker-entrypoint.sh postgres
# entrypoint.sh中执行初始化,然后exec postgres

# 覆盖命令启动(如启动bash)
docker run -it myapp bash
# 执行: docker-entrypoint.sh bash
# entrypoint.sh中跳过初始化(因为$1不是postgres),然后exec bash
CMD vs ENTRYPOINT选择指南
场景 推荐使用 原因
简单应用,不需要额外参数 CMD 简单直接
需要初始化脚本 ENTRYPOINT + CMD 脚本初始化 + 可配置命令
需要固定的入口程序 + 可变参数 ENTRYPOINT + CMD 入口固定,参数可覆盖
工具类镜像(如CLI工具) ENTRYPOINT 命令行参数直接传给工具
多用途基础镜像 CMD 用户可以方便地覆盖命令

4.4 USER指令(切换用户)

USER指令用于设置运行容器时的用户(和用户组)。默认情况下,容器以root用户运行,这在安全性上存在风险。使用USER指令可以切换到非root用户运行。

语法
USER <user>[:<group>]
USER <UID>[:<GID>]
基本用法
# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 切换到非root用户
USER appuser

# 后续指令以appuser身份执行
RUN whoami  # 输出: appuser
COPY app.py /app/  # 文件owner为appuser

# 容器启动时也以appuser身份运行
CMD ["python3", "app.py"]
使用数字UID/GID
# 使用数字UID/GID(不依赖用户名解析)
USER 1000:1000
USER指令的影响范围

USER指令影响其后的所有RUNCMDENTRYPOINT指令:

# 以root用户安装软件
RUN apt-get update && apt-get install -y python3

# 创建用户
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser

# 切换到非root用户
USER appuser

# 后续RUN指令以appuser身份执行
RUN mkdir /app/logs  # 如果/app/logs父目录权限不够,会失败!

# 需要在切换用户前创建好目录并设置权限
# RUN mkdir -p /app/logs && chown appuser:appuser /app/logs
注意事项
  1. 确保文件权限正确: 切换USER之前,需要确保后续操作所需的目录和文件已经创建,并且权限正确。
# 正确做法: 先以root创建目录和设置权限,再切换用户
RUN groupadd -r app && useradd -r -g app app && \
    mkdir -p /app /app/logs /app/data && \
    chown -R app:app /app

USER app
COPY --chown=app:app . /app/
  1. 特权端口: 非root用户无法绑定1024以下的端口。如果应用需要使用80或443端口,有两种解决方案:
# 方案1: 使用非特权端口(推荐)
EXPOSE 8080

# 方案2: 使用setcap赋予绑定低端口的能力
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/nginx
USER nginx
EXPOSE 80
  1. 运行时覆盖USER: 可以在docker run时使用--user参数覆盖Dockerfile中的USER设置:
# 覆盖为root用户运行
docker run --user root -it myapp /bin/bash

# 覆盖为指定UID
docker run --user 1000 -it myapp

4.5 HEALTHCHECK指令(健康检查)

HEALTHCHECK指令用于告诉Docker如何检查容器的健康状态。这对于编排系统(如Docker Swarm、Kubernetes)判断容器是否正常运行非常重要。

语法
HEALTHCHECK [OPTIONS] CMD command
HEALTHCHECK NONE  # 禁用健康检查

常用选项:

选项 默认值 说明
--interval= 30s 检查间隔
--timeout= 30s 检查超时时间
--start-period= 0s 容器启动后的初始化宽限期
--retries= 3 连续失败多少次后标记为unhealthy
基本用法
# 基本健康检查: 每隔30秒检查一次
HEALTHCHECK CMD curl -f http://localhost:5000/health || exit 1

# 自定义参数
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

# 禁用健康检查(包括从基础镜像继承的)
HEALTHCHECK NONE
健康检查命令的退出码

健康检查命令的退出码决定了容器的健康状态:

退出码 含义
0 健康(healthy)
1 不健康(unhealthy)
2 保留(不要使用)
健康检查实战示例

Web应用健康检查:

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -fsS http://localhost:8080/health || exit 1

数据库健康检查:

HEALTHCHECK --interval=10s --timeout=5s --retries=5 \
    CMD pg_isready -U postgres || exit 1

使用脚本进行复杂健康检查:

COPY healthcheck.sh /usr/local/bin/
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
    CMD /usr/local/bin/healthcheck.sh

healthcheck.sh:

#!/bin/bash
# 检查多个服务的健康状态
curl -fsS http://localhost:8080/health/api || exit 1
curl -fsS http://localhost:8080/health/db || exit 1
echo "All health checks passed"
exit 0
查看容器健康状态
# 查看容器健康状态
docker ps
# STATUS列显示: Up 5 minutes (healthy)

# 查看健康检查的详细日志
docker inspect --format='{{json .State.Health}}' myapp | python -m json.tool

# 查看最近的健康检查结果
docker inspect --format='{{range .State.Health.Log}}{{.ExitCode}}: {{.Output}}{{end}}' myapp

4.6 VOLUME指令(声明数据卷)

VOLUME指令用于在镜像中声明一个或多个匿名数据卷挂载点。当容器启动时,Docker会自动在这些挂载点创建匿名卷(除非用户指定了绑定挂载)。

语法
# JSON数组形式(推荐)
VOLUME ["/data"]
VOLUME ["/data", "/logs"]

# 纯文本形式
VOLUME /data /logs
基本用法
# 声明数据卷
VOLUME ["/var/lib/mysql"]

# 声明多个数据卷
VOLUME ["/data", "/logs", "/config"]
VOLUME的行为
# Dockerfile中有: VOLUME ["/data"]

# 1. 默认行为: Docker创建匿名卷
docker run -d myapp
# Docker会在/data创建一个匿名卷,数据持久化在那里

# 2. 指定命名卷
docker run -d -v mydata:/data myapp
# 将命名卷mydata挂载到/data

# 3. 绑定挂载
docker run -d -v /host/path:/data myapp
# 将宿主机目录/host/path挂载到/data
VOLUME的注意事项
  1. VOLUME指令后的RUN指令无法修改卷中的内容:
# 问题: VOLUME之后的RUN无法修改/data中的文件
VOLUME ["/data"]
RUN echo "hello" > /data/test.txt  # 这行可能不生效!
  1. VOLUME应该在Dockerfile的末尾声明,在所有需要修改该目录的RUN指令之后:
# 正确顺序: 先修改,后声明VOLUME
RUN mkdir -p /data && echo "init" > /data/config.txt
VOLUME ["/data"]
  1. VOLUME声明的目录在容器中会自动拥有正确的权限,但如果基础镜像中该目录的权限不正确,需要在VOLUME之前修正:
RUN mkdir -p /data && chown 1000:1000 /data
VOLUME ["/data"]
  1. VOLUME的目的是数据持久化,适合存储数据库数据、日志、上传文件等需要持久化或共享的数据。对于不需要持久化的临时文件,不要使用VOLUME。

4.7 ONBUILD指令(触发器指令)

ONBUILD指令是一个特殊的指令,它不会在当前镜像构建时执行,而是在当前镜像作为其他镜像的基础镜像(FROM)时触发执行。简单来说,ONBUILD指令是给"子镜像"设置的"钩子"。

语法
ONBUILD <INSTRUCTION>
基本用法

创建一个基础镜像(base.Dockerfile):

FROM python:3.12-slim

# 设置工作目录
WORKDIR /app

# ONBUILD指令: 当其他镜像FROM这个镜像时,会自动执行
ONBUILD COPY requirements.txt /app/
ONBUILD RUN pip install --no-cache-dir -r requirements.txt
ONBUILD COPY . /app/

CMD ["python3", "app.py"]
# 构建基础镜像
docker build -t my-python-base -f base.Dockerfile .

创建应用镜像(app.Dockerfile):

# 基于基础镜像构建
FROM my-python-base

# 不需要写COPY和RUN,ONBUILD指令会自动执行:
# - COPY requirements.txt /app/
# - RUN pip install -r requirements.txt
# - COPY . /app/
# 构建应用镜像
docker build -t my-app -f app.Dockerfile .
# 构建过程中会自动执行ONBUILD指令
ONBUILD的注意事项
  1. ONBUILD支持的指令: RUN, COPY, ADD, ENV, EXPOSE, LABEL, USER, WORKDIR, VOLUME, ENTRYPOINT, CMD, STOPSIGNAL, HEALTHCHECK, SHELL
  2. ONBUILD不支持嵌套: ONBUILD ONBUILD是非法的。
  3. ONBUILD不会传递: 如果镜像A有ONBUILD指令,镜像B FROM A会触发ONBUILD,但镜像C FROM B不会再触发A的ONBUILD。
  4. ONBUILD已不推荐使用: Docker官方建议使用多阶段构建替代ONBUILD,因为多阶段构建更灵活、更可读。

4.8 STOPSIGNAL指令

STOPSIGNAL指令设置发送给容器以使其优雅退出的系统调用信号。

语法
STOPSIGNAL <signal>
基本用法
# 使用SIGTERM(默认)停止容器
STOPSIGNAL SIGTERM

# 使用SIGQUIT停止容器(Nginx默认使用)
STOPSIGNAL SIGQUIT

# 使用数字信号
STOPSIGNAL 15  # 等价于SIGTERM
常用信号对比
信号 编号 说明
SIGTERM 15 优雅终止(默认),进程可以捕获并清理
SIGKILL 9 强制终止,不可捕获
SIGQUIT 3 退出并生成core dump
SIGINT 2 中断(等同于Ctrl+C)
SIGHUP 1 挂起,常用于重新加载配置
使用场景
# Nginx使用SIGQUIT进行优雅关闭
STOPSIGNAL SIGQUIT
CMD ["nginx", "-g", "daemon off;"]

# Java应用可能需要更长的关闭时间,但仍使用SIGTERM
STOPSIGNAL SIGTERM

也可以在docker rundocker stop时指定:

# docker stop默认发送SIGTERM,等待10秒后发送SIGKILL
docker stop myapp

# 自定义等待时间(秒)
docker stop -t 30 myapp

# docker run时指定STOPSIGNAL
docker run --stop-signal SIGQUIT myapp

4.9 SHELL指令

SHELL指令用于覆盖Dockerfile中Shell形式指令的默认shell。在Linux上默认为["/bin/sh", "-c"],在Windows上默认为["cmd", "/S", "/C"]

语法
SHELL ["executable", "parameters"]
基本用法
# 切换为bash
SHELL ["/bin/bash", "-c"]

# 后续的shell形式指令使用bash执行
RUN echo $BASH_VERSION
CMD echo "Hello from bash"

# 在Windows上切换为PowerShell
SHELL ["powershell", "-Command"]
RUN Write-Host "Hello from PowerShell"
使用场景
# 场景: 需要使用bash的特定功能(如关联数组)
SHELL ["/bin/bash", "-c"]
RUN declare -A colors && \
    colors[red]="#FF0000" && \
    colors[green]="#00FF00" && \
    echo "Red: ${colors[red]}"

# 场景: 在bash中使用pipefail选项
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update | grep -v "some warning"  # pipefail确保管道中任何命令失败都会返回非0

4.10 各指令的执行时机与生命周期

理解每条Dockerfile指令的执行时机,对于编写正确的Dockerfile至关重要。

构建时指令 vs 运行时指令
执行时机 指令 说明
构建时 FROM, RUN, COPY, ADD docker build时执行,结果固化在镜像层中
构建时(配置) LABEL, ENV, ARG, EXPOSE, WORKDIR, USER, VOLUME, SHELL, STOPSIGNAL, ONBUILD docker build时写入镜像配置,不产生文件层
构建时(运行时配置) CMD, ENTRYPOINT, HEALTHCHECK docker build时写入镜像配置,在容器启动时执行
运行时 CMD, ENTRYPOINT docker run时执行
指令执行顺序图
docker build阶段:
┌─────────────────────────────────────────┐
│  1. FROM: 加载基础镜像层                   │
│  2. RUN/COPY/ADD: 创建新层,执行操作       │
│  3. ENV/LABEL/EXPOSE等: 写入镜像配置JSON   │
│  4. CMD/ENTRYPOINT/HEALTHCHECK: 写入配置   │
│  5. 生成最终镜像                          │
└─────────────────────────────────────────┘

docker run阶段:
┌─────────────────────────────────────────┐
│  1. 创建容器可写层                        │
│  2. 应用镜像配置(ENV, USER, WORKDIR等)    │
│  3. 挂载VOLUME                            │
│  4. 执行ENTRYPOINT + CMD                  │
│  5. 定期执行HEALTHCHECK                   │
│  6. 收到STOPSIGNAL时优雅退出              │
└─────────────────────────────────────────┘
环境变量在指令中的传递

环境变量的传递是理解Dockerfile指令交互的关键。在Dockerfile中,环境变量从声明点开始,在后续的指令中可用:

# ARG在FROM之前使用
ARG PYTHON_VERSION=3.12-slim
FROM python:${PYTHON_VERSION}

# ARG在FROM之后需要重新声明
ARG PYTHON_VERSION
RUN echo "Python: $PYTHON_VERSION"  # 可以使用

# ENV一旦声明,后续所有指令都可使用
ENV APP_DIR=/app
WORKDIR $APP_DIR         # /app
COPY . $APP_DIR/         # 复制到/app/
RUN echo $APP_DIR        # /app

# 在RUN指令中使用ENV(解析发生在构建时)
ENV NAME=World
RUN echo "Hello, $NAME!"  # Hello, World!

# 在CMD和ENTRYPOINT中使用ENV(解析发生在运行时)
# 注意: Exec形式不会在构建时解析变量,而是在运行时由Docker解析
ENV GREETING="Hello"
CMD ["sh", "-c", "echo $GREETING"]  # 正确: sh在运行时解析
# CMD ["echo", "$GREETING"]  # 错误: exec形式不解析变量,会输出字面量$GREETING

重要: 在Exec形式(JSON数组形式)的CMD和ENTRYPOINT中,环境变量不会在构建时被解析。如果需要在运行时使用环境变量,必须通过sh -c来执行,或使用Shell形式。


第五章 Docker Build命令详解

docker build命令是将Dockerfile转换为Docker镜像的核心命令。虽然大部分开发者只会使用最基本的docker build -t myapp .形式,但docker build提供了大量选项来控制构建行为。本章全面讲解docker build命令的所有用法。

5.1 docker build基本用法

基本语法
docker build [OPTIONS] PATH | URL | -
  • PATH: 构建上下文路径(通常是当前目录.)
  • URL: Git仓库URL或tar包URL
  • -: 从标准输入读取Dockerfile
最基本的构建
# 在当前目录查找Dockerfile,使用当前目录作为构建上下文
docker build -t myapp:1.0 .

# 不指定tag时默认为latest
docker build -t myapp .

# 同时打多个标签
docker build -t myapp:1.0 -t myapp:latest .

# 从指定目录构建
docker build -t myapp:1.0 /path/to/build/context
从Git仓库构建
# 从Git仓库构建(Docker会clone仓库并构建)
docker build -t myapp https://github.com/user/repo.git

# 从指定分支构建
docker build -t myapp https://github.com/user/repo.git#develop

# 从指定分支的子目录构建
docker build -t myapp https://github.com/user/repo.git#develop:docker/app

# 从指定tag构建
docker build -t myapp https://github.com/user/repo.git#v1.0.0

# 从指定commit构建
docker build -t myapp https://github.com/user/repo.git#abc123def456
从标准输入构建
# 通过管道传递Dockerfile(无构建上下文)
echo 'FROM busybox
RUN echo "hello"
CMD ["echo", "world"]' | docker build -t simple-app -

# 从文件读取Dockerfile
docker build -t myapp - < Dockerfile

# 使用here文档(在脚本中很有用)
docker build -t myapp - <<EOF
FROM alpine:latest
RUN echo "Built from stdin"
CMD ["echo", "hello"]
EOF

5.2 构建上下文(Build Context)详解

构建上下文(Build Context)是Docker构建镜像时能够访问的文件集合。当你执行docker build -t myapp .时,当前目录.就是构建上下文。Docker会将构建上下文中的所有文件发送给Docker守护进程。

构建上下文的工作原理
1. docker build命令执行
   ↓
2. Docker客户端将构建上下文(PATH指定的目录)打包
   ↓
3. 将打包的上下文发送给Docker守护进程(daemon)
   ↓
4. Docker守护进程接收上下文并缓存
   ↓
5. 开始构建,Dockerfile中的COPY/ADD指令从上下文中读取文件
   ↓
6. 构建完成,清理上下文缓存

关键理解: Dockerfile中的COPYADD指令引用的源文件路径,是相对于构建上下文的,而不是相对于Dockerfile所在目录的。

# 构建上下文是 /home/user/myproject
docker build -t myapp /home/user/myproject

# Dockerfile位于 /home/user/myproject/Dockerfile
# COPY app.py /app/ 会从 /home/user/myproject/app.py 复制
构建上下文大小的影响

构建上下文的大小直接影响构建速度。如果构建上下文包含大量不需要的文件(如.git目录、node_modules__pycache__等),会导致:

  1. 上下文打包和传输变慢
  2. 占用Docker守护进程的内存和磁盘
  3. 构建变慢
# 查看构建上下文的大小
du -sh /path/to/build/context

# 如果上下文很大,考虑使用.dockerignore排除不需要的文件
构建上下文与Dockerfile位置的关系

Dockerfile默认位于构建上下文根目录,但可以不同:

# Dockerfile在上下文根目录(默认)
# 项目结构: /project/Dockerfile, /project/app.py
docker build -t myapp /project

# Dockerfile在子目录中
# 项目结构: /project/docker/Dockerfile, /project/app.py
docker build -t myapp -f docker/Dockerfile /project

# Dockerfile在上下文之外(会报错!)
# docker build -t myapp -f /other/path/Dockerfile /project
# 错误: Dockerfile必须在构建上下文内

5.3 .dockerignore文件配置

.dockerignore文件用于排除构建上下文中不需要的文件,类似于.gitignore。合理配置.dockerignore可以显著减小构建上下文大小,加快构建速度。

基本语法

.dockerignore文件放在构建上下文的根目录中,每行一个匹配模式:

# 注释以#开头

# 排除指定文件
*.md
*.log
*.tmp

# 排除目录
node_modules/
.git/
__pycache__/
*.pyc
.vscode/
.idea/

# 排除但保留特定文件
*.md
!README.md

# 使用通配符
temp-*
debug-*.log

# 排除Dockerfile本身(可选,不影响构建)
Dockerfile
.dockerignore
实用的.dockerignore示例

Node.js项目的.dockerignore:

node_modules
npm-debug.log
.nyc_output
coverage
.git
.gitignore
.env
.env.local
.env.*.local
npm-shrinkwrap.json
Dockerfile
.dockerignore
.vscode
.idea
*.swp
*.swo
dist/
build/

Python项目的.dockerignore:

__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
.git/
.gitignore
.env
venv/
.venv/
env/
ENV/
.tox/
.coverage
htmlcov/
.pytest_cache/
.mypy_cache/
Dockerfile
.dockerignore

Java/Maven项目的.dockerignore:

target/
*.class
*.jar
*.war
*.ear
*.log
.git/
.gitignore
.idea/
*.iml
*.iws
*.ipr
.classpath
.project
.settings/
Dockerfile
.dockerignore
.dockerignore的高级用法
# 排除所有文件,然后只包含需要的(白名单模式)
*
!Dockerfile
!src/
!src/**
!requirements.txt
!app.py
!config/
!config/**

提示: 白名单模式是最安全的做法,只包含构建所需的文件,确保不会意外将敏感文件(如.env、密钥文件)发送到Docker守护进程。

5.4 指定Dockerfile路径(-f)

使用-f选项可以指定Dockerfile的路径,而不限于默认的./Dockerfile

基本用法
# 使用子目录中的Dockerfile
docker build -t myapp -f docker/Dockerfile .

# 使用其他文件名的Dockerfile
docker build -t myapp -f docker/Dockerfile.prod .

# 使用绝对路径(必须在构建上下文内)
docker build -t myapp -f /project/docker/Dockerfile /project

# 从标准输入读取Dockerfile,同时指定上下文
docker build -t myapp -f - . <<'EOF'
FROM alpine:latest
RUN echo "hello"
CMD ["echo", "world"]
EOF
多Dockerfile场景

在实际项目中,经常需要为不同环境使用不同的Dockerfile:

project/
├── docker/
│   ├── Dockerfile.dev      # 开发环境
│   ├── Dockerfile.prod     # 生产环境
│   └── Dockerfile.test     # 测试环境
├── src/
│   └── app.py
└── requirements.txt
# 构建开发镜像
docker build -t myapp:dev -f docker/Dockerfile.dev .

# 构建生产镜像
docker build -t myapp:prod -f docker/Dockerfile.prod .

# 构建测试镜像
docker build -t myapp:test -f docker/Dockerfile.test .

5.5 构建标签(-t)

使用-t选项可以给构建的镜像指定标签(名称和版本)。可以同时指定多个标签。

基本用法
# 指定单个标签
docker build -t myapp:1.0 .

# 指定多个标签
docker build -t myapp:1.0 -t myapp:1.0.0 -t myapp:latest .

# 包含仓库名称的标签
docker build -t registry.example.com:5000/myapp:1.0 .

# 包含Docker Hub用户名的标签
docker build -t myusername/myapp:1.0 .
标签命名规范
# 语义化版本
docker build -t myapp:1.2.3 .

# Git commit hash
docker build -t myapp:abc123 .

# 构建日期+版本
docker build -t myapp:20240101-1.2.3 .

# 环境标识
docker build -t myapp:1.0-prod .
docker build -t myapp:1.0-staging .

5.6 构建参数(–build-arg)

使用--build-arg选项可以传递构建参数给Dockerfile中的ARG指令。

基本用法
# Dockerfile
ARG APP_VERSION=1.0
ARG BUILD_ENV=production
FROM python:3.12-slim
ARG APP_VERSION
ARG BUILD_ENV
LABEL version=${APP_VERSION}
LABEL env=${BUILD_ENV}
# 传递构建参数
docker build \
  --build-arg APP_VERSION=2.0 \
  --build-arg BUILD_ENV=staging \
  -t myapp:2.0 .
构建参数的使用场景
# 使用不同的基础镜像版本
docker build --build-arg PYTHON_VERSION=3.11-slim -t myapp:py311 .

# 使用HTTP代理(预定义ARG,无需在Dockerfile中声明)
docker build --build-arg HTTP_PROXY=http://proxy:8080 -t myapp .

# 传递Git信息
docker build \
  --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  -t myapp:latest .

安全警告: --build-arg传递的值可以通过docker history查看,不要传递密码、密钥等敏感信息。

5.7 不使用缓存(–no-cache)

默认情况下,Docker构建会使用缓存。如果之前的构建中某一层已经存在且指令没有变化,Docker会直接复用该层,跳过实际执行。使用--no-cache可以禁用缓存。

基本用法
# 完全禁用缓存,从头构建
docker build --no-cache -t myapp .

# 禁用缓存并传递构建参数
docker build --no-cache --build-arg APP_VERSION=2.0 -t myapp:2.0 .
缓存的工作原理

Docker的构建缓存基于指令的缓存键(cache key)。对于每条指令,Docker计算一个缓存键:

  1. 对于FROM指令: 缓存键是基础镜像的ID。
  2. 对于COPY/ADD指令: 缓存键是基于源文件内容的校验和。
  3. 对于RUN指令: 缓存键是指令字符串本身。
  4. 对于其他指令(ENV、LABEL等): 缓存键是指令字符串本身。

如果某一层的缓存键与之前构建相同,Docker就使用缓存,跳过该层及其后续所有层的执行。

缓存失效的常见原因
# 1. RUN指令中有动态内容(如时间、随机值)
RUN echo "Built at $(date)" > /app/build-info.txt
# 每次构建结果不同,导致缓存失效

# 2. COPY的源文件内容变化
COPY . /app/
# 任何文件变化都会使缓存失效

# 3. 指令顺序变化
# 交换两条指令的位置会导致缓存失效
部分禁用缓存

有时只需要让某个特定位置之后的指令不使用缓存,可以使用--build-arg技巧:

# 使用一个变化的ARG来打破缓存
ARG CACHE_BUST=1
RUN apt-get update && apt-get install -y some-package
# 每次构建时改变CACHE_BUST的值,使后续指令不使用缓存
docker build --build-arg CACHE_BUST=$(date +%s) -t myapp .

5.8 多平台构建(–platform)

使用--platform选项可以为不同平台(如amd64、arm64)构建镜像。

基本用法
# 为AMD64平台构建
docker build --platform linux/amd64 -t myapp:amd64 .

# 为ARM64平台构建(如Apple M1/M2、树莓派)
docker build --platform linux/arm64 -t myapp:arm64 .

# 为多个平台同时构建(需要BuildKit和push到registry)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:multi --push .
平台格式

平台格式为os/arch[/variant]:

平台 说明
linux/amd64 x86_64 Linux(最常见)
linux/arm64 ARM 64位(Apple M1/M2, AWS Graviton)
linux/arm/v7 ARM 32位(树莓派3)
linux/arm/v6 ARM 32位(树莓派1, 2)
linux/ppc64le PowerPC 64位小端
linux/s390x IBM Z系列
windows/amd64 Windows x86_64
使用buildx进行多平台构建
# 创建并使用builder
docker buildx create --name mybuilder --use

# 构建多平台镜像并推送到registry
docker buildx build \
  --platform linux/amd64,linux/arm64 \
  -t myusername/myapp:latest \
  --push .

# 构建多平台镜像并加载到本地(仅支持单平台)
docker buildx build \
  --platform linux/amd64 \
  -t myapp:latest \
  --load .

5.9 构建输出(–output, --target)

–output选项

--output(简写-o)选项控制构建结果的输出方式:

# 输出为本地目录(不创建Docker镜像)
docker build -o type=local,dest=./output .

# 输出为tar文件
docker build -o type=tar,dest=myapp.tar .

# 输出到Docker镜像(默认行为)
docker build -o type=docker -t myapp:latest .

# 输出到registry(推送但不保留本地)
docker build -o type=registry -t registry.example.com/myapp:latest .

# 输出为OCI格式
docker build -o type=oci,dest=myapp-oci.tar .

# 输出为Docker格式tar
docker build -o type=docker,dest=myapp-docker.tar .
–target选项

在多阶段构建中,使用--target可以只构建到指定阶段,而不构建后续阶段:

# 多阶段构建Dockerfile
FROM golang:1.21 AS builder
COPY . /src
RUN cd /src && go build -o /app/server

FROM alpine:latest AS runtime
COPY --from=builder /app/server /app/
CMD ["/app/server"]
# 只构建到builder阶段(用于调试)
docker build --target builder -t myapp:builder .

# 构建完整的最终镜像
docker build -t myapp:latest .

--target的实用场景:

  1. 调试: 构建中间阶段镜像,用于调试构建过程。
  2. 测试: 构建包含测试工具的阶段,用于运行测试。
  3. 不同环境: 不同阶段对应不同环境的镜像。
FROM node:20 AS base
WORKDIR /app
COPY package*.json ./
RUN npm install

# 开发阶段: 包含开发工具和测试框架
FROM base AS development
RUN npm install --only=dev
COPY . .
CMD ["npm", "run", "dev"]

# 测试阶段: 运行测试
FROM development AS test
RUN npm test

# 生产阶段: 仅包含生产依赖和构建结果
FROM base AS production
COPY . .
RUN npm run build
CMD ["npm", "start"]
# 开发镜像
docker build --target development -t myapp:dev .

# 测试镜像
docker build --target test -t myapp:test .

# 生产镜像
docker build --target production -t myapp:prod .
# 或直接构建最后阶段
docker build -t myapp:prod .

5.10 BuildKit构建引擎详解

BuildKit是Docker的新一代构建引擎,相比传统的构建引擎,它提供了更快的构建速度、更好的缓存管理和更多高级特性。

启用BuildKit
# 方法1: 通过环境变量启用(单次构建)
DOCKER_BUILDKIT=1 docker build -t myapp .

# 方法2: 在daemon.json中永久启用
# /etc/docker/daemon.json
{
    "features": {
        "buildkit": true
    }
}

# 方法3: 使用docker buildx(buildx默认使用BuildKit)
docker buildx build -t myapp .
BuildKit的优势
特性 传统构建 BuildKit
并行构建 不支持 支持无依赖的指令并行执行
缓存挂载 不支持 支持(–mount=type=cache)
密钥挂载 不支持 支持(–mount=type=secret)
SSH挂载 不支持 支持(–mount=type=ssh)
多平台构建 需要多次构建 一次构建多平台
构建输出 仅Docker镜像 支持多种输出格式
进度显示 逐行输出 支持多种进度显示模式
.dockerignore 全部发送 按需读取(延迟发送)
BuildKit的高级特性

1. 缓存挂载(–mount=type=cache)

缓存挂载可以在构建之间持久化包管理器的缓存,避免重复下载:

# syntax=docker/dockerfile:1.6

FROM python:3.12-slim

# 挂载pip缓存目录
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
# syntax=docker/dockerfile:1.6

FROM node:20-slim

# 挂载npm缓存目录
RUN --mount=type=cache,target=/root/.npm \
    npm install
# syntax=docker/dockerfile:1.6

FROM ubuntu:22.04

# 挂载apt缓存目录
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y curl

2. 密钥挂载(–mount=type=secret)

密钥挂载可以安全地在构建中使用敏感信息(如API密钥、密码),不会留在镜像层中:

# syntax=docker/dockerfile:1.6

FROM python:3.12-slim

# 使用密钥下载私有包
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install
# 构建时传递密钥
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .
# syntax=docker/dockerfile:1.6

# 使用SSH密钥拉取私有Git仓库
FROM golang:1.21
RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git
# 构建时使用SSH密钥
docker build --ssh default=$SSH_AUTH_SOCK -t myapp .

3. 并行构建

BuildKit会自动分析Dockerfile中指令的依赖关系,并行执行无依赖的指令:

# syntax=docker/dockerfile:1.6

FROM ubuntu:22.04

# 这两条RUN指令没有依赖关系,BuildKit会并行执行
RUN apt-get update && apt-get install -y python3
RUN apt-get update && apt-get install -y nodejs

# 这条指令依赖前面的结果,会顺序执行
RUN python3 -m http.server

注意: 并行构建要求使用BuildKit。传统构建引擎会严格按顺序执行所有指令。

4. 进度显示模式

# auto: 自动选择(默认)
docker buildx build --progress=auto -t myapp .

# plain: 纯文本输出(适合CI/CD日志)
docker buildx build --progress=plain -t myapp .

# tty: 终端交互式输出(适合本地开发)
docker buildx build --progress=tty -t myapp .

# rawjson: JSON格式输出(适合程序解析)
docker buildx build --progress=rawjson -t myapp .

第六章 多阶段构建(Multi-Stage Build)

多阶段构建(Multi-Stage Build)是Docker 17.05引入的重要特性,它允许在一个Dockerfile中使用多个FROM指令,每个FROM开始一个新的构建阶段。多阶段构建是减小镜像体积、提高安全性的最有效方法之一。

6.1 为什么需要多阶段构建

在多阶段构建出现之前,减小镜像体积是一个非常困难的问题。来看一个典型的痛点场景:

传统方式构建Go应用:

# 传统单阶段Dockerfile
FROM golang:1.21

WORKDIR /app
COPY . .
RUN go mod download
RUN CGO_ENABLED=0 go build -o server main.go

CMD ["./server"]

构建后的镜像大小:

docker images myapp:traditional
# REPOSITORY   TAG          SIZE
# myapp        traditional  850MB  ← 太大了!

为什么这么大?因为golang:1.21镜像包含了完整的Go工具链(编译器、标准库、文档等),但运行编译后的Go二进制文件根本不需要这些。我们只需要编译后的二进制文件本身。

多阶段构建方式:

# 多阶段构建Dockerfile
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o server main.go

FROM alpine:latest
COPY --from=builder /app/server /app/server
CMD ["/app/server"]
docker images myapp:multistage
# REPOSITORY   TAG          SIZE
# myapp        multistage   15MB  ← 缩小了56倍!

多阶段构建的核心思想是: 在构建阶段使用完整的工具链编译代码,在运行阶段只复制编译产物到精简的运行时镜像中

多阶段构建解决的问题
问题 传统单阶段 多阶段构建
镜像体积 包含构建工具和依赖,体积大 仅包含运行时必需文件,体积小
安全性 构建工具可能带来安全风险 运行时镜像不含构建工具,攻击面小
构建工具残留 git、编译器等留在镜像中 构建工具仅在构建阶段使用
依赖管理 需要额外清理步骤 天然隔离,无需手动清理
Dockerfile复杂度 需要多个Dockerfile或脚本 单个Dockerfile搞定一切

6.2 多阶段构建语法详解

基本语法
# 第一阶段: 构建阶段
FROM <image>[:<tag>] [AS <name>]
# ... 构建指令 ...

# 第二阶段: 运行阶段
FROM <image>[:<tag>]
# ... 运行时指令 ...
COPY --from=<previous_stage> <src> <dest>
完整示例
# ========== 第一阶段: 构建阶段 ==========
FROM node:20 AS builder

# 设置工作目录
WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装依赖(包括devDependencies)
RUN npm ci

# 复制源代码
COPY . .

# 构建应用
RUN npm run build

# ========== 第二阶段: 运行阶段 ==========
FROM nginx:alpine

# 从构建阶段复制构建结果到Nginx
COPY --from=builder /app/dist /usr/share/nginx/html

# 暴露端口
EXPOSE 80

# 启动Nginx
CMD ["nginx", "-g", "daemon off;"]
多阶段构建的规则
  1. 每个FROM指令开始一个新阶段,之前的阶段不会影响后续阶段(除了通过COPY --from引用)。
  2. 只有最后一个FROM阶段的镜像才会成为最终镜像。
  3. COPY --from可以引用任何之前的阶段,也可以引用外部镜像。
  4. 未被引用的中间阶段会被自动丢弃,不会出现在最终镜像中。

6.3 命名构建阶段(AS关键字)

使用AS关键字可以给构建阶段命名,使Dockerfile更可读,也方便在COPY --from中引用。

基本用法
# 命名构建阶段
FROM golang:1.21 AS builder
# ... 构建指令 ...

# 使用名称引用
FROM alpine:latest
COPY --from=builder /app/server /app/server
不使用名称(按序号引用)
# 第一个阶段(索引0)
FROM golang:1.21 AS builder
# ...

# 第二个阶段(索引1)
FROM alpine:latest
# 使用数字索引引用第一个阶段
COPY --from=0 /app/server /app/server

推荐: 始终使用AS命名阶段,而不是用数字索引。名称更可读,也不会因阶段顺序变化而出错。

多个命名阶段
# 阶段1: 编译Go后端
FROM golang:1.21 AS backend-builder
WORKDIR /app
COPY backend/ .
RUN go build -o /app/api-server

# 阶段2: 构建前端
FROM node:20 AS frontend-builder
WORKDIR /app
COPY frontend/ .
RUN npm ci && npm run build

# 阶段3: 最终运行镜像
FROM alpine:latest
# 从backend-builder阶段复制后端
COPY --from=backend-builder /app/api-server /app/api-server
# 从frontend-builder阶段复制前端
COPY --from=frontend-builder /app/dist /app/static
# 从外部镜像复制文件(不使用构建阶段)
COPY --from=nginx:alpine /etc/nginx/nginx.conf /etc/nginx/nginx.conf

CMD ["/app/api-server"]

6.4 从指定阶段构建(–target)

使用--target选项可以只构建到指定的阶段,而不构建后续阶段。这在开发和调试时非常有用。

基本用法
# 开发阶段: 包含热重载和调试工具
FROM node:20 AS development
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "run", "dev"]

# 构建阶段: 编译生产版本
FROM development AS builder
RUN npm run build

# 生产阶段: 最小化运行镜像
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
# 只构建开发阶段(用于本地开发)
docker build --target development -t myapp:dev .

# 构建到构建阶段(用于检查构建是否成功)
docker build --target builder -t myapp:build .

# 构建完整的生产镜像
docker build --target production -t myapp:prod .
# 或直接构建到最后阶段
docker build -t myapp:prod .
–target的应用场景
  1. 调试构建过程: 当构建失败时,可以构建到失败前的阶段,进入容器排查问题。
  2. 不同环境不同镜像: 开发镜像包含调试工具,生产镜像最小化。
  3. CI/CD流水线: 在CI中构建测试阶段运行测试,通过后构建生产阶段。
# CI/CD流水线示例
# 步骤1: 构建并运行测试
docker build --target test -t myapp:test .
docker run --rm myapp:test

# 步骤2: 测试通过后构建生产镜像
docker build --target production -t myapp:prod .
docker push myapp:prod

6.5 多阶段构建实战: Go应用(完整示例)

Go是多阶段构建的最佳应用场景,因为Go编译后的二进制文件是静态链接的,不需要任何运行时依赖。

项目结构
go-app/
├── main.go
├── go.mod
├── go.sum
└── Dockerfile
main.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "os"
)

func main() {
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }
    
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello from Go app!")
    })
    
    http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusOK)
        w.Write([]byte("OK"))
    })
    
    log.Printf("Server starting on port %s", port)
    log.Fatal(http.ListenAndServe(":"+port, nil))
}
Dockerfile
# ========== 构建阶段 ==========
# 使用官方Go镜像作为构建环境
FROM golang:1.21-alpine AS builder

# 设置工作目录
WORKDIR /build

# 复制go.mod和go.sum(利用缓存)
COPY go.mod go.sum ./

# 下载依赖
RUN go mod download

# 复制源代码
COPY . .

# 编译Go应用
# CGO_ENABLED=0: 禁用CGO,生成静态链接的二进制文件
# GOOS=linux: 目标操作系统为Linux
# GOARCH=amd64: 目标架构为amd64
# -ldflags="-s -w": 去除调试信息,减小二进制体积
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w" -o server main.go

# ========== 运行阶段 ==========
# 使用scratch作为基础镜像(完全空白,0字节)
FROM scratch

# 从构建阶段复制编译好的二进制文件
COPY --from=builder /build/server /server

# 从构建阶段复制CA证书(用于HTTPS请求)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# 设置环境变量
ENV PORT=8080

# 暴露端口
EXPOSE 8080

# 启动应用
ENTRYPOINT ["/server"]
构建与运行
# 构建镜像
docker build -t go-app:multistage .

# 查看镜像大小
docker images go-app:multistage
# REPOSITORY   TAG          SIZE
# go-app       multistage   7.2MB  ← 极小!

# 运行容器
docker run -d -p 8080:8080 go-app:multistage

# 测试
curl http://localhost:8080/
# 输出: Hello from Go app!
curl http://localhost:8080/health
# 输出: OK
镜像大小对比
# 传统单阶段构建(使用golang镜像)
docker images go-app:traditional
# SIZE: 850MB

# 多阶段构建(使用scratch)
docker images go-app:multistage
# SIZE: 7.2MB

# 体积缩小: 850MB / 7.2MB ≈ 118倍

6.6 多阶段构建实战: Java应用(完整示例)

Java应用的镜像通常很大,因为需要JVM运行时。多阶段构建可以通过使用JRE(而非JDK)和分层来优化。

项目结构
java-app/
├── src/
│   └── main/
│       └── java/
│           └── com/
│               └── example/
│                   └── Application.java
├── pom.xml
└── Dockerfile
Dockerfile
# ========== 构建阶段 ==========
# 使用Maven镜像进行构建
FROM maven:3.9-eclipse-temurin-17 AS builder

# 设置工作目录
WORKDIR /build

# 先复制pom.xml并下载依赖(利用缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源代码并编译
COPY src ./src

# 打包应用(跳过测试)
RUN mvn package -DskipTests -B

# 解压jar包(用于分层优化)
RUN mkdir -p target/extracted && \
    cd target/extracted && \
    jar -xf ../app.jar

# ========== 运行阶段 ==========
# 使用Eclipse Temurin JRE(比JDK小很多)
FROM eclipse-temurin:17-jre-alpine

# 创建非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# 设置工作目录
WORKDIR /app

# 分层复制: 先复制依赖(Spring Boot专用优化)
# 这样新代码变更不会导致依赖层缓存失效
COPY --from=builder /build/target/extracted/BOOT-INF/lib /app/lib
COPY --from=builder /build/target/extracted/BOOT-INF/classes /app/classes
COPY --from=builder /build/target/extracted/META-INF /app/META-INF

# 切换为非root用户
USER appuser

# 暴露端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/actuator/health || exit 1

# 使用分层类路径启动(优化启动速度)
ENTRYPOINT ["java", "-cp", "/app/classes:/app/lib/*:/app/META-INF", "com.example.Application"]
构建与运行
# 构建镜像
docker build -t java-app:multistage .

# 查看镜像大小
docker images java-app:multistage
# REPOSITORY   TAG          SIZE
# java-app     multistage   190MB  ← 使用JRE-alpine

# 运行容器
docker run -d -p 8080:8080 java-app:multistage
镜像大小对比
构建方式 基础镜像 大小
单阶段(JDK) maven:3.9-eclipse-temurin-17 850MB
多阶段(JRE-alpine) eclipse-temurin:17-jre-alpine 190MB
多阶段(JRE-slim) eclipse-temurin:17-jre-jammy 260MB

6.7 多阶段构建实战: Node.js应用(完整示例)

Node.js应用的多阶段构建主要用于分离开发依赖和生产依赖,以及构建前端资源。

项目结构
node-app/
├── src/
│   └── index.js
├── package.json
├── package-lock.json
└── Dockerfile
Dockerfile
# ========== 构建阶段 ==========
FROM node:20-alpine AS builder

WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装所有依赖(包括devDependencies)
RUN npm ci

# 复制源代码
COPY . .

# 如果有构建步骤(如TypeScript编译)
RUN npm run build

# ========== 生产依赖阶段 ==========
# 安装仅生产环境依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# ========== 运行阶段 ==========
FROM node:20-alpine AS runtime

# 安装dumb-init(用于正确的信号处理)
RUN apk add --no-cache dumb-init

# 创建非root用户
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs

WORKDIR /app

# 从deps阶段复制生产依赖
COPY --from=deps /app/node_modules ./node_modules

# 从builder阶段复制构建结果和源代码
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# 切换为非root用户
USER nodejs

# 暴露端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# 使用dumb-init作为PID 1,正确处理信号
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]
构建与运行
# 构建镜像
docker build -t node-app:multistage .

# 查看镜像大小
docker images node-app:multistage
# REPOSITORY   TAG          SIZE
# node-app     multistage   120MB

# 运行容器
docker run -d -p 3000:3000 node-app:multistage
镜像大小对比
构建方式 包含内容 大小
单阶段(全依赖) 全部依赖+源代码+构建工具 350MB
多阶段(生产依赖) 仅生产依赖+构建结果 120MB

6.8 多阶段构建实战: Python应用(完整示例)

Python应用的多阶段构建主要用于分离构建依赖(如C编译器)和运行时依赖。

项目结构
python-app/
├── app/
│   ├── __init__.py
│   └── main.py
├── requirements.txt
├── requirements-dev.txt
└── Dockerfile
Dockerfile
# ========== 构建阶段: 安装依赖 ==========
FROM python:3.12-slim AS builder

# 安装构建C扩展所需的工具
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        gcc \
        g++ \
        libffi-dev \
        libssl-dev && \
    rm -rf /var/lib/apt/lists/*

# 创建虚拟环境
RUN python -m venv /opt/venv
# 激活虚拟环境
ENV PATH="/opt/venv/bin:$PATH"

WORKDIR /app

# 复制依赖文件并安装
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# ========== 运行阶段 ==========
FROM python:3.12-slim AS runtime

# 安装运行时必要的系统库(不安装gcc等编译工具)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        libssl3 \
        curl && \
    rm -rf /var/lib/apt/lists/*

# 从构建阶段复制虚拟环境(包含已编译的Python包)
COPY --from=builder /opt/venv /opt/venv

# 设置环境变量,激活虚拟环境
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser -m -d /home/appuser appuser

WORKDIR /app

# 复制应用代码
COPY --chown=appuser:appuser ./app ./app

# 切换为非root用户
USER appuser

# 暴露端口
EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

# 启动应用
CMD ["python", "-m", "app.main"]
构建与运行
# 构建镜像
docker build -t python-app:multistage .

# 查看镜像大小
docker images python-app:multistage
# REPOSITORY    TAG          SIZE
# python-app    multistage   145MB

# 运行容器
docker run -d -p 5000:5000 python-app:multistage
镜像大小对比
构建方式 包含内容 大小
单阶段(完整Python) Python+gcc+全部包 420MB
多阶段(slim+venv) Python slim+虚拟环境 145MB

6.9 镜像体积对比分析

对以上四种语言的多阶段构建进行综合对比:

语言 单阶段大小 多阶段大小 缩小倍数 运行时基础镜像
Go 850MB 7.2MB 118x scratch
Java 850MB 190MB 4.5x eclipse-temurin:17-jre-alpine
Node.js 350MB 120MB 2.9x node:20-alpine
Python 420MB 145MB 2.9x python:3.12-slim

分析:

  1. Go应用的缩小效果最显著(118倍),因为Go编译后的二进制文件是静态链接的,可以使用scratch作为基础镜像。
  2. Java应用受限于JVM运行时,但通过使用JRE-alpine替代JDK仍然能缩小4.5倍。
  3. Node.js和Python应用受限于运行时环境,但通过分离devDependencies和编译工具仍然能显著缩小。
  4. 多阶段构建对编译型语言(Go、Java)的效果更好,因为可以完全去掉编译器。解释型语言(Node.js、Python)仍需保留运行时。
镜像大小分析工具
# 使用dive工具分析镜像每一层的内容
# 安装dive
# https://github.com/wagoodman/dive

# 分析镜像
dive myapp:latest

# dive会显示:
# - 每一层的详细信息
# - 每一层新增/删除的文件
# - 镜像效率评分(有多少浪费的空间)
# 使用docker history分析镜像层
docker history myapp:latest --no-trunc

# 使用docker save + tar分析镜像内容
docker save myapp:latest -o /tmp/myapp.tar
tar -tf /tmp/myapp.tar | head -20

第七章 镜像构建优化策略

构建高效、小巧、安全的Docker镜像是每个开发者和运维人员的追求。本章系统讲解镜像构建的各种优化策略,从基础镜像选择到高级的BuildKit特性,帮助你在实际项目中构建出最优的镜像。

7.1 合理选择基础镜像(alpine/slim/distroless对比)

基础镜像的选择是影响镜像大小和安全性的最重要因素。本节详细对比四种主流的基础镜像方案。

四种基础镜像对比
特性 完整版(full) slim版 alpine版 distroless
大小 100-300MB 50-100MB 5-50MB 2-30MB
操作系统 Debian/Ubuntu Debian(精简) Alpine Linux 无(仅运行时)
包管理器 apt-get apt-get apk
Shell 有(/bin/bash) 有(/bin/sh) 有(/bin/sh)
兼容性 最好 中等(musl libc) 差(需精确匹配)
安全性 低(包多) 中高 高(最小攻击面)
调试能力 弱(无法exec进入)
完整版(full)基础镜像
# 完整版: 包含完整的操作系统和开发工具
FROM python:3.12
# 大小: 约350MB
# 适合: 开发环境、需要完整工具链的场景

完整版镜像基于Debian,包含完整的操作系统、编译器、调试工具等。优点是兼容性最好,任何包都能安装;缺点是体积大,安全攻击面大。

slim版基础镜像
# slim版: 精简的Debian,去掉不必要的文件
FROM python:3.12-slim
# 大小: 约125MB
# 适合: 生产环境(推荐),平衡了大小和兼容性

slim版本基于Debian,但移除了手册页、文档、开发头文件等不必要的内容。保留了apt-get包管理器,可以按需安装缺失的系统依赖。是生产环境的推荐选择。

alpine版基础镜像
# alpine版: 基于Alpine Linux,极小
FROM python:3.12-alpine
# 大小: 约45MB
# 适合: 追求极致小体积,且不需要复杂C扩展的场景

Alpine Linux是一个基于musl libc和BusyBox的轻量级Linux发行版,基础镜像仅5MB左右。但使用musl libc替代glibc可能导致一些问题:

# Alpine的潜在问题: musl libc兼容性
# 某些Python包(如numpy、pandas、psycopg2)需要编译,可能遇到问题
FROM python:3.12-alpine

# 需要安装额外的编译工具
RUN apk add --no-cache \
    gcc \
    musl-dev \
    python3-dev \
    libffi-dev \
    openssl-dev

# 可能需要更长的编译时间
RUN pip install numpy pandas
distroless基础镜像
# distroless: Google提供的无发行版镜像
FROM gcr.io/distroless/python3-debian12
# 大小: 约25MB
# 适合: 追求极致安全的生产环境

distroless镜像不包含任何操作系统发行版的组件,没有shell、没有包管理器、没有任何不必要文件。只有运行应用所需的最小运行时。

# distroless多阶段构建示例
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 使用distroless作为运行时镜像
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
COPY . /app
WORKDIR /app
USER nonroot
CMD ["python", "main.py"]

distroless的注意事项:

  1. 无法使用docker exec进入容器(没有shell),调试困难。
  2. 无法安装额外的包(没有包管理器),所有依赖必须在构建阶段解决。
  3. 调试技巧: 可以使用相同基础镜像的slim版本调试,然后切换到distroless。
# 调试技巧: 使用slim版本调试
docker run -it --entrypoint /bin/sh myapp:debug
# 调试完成后使用distroless版本部署
docker run -d myapp:prod
选择建议
场景 推荐基础镜像
本地开发 完整版(full)
生产环境(通用) slim版
生产环境(极致优化) alpine版或distroless
需要C扩展的Python应用 slim版(避免alpine的musl问题)
Go编译型应用 scratch或alpine
需要高安全性 distroless

7.2 合并RUN指令减少层数

每条RUN指令都会创建一个新的镜像层。过多的层不仅增加镜像大小,还影响构建和拉取速度。合并相关的RUN指令是优化镜像的基本操作。

优化前: 多个RUN指令
# 不推荐: 8个RUN指令 = 8个层
FROM ubuntu:22.04

RUN apt-get update
RUN apt-get install -y python3
RUN apt-get install -y python3-pip
RUN pip3 install flask
RUN pip3 install requests
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*
RUN mkdir -p /app
优化后: 合并RUN指令
# 推荐: 合并为2个RUN指令 = 2个层
FROM ubuntu:22.04

# 合并安装和清理操作
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        python3 \
        python3-pip && \
    pip3 install flask requests && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

RUN mkdir -p /app
合并RUN指令的技巧
# 使用&&连接命令(前一个成功才执行下一个)
RUN command1 && command2 && command3

# 使用;连接命令(无论前一个是否成功都执行下一个)
RUN command1 ; command2 ; command3

# 使用||处理失败(前一个失败才执行下一个)
RUN command1 || command2

# 复杂场景: 使用set -e确保任何命令失败都终止
RUN set -e && \
    apt-get update && \
    apt-get install -y curl && \
    curl -fsSL https://example.com/install.sh | sh && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*
不宜过度合并的情况

虽然合并RUN指令可以减少层数,但也不宜过度合并。以下情况建议分开:

# 不经常变化的操作放前面(利用缓存)
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 经常变化的操作放后面(避免缓存失效)
COPY . /app
RUN pip install -r /app/requirements.txt

7.3 利用构建缓存(指令顺序优化)

Docker构建缓存是提升构建速度的重要机制。合理安排Dockerfile中的指令顺序,可以最大化利用缓存,避免不必要的重新构建。

缓存优化的核心原则

将不常变化的指令放在前面,将经常变化的指令放在后面。

因为一旦某条指令的缓存失效,其后的所有指令都会重新执行。所以应该把最稳定的指令放在最前面。

优化前: 缓存效率低
# 不推荐: COPY . . 在安装依赖之前
FROM python:3.12-slim

WORKDIR /app

# 每次代码变化都会使这行缓存失效
COPY . .

# 代码变了,这行也要重新执行(即使requirements.txt没变)
RUN pip install -r requirements.txt

CMD ["python", "app.py"]
优化后: 缓存效率高
# 推荐: 先复制依赖文件,再复制代码
FROM python:3.12-slim

WORKDIR /app

# 先复制依赖文件(很少变化,缓存命中率高)
COPY requirements.txt .

# 安装依赖(只要requirements.txt不变,这行就使用缓存)
RUN pip install --no-cache-dir -r requirements.txt

# 最后复制代码(经常变化,但只影响这行及之后)
COPY . .

CMD ["python", "app.py"]
Node.js项目的缓存优化
# 推荐: 分离package.json和源代码
FROM node:20-alpine

WORKDIR /app

# 先复制依赖文件
COPY package.json package-lock.json ./

# 安装依赖(只要package.json不变,这行就使用缓存)
RUN npm ci

# 最后复制源代码
COPY . .

# 构建应用
RUN npm run build

CMD ["node", "dist/index.js"]
Java项目的缓存优化
# Maven项目的缓存优化
FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

# 先复制pom.xml
COPY pom.xml .

# 下载依赖(只要pom.xml不变,这行就使用缓存)
RUN mvn dependency:go-offline -B

# 最后复制源代码
COPY src ./src

# 编译打包
RUN mvn package -DskipTests -B

7.4 清理不必要的文件和缓存

在构建过程中产生的临时文件、缓存、日志等如果不清理,会被打包到镜像层中,增加镜像体积。

包管理器缓存清理
# Debian/Ubuntu (apt-get)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        vim && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# Alpine (apk)
RUN apk add --no-cache curl vim
# --no-cache自动清理apk缓存,不需要额外清理

# CentOS/RHEL (yum)
RUN yum install -y curl vim && \
    yum clean all && \
    rm -rf /var/cache/yum
Python pip缓存清理
# 方式1: 使用--no-cache-dir
RUN pip install --no-cache-dir -r requirements.txt

# 方式2: 手动清理缓存
RUN pip install -r requirements.txt && \
    rm -rf /root/.cache/pip
Node.js npm缓存清理
# 方式1: npm ci自动清理(推荐)
RUN npm ci

# 方式2: 手动清理
RUN npm install && \
    npm cache clean --force
其他清理操作
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl \
        git \
        build-essential && \
    # 执行构建操作
    make install && \
    # 清理构建工具
    apt-get purge -y git build-essential && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

7.5 使用.dockerignore减少上下文

第五章已经详细介绍了.dockerignore的配置,这里强调它在镜像优化中的作用。

# 没有.dockerignore时,以下文件都会被发送到Docker守护进程
# .git/         (可能几百MB)
# node_modules/ (可能几百MB)
# __pycache__/  (缓存文件)
# .env          (敏感信息!)
# *.log         (日志文件)

使用.dockerignore后:

  1. 减小构建上下文: 发送更少的数据,加快构建。
  2. 避免缓存失效: 避免无关文件变化导致COPY指令缓存失效。
  3. 防止敏感信息泄露: 避免将.env、密钥等发送到守护进程。

7.6 使用BuildKit缓存挂载(–mount=type=cache)

BuildKit的缓存挂载功能允许在构建之间持久化缓存目录,避免每次构建都重新下载依赖。

pip缓存挂载
# syntax=docker/dockerfile:1.6

FROM python:3.12-slim

WORKDIR /app

COPY requirements.txt .

# 挂载pip缓存目录,下次构建可以复用下载的包
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

COPY . .
CMD ["python", "app.py"]
npm缓存挂载
# syntax=docker/dockerfile:1.6

FROM node:20-slim

WORKDIR /app

COPY package*.json ./

# 挂载npm缓存目录
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
apt缓存挂载
# syntax=docker/dockerfile:1.6

FROM ubuntu:22.04

# 挂载apt缓存目录(需要两个: cache和lib)
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && \
    apt-get install -y curl vim
Maven/Gradle缓存挂载
# syntax=docker/dockerfile:1.6

FROM maven:3.9-eclipse-temurin-17 AS builder

WORKDIR /build

COPY pom.xml .
# 挂载Maven本地仓库缓存
RUN --mount=type=cache,target=/root/.m2 \
    mvn dependency:go-offline -B

COPY src ./src
RUN --mount=type=cache,target=/root/.m2 \
    mvn package -DskipTests -B
缓存挂载的优势
对比项 传统方式 缓存挂载
缓存位置 在镜像层中(增加体积) 在BuildKit缓存中(不增加体积)
缓存复用 不跨构建复用 跨构建复用
缓存清理 需要手动清理 自动管理
构建速度 每次重新下载 使用缓存,速度大幅提升

7.7 使用BuildKit密钥挂载(–mount=type=secret)

在构建镜像时,有时需要使用敏感信息(如npm私有仓库token、API密钥、SSH密钥等)。传统方式使用ARG或ENV传递,但这些值会留在镜像层中,存在安全风险。BuildKit的密钥挂载解决了这个问题。

问题: 传统方式的安全风险
# 不安全: 密钥留在镜像层中,可通过docker history查看
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > /root/.npmrc
RUN npm install
# .npmrc文件留在镜像层中!
# 可以通过docker history看到NPM_TOKEN的值!
docker history myapp --no-trunc
解决方案: 使用secret挂载
# syntax=docker/dockerfile:1.6

FROM node:20-slim

WORKDIR /app

# 使用secret挂载.npmrc文件(不会留在镜像层中)
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install

COPY . .
RUN npm run build
CMD ["node", "dist/index.js"]
# 构建时传递密钥文件
docker build --secret id=npmrc,src=$HOME/.npmrc -t myapp .
使用环境变量形式的secret
# syntax=docker/dockerfile:1.6

FROM python:3.12-slim

# 将secret挂载为环境变量
RUN --mount=type=secret,id=pypi_token,env=PYPI_TOKEN \
    pip install --index-url https://__token__:${PYPI_TOKEN}@pypi.example.com/simple/ \
    private-package
# 构建时传递token
docker build --secret id=pypi_token,env=PYPI_TOKEN -t myapp .
# 或从文件读取
echo "my_token_here" > /tmp/token
docker build --secret id=pypi_token,src=/tmp/token -t myapp .
使用SSH密钥
# syntax=docker/dockerfile:1.6

FROM golang:1.21

# 使用SSH密钥拉取私有Git仓库
RUN --mount=type=ssh \
    git clone git@github.com:myorg/private-repo.git /app/repo
# 构建时使用SSH agent
docker build --ssh default=$SSH_AUTH_SOCK -t myapp .

7.8 镜像瘦身前后对比实战

本节通过一个完整的实战案例,展示如何逐步优化一个Docker镜像。

初始版本(未优化)
# Dockerfile.v1 - 未优化版本
FROM python:3.12

WORKDIR /app

COPY . .

RUN apt-get update
RUN apt-get install -y gcc
RUN pip install -r requirements.txt
RUN apt-get remove -y gcc

EXPOSE 5000

CMD ["python", "app.py"]
docker build -t myapp:v1 -f Dockerfile.v1 .
docker images myapp:v1
# SIZE: 420MB
优化版本1: 使用slim基础镜像
# Dockerfile.v2 - 使用slim镜像
FROM python:3.12-slim

WORKDIR /app

COPY . .

RUN apt-get update && \
    apt-get install -y gcc && \
    pip install -r requirements.txt && \
    apt-get remove -y gcc && \
    apt-get autoremove -y && \
    rm -rf /var/lib/apt/lists/*

EXPOSE 5000

CMD ["python", "app.py"]
docker build -t myapp:v2 -f Dockerfile.v2 .
docker images myapp:v2
# SIZE: 280MB (减少了140MB)
优化版本2: 利用缓存+清理
# Dockerfile.v3 - 缓存优化+清理
FROM python:3.12-slim

WORKDIR /app

# 先复制依赖文件
COPY requirements.txt .

# 安装依赖(使用--no-cache-dir)
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    pip install --no-cache-dir -r requirements.txt && \
    apt-get purge -y gcc && \
    apt-get autoremove -y && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/*

# 最后复制源代码
COPY . .

EXPOSE 5000

CMD ["python", "app.py"]
docker build -t myapp:v3 -f Dockerfile.v3 .
docker images myapp:v3
# SIZE: 195MB (减少了85MB)
优化版本3: 多阶段构建
# Dockerfile.v4 - 多阶段构建
FROM python:3.12-slim AS builder

WORKDIR /app

COPY requirements.txt .

RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

FROM python:3.12-slim

# 复制虚拟环境
COPY --from=builder /opt/venv /opt/venv

ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

# 创建非root用户
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser

EXPOSE 5000
CMD ["python", "app.py"]
docker build -t myapp:v4 -f Dockerfile.v4 .
docker images myapp:v4
# SIZE: 155MB (减少了40MB)
优化版本4: 使用BuildKit缓存挂载
# Dockerfile.v5 - BuildKit缓存挂载
# syntax=docker/dockerfile:1.6

FROM python:3.12-slim AS builder

WORKDIR /app

COPY requirements.txt .

# 使用缓存挂载
RUN --mount=type=cache,target=/root/.cache/pip \
    python -m venv /opt/venv && \
    /opt/venv/bin/pip install -r requirements.txt

FROM python:3.12-slim

COPY --from=builder /opt/venv /opt/venv

ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app
COPY --chown=appuser:appuser . .
USER appuser

EXPOSE 5000
CMD ["python", "app.py"]
DOCKER_BUILDKIT=1 docker build -t myapp:v5 -f Dockerfile.v5 .
docker images myapp:v5
# SIZE: 150MB (镜像大小没有变化,但构建速度大幅提升)
瘦身效果总结
版本 优化措施 镜像大小 减少量
v1 无优化(完整Python) 420MB -
v2 slim基础镜像 280MB -140MB
v3 缓存优化±-no-cache-dir 195MB -85MB
v4 多阶段构建(虚拟环境) 155MB -40MB
v5 BuildKit缓存挂载 150MB -5MB(但构建更快)

7.9 镜像扫描与安全优化

镜像是安全的一个重要环节。镜像中可能包含已知漏洞的软件包,需要定期扫描和修复。

使用docker scout扫描镜像
# 扫描镜像中的漏洞
docker scout quickview myapp:latest

# 查看详细的漏洞报告
docker scout cves myapp:latest

# 比较两个镜像的漏洞差异
docker scout compare myapp:v1 myapp:v2
使用Trivy扫描镜像
# 安装Trivy
# https://github.com/aquasecurity/trivy

# 扫描镜像漏洞
trivy image myapp:latest

# 只显示高危和严重漏洞
trivy image --severity HIGH,CRITICAL myapp:latest

# 输出JSON格式
trivy image --format json myapp:latest > scan-report.json
安全优化措施

1. 使用非root用户运行

# 创建非root用户
RUN groupadd -r app && useradd -r -g app app

# 设置目录权限
RUN chown -R app:app /app

# 切换用户
USER app

2. 最小化安装的软件包

# 只安装必需的包
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl && \
    rm -rf /var/lib/apt/lists/*

3. 及时更新基础镜像

# 定期重新构建镜像以获取安全更新
docker build --no-cache --pull -t myapp:latest .
# --pull: 强制拉取最新基础镜像

4. 使用固定版本的基础镜像

# 不推荐: latest标签可能指向不同版本
FROM python:latest

# 推荐: 指定具体版本
FROM python:3.12.1-slim

# 更推荐: 使用digest固定
FROM python:3.12.1-slim@sha256:abc123...

5. 删除不必要的文件和工具

# 删除shell和包管理器(仅在distroless或scratch中使用)
# 或者删除不必要的setuid/setgid文件
RUN find / -perm /6000 -type f -exec chmod a-s {} \; || true

第八章 私有镜像仓库

镜像仓库(Registry)是存储和分发Docker镜像的服务。除了使用公共的Docker Hub,在企业环境中通常需要搭建私有镜像仓库。本章讲解从Docker Hub使用到企业级私有仓库搭建的完整知识。

8.1 Docker Hub使用详解(推送、拉取、组织管理)

Docker Hub是Docker官方的公共镜像仓库,也提供私有仓库服务。

注册和登录
# 登录Docker Hub
docker login
# 输入用户名和密码

# 使用Access Token登录(推荐,比密码更安全)
# 在Docker Hub网站: Account Settings -> Security -> New Access Token
docker login -u myusername
# 输入Access Token作为密码

# 退出登录
docker logout
推送镜像到Docker Hub
# 1. 构建镜像时使用Docker Hub用户名作为前缀
docker build -t myusername/myapp:1.0 .

# 2. 或给已有镜像打标签
docker tag myapp:1.0 myusername/myapp:1.0

# 3. 推送镜像
docker push myusername/myapp:1.0

# 4. 推送所有标签
docker push myusername/myapp --all-tags

# 5. 推送后,其他人可以拉取
docker pull myusername/myapp:1.0
Docker Hub的组织管理
# Docker Hub支持组织(Organization)和团队(Team)管理
# 组织名称作为镜像前缀

# 推送到组织仓库
docker tag myapp:1.0 myorg/myapp:1.0
docker push myorg/myapp:1.0

# 组织下的不同仓库
myorg/frontend:1.0
myorg/backend:1.0
myorg/database:1.0
自动构建(Automated Builds)

Docker Hub支持连接GitHub/GitLab仓库,实现代码推送后自动构建镜像:

  1. 在Docker Hub创建仓库时,连接GitHub仓库。
  2. 设置Dockerfile位置和构建规则。
  3. 每次push代码或创建tag,Docker Hub自动构建镜像。

注意: Docker Hub的免费版自动构建功能有限制(构建频率、并行数等)。企业版提供更多配额。

8.2 使用registry官方镜像搭建私有仓库

Docker提供了官方的registry镜像,可以快速搭建一个私有镜像仓库。

快速启动
# 启动registry容器
docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v registry-data:/var/lib/registry \
  registry:2

# 验证registry是否运行
curl http://localhost:5000/v2/
# 输出: {} (空JSON,表示服务正常)
推送镜像到私有仓库
# 1. 给镜像打标签(加上仓库地址前缀)
docker tag myapp:1.0 localhost:5000/myapp:1.0

# 2. 推送到私有仓库
docker push localhost:5000/myapp:1.0

# 3. 从私有仓库拉取
docker pull localhost:5000/myapp:1.0

# 4. 查看仓库中的镜像列表
curl http://localhost:5000/v2/_catalog
# 输出: {"repositories":["myapp"]}

# 5. 查看某个镜像的标签列表
curl http://localhost:5000/v2/myapp/tags/list
# 输出: {"name":"myapp","tags":["1.0"]}
配置数据持久化
# 使用命名卷持久化数据
docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v registry-data:/var/lib/registry \
  registry:2

# 或使用绑定挂载
docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v /data/registry:/var/lib/registry \
  registry:2

8.3 配置HTTP访问与HTTPS证书

默认情况下,Docker要求与registry的通信使用HTTPS。如果使用HTTP(非安全连接),需要在Docker客户端配置insecure-registries

配置HTTP访问(不推荐用于生产)
# 编辑Docker守护进程配置
# /etc/docker/daemon.json (Linux)
# 或 Docker Desktop设置 (Windows/Mac)

{
    "insecure-registries": [
        "registry.example.com:5000",
        "192.168.1.100:5000"
    ]
}
# 修改后重启Docker
sudo systemctl restart docker

# 现在可以使用HTTP推送
docker tag myapp:1.0 registry.example.com:5000/myapp:1.0
docker push registry.example.com:5000/myapp:1.0
配置HTTPS证书(推荐)

1. 生成自签名证书(测试环境):

# 创建证书存放目录
mkdir -p /data/registry/certs

# 生成自签名证书
openssl req -newkey rsa:4096 -nodes -sha256 \
  -keyout /data/registry/certs/domain.key \
  -x509 -days 365 \
  -out /data/registry/certs/domain.crt \
  -subj "/CN=registry.example.com" \
  -addext "subjectAltName = DNS:registry.example.com,IP:192.168.1.100"

2. 使用证书启动registry:

docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v /data/registry/data:/var/lib/registry \
  -v /data/registry/certs:/certs \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  registry:2

3. 在Docker客户端配置信任证书:

# 将证书复制到Docker的证书目录
# Linux:
sudo mkdir -p /etc/docker/certs.d/registry.example.com:5000
sudo cp /data/registry/certs/domain.crt \
  /etc/docker/certs.d/registry.example.com:5000/ca.crt

# 重启Docker
sudo systemctl restart docker

# 现在可以正常使用HTTPS推送
docker push registry.example.com:5000/myapp:1.0

4. 使用Let’s Encrypt证书(生产环境):

# 使用certbot获取Let's Encrypt证书
certbot certonly --standalone -d registry.example.com

# 证书位置:
# /etc/letsencrypt/live/registry.example.com/fullchain.pem
# /etc/letsencrypt/live/registry.example.com/privkey.pem

# 使用证书启动registry
docker run -d \
  --name registry \
  -p 443:443 \
  --restart=always \
  -v /data/registry/data:/var/lib/registry \
  -v /etc/letsencrypt/live/registry.example.com:/certs \
  -e REGISTRY_HTTP_ADDR=0.0.0.0:443 \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/fullchain.pem \
  -e REGISTRY_HTTP_TLS_KEY=/certs/privkey.pem \
  registry:2

8.4 基本认证(用户名密码)配置

默认情况下,私有registry没有认证,任何人都可以推送和拉取镜像。在生产环境中,需要配置认证。

1. 生成认证文件
# 安装htpasswd工具
apt-get install -y apache2-utils

# 创建认证文件和用户
mkdir -p /data/registry/auth
htpasswd -Bbn admin adminpassword > /data/registry/auth/htpasswd

# 添加更多用户
htpasswd -Bbn developer devpassword >> /data/registry/auth/htpasswd
2. 使用认证启动registry
docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v /data/registry/data:/var/lib/registry \
  -v /data/registry/certs:/certs \
  -v /data/registry/auth:/auth \
  -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \
  -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \
  -e REGISTRY_AUTH=htpasswd \
  -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \
  -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
  registry:2
3. 登录认证
# 登录私有仓库
docker login registry.example.com:5000
# 输入用户名和密码

# 登录后可以推送和拉取
docker push registry.example.com:5000/myapp:1.0
docker pull registry.example.com:5000/myapp:1.0

# 退出登录
docker logout registry.example.com:5000
4. 完整的registry配置文件

除了使用环境变量,还可以使用YAML配置文件:

# /data/registry/config.yml
version: 0.1
log:
  level: info
  fields:
    service: registry
storage:
  filesystem:
    rootdirectory: /var/lib/registry
  delete:
    enabled: true
  cache:
    blobdescriptor: inmemory
http:
  addr: :5000
  tls:
    certificate: /certs/domain.crt
    key: /certs/domain.key
auth:
  htpasswd:
    realm: Registry Realm
    path: /auth/htpasswd
docker run -d \
  --name registry \
  -p 5000:5000 \
  --restart=always \
  -v /data/registry/data:/var/lib/registry \
  -v /data/registry/certs:/certs \
  -v /data/registry/auth:/auth \
  -v /data/registry/config.yml:/etc/docker/registry/config.yml \
  registry:2

8.5 Harbor企业级镜像仓库搭建与使用

Harbor是VMware开源的企业级Docker镜像仓库,提供了访问控制、镜像扫描、镜像复制、审计日志等高级功能,适合企业生产环境使用。

Harbor的主要功能
功能 说明
访问控制 基于角色的访问控制(RBAC)
镜像扫描 集成Trivy/Clair进行漏洞扫描
镜像复制 跨registry镜像同步复制
审计日志 记录所有操作日志
垃圾回收 自动清理无用的镜像层
Helm Chart 支持Helm Chart仓库
OIDC 支持OIDC/OAuth认证
高可用 支持高可用部署
Harbor安装
# 1. 下载Harbor安装包
wget https://github.com/goharbor/harbor/releases/download/v2.9.0/harbor-offline-installer-v2.9.0.tgz

# 2. 解压
tar -xzf harbor-offline-installer-v2.9.0.tgz
cd harbor

# 3. 复制配置模板
cp harbor.yml.tmpl harbor.yml

# 4. 编辑配置文件
vi harbor.yml

harbor.yml关键配置:

# Harbor配置文件
hostname: registry.example.com  # 访问域名或IP

http:
  port: 80

https:
  port: 443
  certificate: /data/registry/certs/domain.crt  # HTTPS证书
  private_key: /data/registry/certs/domain.key

harbor_admin_password: Harbor12345  # 管理员密码(请修改)

database:
  password: root123  # 数据库密码
  max_idle_conns: 50
  max_open_conns: 100

data_volume: /data/harbor  # 数据存储目录

# 镜像扫描功能
trivy:
  ignore_unfixed: false
  skip_update: false
  offline_scan: false
  security_check: vuln

# 垃圾回收
garbage_collection:
  enabled: true
  schedule: "0 1 * * *"  # 每天凌晨1点执行
# 5. 运行安装脚本
./install.sh --with-trivy  # 安装并启用Trivy镜像扫描

# 6. 验证安装
docker ps
# 应该看到harbor-core, harbor-db, harbor-registry等容器

# 7. 访问Harbor Web界面
# 浏览器访问: https://registry.example.com
# 默认账号: admin
# 默认密码: Harbor12345
Harbor基本使用
# 1. 登录Harbor
docker login registry.example.com
# 输入用户名和密码

# 2. 在Harbor Web界面创建项目
# 项目名: myproject
# 访问级别: 私有

# 3. 推送镜像到Harbor
docker tag myapp:1.0 registry.example.com/myproject/myapp:1.0
docker push registry.example.com/myproject/myapp:1.0

# 4. 拉取镜像
docker pull registry.example.com/myproject/myapp:1.0

# 5. 在Web界面查看镜像
# 浏览器访问: https://registry.example.com
# 导航到: 项目 -> myproject -> 镜像仓库
Harbor的镜像复制功能
# Harbor支持跨registry的镜像复制
# 在Web界面配置:
# 1. 仓库管理 -> 新建目标 -> 填写远程registry信息
# 2. 复制管理 -> 新建规则 -> 选择源项目和目标registry
# 3. 设置触发模式: 手动/定时/事件驱动

8.6 阿里云容器镜像服务(ACR)使用

阿里云容器镜像服务(ACR, Apsara Container Registry)是阿里云提供的镜像托管服务,在国内有良好的网络访问速度。

ACR的两个版本
版本 说明 适用场景
ACR个人版 免费基础功能 个人开发测试
ACR企业版 高级功能,全球同步 企业生产环境
ACR基本使用
# 1. 在阿里云控制台创建镜像仓库
#    - 创建命名空间: mynamespace
#    - 创建镜像仓库: myapp
#    - 选择仓库类型: 公开/私有

# 2. 登录ACR
docker login --username=你的阿里云账号 registry.cn-hangzhou.aliyuncs.com

# 3. 给镜像打标签
docker tag myapp:1.0 registry.cn-hangzhou.aliyuncs.com/mynamespace/myapp:1.0

# 4. 推送镜像
docker push registry.cn-hangzhou.aliyuncs.com/mynamespace/myapp:1.0

# 5. 拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/mynamespace/myapp:1.0
配置镜像加速器
// /etc/docker/daemon.json
{
    "registry-mirrors": [
        "https://your-namespace.mirror.aliyuncs.com"
    ]
}
# 重启Docker
sudo systemctl restart docker

# 现在拉取Docker Hub镜像时会自动使用阿里云加速器
docker pull nginx:latest

8.7 镜像仓库的备份与恢复

镜像仓库的数据需要定期备份,以防数据丢失。

使用docker save/load备份
# 备份单个镜像
docker save -o backup-myapp.tar registry.example.com:5000/myapp:1.0

# 批量备份所有镜像
docker save -o backup-all.tar $(docker images -q)

# 恢复镜像
docker load -i backup-myapp.tar
使用registry API备份
# 1. 获取仓库中所有镜像列表
curl -s http://registry.example.com:5000/v2/_catalog | python -m json.tool

# 2. 获取每个镜像的标签列表
curl -s http://registry.example.com:5000/v2/myapp/tags/list | python -m json.tool

# 3. 批量拉取所有镜像并保存
#!/bin/bash
REGISTRY="registry.example.com:5000"

# 获取所有仓库
repos=$(curl -s http://$REGISTRY/v2/_catalog | python -c "
import json, sys
data = json.load(sys.stdin)
for repo in data['repositories']:
    print(repo)
")

for repo in $repos; do
    # 获取所有标签
    tags=$(curl -s http://$REGISTRY/v2/$repo/tags/list | python -c "
import json, sys
data = json.load(sys.stdin)
for tag in data.get('tags', []):
    print(tag)
")
    for tag in $tags; do
        echo "Backing up $repo:$tag"
        docker pull $REGISTRY/$repo:$tag
        docker save -o "backup-${repo//\//-}-${tag}.tar" $REGISTRY/$repo:$tag
    done
done
Harbor的备份
# Harbor使用数据库存储元数据,需要备份数据库和存储目录

# 1. 备份Harbor数据库
docker exec harbor-db pg_dump -U postgres registry > harbor-db-backup.sql

# 2. 备份Harbor数据目录
tar -czf harbor-data-backup.tar.gz /data/harbor/

# 3. 恢复
# 恢复数据目录
tar -xzf harbor-data-backup.tar.gz -C /

# 恢复数据库
docker exec -i harbor-db psql -U postgres registry < harbor-db-backup.sql

8.8 镜像生命周期管理(标签策略、清理策略)

随着时间推移,镜像仓库中会积累大量旧版本的镜像,占用大量存储空间。需要制定合理的标签策略和清理策略。

标签策略

1. 语义化版本标签:

# 每次发布打三个标签: 具体版本、次版本、latest
docker tag myapp:1.2.3 registry.example.com/myapp:1.2.3
docker tag myapp:1.2.3 registry.example.com/myapp:1.2
docker tag myapp:1.2.3 registry.example.com/myapp:latest

2. Git信息标签:

# 使用Git commit hash和构建日期
docker tag myapp:latest registry.example.com/myapp:$(git rev-parse --short HEAD)
docker tag myapp:latest registry.example.com/myapp:$(date +%Y%m%d)-$(git rev-parse --short HEAD)

3. 环境标签:

# 不同环境使用不同标签
docker tag myapp:1.0 registry.example.com/myapp:1.0-dev
docker tag myapp:1.0 registry.example.com/myapp:1.0-staging
docker tag myapp:1.0 registry.example.com/myapp:1.0-prod
清理策略

1. 使用registry GC清理:

# registry的垃圾回收
# 1. 停止registry容器
docker stop registry

# 2. 运行GC(标记-清除算法)
docker run -it --rm \
  -v /data/registry/data:/var/lib/registry \
  registry:2 garbage-collect \
  /etc/docker/registry/config.yml

# 3. 重新启动registry
docker start registry

2. Harbor的清理策略:

# 在Harbor Web界面配置:
# 1. 系统设置 -> 垃圾回收 -> 设置定时清理
# 2. 项目 -> 策略 -> 设置标签保留规则
#    例如: 保留最近10个标签,删除更早的
# 3. 手动触发垃圾回收

3. 自动清理脚本:

#!/bin/bash
# 清理超过30天的旧镜像标签
REGISTRY="registry.example.com:5000"
DAYS=30

repos=$(curl -s http://$REGISTRY/v2/_catalog | python -c "
import json, sys
data = json.load(sys.stdin)
for repo in data['repositories']:
    print(repo)
")

for repo in $repos; do
    tags=$(curl -s http://$REGISTRY/v2/$repo/tags/list | python -c "
import json, sys, datetime
data = json.load(sys.stdin)
cutoff = datetime.datetime.now() - datetime.timedelta(days=$DAYS)
for tag in data.get('tags', []):
    # 这里简化处理,实际需要检查manifest的时间
    print(tag)
")
    # 保留latest和最近N个标签,删除其他
    echo "Processing $repo..."
    # ... 删除逻辑 ...
done
镜像生命周期管理最佳实践
镜像生命周期:
开发 → 测试 → 预发布 → 生产 → 归档 → 删除

标签管理:
- 开发: dev-{commit-hash}
- 测试: test-{date}
- 预发布: rc-{version}
- 生产: v{version}, latest
- 归档: archive-{version}

保留策略:
- 开发镜像: 保留7天
- 测试镜像: 保留14天
- 生产镜像: 保留180天(或法规要求的年限)
- 归档镜像: 永久保留(或移到低成本存储)

第九章 Dockerfile实战案例集

理论学习了这么多,本章通过8个贴近真实开发场景的实战案例,将前面学到的知识融会贯通。每个案例都包含完整的项目结构、Dockerfile(逐行注释)、构建命令和运行验证。

9.1 构建Nginx自定义镜像(完整流程)

在官方Nginx镜像基础上,添加自定义配置和静态文件,构建一个定制化的Nginx镜像。

项目结构
nginx-custom/
├── Dockerfile
├── nginx.conf
├── default.conf
└── html/
    ├── index.html
    └── 50x.html
nginx.conf(主配置文件)
# nginx.conf - Nginx主配置文件
user  nginx;                    # 运行用户
worker_processes  auto;         # 工作进程数(自动检测CPU核心数)
error_log  /var/log/nginx/error.log notice;  # 错误日志级别
pid        /var/run/nginx.pid;  # PID文件位置

events {
    worker_connections  1024;   # 每个工作进程的最大连接数
}

http {
    include       /etc/nginx/mime.types;   # MIME类型映射
    default_type  application/octet-stream;

    # 日志格式
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;         # 启用sendfile系统调用
    keepalive_timeout  65;      # 保持连接超时时间

    # gzip压缩配置
    gzip  on;
    gzip_types text/plain text/css application/json application/javascript text/xml;

    include /etc/nginx/conf.d/*.conf;  # 包含其他配置文件
}
default.conf(站点配置文件)
# default.conf - 默认站点配置
server {
    listen       80;                    # 监听80端口
    listen  [::]:80;
    server_name  localhost;

    location / {
        root   /usr/share/nginx/html;   # 网站根目录
        index  index.html index.htm;
    }

    # 自定义50x错误页面
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # 健康检查端点
    location /health {
        access_log off;
        return 200 "healthy\n";
    }
}
Dockerfile
# Dockerfile - 自定义Nginx镜像

# 使用官方Nginx Alpine镜像作为基础(体积小)
FROM nginx:1.25.3-alpine

# 设置镜像元数据
LABEL maintainer="admin@example.com" \
      description="Custom Nginx with custom config and static files" \
      version="1.0"

# 复制自定义Nginx配置文件
COPY nginx.conf /etc/nginx/nginx.conf
COPY default.conf /etc/nginx/conf.d/default.conf

# 复制静态网站文件
COPY html/ /usr/share/nginx/html/

# 设置文件权限
RUN chown -R nginx:nginx /usr/share/nginx/html && \
    chmod -R 755 /usr/share/nginx/html

# 声明端口
EXPOSE 80

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget -qO- http://localhost/health || exit 1

# 使用官方Nginx的启动命令
CMD ["nginx", "-g", "daemon off;"]
构建与运行
# 构建镜像
docker build -t my-nginx:1.0 .

# 查看镜像大小
docker images my-nginx:1.0
# REPOSITORY   TAG   SIZE
# my-nginx     1.0   42MB

# 运行容器
docker run -d \
  --name my-nginx \
  -p 8080:80 \
  my-nginx:1.0

# 验证
curl http://localhost:8080/
curl http://localhost:8080/health
# 输出: healthy

# 查看容器健康状态
docker ps
# STATUS列显示: Up (healthy)

9.2 构建Python Flask应用镜像(完整流程)

构建一个Python Flask Web应用的Docker镜像,包含完整的开发和生产配置。

项目结构
flask-app/
├── app/
│   ├── __init__.py
│   └── routes.py
├── requirements.txt
├── config.py
├── run.py
├── Dockerfile
└── .dockerignore
run.py(应用入口)
from app import create_app

app = create_app()

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)
app/__init__.py
from flask import Flask

def create_app():
    app = Flask(__name__)
    
    # 注册路由
    from app.routes import main
    app.register_blueprint(main)
    
    return app
app/routes.py
from flask import Blueprint, jsonify

main = Blueprint('main', __name__)

@main.route('/')
def index():
    return jsonify({
        'status': 'running',
        'message': 'Welcome to Flask Docker App'
    })

@main.route('/health')
def health():
    return jsonify({'status': 'healthy'}), 200
requirements.txt
Flask==3.0.0
gunicorn==21.2.0
.dockerignore
__pycache__/
*.pyc
.env
.git/
.gitignore
Dockerfile
.dockerignore
venv/
.venv/
Dockerfile(逐行解析)
# ========== 构建阶段 ==========
# 使用Python slim镜像作为基础(平衡大小和兼容性)
FROM python:3.12-slim AS builder

# 设置环境变量,防止Python生成.pyc文件和缓冲输出
ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# 安装构建C扩展所需的编译工具
RUN apt-get update && \
    apt-get install -y --no-install-recommends gcc && \
    rm -rf /var/lib/apt/lists/*

# 创建虚拟环境(隔离依赖,避免与系统Python冲突)
RUN python -m venv /opt/venv
# 将虚拟环境的bin目录加入PATH
ENV PATH="/opt/venv/bin:$PATH"

# 设置工作目录
WORKDIR /app

# 先复制依赖文件(利用缓存,代码变化不会重新安装依赖)
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt

# ========== 运行阶段 ==========
# 使用Python slim镜像(不需要gcc等编译工具)
FROM python:3.12-slim AS runtime

# 安装运行时必要的系统包
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 从构建阶段复制虚拟环境(包含已安装的Python包)
COPY --from=builder /opt/venv /opt/venv

# 激活虚拟环境
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

# 创建非root用户(安全最佳实践)
RUN groupadd -r flaskuser && \
    useradd -r -g flaskuser -m -d /home/flaskuser flaskuser

# 设置工作目录
WORKDIR /app

# 复制应用代码(设置正确的owner)
COPY --chown=flaskuser:flaskuser . .

# 切换为非root用户
USER flaskuser

# 声明端口
EXPOSE 5000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:5000/health || exit 1

# 使用gunicorn作为生产级WSGI服务器启动应用
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "run:app"]
构建与运行
# 构建镜像
docker build -t flask-app:1.0 .

# 运行容器
docker run -d \
  --name flask-app \
  -p 5000:5000 \
  -e FLASK_ENV=production \
  flask-app:1.0

# 验证
curl http://localhost:5000/
# {"message":"Welcome to Flask Docker App","status":"running"}

curl http://localhost:5000/health
# {"status":"healthy"}

9.3 构建Node.js Express应用镜像(完整流程)

构建一个Node.js Express应用镜像,使用多阶段构建优化镜像大小。

项目结构
express-app/
├── src/
│   └── index.js
├── package.json
├── package-lock.json
├── Dockerfile
└── .dockerignore
package.json
{
  "name": "express-app",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  },
  "dependencies": {
    "express": "^4.18.2"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}
src/index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

app.get('/', (req, res) => {
    res.json({ status: 'running', message: 'Welcome to Express Docker App' });
});

app.get('/health', (req, res) => {
    res.json({ status: 'healthy' });
});

app.listen(PORT, () => {
    console.log(`Server running on port ${PORT}`);
});
Dockerfile(逐行解析)
# ========== 构建阶段 ==========
# 使用Node.js Alpine镜像(体积小)
FROM node:20-alpine AS builder

# 设置工作目录
WORKDIR /app

# 先复制依赖文件(利用缓存)
COPY package.json package-lock.json ./

# 安装所有依赖(包括devDependencies,构建需要)
RUN npm ci

# 复制源代码
COPY . .

# ========== 生产依赖阶段 ==========
# 单独安装生产环境依赖
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# ========== 运行阶段 ==========
FROM node:20-alpine AS runtime

# 安装dumb-init(正确的PID 1信号处理)
RUN apk add --no-cache dumb-init

# 创建非root用户
RUN addgroup -S nodejs && adduser -S nodejs -G nodejs

# 设置工作目录
WORKDIR /app

# 从deps阶段复制生产环境依赖
COPY --from=deps /app/node_modules ./node_modules

# 复制源代码和package.json
COPY --chown=nodejs:nodejs . .

# 切换为非root用户
USER nodejs

# 设置环境变量
ENV NODE_ENV=production \
    PORT=3000

# 声明端口
EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:3000/health || exit 1

# 使用dumb-init作为PID 1
ENTRYPOINT ["dumb-init", "--"]

# 启动应用
CMD ["node", "src/index.js"]
构建与运行
# 构建镜像
docker build -t express-app:1.0 .

# 运行容器
docker run -d \
  --name express-app \
  -p 3000:3000 \
  express-app:1.0

# 验证
curl http://localhost:3000/
curl http://localhost:3000/health

9.4 构建Java Spring Boot应用镜像(完整流程)

构建一个Java Spring Boot应用的Docker镜像,使用分层技术优化构建缓存。

项目结构
springboot-app/
├── pom.xml
├── src/
│   └── main/
│       ├── java/
│       │   └── com/example/demo/
│       │       ├── DemoApplication.java
│       │       └── controller/
│       │           └── HealthController.java
│       └── resources/
│           └── application.yml
├── Dockerfile
└── .dockerignore
Dockerfile(逐行解析)
# ========== 构建阶段 ==========
# 使用Maven镜像构建项目
FROM maven:3.9-eclipse-temurin-17 AS builder

# 设置工作目录
WORKDIR /build

# 先复制pom.xml并下载依赖(利用缓存)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# 复制源代码
COPY src ./src

# 编译打包(跳过测试)
RUN mvn package -DskipTests -B

# 解压Spring Boot fat jar(用于分层优化)
RUN mkdir -p target/extracted && \
    cd target/extracted && \
    jar -xf ../demo-1.0.0.jar

# ========== 运行阶段 ==========
# 使用JRE Alpine镜像(比JDK小很多)
FROM eclipse-temurin:17-jre-alpine

# 创建非root用户
RUN addgroup -S spring && adduser -S spring -G spring

# 设置工作目录
WORKDIR /app

# 分层复制: 先复制依赖(变化少,缓存命中率高)
COPY --from=builder /build/target/extracted/BOOT-INF/lib /app/lib
COPY --from=builder /build/target/extracted/META-INF /app/META-INF
# 后复制classes(变化频繁)
COPY --from=builder /build/target/extracted/BOOT-INF/classes /app/classes

# 设置目录权限
RUN chown -R spring:spring /app

# 切换为非root用户
USER spring

# JVM参数: 容器感知内存限制
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"

# 声明端口
EXPOSE 8080

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD wget -qO- http://localhost:8080/actuator/health || exit 1

# 使用分层类路径启动(优化启动速度)
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -cp /app/classes:/app/lib/*:/app/META-INF com.example.demo.DemoApplication"]
构建与运行
# 构建镜像
docker build -t springboot-app:1.0 .

# 运行容器(限制内存)
docker run -d \
  --name springboot-app \
  -p 8080:8080 \
  --memory=512m \
  springboot-app:1.0

# 验证
curl http://localhost:8080/actuator/health

9.5 构建MySQL定制镜像(含初始化脚本)

构建一个包含自定义配置和初始化脚本的MySQL镜像。

项目结构
mysql-custom/
├── Dockerfile
├── my.cnf
├── init/
│   ├── 01-create-database.sql
│   ├── 02-create-tables.sql
│   └── 03-seed-data.sql
└── docker-entrypoint-initdb.d/
    └── init-user.sh
my.cnf(MySQL配置文件)
# MySQL自定义配置
[mysqld]
# 字符集设置
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci

# 性能优化
max_connections = 200
innodb_buffer_pool_size = 256M
innodb_log_file_size = 64M

# 日志设置
slow_query_log = 1
slow_query_log_file = /var/lib/mysql/slow.log
long_query_time = 2

# 安全设置
local_infile = 0
init/01-create-database.sql
-- 创建应用数据库
CREATE DATABASE IF NOT EXISTS appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- 创建只读用户
CREATE USER IF NOT EXISTS 'readonly'@'%' IDENTIFIED BY 'readonly_password';
GRANT SELECT ON appdb.* TO 'readonly'@'%';

FLUSH PRIVILEGES;
init/02-create-tables.sql
USE appdb;

-- 用户表
CREATE TABLE IF NOT EXISTS users (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL UNIQUE,
    email VARCHAR(100) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
init/03-seed-data.sql
USE appdb;

-- 插入初始数据
INSERT INTO users (username, email) VALUES
    ('admin', 'admin@example.com'),
    ('user1', 'user1@example.com');
Dockerfile(逐行解析)
# 使用官方MySQL 8.0镜像
FROM mysql:8.0

# 设置MySQL root密码(可以通过环境变量覆盖)
ENV MYSQL_ROOT_PASSWORD=rootpassword \
    MYSQL_DATABASE=appdb \
    MYSQL_USER=appuser \
    MYSQL_PASSWORD=apppassword

# 复制自定义MySQL配置
COPY my.cnf /etc/mysql/conf.d/my.cnf

# 复制初始化SQL脚本
# MySQL官方镜像会自动执行/docker-entrypoint-initdb.d/目录下的脚本
COPY init/ /docker-entrypoint-initdb.d/

# 设置脚本执行权限
RUN chmod 644 /etc/mysql/conf.d/my.cnf && \
    chmod 644 /docker-entrypoint-initdb.d/*.sql

# 声明端口
EXPOSE 3306

# 声明数据卷(数据库数据持久化)
VOLUME ["/var/lib/mysql"]

# 使用官方MySQL的启动命令
CMD ["mysqld"]
构建与运行
# 构建镜像
docker build -t my-mysql:8.0 .

# 运行容器
docker run -d \
  --name my-mysql \
  -p 3306:3306 \
  -e MYSQL_ROOT_PASSWORD=secret123 \
  -v mysql-data:/var/lib/mysql \
  my-mysql:8.0

# 验证数据库初始化
docker exec -it my-mysql mysql -uroot -psecret123 -e "SHOW DATABASES;"
docker exec -it my-mysql mysql -uroot -psecret123 -e "USE appdb; SELECT * FROM users;"

注意: /docker-entrypoint-initdb.d/目录下的SQL脚本和shell脚本只在容器第一次启动(数据目录为空)时执行。如果数据目录已有数据,这些脚本不会被执行。

9.6 构建数据科学环境镜像(Jupyter + Python库)

构建一个包含Jupyter Notebook和常用数据科学Python库的镜像。

项目结构
datascience-env/
├── Dockerfile
├── requirements.txt
└── notebooks/
    └── welcome.ipynb
requirements.txt
# 数据科学核心库
numpy==1.26.3
pandas==2.1.4
scipy==1.11.4
scikit-learn==1.3.2
matplotlib==3.8.2
seaborn==0.13.0

# Jupyter
jupyter==1.0.0
notebook==7.0.6
ipykernel==6.27.1
ipywidgets==8.1.1

# 深度学习
tensorflow==2.15.0
torch==2.1.2

# 数据可视化
plotly==5.18.0
bokeh==3.3.1

# 数据库连接
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
pymongo==4.6.0
Dockerfile(逐行解析)
# 使用Python完整版镜像(需要完整工具链编译科学计算库)
FROM python:3.12

# 安装系统依赖(部分Python库需要)
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        build-essential \
        libpq-dev \
        libffi-dev \
        libssl-dev \
        git \
        vim \
        && \
    rm -rf /var/lib/apt/lists/*

# 设置工作目录
WORKDIR /workspace

# 复制依赖文件
COPY requirements.txt .

# 安装Python依赖
RUN pip install --no-cache-dir -r requirements.txt

# 配置Jupyter Notebook
RUN jupyter notebook --generate-config && \
    # 允许所有IP访问
    echo "c.NotebookApp.ip = '0.0.0.0'" >> ~/.jupyter/jupyter_notebook_config.py && \
    # 禁用token认证(开发环境,生产环境应设置密码)
    echo "c.NotebookApp.token = ''" >> ~/.jupyter/jupyter_notebook_config.py && \
    # 允许root用户运行
    echo "c.NotebookApp.allow_root = True" >> ~/.jupyter/jupyter_notebook_config.py && \
    # 默认工作目录
    echo "c.NotebookApp.notebook_dir = '/workspace'" >> ~/.jupyter/jupyter_notebook_config.py

# 复制notebooks
COPY notebooks/ /workspace/notebooks/

# 声明端口
EXPOSE 8888

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:8888/api || exit 1

# 启动Jupyter Notebook
CMD ["jupyter", "notebook", "--no-browser", "--port=8888", "--allow-root"]
构建与运行
# 构建镜像(较大,需要较长时间)
docker build -t datascience-env:1.0 .

# 运行容器
docker run -d \
  --name jupyter \
  -p 8888:8888 \
  -v $(pwd)/notebooks:/workspace/notebooks \
  -v $(pwd)/data:/workspace/data \
  datascience-env:1.0

# 访问Jupyter Notebook
# 浏览器打开: http://localhost:8888

9.7 构建前端项目镜像(Vue/React + Nginx)

构建一个前端项目镜像,使用多阶段构建: 第一阶段编译前端代码,第二阶段使用Nginx提供静态文件服务。

项目结构(Vue项目示例)
vue-app/
├── src/
│   ├── App.vue
│   ├── main.js
│   └── components/
├── public/
├── package.json
├── vite.config.js
├── nginx.conf
└── Dockerfile
nginx.conf(针对SPA应用优化)
server {
    listen 80;
    server_name localhost;
    root /usr/share/nginx/html;
    index index.html;

    # 开启gzip压缩
    gzip on;
    gzip_types text/css application/javascript application/json image/svg+xml;
    gzip_min_length 1000;

    # 静态资源缓存(带hash的文件可以长期缓存)
    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA路由: 所有路由都指向index.html
    location / {
        try_files $uri $uri/ /index.html;
    }

    # HTML文件不缓存
    location ~* \.html$ {
        add_header Cache-Control "no-cache, no-store, must-revalidate";
    }

    # 健康检查
    location /health {
        access_log off;
        return 200 "healthy\n";
    }
}
Dockerfile(逐行解析)
# ========== 构建阶段: 编译前端代码 ==========
# 使用Node镜像进行构建
FROM node:20-alpine AS builder

# 设置工作目录
WORKDIR /app

# 复制依赖文件(利用缓存)
COPY package.json package-lock.json ./

# 安装依赖
RUN npm ci

# 复制源代码
COPY . .

# 构建生产版本
RUN npm run build

# ========== 运行阶段: Nginx提供静态文件服务 ==========
FROM nginx:1.25-alpine

# 复制自定义Nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf

# 从构建阶段复制编译结果到Nginx静态文件目录
COPY --from=builder /app/dist /usr/share/nginx/html

# 声明端口
EXPOSE 80

# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --retries=3 \
    CMD wget -qO- http://localhost/health || exit 1

# 启动Nginx
CMD ["nginx", "-g", "daemon off;"]
构建与运行
# 构建镜像
docker build -t vue-app:1.0 .

# 查看镜像大小(应该很小,只有Nginx + 静态文件)
docker images vue-app:1.0
# REPOSITORY   TAG   SIZE
# vue-app      1.0   25MB

# 运行容器
docker run -d \
  --name vue-app \
  -p 8080:80 \
  vue-app:1.0

# 访问应用
# 浏览器打开: http://localhost:8080

9.8 每个案例的Dockerfile逐行解析

本节对以上案例中的关键Dockerfile设计决策进行汇总分析。

共同的设计模式

通过分析以上7个实战案例,可以总结出以下共同的设计模式:

1. 多阶段构建

所有生产级案例都使用了多阶段构建,将构建环境与运行时环境分离:

FROM xxx AS builder    # 构建阶段(包含编译工具)
# ...编译代码...
FROM yyy AS runtime    # 运行阶段(仅运行时依赖)
COPY --from=builder ... # 只复制编译产物

2. 非root用户运行

所有生产级案例都创建了非root用户:

RUN addgroup -S app && adduser -S app -G app
USER app

3. 健康检查

所有Web应用都配置了健康检查:

HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
    CMD curl -f http://localhost:PORT/health || exit 1

4. 缓存优化

所有案例都先复制依赖文件,后复制源代码:

COPY package.json package-lock.json ./    # 依赖文件(很少变化)
RUN npm ci                                # 安装依赖(利用缓存)
COPY . .                                  # 源代码(经常变化)

5. 清理不必要的文件

所有案例都在安装后清理缓存:

RUN apt-get update && apt-get install -y xxx && rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
各案例特点对比
案例 基础镜像 多阶段 非root 健康检查 特殊优化
Nginx nginx:alpine 否(Nginx自带) 自定义配置
Flask python:slim 虚拟环境+gunicorn
Express node:alpine dumb-init信号处理
Spring Boot temurin:jre-alpine 分层JAR+JVM容器感知
MySQL mysql:8.0 初始化脚本+数据卷
数据科学 python:full 完整工具链
前端Vue nginx:alpine SPA路由+静态缓存

第十章 Dockerfile最佳实践与规范

本章是全篇的总结与提升。在前九章详细讲解的基础上,本章系统性地整理Dockerfile的最佳实践、规范和常见问题,帮助你在实际项目中写出高质量的Dockerfile。

10.1 Dockerfile编写规范(官方建议)

Docker官方提供了一系列Dockerfile编写建议,以下是核心规范的总结。

1. 指令使用大写
# 推荐: 指令大写,参数小写,便于区分
FROM python:3.12-slim
RUN apt-get update
COPY . /app
CMD ["python", "app.py"]

# 不推荐: 全部小写
from python:3.12-slim
run apt-get update
2. 合理安排指令顺序

将不常变化的指令放前面,常变化的放后面:

# 推荐顺序
FROM python:3.12-slim          # 1. 基础镜像(极少变化)
WORKDIR /app                    # 2. 工作目录(极少变化)
COPY requirements.txt .         # 3. 依赖文件(偶尔变化)
RUN pip install -r requirements.txt  # 4. 安装依赖(依赖3的变化)
COPY . .                        # 5. 源代码(经常变化)
CMD ["python", "app.py"]        # 6. 启动命令(偶尔变化)
3. 使用明确的版本标签
# 推荐: 明确版本
FROM python:3.12.1-slim

# 不推荐: latest标签
FROM python:latest

# 最佳: 使用digest(最精确)
FROM python:3.12.1-slim@sha256:abc123...
4. 合并RUN指令
# 推荐: 合并相关命令
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# 不推荐: 分散的RUN指令
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
5. 使用COPY而非ADD
# 推荐: 使用COPY(透明、可预测)
COPY app.py /app/

# 仅在需要自动解压时使用ADD
ADD app.tar.gz /app/
6. 使用Exec形式
# 推荐: Exec形式(JSON数组)
CMD ["python", "app.py"]
ENTRYPOINT ["python", "app.py"]

# 不推荐: Shell形式(信号传递问题)
CMD python app.py
7. 使用WORKDIR而非RUN cd
# 推荐: 使用WORKDIR
WORKDIR /app
RUN python app.py

# 不推荐: 使用RUN cd
RUN cd /app && python app.py

10.2 镜像版本管理策略

标签命名规范
# 语义化版本(SemVer): MAJOR.MINOR.PATCH
myapp:1.0.0
myapp:1.0
myapp:1
myapp:latest

# Git信息
myapp:abc123d          # Git commit短hash
myapp:20240101-abc123d # 日期+commit

# 环境
myapp:1.0-prod
myapp:1.0-staging
myapp:1.0-dev
版本管理最佳实践
  1. 不要使用latest标签用于生产: latest标签会变化,无法追踪运行的是哪个版本。

  2. 保留版本历史: 每次发布都打一个具体版本的标签,方便回滚。

  3. 使用不可变标签: 一旦推送的标签不要修改(覆盖)。

  4. 记录镜像来源: 使用LABEL记录构建信息:

ARG GIT_COMMIT=unknown
ARG BUILD_DATE=unknown
ARG IMAGE_VERSION=unknown

LABEL org.opencontainers.image.revision="${GIT_COMMIT}" \
      org.opencontainers.image.created="${BUILD_DATE}" \
      org.opencontainers.image.version="${IMAGE_VERSION}"
# 构建时传递信息
docker build \
  --build-arg GIT_COMMIT=$(git rev-parse --short HEAD) \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  --build-arg IMAGE_VERSION=1.0.0 \
  -t myapp:1.0.0 .

10.3 安全最佳实践(非root用户、最小权限)

1. 使用非root用户
# 创建专用用户和组
RUN groupadd -r appuser && useradd -r -g appuser appuser

# 设置目录权限
RUN chown -R appuser:appuser /app

# 切换用户
USER appuser
2. 最小化安装
# 只安装必需的包,使用--no-install-recommends
RUN apt-get update && \
    apt-get install -y --no-install-recommends \
        curl && \
    rm -rf /var/lib/apt/lists/*
3. 不在镜像中存储敏感信息
# 不安全: 密码留在镜像中
ENV DB_PASSWORD=secret123

# 安全: 运行时通过环境变量传入
# docker run -e DB_PASSWORD=secret123 myapp
4. 使用secret挂载传递构建时密钥
# syntax=docker/dockerfile:1.6
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
    npm install
5. 定期扫描漏洞
# 定期扫描镜像漏洞
trivy image myapp:latest
docker scout cves myapp:latest
6. 及时更新基础镜像
# 定期使用--pull拉取最新基础镜像重新构建
docker build --pull --no-cache -t myapp:latest .

10.4 可维护性与可读性

1. 添加注释
# 使用Python 3.12 slim版本(约125MB,适合生产环境)
FROM python:3.12-slim

# 设置工作目录
WORKDIR /app

# 先复制依赖文件,利用构建缓存
# requirements.txt变化频率低,放在前面可以提高缓存命中率
COPY requirements.txt .

# 安装Python依赖
# --no-cache-dir: 不保存pip缓存,减少镜像体积
RUN pip install --no-cache-dir -r requirements.txt

# 复制应用代码(变化频繁,放在最后)
COPY . .

# 声明应用端口
EXPOSE 5000

# 使用gunicorn启动(生产级WSGI服务器)
# 4个worker进程,绑定5000端口
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "app:app"]
2. 使用LABEL记录元数据
LABEL org.opencontainers.image.title="My App" \
      org.opencontainers.image.description="A Flask web application" \
      org.opencontainers.image.authors="dev-team@example.com" \
      org.opencontainers.image.version="1.0.0" \
      org.opencontainers.image.source="https://github.com/myorg/myapp" \
      org.opencontainers.image.licenses="MIT"
3. 使用ARG参数化
# 使用ARG参数化版本号,方便升级
ARG PYTHON_VERSION=3.12-slim
FROM python:${PYTHON_VERSION}

ARG APP_PORT=5000
ENV PORT=${APP_PORT}
EXPOSE ${APP_PORT}

10.5 CI/CD中的镜像构建

在CI/CD流水线中构建Docker镜像,需要考虑构建效率、缓存管理和安全。

GitHub Actions示例
# .github/workflows/docker-build.yml
name: Build and Push Docker Image

on:
  push:
    branches: [main]
    tags: ['v*']

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      # 1. 检出代码
      - name: Checkout
        uses: actions/checkout@v4

      # 2. 设置Docker Buildx
      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      # 3. 登录Docker Hub
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_TOKEN }}

      # 4. 提取标签信息
      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: myusername/myapp
          tags: |
            type=ref,event=branch
            type=ref,event=tag
            type=sha,prefix={{branch}}-

      # 5. 构建并推送镜像
      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
          platforms: linux/amd64,linux/arm64
GitLab CI示例
# .gitlab-ci.yml
build_image:
  stage: build
  image: docker:24.0
  services:
    - docker:24.0-dind
  variables:
    DOCKER_TLS_CERTDIR: "/certs"
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" "$CI_REGISTRY"
  script:
    # 构建镜像
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" .
    - docker build -t "$CI_REGISTRY_IMAGE:latest" .
    # 推送镜像
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
    - docker push "$CI_REGISTRY_IMAGE:latest"
  only:
    - main
CI/CD中的最佳实践
  1. 使用BuildKit缓存: 利用GitHub Actions cache或GitLab CI cache。
  2. 多平台构建: 使用docker buildx构建多平台镜像。
  3. 镜像扫描: 在构建后自动扫描漏洞。
  4. 签名验证: 使用Cosign对镜像签名。
  5. 不可变标签: 不要覆盖已推送的标签。

10.6 hadolint工具检查Dockerfile

hadolint是一个Dockerfile静态分析工具,可以检查Dockerfile中的问题和不符合规范的地方。

安装hadolint
# Linux
sudo wget -O /usr/bin/hadolint \
  https://github.com/hadolint/hadolint/releases/download/v2.12.0/hadolint-Linux-x86_64
sudo chmod +x /usr/bin/hadolint

# macOS
brew install hadolint

# Docker方式(无需安装)
docker run --rm -i hadolint/hadolint < Dockerfile

# npm
npm install -g hadolint
使用hadolint
# 检查Dockerfile
hadolint Dockerfile

# 检查并输出特定格式
hadolint --format json Dockerfile

# 在CI中使用Docker运行
docker run --rm -i hadolint/hadolint < Dockerfile
hadolint的常见规则
# DL3006: 始终指定基础镜像的版本标签
# 不推荐: FROM node
# 推荐: FROM node:20-alpine
FROM node:20-alpine

# DL3008: 使用--no-install-recommends安装apt包
# 不推荐: RUN apt-get install -y curl
# 推荐:
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl && \
    rm -rf /var/lib/apt/lists/*

# DL3013: 指定pip包的版本
# 不推荐: RUN pip install flask
# 推荐:
RUN pip install flask==3.0.0

# DL3059: 多条RUN指令可以合并
# 不推荐:
# RUN apt-get update
# RUN apt-get install -y curl
# 推荐:
RUN apt-get update && apt-get install -y curl

# DL3025: 使用--chown选项
# 不推荐: COPY app.py /app/
# 推荐:
COPY --chown=appuser:appuser app.py /app/
配置hadolint
# .hadolint.yaml
failure-threshold: warning
ignored:
  - DL3008  # 忽略--no-install-recommends规则
  - DL3013  # 忽略指定pip版本规则
trustedRegistries:
  - docker.io
  - gcr.io

10.7 镜像构建常见问题汇总

问题1: 构建缓存失效过快

原因: 变化频繁的文件被COPY到镜像中,导致后续指令缓存失效。

解决方案: 先复制不常变化的依赖文件,后复制源代码:

# 先复制依赖文件
COPY package.json package-lock.json ./
RUN npm ci
# 后复制源代码
COPY . .
问题2: 镜像体积过大

原因: 使用了完整基础镜像,未清理缓存,包含构建工具。

解决方案: 使用slim/alpine基础镜像,多阶段构建,清理缓存:

FROM python:3.12-slim AS builder
# ...构建...
FROM python:3.12-slim
COPY --from=builder ...
问题3: 容器无法优雅关闭

原因: CMD/ENTRYPOINT使用了Shell形式,信号无法传递到应用进程。

解决方案: 使用Exec形式,或使用dumb-init/tini作为PID 1:

# 使用Exec形式
CMD ["python", "app.py"]

# 或使用dumb-init
RUN apk add --no-cache dumb-init
ENTRYPOINT ["dumb-init", "--"]
CMD ["python", "app.py"]
问题4: 容器内时间不正确

原因: 容器默认使用UTC时区。

解决方案: 设置时区:

RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
    echo "Asia/Shanghai" > /etc/timezone
问题5: COPY文件权限不正确

原因: COPY默认以root用户复制文件。

解决方案: 使用–chown选项:

COPY --chown=appuser:appuser . /app/
问题6: 构建上下文过大

原因: 构建目录中包含大量不需要的文件。

解决方案: 使用.dockerignore排除不需要的文件:

.git/
node_modules/
__pycache__/
*.log
.env
问题7: alpine镜像中Python包安装失败

原因: alpine使用musl libc,某些C扩展包需要glibc。

解决方案: 使用slim版本替代alpine,或安装编译工具:

# 方案1: 使用slim版本
FROM python:3.12-slim

# 方案2: 在alpine中安装编译工具
FROM python:3.12-alpine
RUN apk add --no-cache gcc musl-dev python3-dev libffi-dev
问题8: 多阶段构建中COPY --from失败

原因: 引用的阶段名不存在或路径不正确。

解决方案: 确保阶段名正确,路径在构建阶段中存在:

FROM node:20 AS builder  # 确保AS后面有名称
WORKDIR /app
COPY . .
RUN npm run build

FROM nginx:alpine
# 确保路径正确: /app/dist是builder阶段中存在的路径
COPY --from=builder /app/dist /usr/share/nginx/html
问题9: docker build提示"Cannot connect to the Docker daemon"

原因: Docker守护进程未运行或当前用户无权限访问。

解决方案:

# 检查Docker是否运行
sudo systemctl status docker

# 启动Docker
sudo systemctl start docker

# 将当前用户加入docker组(避免每次sudo)
sudo usermod -aG docker $USER
# 需要重新登录生效
问题10: 镜像推送时认证失败

原因: 未登录或密码错误。

解决方案:

# 登录
docker login registry.example.com

# 使用Access Token而非密码(更安全)

# 检查认证文件
cat ~/.docker/config.json

10.8 本章总结与下一期预告

全文总结

本文系统讲解了Docker镜像管理与Dockerfile的全部知识,从底层原理到实战应用,涵盖了以下内容:

第一章 深入剖析了Docker镜像的底层原理,包括联合文件系统(UnionFS)、Overlay2文件系统、镜像分层结构、写时复制机制和存储驱动对比。理解这些原理是掌握Docker镜像的基础。

第二章 讲解了镜像管理的基础操作,包括搜索、拉取、查看、标签管理、删除、详细信息查看、构建历史、导入导出和清理悬空镜像等全部命令。

第三章 详细讲解了Dockerfile的基础构建指令,包括FROM、LABEL、RUN、COPY、ADD、WORKDIR、ENV、ARG、EXPOSE等指令的语法、参数和使用注意事项。

第四章 讲解了影响容器运行时行为的指令,包括CMD、ENTRYPOINT、USER、HEALTHCHECK、VOLUME、ONBUILD、STOPSIGNAL和SHELL指令,以及各指令的执行时机。

第五章 全面讲解了docker build命令的所有参数,包括构建上下文、.dockerignore、-f、-t、–build-arg、–no-cache、–platform、–output、–target和BuildKit构建引擎。

第六章 详细讲解了多阶段构建,包括语法、命名阶段、–target选项,以及Go、Java、Node.js、Python四种语言的完整实战案例和镜像体积对比分析。

第七章 系统讲解了镜像构建优化策略,包括基础镜像选择、合并RUN指令、构建缓存优化、清理缓存、.dockerignore、BuildKit缓存挂载、密钥挂载、镜像瘦身实战和安全扫描。

第八章 讲解了私有镜像仓库,包括Docker Hub使用、registry官方镜像搭建、HTTPS证书配置、基本认证、Harbor企业级仓库、阿里云ACR、备份恢复和镜像生命周期管理。

第九章 通过7个实战案例(Nginx、Flask、Express、Spring Boot、MySQL、数据科学环境、前端Vue),展示了Dockerfile在真实开发场景中的应用。

第十章 总结了Dockerfile最佳实践与规范,包括编写规范、版本管理、安全实践、可维护性、CI/CD集成、hadolint工具和常见问题汇总。

Dockerfile最佳实践速查表
最佳实践 说明
使用slim/alpine基础镜像 减小镜像体积
使用多阶段构建 分离构建和运行环境
合并RUN指令 减少镜像层数
清理缓存和临时文件 减小镜像体积
先复制依赖文件 优化构建缓存
使用COPY而非ADD 更透明、可预测
使用Exec形式 正确的信号处理
使用非root用户 提高安全性
添加健康检查 便于编排系统管理
添加.dockerignore 减小构建上下文
使用BuildKit缓存挂载 加速构建
使用secret挂载 安全传递密钥
固定基础镜像版本 确保可重复构建
使用LABEL添加元数据 提高可维护性
定期扫描漏洞 安全保障

本文总结: Docker镜像是容器化应用的基础,理解镜像的分层原理和写时复制机制是高效使用Docker的关键。编写高质量的Dockerfile需要掌握每一条指令的语法和行为,合理利用多阶段构建和构建缓存来优化镜像大小和构建速度。在生产环境中,还需要关注镜像安全性(非root用户、漏洞扫描)和镜像生命周期管理(版本标签、清理策略)。希望本文能帮助你在实际项目中构建出更小、更快、更安全的Docker镜像。

更多推荐