跳到内容

容器中的 FastAPI - Docker

在部署 FastAPI 应用程序时,一种常见的方法是构建一个 Linux 容器镜像。通常使用 Docker 来完成。然后,您可以有几种可能的方式来部署该容器镜像。

使用 Linux 容器具有多项优势,包括 安全性可复制性简单性 等。

提示

赶时间并且已经知道这些内容?跳转到下面的 Dockerfile 👇

Dockerfile 预览 👀
FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

CMD ["fastapi", "run", "app/main.py", "--port", "80"]

# If running behind a proxy like Nginx or Traefik add --proxy-headers
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]

什么是容器

容器(主要是 Linux 容器)是一种非常 轻量级 的方式来打包应用程序,包括其所有依赖项和必要文件,同时将它们与同一系统中的其他容器(其他应用程序或组件)隔离。

Linux 容器运行在宿主机的相同 Linux 内核上(机器、虚拟机、云服务器等)。这仅仅意味着它们非常轻量级(与模拟整个操作系统的完整虚拟机相比)。

这样,容器消耗的 资源很少,其数量与直接运行进程相当(虚拟机将消耗更多)。

容器还拥有自己 隔离的 运行进程(通常只有一个进程)、文件系统和网络,从而简化了部署、安全、开发等。

什么是容器镜像

一个 容器 是从一个 容器镜像 运行的。

容器镜像是一个 静态 版本,包含所有文件、环境变量以及应存在于容器中的默认命令/程序。静态 在这里意味着容器 镜像 未在运行,未被执行,它只是打包的文件和元数据。

与存储了静态内容的“容器镜像”相反,“容器”通常指的是运行实例,即正在 执行 的事物。

容器 启动并运行时(从 容器镜像 启动),它可以创建或更改文件、环境变量等。这些更改将只存在于该容器中,但不会持久化到基础容器镜像中(不会保存到磁盘)。

容器镜像可与 程序 文件和内容相媲美,例如 python 和某个文件 main.py

容器 本身(与 容器镜像 相反)是镜像的实际运行实例,可与 进程 相媲美。事实上,只有当容器具有 正在运行的进程 时,它才在运行(通常只有一个进程)。当容器内没有进程运行时,容器将停止。

容器镜像

Docker 一直是创建和管理 容器镜像容器 的主要工具之一。

并且有一个公共的 Docker Hub,其中包含许多工具、环境、数据库和应用程序的预制 官方容器镜像

例如,有一个官方的 Python 镜像

还有许多其他针对不同事物的镜像,例如数据库,例如

通过使用预制的容器镜像,可以非常容易地 组合 和使用不同的工具。例如,尝试一个新的数据库。在大多数情况下,您可以使用 官方镜像,并通过环境变量对其进行配置。

这样,在许多情况下,您可以学习容器和 Docker 的知识,并将这些知识应用于许多不同的工具和组件。

因此,您将运行 多个容器 来处理不同的事物,例如数据库、Python 应用程序、带有 React 前端应用程序的 Web 服务器,并通过它们的内部网络将它们连接在一起。

所有容器管理系统(如 Docker 或 Kubernetes)都集成了这些网络功能。

容器与进程

一个 容器镜像 通常在其元数据中包含启动 容器 时应运行的默认程序或命令,以及要传递给该程序的参数。这与在命令行中进行的操作非常相似。

容器 启动时,它将运行该命令/程序(尽管您可以覆盖它并使其运行不同的命令/程序)。

只要 主进程(命令或程序)正在运行,容器就处于运行状态。

容器通常有一个 单个进程,但也可以从主进程启动子进程,这样您就可以在同一个容器中拥有 多个进程

但是,没有 至少一个正在运行的进程 的容器是无法运行的。如果主进程停止,容器也将停止。

为 FastAPI 构建 Docker 镜像

好的,现在我们来构建一些东西!🚀

我将向您展示如何 从头开始 为 FastAPI 构建一个 Docker 镜像,基于 官方 Python 镜像。

这就是您在 大多数情况下 想做的事情,例如

  • 使用 Kubernetes 或类似工具
  • Raspberry Pi 上运行时
  • 使用为您运行容器镜像的云服务等。

打包需求

您通常会将应用程序的 打包需求 放在某个文件中。

这主要取决于您使用的工具来 安装 这些需求。

