首页
/ FastAPI Docker 容器化部署完全指南:从零构建镜像到生产级部署实践

FastAPI Docker 容器化部署完全指南:从零构建镜像到生产级部署实践

2026-09-07 09:18:38作者:董宙帆

将 FastAPI 应用打包进 Linux 容器并以 Docker 镜像的形式交付,是当前最常见、最推荐的部署方式之一。本文以官方文档 docs/es/docs/deployment/docker.md(西班牙语译本,内容与英文原版一致)为核心骨架,结合本仓库源码(fastapipyproject.toml、测试用例)逐一拆解:什么是容器与容器镜像、如何基于官方 Python 镜像从零编写一个高效的 Dockerfile、如何利用 Docker 层缓存缩短镜像构建时间,以及 HTTPS、开机自启、重启、副本数、内存、启动前步骤等部署概念在容器体系中如何落地。读完你将能够独立构建并启动一个可用于开发与生产验证的 FastAPI 容器镜像。

为什么用 Linux 容器部署 FastAPI

在部署 FastAPI 应用时,一个很常见的方案是构建 Linux 容器镜像,通常借助 Docker 完成,随后可以把这份镜像以多种方式(单机 Docker Compose、Kubernetes 集群、云服务等)部署出去。

使用 Linux 容器带来的收益包括:

  • 安全性:应用与宿主机上的其他进程、文件系统、网络相互隔离;
  • 可复现性(replicability):镜像承载了应用代码、依赖与运行元数据,任何环境下得到的运行结果一致;
  • 简单性:只需一次构建,即可在不同主机、不同工具链上以相同方式启动。

什么是容器

容器(主要是 Linux 容器)是一种非常轻量的应用打包方式:把应用连同其全部依赖与必要文件打包在一起,同时与同一系统上的其他容器(其他应用或组件)保持隔离。

Linux 容器与宿主机(物理机、虚拟机、云服务器等)共享同一个 Linux 内核,因此相比需要模拟一整个操作系统的完整虚拟机,它要轻量得多,所消耗的资源与直接运行进程相当——这是它在资源占用上的关键优势。

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

什么是容器镜像

容器容器镜像运行而来。二者是不同层面的事物:

  • 容器镜像是全部文件、环境变量、默认启动命令/程序的静态版本。所谓"静态",指镜像并没有在运行,它只是打包好的文件与元数据。可以把镜像类比为"程序文件及其内容"(比如 python 解释器加某个 main.py);
  • 容器通常指正在运行的实例,也就是真正被执行的进程实体。容器启动后可以在其中创建或修改文件、环境变量等,但这些变更只存在于当前容器,不会持久化到底层镜像(不会写回磁盘)。

进一步类比:容器只有当其中有进程在运行时才处于运行状态(通常只有一个进程);一旦主进程退出,容器即停止。

现成的容器镜像生态

Docker 是创建和管理容器镜像与容器的主要工具之一。公开的 Docker Hub 提供了大量官方预构建镜像,例如:

借助预构建镜像,可以非常容易地组合不同工具,例如快速试用一种新数据库。多数情况下你只需使用官方镜像并通过环境变量完成配置。也因此,你在容器与 Docker 上习得的知识可以复用到大量工具上:实际部署时你常常会运行多个容器——一个数据库、一个 Python 应用、一个承载前端 React 应用的 Web 服务器——并通过容器内部网络把它们连接起来。包括 Docker、Kubernetes 在内的容器管理系统都内置了这类组网能力。

容器与进程的关系

容器镜像的元数据中,通常记录着容器启动时应运行的默认程序/命令,以及传给该程序的参数,这和你在命令行直接敲入命令非常相似。容器启动后会执行该命令(你也可以覆盖它,让它运行别的命令)。

核心规则很简单:

  • 只要主进程(命令或程序)在运行,容器就在运行;
  • 一个容器通常只有一个单进程,但从主进程派生子进程也是允许的,此时同一容器内会有多个进程;
  • 不存在"没有任何运行中进程"却仍在运行的容器——主进程一停,容器即停。

