Docker 部署 FastAPI 从零到生产:踩了一堆坑后的完整记录

5 阅读1分钟

上周末帮朋友把一个 FastAPI 项目部署到服务器上,本以为 Docker 部署嘛,写个 Dockerfile 构建一下就完事了。结果从镜像体积爆炸、热重载失效、到健康检查不通过,整整折腾了一个下午。事后把所有坑和最终方案整理了一遍,写成这篇教程,省得下次再踩。

说实话 FastAPI 开发体验确实丝滑,但到了部署这一步,很多人(包括之前的我)都是直接 pip install 然后 uvicorn main:app 就完事了。放到 Docker 里门道还挺多的。

项目结构

先看最终的项目结构,后面所有操作都基于这个:

my-fastapi-app/
├── app/
│ ├── __init__.py
│ ├── main.py
│ ├── routers/
│ │ └── users.py
│ └── models.py
├── requirements.txt
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
└── .env

先写一个能跑的 FastAPI 应用

app/main.py

from fastapi import FastAPI
from app.routers import users

app = FastAPI(title="My API", version="1.0.0")

app.include_router(users.router, prefix="/api/v1")


@app.get("/health")
async def health_check():
 return {"status": "ok", "service": "my-fastapi-app"}

app/routers/users.py

from fastapi import APIRouter

router = APIRouter(tags=["users"])


@router.get("/users")
async def get_users():
 return [
 {"id": 1, "name": "张三"},
 {"id": 2, "name": "李四"},
 ]


@router.get("/users/{user_id}")
async def get_user(user_id: int):
 return {"id": user_id, "name": f"用户{user_id}"}

requirements.txt

fastapi==0.115.12
uvicorn[standard]==0.34.2
pydantic==2.11.3

Dockerfile:从踩坑版到生产版

踩坑版(别用这个)

我一开始写的 Dockerfile 长这样:

FROM python:3.12
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

能跑,但问题一堆:

  • 镜像 1.2GB,推送慢得要死
  • 每次改一行代码,依赖全部重新安装
  • 用 root 用户运行,安全隐患
  • 没有多阶段构建,把构建工具也打包进去了

生产版(用这个)

# 阶段一:安装依赖
FROM python:3.12-slim AS builder

WORKDIR /app

# 先复制依赖文件,利用 Docker 层缓存
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# 阶段二:运行环境
FROM python:3.12-slim AS runtime

# 创建非 root 用户
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app

# 从 builder 阶段复制已安装的依赖
COPY --from=builder /install /usr/local

# 复制应用代码
COPY --chown=appuser:appuser ./app ./app

# 切换到非 root 用户
USER appuser

EXPOSE 8000

# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
 CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

两个版本对比:

对比项踩坑版生产版
基础镜像python:3.12 (1GB+)python:3.12-slim (150MB)
最终镜像大小~1.2GB~210MB
层缓存利用每次全量重建依赖层独立缓存
运行用户root非 root (appuser)
健康检查
多阶段构建
改代码后重建耗时2-3 分钟10 秒内

镜像体积从 1.2GB 降到 210MB,这个差距在 CI/CD 推镜像的时候非常明显。

.dockerignore 别忘了

__pycache__
*.pyc
*.pyo
.git
.gitignore
.env
.venv
venv
*.md
.mypy_cache
.pytest_cache
docker-compose*.yml
Dockerfile

很多人忘了写这个文件,结果把 .git 目录和虚拟环境都 COPY 进去了,镜像大小直接翻倍。

docker-compose.yml:一键启动

单独 docker build + docker run 当然可以,但参数多了记不住。我习惯用 docker-compose,就算只有一个服务也用:

services:
 api:
 build:
 context: .
 dockerfile: Dockerfile
 container_name: fastapi-app
 ports:
 - "8000:8000"
 environment:
 - APP_ENV=production
 - LOG_LEVEL=info
 env_file:
 - .env
 restart: unless-stopped
 deploy:
 resources:
 limits:
 memory: 512M
 cpus: "1.0"
 logging:
 driver: "json-file"
 options:
 max-size: "10m"
 max-file: "3"

启动:

docker compose up -d --build

查看日志:

docker compose logs -f api

完整部署流程

