上周末帮朋友把一个 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
- 健康检查和日志限制一定要加,出了问题不至于两眼一黑
这套配置现在基本上每个新项目都复制一份改改就用,算是自己的脚手架了。希望能帮你少踩几个坑。