提示:完整的部署知识脉络可参考同一目录下的 部署概念;本文后半部分会把其中的概念逐一带入容器语境重新审视。

动手构建一个 FastAPI 的 Docker 镜像

下面从零开始,基于官方 Python 镜像演示如何构建 FastAPI 的 Docker 镜像。这适用于绝大多数场景,例如:使用 Kubernetes 或类似工具、在 Raspberry Pi 上运行、或使用某个替你运行容器镜像的云服务。

文中 Dockerfile 的完整预览如下(TLS 终止代理场景的用法见后文专门小节):

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"]

# 如果运行在 Nginx 或 Traefik 这类代理之后,追加 --proxy-headers
# CMD ["fastapi", "run", "app/main.py", "--port", "80", "--proxy-headers"]

第 1 步:准备依赖清单

uv 管理项目时,直接依赖声明在 pyproject.toml 中,精确解析后的版本锁定在 uv.lock 中。可以用如下命令为应用添加所需包:

$ uv add "fastapi[standard]" pydantic

关于 fastapi[standard]:本仓库的 pyproject.toml 中定义了 standard 额外依赖组,它聚合了运行 FastAPI 生产服务所必需的组件,包括提供 fastapi 命令的 fastapi-cli[standard]、提供 ASGI 服务器的 uvicorn[standard]、用于测试客户端的 httpx、用于模板的 jinja2、用于表单与文件上传的 python-multipart 等。也就是说,一条 uv add "fastapi[standard]" 就能把"写代码 + 跑服务"所需的一整套运行环境装齐。

注意:下面 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 变化时重新生成它。本仓库根目录下的 uv.lockpyproject.toml 即采用这一套管理方式。

第 2 步:编写 FastAPI 应用代码

创建目录 app 并进入;在其中创建一个空的 __init__.py(把 app 声明为一个 Python 包),再创建一个 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}

第 3 步:编写 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 基础镜像python:3.14)。本仓库要求 Python 版本不低于 3.10,并已在 pyproject.toml 的分类器中声明对 Python 3.14 的支持;
  2. 设置当前工作目录为 /code。后续的 requirements.txt 文件与 app 目录都会放在这里;
  3. 先把依赖清单文件复制到 /code。注意此时只复制依赖文件、不复制其余代码——因为该文件不常变化,Docker 会命中缓存,并让下一步也命中缓存;
  4. 安装依赖--no-cache-dirpip 不在本地保存下载的包(这只与 pip 相关,与 Docker 缓存无关;容器场景下通常不会重跑安装同一批包);--upgrade 则要求 pip 在包已存在时进行升级。由于上一步复制文件可被 Docker 缓存命中,本步骤在可用时同样会复用 Docker 缓存,从而避免开发期反复构建镜像时每次都重新下载并安装全部依赖——这一步往往能省下大量时间;
  5. 复制 ./app 代码目录到 /code。代码是变更最频繁的部分,一旦此步的文件发生变化,本步骤及其之后的步骤都无法轻易命中缓存,因此要把它放在 Dockerfile 靠近末尾的位置,以优化镜像构建时长;
  6. 设置容器启动命令fastapi run(底层由 Uvicorn 承载服务)。CMD 接收一个字符串列表,列表里的每一项等同于你在命令行以空格分隔输入的内容。命令会在 WORKDIR /code 指定的当前工作目录下执行。

必须使用 CMD 的 exec 形式

Docker 的 CMD 指令有两种写法:

# 推荐:exec 形式
CMD ["fastapi", "run", "app/main.py", "--port", "80"]
# 不推荐:shell 形式
CMD fastapi run app/main.py --port 80

始终使用 exec 形式。因为 exec 形式下,fastapi run 进程本身成为容器的主进程(PID 1),当容器收到停止信号时 FastAPI 才能被优雅关闭,触发应用注册的 lifespan 启动/关闭事件;而 shell 形式会先启动一层 shell,信号与进程生命周期管理会变得不可靠。这一点在使用 docker compose 时尤为明显——shell 形式往往导致服务重建或停止要等待约 10 秒(超时后才被强制终止)。

