上周接了个私活,要把一个 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 了。至少出了问题能快速回滚,不用在生产环境上玩心跳。
有问题评论区聊,踩坑的兄弟互相帮一把。