Docker 部署 FastAPI 从零到生产:我踩完的坑你别再踩了

6 阅读1分钟

上周接了个私活,要把一个 FastAPI 写的后端服务部署到客户服务器上。心想这不简单嘛,Docker 一把梭就完事了。结果折腾了整整两天——镜像打出来 1.2GB、容器跑起来接口 502、热重载不生效、日志看不到……各种幺蛾子轮番上阵。

痛定思痛,我把整个流程从 Dockerfile 到 docker-compose 到生产优化全部整理了一遍。代码全部可运行,直接抄就能用。

先说项目结构

动手写 Dockerfile 之前先把项目结构理清楚,不然后面各种路径问题会搞死你:

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

核心入口文件 app/main.py

from fastapi import FastAPI
from app.routers import health

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

app.include_router(health.router)

@app.get("/")
async def root():
 return {"message": "running", "status": "ok"}

app/routers/health.py

from fastapi import APIRouter

router = APIRouter(prefix="/health", tags=["health"])

@router.get("/")
async def health_check():
 return {"status": "healthy"}

requirements.txt

fastapi==0.115.0
uvicorn[standard]==0.30.0
pydantic==2.9.0

第一步:写一个能用的 Dockerfile

见过太多教程上来就给你一个单阶段 Dockerfile,打出来的镜像又大又慢。直接上多阶段构建:

# ---- 构建阶段 ----
FROM python:3.12-slim AS builder

WORKDIR /build

COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ---- 运行阶段 ----
FROM python:3.12-slim

# 创建非 root 用户(生产必须!)
RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /code

# 从构建阶段拷贝依赖
COPY --from=builder /install /usr/local

# 拷贝项目代码
COPY ./app /code/app

# 切换到非 root 用户
USER appuser

EXPOSE 8000

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

几个关键点:

多阶段构建把镜像从 1.2GB 压到了 180MB。builder 阶段装依赖,运行阶段只拷贝编译好的包,编译工具链不会带进最终镜像。

非 root 用户不是可选项。我之前图省事用 root 跑,客户安全团队直接把我打回来了。

--no-cache-dir 别忘了。pip 默认会缓存下载的包,容器里完全没必要,白白浪费空间。

第二步:.dockerignore 别偷懒

这个文件很多人不写,然后纳闷为什么 build context 传了半天:

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

加了 .dockerignore 之后,build context 从 340MB 降到了 2MB,构建速度快了一个量级。

第三步:docker-compose 完整配置

单独跑 docker build + docker run 太原始了,生产环境用 docker-compose 管理:

version: "3.9"

services:
 api:
 build:
 context: .
 dockerfile: Dockerfile
 container_name: fastapi-app
 ports:
 - "8000:8000"
 environment:
 - DATABASE_URL=postgresql://user:pass@db:5432/mydb
 - LOG_LEVEL=info
 env_file:
 - .env
 depends_on:
 db:
 condition: service_healthy
 restart: unless-stopped
 healthcheck:
 test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"]
 interval: 30s
 timeout: 10s
 retries: 3
 start_period: 15s
 deploy:
 resources:
 limits:
 memory: 512M
 cpus: "1.0"

 db:
 image: postgres:16-alpine
 container_name: fastapi-db
 environment:
 POSTGRES_USER: user
 POSTGRES_PASSWORD: pass
 POSTGRES_DB: mydb
 volumes:
 - pgdata:/var/lib/postgresql/data
 healthcheck:
 test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
 interval: 10s
 timeout: 5s
 retries: 5

volumes:
 pgdata:

整个服务编排的流程:

graph TD
 A[docker-compose up] --> B[启动 PostgreSQL]
 B --> C{数据库健康检查}
 C -->|pg_isready 通过| D[启动 FastAPI 容器]
 C -->|失败| E[重试 5 次]
 E --> C
 D --> F{API 健康检查}
 F -->|/health/ 返回 200| G[服务就绪 ✅]
 F -->|失败| H[重试 3 次]
 H --> F
 G --> I[对外暴露 :8000]

depends_on + condition: service_healthy 是关键。我之前不写健康检查条件,FastAPI 容器先于数据库启动,连接直接报错,容器挂掉、重启、又挂掉,循环往复。加上健康检查之后,compose 会等 PostgreSQL 真正 ready 了再启动 API 服务。

踩坑记录

坑 1:uvicorn workers 和 --reload 冲突

开发时习惯性加了 --reload,同时又设了 --workers 4,容器起不来,报错:

ERROR: You must not set --workers with --reload

报错信息倒是很明确,但我 Dockerfile 里写的 CMD 是生产配置带 workers,docker-compose 里又通过 command 覆盖加了 reload,两边打架了。

解决方案是开发和生产用不同的 compose 文件。

docker-compose.override.yml(开发环境自动加载):

version: "3.9"

services:
 api:
 build:
 context: .
 dockerfile: Dockerfile
 command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
 volumes:
 - ./app:/code/app

开发时 docker-compose up 自动合并 override 文件,代码修改实时生效。生产环境用 docker-compose -f docker-compose.yml up,只加载主文件。

坑 2:容器里日志看不到

FastAPI 用 uvicorn 跑的时候,日志默认输出到 stdout,但 docker logs 啥也看不到。排查了半天,是 Python 的输出缓冲机制在搞鬼。

在 Dockerfile 里加环境变量:

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1

PYTHONUNBUFFERED=1 禁用输出缓冲,日志实时刷到 stdout。PYTHONDONTWRITEBYTECODE=1 不生成 .pyc 文件,容器里没必要留这东西。

坑 3:镜像层缓存失效,每次都重装依赖

一开始 Dockerfile 是这样写的:

COPY . /code
RUN pip install -r requirements.txt

每次改一行业务代码,COPY . /code 这层缓存就失效了,后面的 pip install 得重来一遍,构建一次 3 分钟。

解法就是上面多阶段构建里的写法:先只 COPY requirements.txt,装完依赖再 COPY 代码。只要依赖没变,pip install 那层就走缓存,构建时间从 3 分钟降到 8 秒。

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

最开始健康检查这样写:

test: ["CMD", "curl", "-f", "http://localhost:8000/health/"]

python:3.12-slim 里压根没有 curl。要么装 curl(镜像变大),要么用 Python 自带的 urllib,我选后者:

test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health/')"]

丑是丑了点,但不用额外装任何东西。

生产部署 Checklist

跑通之后别急着上线,过一遍这个清单:

  • Dockerfile 用多阶段构建,镜像 < 300MB
  • 非 root 用户运行
  • PYTHONUNBUFFERED=1 确保日志可见
  • .dockerignore 排除无关文件
  • 健康检查配置完整
  • 资源限制(memory/cpus)已设置
  • 敏感信息走 .env 文件,不要硬编码在 compose 里
  • restart: unless-stopped 保证自动重启
  • 数据库用 volume 持久化,别用 bind mount

一键启动命令

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

# 看日志
docker-compose logs -f api

# 重启 API 服务
docker-compose restart api

# 完全清理重来
docker-compose down -v --rmi all

小结

Docker 部署 FastAPI 这事本身不复杂,魔鬼全在细节里。多阶段构建、层缓存策略、非 root 用户、健康检查依赖顺序……每一个都是我实打实踩过的坑。

2026 年了,还在服务器上裸跑 nohup uvicorn ... 的话,是时候迁移到 Docker 了。至少出了问题能快速回滚,不用在生产环境上玩心跳。

有问题评论区聊,踩坑的兄弟互相帮一把。