目录结构

完成以上步骤后,项目结构应如下:

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

运行在 TLS 终止代理之后

如果容器运行在 Nginx、Traefik 之类的 TLS 终止代理(负载均衡器)之后,需要追加 --proxy-headers 选项,让 Uvicorn(经由 FastAPI CLI)信任代理转发来的请求头,从而正确感知"应用实际运行在 HTTPS 之后"等信息(如 X-Forwarded-Proto 等):

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

第 4 步:理解 Docker 层缓存技巧

这个 Dockerfile 的关键技巧在于先单独复制依赖清单文件

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

原因如下:Docker 等工具是增量式地构建镜像的——从 Dockerfile 顶部开始,一层一层叠加(每条指令生成的产物形成一层);同时它维护一份内部缓存:如果某文件自上次构建以来没有变化,就直接复用上次构建生成的层,而不是重新复制文件、从零创建新层。

仅仅避免复制文件本身收益有限,真正重要的是:该步骤命中缓存后,下一步也能命中缓存,即:

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

依赖清单文件不会频繁变化,因此 Docker 总能命中此步缓存,进而命中"下载并安装依赖"这最耗时的一步。下载安装依赖可能耗时数分钟,而命中缓存时最多几秒;由于开发过程中你会反复构建镜像来验证代码改动,日积月累节省的时间非常可观。

紧接着才在 Dockerfile 末尾复制全部代码:

COPY ./app /code/app

代码是变化最频繁的内容,几乎必然使本步及之后步骤无法命中缓存,所以它被刻意放在最靠近结尾的位置。

第 5 步:构建镜像并启动容器

进入包含 Dockerfile(以及 app 目录)的项目目录,构建镜像:

$ docker build -t myimage .

提示:命令末尾的 . 等价于 ./,它告诉 Docker 使用哪个目录作为构建上下文;这里就是当前目录。

基于该镜像运行容器:

$ docker run -d --name mycontainer -p 80:80 myimage

-d 让容器在后台运行,--name mycontainer 为容器命名,-p 80:80 把宿主机的 80 端口映射到容器的 80 端口(对应 CMD 中的 --port 80)。

验证部署结果

容器启动后,访问容器的 URL 即可验证接口,例如:

  • http://192.168.99.100/items/5?q=somequery
  • 或 http://127.0.0.1/items/5?q=somequery

(依据你的 Docker 主机地址取其一。)

请求会返回类似如下的 JSON:

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

还可以验证两套自动生成的 API 文档:

  • 交互式 API 文档(Swagger UI):访问 /docs,即 http://127.0.0.1/docs ;
  • 替代式 API 文档(ReDoc):访问 /redoc,即 http://127.0.0.1/redoc 。

FastAPI 应用无需任何额外配置,启动后即自动挂载这两套文档路由。

单文件 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 直接指定单文件。

当你把文件路径传给 fastapi run 时,它会自动识别这是一个单文件而非包中的模块,并知道如何正确导入并托管你的 FastAPI 应用。

把部署概念代入容器语境

容器本质上是简化构建与部署流程的工具,它并不强制你用某种特定方式处理下述部署概念——好消息是,无论采用哪种策略,这些概念都能被覆盖。逐一看它们在容器语境下的含义:

  • HTTPS
  • 开机启动与重启
  • 副本数(正在运行的进程数量)
  • 内存
  • 启动前的准备步骤

HTTPS:交给容器之外的组件

如果只聚焦于 FastAPI 应用的容器镜像(以及运行中的容器),HTTPS 通常由外部工具处理。它可以是另一个容器——例如使用 Traefik 来承担 HTTPS 与证书的自动申请与续期(Traefik 对 Docker、Kubernetes 等有原生集成,配置非常方便);也可以由云服务商作为托管服务提供(应用仍运行在容器里)。

