在 Docker 容器中部署 FastAPI:从零构建镜像与生产级部署的完整指南
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
容器化是现代 FastAPI 应用交付的主流方式:你只需写好一份Dockerfile,就能把应用连同依赖打包成可在任意 Linux 主机、云服务乃至 Kubernetes 集群中重复运行的标准镜像。本文以当前仓库的官方部署文档(docs/ko/docs/deployment/docker.md)为骨架,逐行拆解如何基于官方 Python 镜像从零编写 FastAPI 的Dockerfile,并结合仓库源码说明fastapi run、--proxy-headers、--workers等关键选项背后的实现,帮助你用最小的构建时间换回最大的生产可维护性。
为什么选择 Linux 容器:安全、可复现、轻量
在服务器(物理机、虚拟机或云主机)上部署 FastAPI 时,最常见的做法是构建Linux 容器镜像,随后用 Docker、Kubernetes 等工具把它运行起来。容器之所以成为主流,主要得益于三点:
- 可复现性:应用及其全部依赖、配置文件被完整打包,换一台机器运行结果一致,不再有"在我机器上能跑"的问题;
- 安全性:容器内运行的应用与其他容器(其他应用或组件)彼此隔离,天然具备进程级边界;
- 简单性:整个交付单元只有一个镜像,启动、分发、回滚的语义都变得非常清晰。
Linux 容器与完整虚拟机(VM)的本质差异在于:容器与宿主机共享同一个 Linux 内核,只对文件系统、进程、网络等做隔离,因此它比"模拟整套操作系统"的虚拟机轻量得多——消耗的资源接近直接运行一个进程,而不是为每个实例单独准备一套完整 OS。
先厘清两个基础概念:容器镜像 vs 容器
官方文档特意强调了一组容易混淆的概念,理解了它们,后面的部署讨论才有共同语言:
- **容器镜像(Container Image)**是"静态"的:它是容器启动所需全部文件、环境变量、默认命令与元数据的一次性打包产物。镜像本身从不"运行",只是存储下来的文件与元数据。
- 容器(Container)则指镜像的运行实例:它从镜像启动后可以创建、修改文件与环境变量,但这些改动只存在于当前这个运行中的容器内,不会写回到底层镜像(不会持久化到磁盘)。
一个直观的类比是:容器镜像 ≈ 一份"程序文件 + 它的内容"(如python解释器加上main.py);而容器 ≈ 由该程序启动出的进程。容器只有在至少一个主进程存活时才处于运行状态——主进程一旦退出,容器随即停止。
借助 Docker Hub 的官方镜像组合技术栈
Docker 是创建与管理容器镜像、容器的主流工具之一。Docker Hub 上托管着大量预置的官方容器镜像,例如:
- 官方 Python 镜像(本文构建 FastAPI 镜像的基础)
- 以及数据库、缓存类镜像:PostgreSQL、MySQL、MongoDB、Redis 等
使用预置官方镜像最大的好处是"即取即用":多数场景下只需配置环境变量即可完成初始化。这样你可以同时运行多个容器——比如一个跑 Python 后端、一个跑数据库、一个跑 React 前端——让它们通过内部网络相互通信。Docker、Kubernetes 等所有容器编排系统都内置了这种容器间组网能力。
从零构建 FastAPI 容器镜像:完整实操
官方部署文档推荐在绝大多数场景下都采用"从零构建"的方式,而不是依赖某个封装好的第三方镜像,典型场景包括:
- 在Kubernetes或类似编排工具中运行;
- 在Raspberry Pi等边缘设备上运行;
- 使用"帮你运行容器镜像"的各类云服务。
下面按官方文档的步骤,从依赖管理、应用代码到Dockerfile逐步走一遍。
第一步:生成依赖清单(requirements.txt)
官方文档假设你使用uv管理 Python 项目:直接依赖声明在pyproject.toml,精确解析出的版本锁定在uv.lock。为应用添加包依赖可执行:
$ uv add "fastapi[standard]" pydantic ---> 100%需要说明的是,fastapi[standard]这个 extra 非常重要。查看本仓库 pyproject.toml 中的[project.optional-dependencies]定义可以看到,它实际会拉入:
standard = [ "fastapi-cli[standard] >=0.0.32", # 提供 fastapi run / fastapi dev 命令 "uvicorn[standard] >=0.12.0", # 内置 ASGI 服务器(含 uvloop) "python-multipart >=0.0.18", # 表单与文件上传 "jinja2 >=3.1.5", # 模板渲染 "httpx >=0.23.0,<1.0.0", # 测试客户端 # ... 邮件校验、pydantic-settings 等 ]也就是说,fastapi[standard]已经自带了文档中CMD所使用的fastapi run命令及其背后真正干活的 Uvicorn 服务器。
不过,下文Dockerfile中是在容器内部使用pip安装依赖的。对于 uv 项目,需要先把uv.lock中锁定的依赖导出为 Dockerfile 所期望的requirements.txt格式:
$ uv export --format requirements-txt --no-dev --no-emit-project --output-file requirements.txt官方文档特别提示:导出的requirements.txt只是用于容器构建的产物,日常依赖管理仍应继续使用uv add,并记得在uv.lock变化后重新导出该文件。
第二步:编写 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}其中app是模块级创建的FastAPI()实例,fastapi run会自动发现并为其提供服务。
第三步:编写 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"]逐条指令的含义如下:
FROM python:3.14:以官方 Python 镜像为基础,里面已预装 Python 解释器,后续所有操作都叠加在这层之上。WORKDIR /code:把当前工作目录设置为/code。之后requirements.txt与app目录都会放在这里,CMD中的命令也在此目录下执行。COPY ./requirements.txt /code/requirements.txt:只把依赖清单单独复制进镜像。因为它极少变动,Docker 会在此层命中缓存,并让后续依赖安装层也吃到缓存(详见下文"Docker 缓存"一节)。RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt:安装清单中的全部依赖。--no-cache-dir阻止pip在本地缓存下载的包——该缓存只对"在同一环境中重复安装同一批包"有意义,而在容器构建中每次都是从干净层开始,因此应关掉以减小镜像体积。官方文档特别注明:该参数只与pip有关,与 Docker / 容器本身无关。--upgrade让pip升级任何已存在的同包旧版本。- 由于上一步的文件复制能被 Docker 缓存机制识别,本步骤也尽量复用缓存层;依赖下载安装通常需要数分钟,命中缓存后最快几秒即可完成。
COPY ./app /code/app:把包含业务代码的app目录复制进去。这是最频繁变化的内容,几乎必然导致该层及其后各层缓存失效,因此务必把它放在 Dockerfile靠后的位置。CMD ["fastapi", "run", "app/main.py", "--port", "80"]:设定容器启动时执行的命令。fastapi run在内部使用 Uvicorn 作为服务器运行应用(--port 80监听 80 端口),并自动以模块方式导入app.main中的app。
需要留意的一点是:若容器环境中没有安装fastapi-cli,直接调用fastapi命令会失败。这一点可以直观地从本仓库 fastapi/cli.py 的入口实现看到——当fastapi_cli未被引入(即未安装fastapi[standard])时,它会打印提示并抛出错误:
def main() -> None: if not cli_main: message = 'To use the fastapi command, please install "fastapi[standard]":\n\n\tpip install "fastapi[standard]"\n' print(message) raise RuntimeError(message)因此,文档第一步用uv add "fastapi[standard]"(或等价的pip install "fastapi[standard]")安装依赖,正是让CMD里的fastapi run可用的前提。
CMD 必须使用 exec form(shell form 的坑)
Docker 的CMD指令有两种写法,官方文档强烈建议始终使用 exec form:
# ✅ 正确:exec form(把命令与各参数作为字符串数组) CMD ["fastapi", "run", "app/main.py", "--port", "80"]# ⛔ 错误:shell form(整条命令当作字符串) CMD fastapi run app/main.py --port 80原因是:exec form 下CMD直接作为容器主进程执行,容器停止时 Docker 能把终止信号(SIGTERM)直接交给该进程,从而让 FastAPI 完成优雅关闭(graceful shutdown),并正确触发应用生命周期(lifespan)中的关闭事件;而 shell form 会把命令交给/bin/sh启动一层子 shell,信号先到达 shell,容易导致子进程收不到信号、服务无法平滑退出。该问题在使用docker compose时尤为明显——容器重建或停止往往会卡住整整约 10 秒等待超时。
目录结构确认
完成上述步骤后,项目应呈现如下结构:
. ├── app │ ├── __init__.py │ └── main.py ├── Dockerfile └── requirements.txt运行在 TLS 终止代理之后:加上 --proxy-headers
如果你把容器放在 Nginx、Traefik 这类TLS 终止代理(负载均衡器)后面,需要在CMD中追加--proxy-headers选项:
CMD ["fastapi", "run", "app/main.py", "--proxy-headers", "--port", "80"]该选项会让 Uvicorn(经由fastapi run透传)信任代理转发的请求头(主要是X-Forwarded-*系列),从而让应用正确感知自己正运行在 HTTPS 之后,保证request.url、重定向生成等逻辑拿到的是外部 HTTPS 地址而非内部明文地址。
镜像构建的时间密码:Docker 缓存
这份 Dockerfile 暗含一个重要的性能技巧——先只复制依赖清单、后复制业务代码:
COPY ./requirements.txt /code/requirements.txt # 先:低频变化 → 缓存友好 RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt COPY ./app /code/app # 后:高频变化 → 放末尾Docker(以及同类构建工具)是从 Dockerfile 顶部开始、按每条指令产生的文件一层层向上叠加来增量构建镜像的,同时维护内部缓存:若某个输入文件自上次构建以来没有变化,就直接复用先前生成的同层,而不重新复制文件、重算层。要点在于,只要某一步命中缓存,其后所有步骤也都能基于缓存层继续——依赖安装那一步通常耗时数分钟,全靠这一机制被压缩到几秒。而业务代码是迭代最频繁的部分,放在 Dockerfile 末尾意味着:改一行代码只会让"复制代码"这一层及其之后的层失效,代价最小。开发期反复docker build验证改动的场景下,积少成多能节省大量等待时间。
构建镜像并启动容器
构建镜像
确认所有文件就位后,在项目目录(即存放Dockerfile与app目录的位置)执行:
$ docker build -t myimage . ---> 100%-t myimage为镜像命名;行尾的.等价于./,它告诉 Docker 使用哪个目录作为构建上下文(本例为当前目录)。
启动容器
基于构建好的镜像启动容器:
$ docker run -d --name mycontainer -p 80:80 myimage各参数含义:-d后台(detached)运行;--name mycontainer为容器命名便于管理;-p 80:80把宿主机的 80 端口映射到容器的 80 端口(对应镜像内 Uvicorn 监听的端口)。
验证服务是否就绪
容器运行后,即可通过宿主机地址访问。例如:
http://127.0.0.1/items/5?q=somequery(或换成 Docker 主机的实际 IP,如http://192.168.99.100/items/5?q=somequery)
应看到:
{"item_id": 5, "q": "somequery"}这证明路径参数item_id与查询参数q均已被正确解析,FastAPI 应用在容器内正常工作。
顺带收获:自动生成的交互式 API 文档
同一套镜像还免费附带自动生成的交互式文档。访问http://127.0.0.1/docs(或http://192.168.99.100/docs),即可看到基于Swagger UI的接口文档页,可以直接在页面上对每个接口发起请求调试:
如果偏好另一种风格,访问http://127.0.0.1/redoc可看到基于ReDoc的替代文档视图,同样由 FastAPI 自动生成:
这两份文档能直接证明:容器内外没有任何差异,应用暴露的全部 OpenAPI 能力都被完整携带。
单文件 FastAPI 应用的镜像变体
如果应用没有app包、只有一个独立的main.py文件,目录结构会是:
. ├── 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服务单文件main.py中的应用。
fastapi run收到一个普通文件路径时,会自动识别出它不属于某个包,并推算出应如何导入其中定义的 FastAPI 应用来提供服务,无需额外配置模块路径。
从部署概念的高度审视容器化方案
容器主要用于简化应用的构建与部署,但它并不强制你用某种特定方式处理部署中的通用问题。好在针对下面每一项概念,容器生态都提供了可行策略。官方文档将这些概念定义为:HTTPS、开机自启、重启、复制(进程数)、内存、启动前的预置步骤——这些概念在 docs/ko/docs/deployment/concepts.md 中有更完整的展开。
HTTPS:通常交给容器外部处理
如果只聚焦应用自身的镜像与容器,HTTPS 一般由外部组件负责终结(TLS Termination)。例如:
- 用另一个运行Traefik的容器处理 HTTPS 与证书自动申请/续期(Traefik 深度集成 Docker、Kubernetes,为容器挂 HTTPS 非常省事);
- 或者由云服务商把 HTTPS 作为服务的一部分提供(应用仍以容器方式运行)。
开机自启与失败重启
通常存在另一个组件负责"把容器拉起来并保持其运行",可能是 Docker 本身、Docker Compose、Kubernetes 或云平台。它们大都提供一行式开关,例如 Docker 的--restart命令行选项。这正是容器相对裸进程的一大优势:不做容器化时,"开机自启 + 自动重启"往往要写 systemd 单元并反复调试,而容器化后这通常是开箱即用的默认能力。
复制(进程数):集群级复制优先于容器内多进程
当使用 Kubernetes、Docker Swarm Mode、Nomad 这类可在多台机器上调度容器的集群系统时,官方建议在集群层面处理复制,而不是在单个容器内塞入多进程:
- 集群系统自带把请求负载均衡到多个容器副本的机制;
- 每个副本容器通常只跑一个 Uvicorn 进程,各自拥有独立进程与内存,可充分利用不同 CPU 核心乃至不同机器并行;
- 集群里的负载均衡组件把请求轮流分发到各个副本,任意请求都可能被任意副本处理;
- 这些负载均衡组件通常同时也是TLS 终止代理。
因此在这一场景中,更推荐本文前面"从零构建"的单进程 Dockerfile,而不是在容器内使用多 worker 的进程管理器——后者只是在集群已处理的复制问题上额外引入复杂度。
特殊情形:单个容器内跑多个 worker
某些场景下,单容器多 worker 反而是合理选择,此时给CMD追加--workers选项即可(fastapi run直接透传给 Uvicorn):
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 4让容器内启动 4 个 Uvicorn worker 进程。
典型适用场景包括:
- 足够简单的单机应用:应用整体能在单一服务器上跑,没必要上集群;
- 用 Docker Compose 部署到单台服务器:Compose 对"共享网络 + 负载均衡"下的容器级复制支持有限,此时更实际的做法是让容器内的进程管理器拉起多个 worker 进程。
需要强调的是,以上没有一条是绝对铁律。官方文档的意图是让读者基于这些思路评估自己的用例,围绕安全(HTTPS)、自启、重启、复制、内存、启动前预置这六项概念,为系统选择最合适的策略组合。
内存
采用每容器单进程模型时,每个(可能被复制的)容器消耗的内存通常更稳定、可预期、有明确上限。之后可在编排系统(如 Kubernetes)中设置等价的内存 limit/request,调度器会结合集群机器的可用内存决定副本数量。对简单应用,通常无需苛刻的内存上限;但对高内存应用(例如加载机器学习模型),应测量单实例峰值内存,再据此决定每台机器跑多少个容器(必要时为集群增加机器)。
反过来,若在单个容器内运行多个进程,则需要自行确保 worker 数量不会让总内存超配。
启动前的预置步骤(migrations 等)
需要"在服务启动前先做某件事"(如数据库迁移)时,取决于你的容器拓扑:
- 多容器(推荐于集群):在副本 worker 容器就绪之前,用一个独立的一次性容器执行预置步骤——在 Kubernetes 中这通常对应Init Container。若预置步骤可以安全地并行执行多次(例如只是轮询等待数据库就绪,而不是跑迁移),也可以把该步骤写进每个容器的启动流程里。
- 单容器:如果只是单容器多 worker(或单 worker)的简单拓扑,则在同一容器内、启动应用主进程之前先执行预置步骤即可。
历史遗留:为什么不再使用第三方"全家桶"镜像
过去官方曾维护过专门的 FastAPI 容器镜像(tiangolo/uvicorn-gunicorn-fastapi),但如今它已被deprecated,不建议继续使用(也不建议再找同类镜像替代)。官方文档给出的技术背景是:这套镜像诞生于Uvicorn 还不支持管理/重启失效 worker的年代,因此不得不引入 Gunicorn 作为进程管理器来拉起并守护多个 Uvicorn worker 进程,这带来了相当可观的额外复杂度。而现在 Uvicorn(以及fastapi run命令)已原生支持--workers,亲手写一份简单 Dockerfile 的成本与历史镜像几乎持平,却换来完全可控的镜像内容,因此没有理由再依赖这类封装镜像。
镜像构建完成之后:分发与部署的多种途径
得到容器镜像只是第一步,官方文档列举了若干部署途径:
- 单台服务器上用Docker Compose编排;
- 部署进Kubernetes集群;
- 使用Docker Swarm Mode集群;
- 使用Nomad等其它调度工具;
- 直接推给支持托管运行容器镜像的云服务。
若项目的安装与依赖管理使用 uv,可进一步参考 uv 官方的 Docker 集成指南,用其原生能力替代本文中"导出 requirements.txt + pip 安装"的路径,以获得更优的构建缓存与更快的安装速度。
小结
围绕文档给出的六项部署概念(HTTPS、开机自启、重启、复制、内存、启动前预置),容器系统(Docker / Kubernetes 等)都能提供简单直接的对策,让生产部署的复杂度大幅下降:
- 多数情况下建议弃用第三方封装镜像,直接基于官方 Python 镜像从零构建;
- 精心安排
Dockerfile指令顺序以最大化 Docker 缓存命中——把不变的依赖层放前、易变的业务代码放后,能把反复构建的等待时间从分钟级压到秒级,是提升日常迭代效率的关键细节。
掌握了从零编写 Dockerfile、缓存编排、exec form、代理头透传与单容器多 worker 的取舍之后,你的 FastAPI 应用就具备了进入任何容器化生产环境的最后一块拼图。
【免费下载链接】fastapiFastAPI framework, high performance, easy to learn, fast to code, ready for production项目地址: https://gitcode.com/GitHub_Trending/fa/fastapi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考