graph TD
 A[编写代码 + requirements.txt] --> B[编写 Dockerfile 多阶段构建]
 B --> C[编写 .dockerignore]
 C --> D[编写 docker-compose.yml]
 D --> E[docker compose up -d --build]
 E --> F{健康检查通过?}
 F -->|是| G[部署完成 ✅]
 F -->|否| H[查看日志排查问题]
 H --> I[docker compose logs -f]
 I --> J[修复问题]
 J --> E

踩坑记录

坑 1:uvicorn workers 数量设错

一开始设了 --workers 1,压测的时候 QPS 惨不忍睹。查了下,workers 数量的经验公式是:

workers = CPU 核心数 × 2 + 1

2 核机器设 4-5 个 workers 比较合理。但有一点要注意,如果用了 WebSocket,workers 必须设为 1,否则连接会被分配到不同 worker 导致断开。这个坑我找了好久才定位到。

坑 2:容器里 reload 不生效

开发时习惯加 --reload,但放到 Docker 里改了代码容器不会自动重启。原因很简单:你改的是宿主机的文件,容器里是 COPY 进去的副本。

开发环境用 volume 挂载就行,我单独写了个 docker-compose.dev.yml

services:
 api:
 build:
 context: .
 dockerfile: Dockerfile
 ports:
 - "8000:8000"
 volumes:
 - ./app:/app/app # 挂载源码目录
 command: ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

开发时用:

docker compose -f docker-compose.dev.yml up

坑 3:时区问题

容器默认 UTC,日志里的时间看着总差 8 小时。在 docker-compose.yml 里加个环境变量:

environment:
 - TZ=Asia/Shanghai

或者在 Dockerfile 里:

ENV TZ=Asia/Shanghai

坑 4:健康检查用 curl 但镜像里没有

slim 镜像里没有 curl,装一个又增大体积。改用 Python 自带的 urllib,就是上面 Dockerfile 里那种写法。也有人用 wget,slim 镜像里倒是有,但 Python 方案更稳。

坑 5:日志看不到

uvicorn 默认日志输出到 stdout,但 Docker 的日志驱动如果配了 none 就啥也看不到。确保 docker-compose.yml 里日志驱动是 json-file,同时限制日志文件大小,不然磁盘迟早被撑爆。

生产环境补充:Nginx 反代

真正上生产不会直接暴露 uvicorn,前面再挡一层 Nginx。docker-compose.yml 加个 nginx 服务:

services:
 api:
 build: .
 container_name: fastapi-app
 expose:
 - "8000"
 restart: unless-stopped

 nginx:
 image: nginx:1.27-alpine
 container_name: nginx-proxy
 ports:
 - "80:80"
 volumes:
 - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
 depends_on:
 - api
 restart: unless-stopped

nginx.conf

upstream fastapi {
 server api:8000;
}

server {
 listen 80;
 server_name _;

 location / {
 proxy_pass http://fastapi;
 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;
 }
}
graph LR
 Client[客户端] --> Nginx[Nginx :80]
 Nginx --> Worker1[uvicorn worker 1]
 Nginx --> Worker2[uvicorn worker 2]
 Nginx --> Worker3[uvicorn worker 3]
 Nginx --> Worker4[uvicorn worker 4]
 subgraph FastAPI Container
 Worker1
 Worker2
 Worker3
 Worker4
 end

常用运维命令速查

# 构建并启动(后台运行)
docker compose up -d --build

# 查看运行状态
docker compose ps

# 实时日志
docker compose logs -f api

# 进入容器排查问题
docker compose exec api /bin/bash

# 重启单个服务
docker compose restart api

# 停止并删除容器(保留镜像)
docker compose down

# 停止并删除容器+镜像+volume(慎用)
docker compose down --rmi all -v

小结

Docker 部署 FastAPI 说难不难,细节确实多。几个核心要点:

  • 用 slim 镜像 + 多阶段构建,镜像体积能小 5-6 倍
  • 先 COPY requirements.txt 再 COPY 源码,利用层缓存
  • 非 root 用户运行,安全基本操作
  • 开发和生产用不同的 compose 文件,开发挂载 volume 开 reload
  • 健康检查和日志限制一定要加,出了问题不至于两眼一黑

这套配置现在基本上每个新项目都复制一份改改就用,算是自己的脚手架了。希望能帮你少踩几个坑。