最常见的方法是有一个文件 requirements.txt,其中包含每个包的名称和版本,每行一个。

您当然会使用您在 关于 FastAPI 版本 中阅读到的相同想法来设置版本范围。

例如,您的 requirements.txt 可能看起来像

fastapi[standard]>=0.113.0,<0.114.0
pydantic>=2.7.0,<3.0.0

您通常会使用 pip 来安装这些包依赖项,例如

$ pip install -r requirements.txt
---> 100%
Successfully installed fastapi pydantic

信息

还有其他格式和工具可以定义和安装包依赖项。

创建 FastAPI 代码

  • 创建一个 app 目录并进入。
  • 创建一个空的 __init__.py 文件。
  • 创建一个 main.py 文件,内容如下
from typing import Union

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"Hello": "World"}


@app.get("/items/{item_id}")
def read_item(item_id: int, q: Union[str, None] = None):
    return {"item_id": item_id, "q": q}

Dockerfile

现在,在同一个项目目录中创建一个名为 Dockerfile 的文件,内容如下

# (1)!
FROM python:3.9

# (2)!
WORKDIR /code

# (3)!
COPY ./requirements.txt /code/requirements.txt

# (4)!
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (5)!
COPY ./app /code/app

# (6)!
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
  1. 从官方 Python 基础镜像开始。

  2. 将当前工作目录设置为 /code

    这里我们将放置 requirements.txt 文件和 app 目录。

  3. 将需求文件复制到 /code 目录。

    先复制需求文件,而不是其余的代码。

    由于此文件 不经常更改,Docker 将检测到它并为此步骤使用 缓存,从而为下一步也启用缓存。

  4. 安装需求文件中的包依赖项。

    --no-cache-dir 选项告诉 pip 不要将下载的包本地保存,因为这仅在 pip 将再次运行以安装相同的包时才需要,但在使用容器时不是这种情况。

    注意

    --no-cache-dir 仅与 pip 相关,与 Docker 或容器无关。

    --upgrade 选项告诉 pip 如果包已安装,则升级它们。

    由于之前的复制文件步骤可以被 Docker 缓存 检测到,因此在此步骤中,如果可用,也将 使用 Docker 缓存

    在此步骤中使用缓存将 节省 您在开发过程中反复构建镜像时的 大量时间,而不是 每次下载和安装 所有依赖项。

  5. ./app 目录复制到 /code 目录中。

    由于这包含最 频繁更改 的所有代码,因此 Docker 缓存 不会轻松地用于此步骤或任何 后续步骤

    因此,将其放在 Dockerfile末尾附近 非常重要,以优化容器镜像的构建时间。

  6. 设置 命令 以使用 fastapi run,它底层使用 Uvicorn。

    CMD 接受一个字符串列表,其中每个字符串都是您在命令行中输入的内容,用空格分隔。

    此命令将从 当前工作目录 运行,与上面用 WORKDIR /code 设置的 /code 目录相同。

提示

通过单击代码中的每个数字气泡来查看每行所做的操作。👆

警告

务必 使用 CMD 指令的 执行格式,如下所述。

使用 CMD - 执行格式

CMD Docker 指令可以写成两种格式

执行 格式

# ✅ Do this
CMD ["fastapi", "run", "app/main.py", "--port", "80"]

⛔️ Shell 格式

# ⛔️ Don't do this
CMD fastapi run app/main.py --port 80

请务必始终使用 执行格式,以确保 FastAPI 可以正常关闭并且 生命周期事件 被触发。

您可以在 Docker 的 shell 和 exec 格式文档 中阅读更多相关信息。

这在使用 docker compose 时会很明显。有关更多技术细节,请参阅此 Docker Compose FAQ 部分:为什么我的服务需要 10 秒才能重新创建或停止?

目录结构

您现在应该拥有一个类似以下的目录结构

.
├── app
│   ├── __init__.py
│   └── main.py
├── Dockerfile
└── requirements.txt

位于 TLS 终止代理之后

如果您正在 Nginx 或 Traefik 等 TLS 终止代理(负载均衡器)后面运行容器,请添加 --proxy-headers 选项。这将告诉 Uvicorn(通过 FastAPI CLI)信任该代理发送的标头,告知它应用程序正在 HTTPS 后面运行等。

CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]

Docker 缓存

这个 Dockerfile 中有一个重要的技巧,我们首先复制 仅包含依赖项的文件,而不是其余的代码。让我解释一下原因。

