☰
FastAPI 项目 Docker 部署实战:从镜像构建到生产上线
2026/10/21 15:16:53 网站建设 项目流程

1. 为什么我最终选择了 Docker 来跑 FastAPI 项目

刚把本地调试通过的 FastAPI 项目往服务器上搬的那会儿,我踩过一个特别典型的坑:本地 Python 3.11 跑得好好的,服务器上是 3.9,match语法直接报错;本地装的pydantic是 2.x,服务器上残留着 1.x 的旧版本,接口一调就抛ValidationError。折腾了大半天,最后靠手动降级、升级一堆包才勉强跑起来。那次之后我就下定决心,以后所有 FastAPI 项目一律用 Docker 打包,环境问题一次性解决。

FastAPI 这个框架本身很轻,启动快、写起来舒服,自带 OpenAPI 文档,做后端接口特别顺手。但它对依赖版本比较敏感,尤其是pydantic、starlette、uvicorn这几个核心组件,版本错一个就可能出玄学问题。Docker 的价值就在这儿:把 Python 解释器、依赖包、系统库、启动命令全部封进一个镜像里,换台机器照样跑,不用再问“你那边 Python 什么版本”。

这篇内容适合三类人看:一是刚写完 FastAPI 项目、准备部署上线的开发者;二是被环境问题折磨过、想彻底告别“本地能跑线上不行”的人;三是已经会用 Docker 但没系统整理过 FastAPI 部署套路的同学。我会从镜像构建、依赖管理、启动配置、反向代理、日志排查一路讲下来,把每一步为什么这么做都说明白,最后给一份可以直接抄的完整方案。

2. 部署前的整体思路与方案选型

2.1 单容器还是多容器,先想清楚

很多人一上来就问“要不要上 docker-compose”,其实这个问题得看项目规模。如果你的 FastAPI 项目就是一个纯 API 服务,没有数据库、没有 Redis、没有定时任务,那单容器完全够用,一个docker run就搞定,简单直接。但现实里大部分项目多少要连个数据库或者缓存,这时候用 docker-compose 把 FastAPI、数据库、缓存编排在一起,网络互通、启动顺序可控,维护起来舒服得多。

我的建议是这样:开发阶段用 docker-compose,生产环境看情况。如果生产环境数据库是独立的外部服务(比如云数据库),那 FastAPI 本身还是单容器部署,compose 只用来本地联调。如果整套服务都自己管,那就 compose 一把梭。别为了“看起来专业”硬上 Kubernetes,一个中小项目上 K8s 纯属给自己找活干。

2.2 基础镜像怎么选,别只看体积

选基础镜像是个绕不开的决策。常见的有这么几种:

镜像体积特点适用场景
python:3.11约 1GB完整 Debian,工具齐全需要编译 C 扩展、依赖系统库
python:3.11-slim约 150MB精简 Debian,缺部分编译工具大多数纯 Python 项目
python:3.11-alpine约 50MB极小,用 musl libc追求极致体积、无 C 扩展依赖

我个人的选择是slim 版本。alpine 虽然小,但 musl libc 和 glibc 不兼容,很多预编译的 wheel 包在 alpine 上装不了,得现场编译,反而更慢更折腾。slim 体积已经够小,兼容性也好,是性价比最高的选择。除非你的项目确实一个 C 扩展都不依赖,否则别碰 alpine。

2.3 依赖管理:requirements.txt 还是 poetry

FastAPI 项目常见的依赖管理方式有两种。一种是老老实实requirements.txt,pip install -r一把装;另一种是用 poetry 或 pdm 这类现代工具,锁版本更严格。

我的经验是:小项目用 requirements.txt,团队协作或长期维护的项目用 poetry。requirements.txt 简单直接,但容易出现“装了 A 包顺带升级了 B 包”的连锁反应。poetry 有poetry.lock文件,能保证每次安装的依赖树完全一致,这在多人协作时特别重要。

不过要注意,用 poetry 的话 Dockerfile 写法会不一样,得先装 poetry 再poetry install,或者用poetry export导出成 requirements.txt 再装。后者更干净,镜像里不用留 poetry 本身。我一般用后者:

poetry export -f requirements.txt --output requirements.txt --without-hashes

这样导出的 requirements.txt 直接给 Docker 用,镜像里干干净净。

3. Dockerfile 逐行拆解与关键细节

3.1 一份可直接用的 Dockerfile

先上完整版本,然后逐段解释:

FROM python:3.11-slim ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 WORKDIR /app RUN apt-get update && apt-get install -y --no-install-recommends \ curl \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8000/health || exit 1 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]

3.2 环境变量那几个参数到底在干嘛

PYTHONDONTWRITEBYTECODE=1是禁止 Python 生成.pyc缓存文件。容器里这些缓存没意义,反而让镜像层变大,关掉更干净。

