> For the complete documentation index, see [llms.txt](https://stablebuild.gitbook.io/stablebuild-docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://stablebuild.gitbook.io/stablebuild-docs/docs/cn/why-stablebuild.md).

# 为什么需要 StableBuild？

如今 Docker 容器已经成为打包 Web 应用的标准方式。它提供了一种干净、标准化的方法，把应用及其所有依赖一起打包。但这里存在一种虚假的安全感。乍一看，Dockerfile 似乎是确定性的（deterministic）——同一个 Dockerfile 应该生成同一个 Docker 镜像。可实际上，你依赖着大量外部服务：镜像站、软件包仓库和各种代码仓库。这些服务中的任何一个都可能随时发生变化。因此，看似已经被整齐封装进容器的依赖列表，其实随时可能发生变化，从而导致你的应用突然崩溃。

### 一个典型的 Dockerfile

来看一个常见的 Dockerfile 示例（这个例子安装一个 Python 应用，但同样的问题适用于其他语言）：

```docker
# Base this Dockerfile on Ubuntu 20.04
FROM ubuntu:20.04

# Don't prompt us when installing packages
ARG DEBIAN_FRONTEND=noninteractive

# Install some OS level packages
RUN apt update && apt install -y curl software-properties-common

# We need a newer Python version, grab it from Deadsnakes PPA
RUN add-apt-repository -y ppa:deadsnakes/ppa && \
    apt update && \
    apt install -y python3.9 python3.9-distutils

# Install pip (the Python package manager)
RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
    python3.9 get-pip.py

# Install a Python package through pip
RUN pip3 install onnx==1.14.0
```

下面逐行分析这个 Dockerfile。

```
FROM ubuntu:20.04
```

这一行会从 Docker 镜像仓库（通常是 Docker Hub）拉取标签为 `20.04` 的 `ubuntu` 基础镜像。Docker Hub 中的镜像和标签并不是不可变的，它们可能被覆盖（例如每次发布新的 Ubuntu 20.04 版本时），也可能被删除。最终拉取到哪个基础镜像取决于你构建的时间以及构建缓存中的内容。这会让问题调试变得困难（你运行的基础镜像可能和生产环境中的不同），重新构建容器时基础系统版本可能突然改变（例如当构建缓存为空时），甚至基础镜像被删除导致容器无法构建。

```
apt update && apt install -y curl software-properties-common
```

之后从 Ubuntu 软件仓库安装一些系统级软件包。实际上安装的是这些软件包的最新版本，因为 Ubuntu 的软件仓库一直在更新。同时也无法固定到具体版本，因为仓库会主动删除旧版本。这意味着容器的核心依赖可能突然升级，或者不再可用。和前面的情况一样，最终安装的包取决于容器构建的时间以及构建缓存中的内容，因此构建随时可能失败。

```
add-apt-repository -y ppa:deadsnakes/ppa && \
    apt update && \
    apt install -y python3.9 python3.9-distutils
```

接下来需要一个 Ubuntu 官方仓库中没有的软件包（Python 3.9），于是添加 deadsnakes PPA（一个替代的软件仓库），并从那里安装该软件包。这一步存在和前面相同的问题，同时还增加了一个额外依赖——PPA 仓库，它可能随时下线或停止工作。

```
curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py && \
    python3.9 get-pip.py
```

然后需要安装一个不在软件仓库中的依赖。通过下载 get-pip.py 文件并运行它来安装 pip 包管理器。这个 URL 的内容可能会改变，也可能被删除。该脚本本身很可能还会发起额外的 HTTP 请求来下载其他资源。因此容器中最终安装的 pip 版本同样取决于容器构建的时间，一旦有更新发布，应用就可能出现问题。

```
pip3 install onnx==1.14.0
```

最后安装一个 Python 包。表面上看似乎已经把这个包固定在 1.14.0 版本，但实际上并没有，因为 onnx 本身依赖未固定版本的其他依赖（例如 `protobuf>=3.20.2`）。当 onnx 的某个子依赖更新（例如 protobuf 5 发布并带来破坏性变更），容器重新构建时应用就会出现问题。因为 pip 会选择这些子依赖的最新版本，最终导致不同时间构建的容器可能安装完全不同的 Python 包（同时也取决于构建缓存）。

### 一个 Dockerfile，很多可能出错的地方

从这个简单的 Dockerfile 可以看到，它依赖了 5 个服务：Docker Hub、Ubuntu 软件仓库、deadsnakes PPA、pypa.io 和 PyPI 仓库，而这些服务都不是稳定不变的。任何依赖都可能随时被更新、修改或删除，从而导致应用崩溃。

此外，容器中的内容还取决于容器构建的时间以及构建缓存中的内容。这会带来一个问题：同一个 Dockerfile 可能生成完全不同的容器，这会让调试依赖更新导致的问题变得非常困难（例如在自己机器上可以运行，但在同事那里却不行）。

这些问题带来的维护成本很高，而且通常是被动的。有人对代码库做了一个看似很小的修改，构建服务器从头开始构建容器，结果构建因为一个完全无关的错误失败。然后就必须立刻更新应用代码以适配新的依赖版本，因为构建已经无法通过。

### 所以，用StableBuild吧！

为了解决这个问题，我们创建了 StableBuild。StableBuild 是一组镜像服务和软件包仓库，用于让容器构建变得可靠和可重复。本质上，这意味着可以通过 StableBuild 冻结任何依赖，这样同一个 Dockerfile 就会生成同一个 Docker 容器。目前 StableBuild 已经支持 Docker 基础镜像、完整的 Ubuntu / Debian / Alpine 软件仓库、最常用的 PPA，以及 PyPI Python 包仓库，并且正在继续扩展更多功能，例如冻结互联网上的任意文件。

现在有兴趣了吗？如果想了解如何让上面的 Dockerfile 变成确定性的构建，可以继续阅读 [使用 StableBuild 固定你的第一个容器](/stablebuild-docs/docs/cn/pinning-your-first-container.md). 🎉