COPY ./requirements.txt /code/requirements.txt

Docker 和其他工具 增量地构建 这些容器镜像,一层一层地叠加,从 Dockerfile 的顶部开始,并添加 Dockerfile 的每个指令创建的任何文件。

Docker 和类似工具在构建镜像时还使用 内部缓存。如果一个文件自上次构建容器镜像以来没有更改,那么它将 重用上次创建的同一层,而不是再次复制文件并从头开始创建新层。

仅仅避免复制文件不一定会带来太多改进,但因为它在该步骤使用了缓存,所以它可以 为下一步使用缓存。例如,它可以为使用以下命令安装依赖项的指令使用缓存

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

包依赖项文件 不会经常更改。因此,只复制该文件,Docker 将能够 使用缓存 来执行此步骤。

然后,Docker 将能够 为下一步 下载和安装这些依赖项 使用缓存。而这正是我们 节省大量时间 的地方。✨...并避免等待的无聊。😪😆

下载和安装包依赖项 可能需要几分钟,但使用 缓存 最多 只需几秒钟

并且由于您将在开发过程中反复构建容器镜像以检查您的代码更改是否正常工作,因此这可以节省大量累积的时间。

然后,在 Dockerfile 的末尾附近,我们复制所有代码。由于这 更改最频繁,因此我们将其放在末尾附近,因为几乎所有在此步骤之后的步骤都将无法使用缓存。

COPY ./app /code/app

构建 Docker 镜像

现在所有文件都已就位,让我们构建容器镜像。

  • 转到项目目录(在您的 Dockerfile 所在的位置,其中包含您的 app 目录)。
  • 构建您的 FastAPI 镜像
$ docker build -t myimage .

---> 100%

提示

注意末尾的 .,它等同于 ./,它告诉 Docker 使用哪个目录来构建容器镜像。

在这种情况下,它是相同的当前目录(.)。

启动 Docker 容器

  • 运行基于您的镜像的容器
$ docker run -d --name mycontainer -p 80:80 myimage

检查一下

您应该能够在您的 Docker 容器的 URL 中进行检查,例如:http://192.168.99.100/items/5?q=somequeryhttp://127.0.0.1/items/5?q=somequery(或等效的,使用您的 Docker 主机)。

您将看到类似以下内容

{"item_id": 5, "q": "somequery"}

交互式 API 文档

现在您可以转到 http://192.168.99.100/docshttp://127.0.0.1/docs(或等效的,使用您的 Docker 主机)。

您将看到自动交互式 API 文档(由 Swagger UI 提供)

Swagger UI

备选 API 文档

您还可以转到 http://192.168.99.100/redochttp://127.0.0.1/redoc(或等效的,使用您的 Docker 主机)。

您将看到备选的自动文档(由 ReDoc 提供)

ReDoc

构建单文件 FastAPI 的 Docker 镜像

如果您的 FastAPI 是一个单文件,例如,main.py 而没有 ./app 目录,您的文件结构可能如下所示

.
├── Dockerfile
├── main.py
└── requirements.txt

那么您只需要在 Dockerfile 中更改相应的路径来复制文件

FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

# (1)!
COPY ./main.py /code/

# (2)!
CMD ["fastapi", "run", "main.py", "--port", "80"]
  1. main.py 文件直接复制到 /code 目录(无需任何 ./app 目录)。

  2. 使用 fastapi run 来服务您在单个文件 main.py 中的应用程序。

当您将文件传递给 fastapi run 时,它会自动检测它是一个单文件而不是包的一部分,并且知道如何导入它并服务您的 FastAPI 应用。😎

部署概念

让我们再次讨论一些相同的 部署概念,以容器的形式。

容器主要是简化 构建和部署 应用程序过程的工具,但它们不强制执行特定的方法来处理这些 部署概念,并且有几种可能的策略。

好消息 是,每种不同的策略都有一个覆盖所有部署概念的方法。🎉

让我们以容器的形式回顾这些 部署概念

  • HTTPS
  • 启动时运行
  • 重启
  • 复制(运行的进程数量)
  • 内存
  • 启动前的准备步骤

HTTPS

如果我们只关注 FastAPI 应用程序的 容器镜像(以及后续的运行 容器),HTTPS 通常会由另一个工具 外部处理