PYTHONUNBUFFERED=1这个特别关键。默认情况下 Python 的 stdout 是带缓冲的,日志不会实时输出。容器里如果日志不实时,你docker logs看到的永远是延迟的,排查问题能急死人。设成 1 之后日志实时刷出来,这个坑我踩过,找了半天以为服务卡死了,其实是日志被缓冲了。

PIP_NO_CACHE_DIR=1和PIP_DISABLE_PIP_VERSION_CHECK=1都是优化项,前者不缓存下载的包,后者不检查 pip 新版本,都能加快构建、减小体积。

3.3 为什么先 COPY requirements 再 COPY 代码

这是 Docker 层缓存的核心技巧。Docker 构建时每一层都会缓存,只要该层依赖的文件没变,就直接用缓存。requirements.txt变动频率远低于业务代码,所以先装依赖再拷代码,改代码时依赖层直接命中缓存,构建速度从几分钟降到几秒。

如果你把COPY . .放前面,那每次改一行代码,整个依赖安装层都失效,重新装一遍,纯属浪费时间。这个顺序千万别搞反。

3.4 非 root 用户运行,别偷懒

默认容器里是 root 用户,一旦服务被攻破,攻击者就是 root 权限,风险很大。加一个普通用户跑服务是基本的安全习惯:

RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app USER appuser

-u 1000指定 UID,方便和宿主机文件权限对应。chown把工作目录权限给这个用户,否则它读不了代码。这一步很多人省掉,觉得麻烦,但真出事的时候就知道值了。

3.5 HEALTHCHECK 不是摆设

HEALTHCHECK让 Docker 定期检查容器是否健康。上面配置是每 30 秒请求一次/health接口,连续失败 3 次就标记为 unhealthy。配合 compose 的depends_on或者编排工具,能实现“服务真起来了才转发流量”,避免启动瞬间的 502。

前提是你的 FastAPI 里得有个健康检查接口:

@app.get("/health") def health_check(): return {"status": "ok"}

这个接口别做重逻辑,就返回个状态,越轻越好。

4. 启动命令与生产级配置

4.1 uvicorn 参数怎么调

CMD里那行启动命令有几个参数值得说。--host 0.0.0.0必须写,否则只监听容器内部的 127.0.0.1,外面访问不到,这是新手最常见的坑之一。--port 8000对应EXPOSE。

--workers 2是工作进程数。uvicorn 单进程是单核的,多核机器上开多个 worker 能提升并发。经验公式是CPU 核数 * 2 + 1,但别盲目开太多,每个 worker 都占内存。2 核机器开 2 到 4 个差不多,具体压测一下再定。

如果你需要更细的控制,可以用 gunicorn 配合 uvicorn worker:

gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

gunicorn 负责进程管理,uvicorn 负责异步处理,这是 FastAPI 官方推荐的生产部署方式之一。不过纯 uvicorn 加--workers对大多数项目也够用,看你的并发压力。

4.2 配置文件别硬编码

数据库地址、密钥这些别写死在代码里,用环境变量注入。FastAPI 里用 pydantic 的BaseSettings读环境变量特别方便:

from pydantic_settings import BaseSettings class Settings(BaseSettings): database_url: str secret_key: str debug: bool = False class Config: env_file = ".env" settings = Settings()

然后 docker run 的时候用-e或者--env-file传进去:

docker run -d --name fastapi-app \ --env-file .env.production \ -p 8000:8000 \ fastapi-app:latest

.env.production文件里写DATABASE_URL=xxx这种,注意这个文件别提交到代码仓库,加进.gitignore。

4.3 时区问题别忽略

容器默认是 UTC 时间,如果你的业务涉及本地时间(比如日志时间戳、定时任务),会发现时间对不上。解决办法是在 Dockerfile 里设时区:

ENV TZ=Asia/Shanghai RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

或者启动时挂载宿主机的时区文件。这个坑很隐蔽,日志时间差 8 小时,排查半天才发现是时区问题。

5. 反向代理与对外暴露的正确姿势

5.1 为什么前面要挂个 Nginx

uvicorn 直接对外也能跑,但生产环境一般前面挂个 Nginx。原因有几个:一是 Nginx 处理静态文件、TLS 终止、限流更成熟;二是能做负载均衡,后面挂多个 FastAPI 实例;三是 uvicorn 直接暴露公网,一些畸形请求可能让它不舒服,Nginx 挡一层更稳。

典型的 Nginx 配置长这样:

server { listen 80; server_name api.example.com; location / { proxy_pass http://fastapi-app:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 60s; } }

X-Forwarded-For和X-Forwarded-Proto这两个头很重要,FastAPI 里拿客户端真实 IP、判断是不是 HTTPS 都靠它们。不传的话,你代码里request.client.host拿到的永远是 Nginx 的 IP。

5.2 docker-compose 编排示例

把 FastAPI 和 Nginx 放一起,用 compose 管理:

version: "3.8" services: api: build: . container_name: fastapi-app restart: unless-stopped env_file: - .env.production expose: - "8000" networks: - app-net healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 5s retries: 3 nginx: image: nginx:alpine container_name: fastapi-nginx restart: unless-stopped ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - api networks: - app-net networks: app-net: driver: bridge

注意 api 服务用的是expose而不是ports,意思是只在内部网络暴露 8000,不映射到宿主机。外部流量统一走 Nginx 的 80 端口进来。这样更安全,也避免端口冲突。

depends_on只保证启动顺序,不保证 api 真的就绪了。要严格等健康检查通过,得用depends_on的condition: service_healthy写法(compose 3.x 部分版本支持),或者干脆在 Nginx 里配重试。

6. 常见问题与排查技巧实录

6.1 容器起来了但访问不了

这是最高频的问题,按这个顺序排查:

现象可能原因排查方法
连接被拒绝服务没监听 0.0.0.0检查 CMD 里的 --host
502 Bad Gateway后端没起来或端口不对docker logs 看后端日志
404路由路径不对检查 Nginx location 和 FastAPI 路由
超时服务卡住或死锁看 CPU 内存,加日志定位

我遇到最多的是第一种,--host忘了写0.0.0.0,容器内curl localhost:8000能通,外面就是连不上。记住:容器里的 localhost 是容器自己,不是宿主机。

6.2 镜像构建慢得离谱

如果每次构建都重新装依赖,八成是COPY顺序错了。确认COPY requirements.txt .在COPY . .之前。另外可以在构建时加--cache-from复用远程缓存,CI 环境里很有用。

还有个隐藏问题:.dockerignore没配。如果你把venv、.git、__pycache__这些目录也 COPY 进去,构建上下文巨大,传输就慢。加个.dockerignore:

venv/ .git/ __pycache__/ *.pyc .env .pytest_cache/

6.3 内存占用越来越高

FastAPI 本身内存不大,但如果代码里有全局缓存、没关的连接、循环引用,跑久了内存会涨。排查方法是用docker stats看内存曲线,如果持续上升不回落,基本就是内存泄漏。

常见原因是数据库连接没释放、异步任务没取消、大对象常驻内存。用async with管理连接,用lifespan管理应用生命周期资源,能避免大部分问题:

from contextlib import asynccontextmanager @asynccontextmanager async def lifespan(app: FastAPI): # 启动时初始化 app.state.db_pool = await create_pool() yield # 关闭时清理 await app.state.db_pool.close() app = FastAPI(lifespan=lifespan)

6.4 日志看不到或乱码

前面提过PYTHONUNBUFFERED=1解决日志延迟。乱码一般是编码问题,容器默认 locale 可能是 POSIX,中文日志会乱。在 Dockerfile 里设:

ENV LANG=C.UTF-8 ENV LC_ALL=C.UTF-8

这样中文日志就正常了。另外建议日志输出成 JSON 格式,方便采集和分析,用python-json-logger这类库几行就配好。

6.5 几个我踩过的坑

第一个坑:requirements.txt里写了fastapi但没锁版本,某天构建突然失败,因为上游发新版有 breaking change。所有依赖都要锁版本,用==而不是>=。

第二个坑:容器里用localhost连数据库,结果连不上。容器网络里localhost是容器自己,连宿主机上的数据库得用host.docker.internal(Mac/Windows)或者宿主机内网 IP(Linux)。用 compose 的话,服务名就是主机名,直接写服务名。

第三个坑:改了代码但容器里没生效。因为镜像是构建时快照,改代码得重新 build。开发阶段可以用 volume 挂载代码目录实现热更新:

volumes: - ./app:/app

配合 uvicorn 的--reload参数,改代码自动重启。但生产环境千万别开--reload,性能差还不稳定。

7. 上线前的检查清单与个人体会

部署前我习惯过一遍这个清单,能挡掉大部分低级错误:

  • 依赖版本全部锁定,没有>=或浮动版本
  • --host 0.0.0.0确认写上
  • 敏感配置走环境变量,代码里没有硬编码密钥
  • 非 root 用户运行
  • 健康检查接口可用
  • .dockerignore配好,构建上下文干净
  • 日志实时输出,编码正确
  • 时区设置正确
  • 反向代理头传递完整
  • 有重启策略(restart: unless-stopped)

这套流程我用了大半年,从最初的手忙脚乱到现在十几分钟就能把一个新 FastAPI 项目部署上线,最大的感受就是:Docker 不是银弹,但它把“环境”这个变量彻底消除了。以前部署一半时间在调环境,现在基本一次过。真正花时间的反而是配置的合理性,比如 worker 数量、超时时间、连接池大小这些,得根据实际压测数据调,没有万能值。

最后分享一个小技巧:把常用的 Dockerfile 和 compose 配置存成一个模板仓库,新项目直接复制改改就能用,省得每次从头写。我现在的模板里连健康检查、日志配置、非 root 用户这些都预置好了,新项目接入成本几乎为零。这套东西越用越顺,值得花点时间沉淀下来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询