开机启动与重启:由容器编排系统负责

通常由另一个工具负责启动与托管容器,例如 Docker、Docker Compose、Kubernetes 或云服务。多数(乃至全部)这类工具都提供了简单选项来支持"开机自启"与"失败自动重启",例如 Docker 的 --restart 命令行选项。不使用容器时,让应用开机自启并支持故障重启往往繁琐困难;而使用容器后,多数情况下这些能力开箱即用

副本与进程数:优先在集群层面复制

如果使用 Kubernetes、Docker Swarm Mode、Nomad 等分布式容器管理系统在多台机器上管理容器(这些机器组成一个集群),那么你更应该在集群层面处理副本数,而不是在每个容器内使用进程管理器(如多 worker 的 Uvicorn)。这类系统通常内置了容器复制与请求负载均衡能力,全部发生在集群层面。此时的最佳实践是像上文一样从零构建镜像、每个容器运行单个 Uvicorn 进程,而非启用多个 worker。

负载均衡器(Load Balancer)

使用容器时,通常会有一个组件监听主端口——它可能是另一个容器,兼作处理 HTTPS 的 TLS 终止代理。由于该组件接管请求负载并以(希望是)均衡的方式分发给 worker,它通常也被称为负载均衡器。做 HTTPS 的那个 TLS 终止代理组件,往往同时就是负载均衡器。容器启动与管理系统的内部网络机制,会负责把来自负载均衡器的 HTTP 通信转发给承载应用的容器。

一个负载均衡器 + 多个 worker 容器

在 Kubernetes 等分布式容器管理系统中,借助其内部组网机制,监听主端口的单个负载均衡器可以把请求转发给多个运行着同一应用的容器。每个容器通常只跑一个进程(一个运行 FastAPI 的 Uvicorn 进程);这些容器彼此完全一致,各自拥有独立的进程、内存等,从而把并行能力扩展到 CPU 的不同核心乃至不同机器。负载均衡器以轮询方式把请求依次分发给每个副本容器。它还可以同时代理集群中其他应用(不同域名或不同 URL 路径前缀)的请求,并转发给对应应用的正确容器。

每容器单进程

在集群复制场景下,建议每个容器只运行一个 Uvicorn 进程,而不要在容器内用 --workers 开多个 worker——副本已经由集群层管理,容器内再引入进程管理器只会增加不必要的复杂度

多进程容器与特殊场景

当然也存在希望在一个容器里跑多个 Uvicorn worker 进程的特殊场景,此时直接使用 --workers 选项:

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

# 用 --workers 把 worker 数量设为 4
CMD ["fastapi", "run", "app/main.py", "--port", "80", "--workers", "4"]

以下几种情况可能更适合这种"单容器多 worker":

  • 简单应用:应用足够简单,跑在单台服务器而非集群上,容器内的进程管理器反而省事;
  • Docker Compose 单机部署:只部署到单台服务器(非集群)时,Docker Compose 不便在保持共享网络与负载均衡的同时管理容器副本,因此用一个容器内进程管理器拉起多个 worker 进程更为直接。

关键提醒:这些都不是必须盲从的铁律。你可以借助这些思路评估自己的用例,针对"安全(HTTPS)、开机启动、重启、副本数、内存、启动前步骤"逐一确认最适合自己系统的方案。

内存:单进程容器的可预期性

如果每容器只跑一个进程,每个容器(复制多个亦然)的内存消耗是相对明确、稳定且有界的。你可以在容器管理系统中(例如 Kubernetes)把同样的内存限制写入配置,系统就能依据"每个容器需要多少内存、集群各机器有多少可用内存"来决定在哪些机器上复制多少个容器

应用简单时通常不必设严格内存上限;但如果内存占用很大(例如加载机器学习模型),应实测内存消耗,据此调整每台机器上运行的容器数量(必要时为集群扩容)。反之,若每容器跑多个进程,则必须确保进程数不会耗尽可用内存。

启动前准备步骤与容器