它可以是另一个容器,例如带有 Traefik,它负责处理 HTTPS自动 获取 证书

提示

Traefik 与 Docker、Kubernetes 等集成,因此使用它来为容器设置和配置 HTTPS 非常容易。

或者,HTTPS 可以由云提供商作为其服务之一来处理(同时仍然在容器中运行应用程序)。

开机启动和重启

通常有另一个工具负责 启动和运行 您的容器。

它可以是 Docker 本身、Docker ComposeKubernetes云服务 等。

在大多数(或所有)情况下,都有一个简单的选项可以启用容器的开机启动和故障重启。例如,在 Docker 中,它是命令行选项 --restart

如果不使用容器,让应用程序开机启动并具有重启功能可能会很麻烦且困难。但是,当 使用容器 时,在大多数情况下,该功能已默认包含。✨

复制 - 进程数

如果您有一个 Kubernetes、Docker Swarm Mode、Nomad 或其他类似的复杂系统来管理多台机器上的分布式容器的 集群,那么您可能希望在 集群级别 处理复制,而不是在每个容器中使用 进程管理器(如带工作进程的 Uvicorn)。

像 Kubernetes 这样的分布式容器管理系统通常具有某种集成的复制容器的方法,同时仍然支持对传入请求的 负载均衡。所有这些都在 集群级别

在这种情况下,您可能希望 从头开始构建 Docker 镜像,如上 所述,安装您的依赖项,并运行 单个 Uvicorn 进程,而不是使用多个 Uvicorn 工作进程。

负载均衡器

使用容器时,通常会有一个组件 监听主端口。它可能可能是另一个容器,该容器也是一个 TLS 终止代理,用于处理 HTTPS 或类似的工具。

由于该组件将承担请求的 负载 并以(希望)平衡 的方式将其分配给工作进程,因此它通常也称为 负载均衡器

提示

用于 HTTPS 的相同 TLS 终止代理 组件很可能也是一个 负载均衡器

当使用容器时,您用来启动和管理它们的系统将已经内置了内部工具,用于将来自 负载均衡器(也可能是 TLS 终止代理)的 网络通信(例如 HTTP 请求)传输到带有您的应用的容器。

一个负载均衡器 - 多个工作容器

当使用 Kubernetes 或类似的分布式容器管理系统时,使用其内部网络机制将允许监听主 端口 的单个 负载均衡器 将通信(请求)传输到可能 运行您的应用程序的多个容器

这些运行您的应用程序的容器中的每一个通常都 只有一个进程(例如,运行您的 FastAPI 应用程序的 Uvicorn 进程)。它们将都是 相同的容器,运行相同的东西,但每个都有自己的进程、内存等。这样,您就可以利用 CPU 的不同核心 甚至 不同机器 上的 并行处理

分布式容器系统以及 负载均衡器轮流将请求 分发给每个带有您的应用的容器。因此,每个请求都可以由运行您的应用程序的多个 复制容器 中的一个来处理。

通常,此 负载均衡器 将能够处理发送到您集群中其他应用程序的请求(例如,发送到不同的域,或在不同的 URL 路径前缀下),并将该通信传输到集群中运行该其他应用程序的正确容器。

每个容器一个进程

在这种场景下,您可能希望 每个容器有一个(Uvicorn)进程,因为您已经在集群级别处理了复制。

所以,在这种情况下,您 希望在容器中拥有多个工作进程,例如使用 --workers 命令行选项。您只想让每个容器有一个 单个 Uvicorn 进程(但可能有多个容器)。

在容器中拥有另一个进程管理器(如使用多个工作进程时)只会增加 不必要的复杂性,而这些复杂性很可能已经被您的集群系统处理了。

具有多个进程的容器和特殊情况

当然,也有 特殊情况,您可能希望在一个 容器 中拥有几个 Uvicorn 工作进程

在这些情况下,您可以使用 --workers 命令行选项来设置您想要运行的工作进程数

FROM python:3.9

WORKDIR /code

COPY ./requirements.txt /code/requirements.txt

RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt

COPY ./app /code/app

# (1)!
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]
  1. 这里我们使用 --workers 命令行选项将工作进程数设置为 4。

以下是何时可能的情况的一些示例

一个简单的应用

如果您的应用程序 足够简单,可以在 单个服务器 上运行,而不是集群,您可能会希望在容器中有一个进程管理器。

Docker Compose

