FastAPI 容器化部署实战:容器原理、Dockerfile 构建与生产环境部署策略
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
本文基于 FastAPI 官方文档的部署章节 FastAPI in Containers – Docker,系统讲解如何为 FastAPI 应用构建 Linux 容器镜像:从容器与容器镜像的基本概念、进程模型出发,完整给出Dockerfile的逐行构建方法、Docker 缓存优化技巧,以及 HTTPS、进程复制、内存管理、启动前步骤等生产级部署策略。读完后,你将能够独立为一个 FastAPI 项目编写可运行的容器镜像,并根据单机或集群场景选择合适的进程与复制策略。
什么是容器
在部署 FastAPI 应用时,一种常见做法是构建一个Linux 容器镜像(通常使用 Docker),然后再以多种不同方式部署该镜像。使用 Linux 容器可以带来安全性、可复现性、简便性等多方面的收益。
容器(主要指 Linux 容器)是一种极其轻量的应用打包方式:它把应用连同其全部依赖与必需文件一起封装,同时与同一系统中的其他容器(其他应用或组件)相互隔离。
- Linux 容器运行在宿主机(物理机、虚拟机、云服务器等)相同的 Linux 内核之上,因此非常轻量(与需要模拟完整操作系统的完整虚拟机相比)。
- 容器消耗的资源很少,大致相当于直接运行进程的开销(而虚拟机需要消耗得多得多)。
- 容器拥有各自隔离运行的进程(通常只有一个进程)、独立文件系统和独立网络,从而简化了部署、安全与开发流程。
容器与容器镜像:静态内容 vs 运行实例
容器由容器镜像运行而来,两者需要区分:
- 容器镜像是一个静态的、打包好的所有文件、环境变量与默认命令/程序集合。"静态"意味着镜像本身不运行、不执行,只包含打包的文件和元数据。
- 容器通常指运行中的实例。当容器被启动(由某个镜像启动)后,它可以创建或修改文件、环境变量等,这些变更只存在于该容器内,不会写回底层镜像(不落盘)。
一个直观的类比:
| 概念 | 类比 |
|---|---|
| 容器镜像 | 程序文件本身,例如python解释器和main.py文件 |
| 容器 | 实际运行的进程;只有存在至少一个运行中的进程,容器才算在运行(通常只有一个进程),进程停止则容器停止 |
容器镜像生态:官方镜像
Docker 是创建和管理容器镜像与容器的重要工具之一。公开镜像仓库中有大量官方容器镜像,覆盖各类工具、环境和数据库,例如:
- 官方 Python 镜像
- PostgreSQL、MySQL、MongoDB、Redis 等数据库镜像
借助现成的容器镜像,可以非常轻松地组合使用各种工具(比如快速试用一个新数据库)。大多数情况下直接使用官方镜像,并通过环境变量进行配置即可。
典型架构下,你会运行多个容器——例如一个数据库容器、一个 Python 应用容器、一个承载 React 前端应用的 Web 服务器容器——并通过内部网络相互连接。所有容器管理系统(Docker、Kubernetes 等)都内置了这些网络能力。
容器与进程模型
一个容器镜像通常在其元数据中定义了容器启动时要执行的默认程序/命令,以及传递给该程序的参数,方式与在命令行中启动它几乎相同。
- 容器启动时会执行该命令(你也可以覆盖它,执行其他命令或程序)。
- 容器只要主进程(命令或程序)还在运行,就会保持运行。
- 容器通常只有一个进程,但主进程可以派生子进程,从而在同一容器内得到多个进程。
- 容器不可能在没有至少一个运行中进程的情况下存活;主进程停止,容器即停止。
这一点直接决定了后文 Dockerfile 中CMD指令的设计,也决定了为什么容器内要跑"单进程 + 由集群负责复制"的架构。
为 FastAPI 从零构建 Docker 镜像
下面基于官方 Python 镜像,从零构建一个 FastAPI 的 Docker 镜像。这是大多数场景下的推荐做法,例如:
- 使用Kubernetes或类似工具
- 在Raspberry Pi上运行
- 使用替你运行容器镜像的云服务,等等
包依赖:使用 uv 管理
如果你用uv管理项目,直接依赖声明在pyproject.toml中,精确解析后的版本记录在uv.lock中。添加应用所需的包:
$ uv add "fastapi[standard]" pydantic注意:Dockerfile 在容器内使用pip安装依赖,因此可以把 uv 项目中的锁定依赖导出为requirements.txt格式:
$ uv export --format requirements-txt --no-dev --no-emit-project --output-file requirements.txt生成的requirements.txt是专供容器构建使用的导出文件:依赖仍通过uv add管理,当uv.lock变化时重新生成即可。
为什么必须安装fastapi[standard]?从本仓库源码可以印证这一点:fastapi/cli.py 中fastapi命令的入口直接委托给fastapi_cli.cli.main,并且若未安装fastapi-cli会明确提示pip install "fastapi[standard]"。而 pyproject.toml 中standard额外依赖组包含了fastapi-cli[standard]和uvicorn[standard]——也就是说,fastapi run命令及其底层的 Uvicorn 服务器都由fastapi[standard]提供,这正是容器内运行应用所需的核心运行时。同时该文件声明requires-python = ">=3.10",因此基础镜像选用python:3.14完全满足要求。
创建 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"] # 若你位于 Nginx 或 Traefik 等代理之后,需追加 --proxy-headers # CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]各步骤说明:
- 使用官方 Python 基础镜像
python:3.14开始。 - 将当前工作目录设为
/code,requirements.txt与app目录都会放在这里。 - 先只拷贝依赖清单文件,而不是代码。由于该文件很少变化,Docker 会识别并为此步骤使用缓存,同时激活下一步骤的缓存。
- 从依赖文件安装包:
--no-cache-dir指示pip不要本地保留已下载的包——它只在需要重新运行pip安装相同包时才有用,而容器构建场景中并不适用。注意:该选项只与pip相关,与 Docker 或容器无关。--upgrade指示pip在包已存在时进行升级。- 由于前一步的
COPY可被 Docker 缓存命中,本步骤同样会命中缓存。开发阶段反复构建镜像时,缓存命中能省下每次重新下载并安装全部依赖的时间。
- 将
./app目录拷贝到/code。这里包含变化最频繁的代码,因此 Docker 缓存从这一步起基本无法再被复用。将其靠近 Dockerfile 末尾放置,是为了优化镜像构建时间。 - 用
CMD指定运行命令fastapi run,其底层调用 Uvicorn。CMD接收一个字符串列表,每一项等价于你在命令行中以空格分隔输入的一个词。该命令从当前工作目录(即前面WORKDIR指定的/code)执行。
CMD:务必使用 Exec Form
Docker 的CMD指令有两种写法:
✅Exec form(正确用法):
# ✅ 这样做 CMD ["fastapi", "run", "app/main.py", "--port", "80"]⛔️Shell form(避免使用):
# ⛔️ 不要这样做 CMD fastapi run app/main.py --port 80始终使用exec form,以确保 FastAPI 能正确收到终止信号、正常关停,并且 Lifespan 事件能被正确触发。这一点在使用docker compose时会体现得尤为明显:Shell form 下信号被中间 shell 吞掉,导致服务停止需要额外等待超时;而 exec form 让主进程直接收到 SIGTERM,关停迅速完成。
目录结构
最终的项目结构应如下:
. ├── app │ ├── __init__.py │ └── main.py ├── Dockerfile └── requirements.txt位于 TLS 终止代理之后
如果你的容器运行在 Nginx 或 Traefik 等 **TLS 终止代理(负载均衡器)**之后,需要追加--proxy-headers选项。它告诉 Uvicorn(经由 FastAPI CLI)信任该代理发来的头部,并据此判定应用实际运行在 HTTPS 之后(影响重定向、客户端真实 IP 等行为)。
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]Docker 缓存:构建提速的关键技巧
这个 Dockerfile 里有一个重要技巧:先只拷贝依赖文件,不拷代码。原理如下:
Docker 等工具增量构建容器镜像,自Dockerfile顶部起逐条指令、一层一层地叠加文件。构建过程中工具还会维护内部缓存:若某文件自上次构建以来没有变化,就直接复用上次创建的层,而不是重新拷贝文件、从零创建新层。
单看"避免拷贝文件"本身收益有限,关键在于:这一步命中缓存后,下一步也能命中缓存——即依赖安装指令:
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt依赖文件很少变化,所以 Docker 只拷贝这一份文件并命中缓存,随后下载并安装依赖的下一步也能命中缓存,从而省下大量时间:下载安装包依赖可能需要数分钟,而缓存命中只需数秒。开发阶段你需要反复构建镜像以验证代码改动,缓存带来的时间收益非常可观。
而在Dockerfile靠后的位置才拷贝全部代码:
COPY ./app /code/app因为代码变化最频繁,几乎每次都会导致这一步及其之后的所有层无法再使用缓存,所以放在末尾。
构建 Docker 镜像
所有文件就位后,在项目目录(即Dockerfile与app目录所在目录)中构建镜像:
$ docker build -t myimage .注意结尾的.:它等价于./,告诉 Docker 使用哪个目录来构建容器镜像——此处即当前目录。
启动 Docker 容器
基于镜像运行一个容器:
$ docker run -d --name mycontainer -p 80:80 myimage验证接口
在容器的地址下访问,例如http://127.0.0.1/items/5?q=somequery(或使用你的 Docker 主机地址),应返回:
{"item_id": 5, "q": "somequery"}交互式 API 文档
访问http://127.0.0.1/docs(或 Docker 主机地址的等价路径),可以看到自动生成的交互式 API 文档(Swagger UI 提供):
备用 API 文档
访问http://127.0.0.1/redoc(或等价路径),可以看到由 ReDoc 提供的备用自动文档:
单文件 FastAPI 应用的 Docker 镜像
如果你的应用只是单个文件(例如直接是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"]- 直接把
main.py拷贝到/code(不再有./app目录)。 - 使用
fastapi run运行单文件应用。
把文件直接传给fastapi run后,它会自动识别出这是一个单文件(而非包的一部分),从而知道如何导入并托管你的 FastAPI 应用。
容器环境下的部署策略
结合 部署概念回顾:容器主要简化构建与部署流程,但不强制规定如何落地各项部署策略,实际有多种可选方案——而每种方案都能覆盖以下概念:HTTPS、开机自启、故障重启、复制(运行中的进程数)、内存、启动前步骤。
HTTPS
只聚焦于 FastAPI 应用的容器镜像(以及后来的运行中容器)时,HTTPS 通常由外部其他工具处理。它可以是另一个容器,例如 Traefik,负责HTTPS与证书自动签发/续期(Traefik 与 Docker、Kubernetes 等均有集成,可为容器轻松配置 HTTPS)。也可以由云服务商作为托管服务的一部分处理(应用仍在容器中运行)。
开机自启与故障重启
通常由另一个工具负责启动并保活你的容器:可能是 Docker 本身、Docker Compose、Kubernetes 或某个云服务。大多数情况下都有简单的选项让容器开机启动并在故障时重启,例如 Docker 的--restart命令行选项。不用容器时,实现开机自启与自动重启往往麻烦且困难;而容器化场景下,这类能力在大多数情况下开箱即用。
复制:进程数量策略
当你用Kubernetes、Docker Swarm Mode、Nomad 或类似的分布式容器系统管理多机集群时,应当把复制放在集群层面解决,而不是在每个容器内再放一个进程管理器(如 Uvicorn 多 Worker)。
这类分布式容器系统通常内置容器复制能力,同时对入站请求做负载均衡,全部在集群层面完成。此时应该如前文所述从零构建 Docker 镜像,安装依赖后运行单个 Uvicorn 进程,而不是多个 Uvicorn Worker。
负载均衡器
使用容器时,通常有一个组件监听主端口。它可能是另一个容器,同时也是TLS 终止代理(处理 HTTPS)或类似工具。该组件承接请求负载,并将其(理想情况下)均匀地分发到各 Worker,因此通常也被称为负载均衡器。使用 HTTPS 的同一个 TLS 终止代理组件,往往同时就是负载均衡器。
当你用某个系统启动并管理容器时,该系统已经具备内部工具,能把这个负载均衡器(可能兼作 TLS 终止代理)的流量转发到你应用所在的各个容器。
一个负载均衡器 + 多个 Worker 容器
在Kubernetes或类似系统中,监听主端口的单个负载均衡器可以把请求转发到多个运行着你应用的容器。
每个这样的容器通常只有一个进程(例如一个运行 FastAPI 应用的 Uvicorn 进程)。它们都是相同的容器,运行相同的内容,但各自拥有独立的进程、内存等。这样就能利用 CPU 的不同核心,甚至不同机器实现并行;负载均衡器会轮流把请求分给每个复制容器,任一请求都可能由其中一个容器处理。
此外,这个负载均衡器通常还能处理发往集群中其他应用的请求(其他域名或 URL 前缀),并把它们转发到对应应用所在的容器。
每个容器一个进程
在这种场景下,你希望每个容器只有一个(Uvicorn)进程,因为复制已经在集群层面完成了。因此不要用--workers在容器内起多个 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"]- 用
--workers命令行选项将 Worker 数设为 4。
适用示例:
- 简单应用:应用足够简单,部署在单台服务器上而非集群,此时在容器内使用进程管理器是合理的。
- Docker Compose:在单台服务器(非集群)上用 Docker Compose 部署时,可能无法方便地管理容器复制并配合共享网络做负载均衡。此时更适合单个容器 + 进程管理器的方式,在其中启动多个 Worker 进程。
核心要点:以上规则没有一条是刻在石头上、必须盲从的。用它们来评估自己的场景,为系统选择最合适的方案,并据此处理:安全(HTTPS)、开机自启、重启、复制(运行中的进程数)、内存、启动前步骤。
内存
运行每容器单进程时,每个容器(以及复制出来的每个容器)消耗的内存大致是明确、稳定、有界的。这样你可以在容器管理系统(如Kubernetes)的配置中设定相同的内存限制与需求,使其根据所需内存与集群机器上的可用内存,把容器合理地调度、复制到可用机器上。
- 如果应用简单,通常不是问题,可以不设硬性内存限制。
- 如果内存消耗很大(例如加载机器学习模型),应测量实际内存用量,调整每台机器上运行的容器数(必要时给集群增加机器)。
- 如果运行每容器多进程,务必确保进程总数消耗的内存不超过可用内存。
启动前步骤(数据库迁移等)与容器
使用容器时(如 Docker、Kubernetes),主要有两种做法:
多个容器
如果运行着多个容器、每个容器跑单个进程(如Kubernetes集群),通常希望有一个独立容器,用单个进程在所有被复制的 Worker 容器启动之前完成启动前步骤的工作。使用 Kubernetes 时,这通常对应Init Container。
如果你的场景允许这些前置步骤多次并行执行(例如只是检查数据库是否就绪,而非执行数据库迁移),也可以简单地让每个容器在启动主进程前直接执行这些步骤。
单个容器
如果是简单的单容器配置(内部启动多个 Worker 进程或仅一个进程),可以把前置步骤放在同一容器内,直接在应用进程启动之前执行。
关于"FastAPI 基础镜像"
曾存在一个官方 FastAPI Docker 基础镜像tiangolo/uvicorn-gunicorn-fastapi,但它已被弃用,不应该再使用这个(或类似)基础镜像。
技术背景:该镜像诞生于 Uvicorn 尚不支持管理与重启崩溃 Worker 的时代,因此需要用 Gunicorn 托管 Uvicorn Worker,引入了相当多的复杂度。而如今 Uvicorn(以及fastapi命令)已支持--workers,已没有理由使用第三方基础镜像——从零自建镜像即可,且其中实际包含几乎相同的代码。如前文所述,从零构建(基于官方 Python 镜像)是推荐做法;需要多个 Worker 时,直接用--workers命令行选项即可。
部署容器镜像
构建好容器镜像后,有多种部署途径,例如:
- 用Docker Compose部署到单台服务器
- 部署到Kubernetes集群
- 部署到 Docker Swarm Mode 集群
- 使用 Nomad 等其他工具
- 使用接收你的容器镜像并替你部署的云服务
如果项目使用uv管理依赖,可参考uv官方的 Docker 集成指南来编写更细化的构建流程(如按层安装、锁定依赖等)。
小结
借助容器系统(Docker、Kubernetes 等),落地全部部署概念变得相当简单:
- HTTPS
- 开机自启
- 故障重启
- 复制(运行中的进程数)
- 内存
- 启动前步骤
大多数情况下,不建议使用第三方基础镜像,而应从零构建容器镜像,基于官方 Python Docker 镜像。通过关注Dockerfile中指令的顺序与Docker 缓存,可以最小化构建时间,从而提升开发效率。配合仓库中的 FastAPI CLI 入口实现与 pyproject.toml,可以确认fastapi run+fastapi[standard]是容器内运行的标准运行时组合。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考