运行迁移脚本、等待数据库就绪这类"启动前准备",在容器体系下有两种主流做法:

  • 多容器场景(如 Kubernetes 集群中每个容器跑单进程):建议让一个独立容器(单进程)在副本 worker 容器启动之前完成准备步骤——在 Kubernetes 中这通常对应 Init Container(初始化容器)。如果准备步骤可以安全地并行多次执行(例如不是执行数据库迁移,而只是轮询数据库是否就绪),也可以直接把它放进每个容器、在启动主进程前执行;
  • 单容器场景:配置简单、单个容器内启动多个 worker(或单进程)时,直接在同一容器里、于启动应用进程前执行这些准备步骤即可。

不再建议使用的官方基础镜像

历史上 FastAPI 曾有一个官方 Docker 基础镜像 tiangolo/uvicorn-gunicorn-fastapi,但现已废弃。建议不要再使用它(或其他类似的基础镜像):

  • 若使用 Kubernetes 等并在集群层面做复制(多容器),更优做法是像上文那样从零构建镜像
  • 若确需多 worker,直接用 --workers 即可。

技术背景:该镜像诞生时 Uvicorn 尚不支持管理与重启死掉的 worker,必须借助 Gunicorn 来托管并重启 Uvicorn worker 进程,复杂度因此大增;如今 Uvicorn(以及 fastapi 命令)已原生支持 --workers,自己从零写一个镜像与使用基础镜像的代码量几乎相当,没有必要再引入 Gunicorn 那层间接管理。

部署容器镜像的几种方式

得到 Docker 镜像之后,可按以下任一方式部署:

  • 单台服务器上用 Docker Compose
  • Kubernetes 集群;
  • 用 Docker Swarm Mode 集群;
  • 用 Nomad 等其他同类工具;
  • 用云服务商提供的"托管镜像并自动部署"服务。

使用 uv 的用户

如果你用 uv 安装和管理项目,可参考 uv 官方提供的 Docker 集成指南,在容器内以 uv sync 等方式安装依赖(本仓库自身的 uv.lock 锁文件即是 uv 工作流的体现)。当然,本文所述的"uv export 导出 requirements.txt + 容器内 pip 安装"也是一种简洁可行的路径。

关于命令与源码的佐证

为便于你在当前仓库中核对文中结论,这里列出相关实现依据:

  • fastapi CLI 的入口:fastapi 命令的入口脚本声明在 pyproject.tomlfastapi = "fastapi.cli:main");fastapi/cli.py 中把具体实现委托给 fastapi_cli 包,若未安装会提示先执行 pip install "fastapi[standard]"
  • standard 额外依赖组(含 fastapi-cli[standard]uvicorn[standard] 等)定义于 pyproject.toml,这正是 fastapi run 命令可用、并由 Uvicorn 承载服务的安装前提;
  • 命令行为测试:tests/test_fastapi_cli.py 验证了 CLI 对不存在路径报错的退出码(returncode == 1)以及未安装 CLI 时抛出带提示信息的 RuntimeError,从侧面印证了 fastapi 命令由 fastapi[standard] 提供的事实。

小结

借助容器体系(如 DockerKubernetes),前述所有部署概念都能得到简洁优雅的解决:

  • HTTPS:由外部 TLS 终止代理/负载均衡器处理;
  • 开机自启与重启:由容器管理系统内置;
  • 副本数:优先在集群层面复制、每容器单进程;单机特殊场景可用 --workers
  • 内存:单进程容器内存可预期,便于在编排系统中设置限制;
  • 启动前准备步骤:多容器用独立前置容器(Kubernetes Init Container),单容器则直接在启动前执行。

多数情况下,你并不需要任何现成的基础镜像,而是基于官方 Python Docker 镜像从零构建自己的容器镜像。只要注意 Dockerfile 中指令的先后顺序并善用 Docker 层缓存(依赖先行、代码后置),就能把镜像构建时间压到最低,把等待时间还给真正的开发工作。

登录后查看全文
热门项目推荐
相关项目推荐