您可能正在 Docker Compose单个服务器(非集群)上部署,因此您将无法轻松地管理容器的复制(使用 Docker Compose),同时保留共享网络和 负载均衡

然后,您可能希望有一个 单个容器,其中有一个 进程管理器 启动 多个工作进程


关键点是,没有 任何这些是 硬性规定 您必须盲目遵循的。您可以使用这些想法来 评估您自己的用例,并决定最适合您系统的方案,并查看如何管理以下概念

  • 安全性 - HTTPS
  • 启动时运行
  • 重启
  • 复制(运行的进程数量)
  • 内存
  • 启动前的准备步骤

内存

如果您运行 每个容器一个进程,您将拥有一个或多或少定义明确、稳定且内存消耗有限的容器(如果它们被复制,则不止一个)。

然后,您可以将这些相同的内存限制和要求设置在您的容器管理系统(例如 Kubernetes)的配置中。这样,它就能在 可用机器复制容器,同时考虑它们所需的内存量以及集群中机器上的可用内存量。

如果您的应用程序 很简单,这可能 不是问题,您可能不需要指定硬性内存限制。但如果您 使用大量内存(例如,用于 机器学习 模型),您应该检查您正在消耗多少内存,并调整 运行在 每台机器 上的 容器数量(并可能向您的集群添加更多机器)。

如果您运行 每个容器多个进程,您将需要确保启动的进程数量 不会消耗超过可用内存

启动和容器之前的步骤

如果您正在使用容器(例如 Docker、Kubernetes),那么您可以使用两种主要方法。

多个容器

如果您有 多个容器,每个容器可能运行一个 单个进程(例如,在 Kubernetes 集群中),那么您可能希望有一个 单独的容器 在运行复制的工作容器 之前,在一个容器中完成 先前步骤 的工作,运行一个进程。

信息

如果您正在使用 Kubernetes,这可能是一个 Init Container

如果您的用例中,并行多次 运行这些先前步骤没有问题(例如,如果您只是检查数据库是否准备就绪,而不是运行数据库迁移),那么您也可以将它们放在每个容器中,然后在启动主进程之前。

单个容器

如果您有一个简单的设置,只有一个 单个容器,然后启动多个 工作进程(或也只有一个进程),那么您可以在启动应用程序进程之前,在同一个容器中运行这些先前步骤。

基础 Docker 镜像

曾经有一个官方的 FastAPI Docker 镜像:tiangolo/uvicorn-gunicorn-fastapi。但它现在已弃用。⛔️

您可能 不应该 使用这个基础 Docker 镜像(或任何其他类似的镜像)。

如果您正在使用 Kubernetes(或其他),并且您已经在集群级别设置了 复制,具有多个 容器。在这些情况下,您最好 从头开始构建镜像,如上所述:为 FastAPI 构建 Docker 镜像

如果您需要多个工作进程,您可以简单地使用 --workers 命令行选项。

技术细节

该 Docker 镜像是在 Uvicorn 不支持管理和重启死机的工作进程时创建的,因此需要使用 Gunicorn 和 Uvicorn,这增加了相当多的复杂性,只是为了让 Gunicorn 管理和重启 Uvicorn 工作进程。

但现在 Uvicorn(和 fastapi 命令)支持使用 --workers,没有理由不使用基础 Docker 镜像而不是自己构建(代码量几乎相同 😅)。

部署容器镜像

拥有容器(Docker)镜像后,有几种部署方式。

例如

  • 使用 Docker Compose 在单个服务器上
  • 使用 Kubernetes 集群
  • 使用 Docker Swarm Mode 集群
  • 使用 Nomad 等其他工具
  • 使用接受您的容器镜像并进行部署的云服务

uv 的 Docker 镜像

如果您使用 uv 来安装和管理您的项目,您可以遵循他们的 uv Docker 指南

总结

使用容器系统(例如,使用 DockerKubernetes)可以相对轻松地处理所有 部署概念

  • HTTPS
  • 启动时运行
  • 重启
  • 复制(运行的进程数量)
  • 内存
  • 启动前的准备步骤

在大多数情况下,您可能不想使用任何基础镜像,而是 从头开始构建容器镜像,基于官方 Python Docker 镜像。

通过注意 Dockerfile 中指令的 顺序Docker 缓存,您可以 最小化构建时间,以最大化您的生产力(并避免无聊)。😎