跳到内容

FastAPI 容器化 - Docker

部署 FastAPI 应用时,一种常用的方法是构建一个 Linux 容器镜像。这通常使用 Docker 来完成。之后,你可以通过几种可能的方式来部署该容器镜像。

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

提示

如果你赶时间且已经了解这些内容?直接跳转到下方的 Dockerfile 👇

Dockerfile 预览 👀
FROM python:3.14

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 镜像

好了,现在开始构建吧!🚀

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

这是在大多数情况下你想要做的事情,例如:

  • 使用 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 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: str | None = None):
    return {"item_id": item_id, "q": q}

Dockerfile

现在,在同一项目目录下创建一个 Dockerfile 文件:

# (1)!
FROM python:3.14

# (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 指令的 exec 格式,如下所述。

使用 CMD - Exec 格式

CMD Docker 指令有两种编写格式:

Exec 格式

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

⛔️ Shell 格式

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

请务必始终使用 exec 格式,以确保 FastAPI 能够优雅地关闭,并触发 生命周期事件 (lifespan events)

你可以在 Docker 关于 shell 和 exec 格式的文档 中阅读更多内容。

在使用 docker compose 时,这一点相当明显。请参阅 Docker Compose 常见问题解答部分以了解更多技术细节:为什么我的服务需要 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 的顶部开始,将每一条指令创建的文件一层接一层地叠加。

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.14

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 可以由云服务提供商作为其服务之一进行处理(同时仍然在容器中运行应用)。

启动和重启运行

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

它可能是直接使用 DockerDocker ComposeKubernetes云服务等。

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

不使用容器时,使应用在启动时运行并实现自动重启可能会很繁琐且困难。但使用容器时,大多数情况下该功能是默认包含的。✨

副本 - 进程数量

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

像 Kubernetes 这样的分布式容器管理系统通常有某种内置方式来处理容器的副本,同时仍然支持传入请求的负载均衡。这一切都在集群级别完成。

在这种情况下,你可能希望从零开始构建 Docker 镜像,如上述说明,安装依赖,并运行单个 Uvicorn 进程,而不是使用多个 Uvicorn worker。

负载均衡器

使用容器时,你通常会有一些组件监听主端口。这可能是一个同时作为 TLS 终止代理的容器,用于处理 HTTPS 或类似工具。

由于此组件将承担请求的负载并以(希望是平衡的)方式将请求分发给 worker,它通常也被称为负载均衡器

提示

用于 HTTPS 的同一个 TLS 终止代理组件可能也同时充当负载均衡器

并且在使用容器工作时,用于启动和管理它们的系统本身就已经内置了工具,可以将网络通信(例如 HTTP 请求)从该负载均衡器(也可能是 TLS 终止代理)传输到运行你应用的容器中。

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

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

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

分布式容器系统配合负载均衡器轮询地将请求分发到运行你应用的每个容器中。因此,每个请求都可以由运行你应用的多个副本容器中的某一个来处理。

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

每个容器一个进程

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

因此,在这种情况下,你不会希望在容器中拥有多个 worker,例如通过 --workers 命令行选项。你只需要每个容器单个 Uvicorn 进程(但可能有多个容器)。

在容器内部再拥有一个进程管理器(如多个 worker 那样)只会增加不必要的复杂性,而这些复杂性你很可能已经在集群系统中处理过了。

多进程容器及特殊情况

当然,也有特殊情况,你可能希望在一个容器中拥有多个 Uvicorn worker 进程

在这种情况下,你可以使用 --workers 命令行选项来设置你想要运行的 worker 数量:

FROM python:3.14

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 命令行选项将 worker 数量设置为 4。

以下是一些可能有意义的例子:

简单的应用

如果你的应用足够简单,可以在单台服务器(非集群)上运行,你可能希望在容器中使用进程管理器。

Docker Compose

你可能正在使用 Docker Compose 部署到单台服务器(非集群),因此你可能无法在保持共享网络和负载均衡的同时轻松管理容器的副本(使用 Docker Compose)。

这时你可能希望拥有单个容器,其中包含一个进程管理器,在内部启动多个 worker 进程


关键点是,这些都不是必须盲目遵循的铁律。你可以利用这些想法来评估你自己的用例,并决定最适合你系统的方法,评估如何管理以下概念:

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

内存

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

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

如果你的应用很简单,这通常不会成为问题,你可能不需要指定严格的内存限制。但如果你消耗了大量内存(例如使用机器学习模型),你应该检查你的内存消耗情况,并调整每台机器运行的容器数量(或者向你的集群添加更多机器)。

如果你每个容器运行多个进程,你必须确保启动的进程数量不会消耗超过可用内存的资源。

启动及容器化之前的步骤

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

多个容器

如果你有多个容器,可能每个容器都运行单个进程(例如在 Kubernetes 集群中),那么你可能希望有一个单独的容器运行副本 worker 容器之前,先在单个容器中执行之前步骤的工作并运行单个进程。

信息

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

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

单个容器

如果你有一个简单的设置,只有一个单个容器启动多个 worker 进程(或只有一个进程),那么你可以在同一个容器中,在启动应用进程之前运行那些之前的步骤。

基础 Docker 镜像

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

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

如果你正在使用 Kubernetes(或其他系统),并且已经在集群级别设置了包含多个容器副本。在这种情况下,最好从零开始构建镜像,如上所述:为 FastAPI 构建 Docker 镜像

如果你需要多个 worker,只需使用 --workers 命令行选项即可。

技术细节

该 Docker 镜像创建于 Uvicorn 不支持管理和重启死掉的 worker 的时代,因此当时需要将 Gunicorn 与 Uvicorn 结合使用,这增加了一些复杂性,只是为了让 Gunicorn 来管理和重启 Uvicorn worker 进程。

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

部署容器镜像

在拥有容器(Docker)镜像后,有多种部署方法:

例如

  • 使用单台服务器上的 Docker Compose
  • 使用 Kubernetes 集群
  • 使用 Docker Swarm 模式集群
  • 使用 Nomad 等其他工具
  • 使用接收你的容器镜像并进行部署的云服务

使用 uv 构建 Docker 镜像

如果你使用 uv 来安装和管理你的项目,你可以参考他们的 uv Docker 指南

总结

使用容器系统(例如配合 DockerKubernetes),处理所有部署概念变得相当简单。

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

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

注意 Dockerfile 中指令的顺序Docker 缓存的使用,你可以最小化构建时间,从而最大化你的工作效率(并避免无聊)。😎