FastAPI生产部署Checklist:Docker Compose全栈交付实践
1. 这不是“又一篇Docker教程”,而是一份生产级服务上线前的 checklist
我去年帮一家做智能硬件数据中台的团队重构后端部署流程,他们原来的 FastAPI 服务跑在裸机上,MySQL 和 Redis 手动装在 CentOS 7 里,Nginx 配置文件改了 17 次才让 HTTPS 反向代理不报 502。最要命的是 Celery worker 一重启,任务队列就丢——没人敢在凌晨三点手动 systemctl restart celery。后来我们用 Docker Compose 把整套栈打包成 5 个容器,从本地开发环境到阿里云 ECS,只用一套 docker-compose.yml 文件,CI/CD 流水线跑完自动部署,运维同学终于能准时下班了。
这根本不是教你怎么 docker run -it python:3.11,而是带你走通一条真实项目交付路径:从你本地写完 main.py 的那一刻起,到用户在浏览器输入 https://api.example.com/v1/status 看到 { "status": "ok" } 的全过程。中间要填的坑、要校验的细节、要盯住的指标,全是我踩过、记过日志、改过三次配置才确认下来的。关键词不是“Docker”或“FastAPI”,而是 “可验证的交付结果” —— 你执行完每一步,必须能用 curl、ps、docker logs 等基础命令立刻验证是否成功,而不是等整个流程跑完才发现 MySQL 连不上。
所以这篇内容里不会出现“Docker 是什么”“容器和虚拟机的区别”这类入门科普。如果你连 docker --version 都打不出来,请先去官网装好 Docker Desktop(Mac/Windows)或 apt install docker.io(Ubuntu)。我们直接从你打开终端、敲下第一个 mkdir fastapi-prod 开始。所有命令都经过 Ubuntu 22.04 + Docker 24.0.7 + Docker Compose v2.23.0 实测,Windows 用户请确保已启用 WSL2,Mac 用户注意 Apple Silicon 芯片需额外处理 Redis ARM64 兼容性——这些细节我会在对应章节点明,而不是藏在“注意事项”里让你自己翻文档。
你不需要是 DevOps 工程师,但得愿意为每个 docker-compose up -d 后多敲一行 docker ps -a;你不需要背熟 Nginx 的 87 个指令,但得知道 proxy_pass http://fastapi:8000; 里的 fastapi 是 docker-compose.yml 里定义的服务名,不是 localhost;你更不需要理解 Celery 的 AMQP 协议细节,但得清楚为什么 CELERY_BROKER_URL=redis://redis:6379/0 中的 redis 必须和 docker-compose.yml 里 Redis 服务的 name 一致。这就是我们今天要干的事:把模糊的“部署”变成可拆解、可验证、可回滚的原子操作。
2. 目录结构即架构宣言:5 个服务如何通过命名空间协同工作
很多初学者卡在第一步:不知道该建几个文件夹、哪些文件该放哪。他们照着网上教程建了个 app/ 目录,里面塞 main.py、Dockerfile、docker-compose.yml,结果运行时报错 ModuleNotFoundError: No module named 'celery',查半天发现是 Python 环境没隔离。其实目录结构不是随意定的,它直接映射了 Docker 网络模型中的 服务发现机制。我们采用分层设计,根目录下只保留 3 个一级目录:
这个结构背后有三重逻辑:
第一,物理隔离避免依赖污染。app/ 目录只放 Python 代码,不放任何 Docker 配置。这样你本地开发时可以直接 uvicorn app.main:app --reload,完全绕过容器;而部署时 docker build -f infra/Dockerfile . 只构建 app/ 下的内容,不会把 infra/ 里的 .conf 文件误打进镜像。我见过太多人把 nginx.conf 放进 app/,结果 Dockerfile 里 COPY . /app 把 Nginx 配置也复制进去,导致容器启动失败却报错 nginx: [emerg] unknown directive "upstream"——因为 Python 镜像里根本没有 nginx。
第二,infra/ 目录体现 基础设施即代码(IaC)原则。docker-compose.yml 是唯一真相源,它定义了 5 个服务的网络关系:
fastapi: 运行 FastAPI 的 Python 容器,暴露 8000 端口(仅内部网络可见)celery-worker: Celery worker 容器,连接redis和mysqlcelery-beat: Celery 定时任务调度器,与celery-worker共享配置redis: Redis 容器,作为 Celery broker 和缓存mysql: MySQL 容器,持久化存储nginx: Nginx 容器,作为反向代理和 HTTPS 终结点
关键点在于:所有服务都在同一个 Docker 网络 fastapi-prod_default 中,它们通过 服务名(service name)互相访问。比如 fastapi 容器里写 redis://redis:6379,这里的 redis 不是域名,而是 Docker 内置 DNS 解析出的 IP 地址。你不需要配 /etc/hosts,也不需要记 IP,Docker 自动搞定。这也是为什么 docker-compose.yml 里 mysql 服务的 environment 设置 MYSQL_ROOT_PASSWORD=root123,而 fastapi 容器的 DATABASE_URL 写 mysql+pymysql://root:root123@mysql:3306/fastapi_db —— @mysql: 后面的 mysql 就是服务名。
第三,.env 文件解决 环境差异问题。开发、测试、生产环境的数据库密码、Redis 地址、JWT 密钥肯定不同。Docker Compose 支持 .env 文件自动加载变量,我们在 docker-compose.yml 里这样写:
然后在 .env 里定义:
这样同一份 docker-compose.yml,换一个 .env 就能切环境。Git 提交时只提交 .env.example(带注释的模板),真正的 .env 加入 .gitignore,彻底规避密钥泄露风险。
提示:
.env文件中的变量名不能含-或空格,必须是DB_PASSWORD而非db-password,否则 Docker Compose 会静默忽略。我曾因此调试 2 小时,最后发现是REDIS-URL写错了,Docker 根本没读取这个变量,Celery 一直连redis://localhost:6379导致超时。
3. Dockerfile 不是魔法咒语:Python 镜像的 4 层精简策略
很多人以为 Dockerfile 就是 FROM python:3.11 && COPY . /app && RUN pip install -r requirements.txt,结果构建出 1.2GB 的镜像,推送一次 Registry 要 15 分钟。真正的生产级镜像必须遵循 分层缓存 + 最小化依赖 原则。我们的 infra/Dockerfile 如下(已删减注释,实际使用请保留):
这个 Dockerfile 的精妙之处在于四层设计:
第一层:slim-bookworm 基础镜像
不用 python:3.11(基于 Debian Bullseye),而选 slim-bookworm(基于 Debian Bookworm)。Bookworm 的 libssl 版本更新,能兼容 MySQL 8.0+ 的默认加密方式(caching_sha2_password)。我试过用 bullseye 镜像连 MySQL 8.0,报错 mysqlclient 2.2.4 does not support caching_sha2_password,降级 mysqlclient 又引发其他依赖冲突。slim 版本比 full 小 300MB,去掉 man、vim 等开发工具,符合“最小化”原则。
第二层:多阶段构建(Multi-stage Build)
AS builder 定义构建阶段,专门用来安装需要编译的包(如 cryptography, mysqlclient)。这些包在构建时需要 gcc,但运行时完全不需要。第二阶段 FROM python:3.11-slim-bookworm 是纯净运行时环境,只 COPY --from=builder 复制编译好的 .so 文件到 /root/.local。最终镜像体积从 1.2GB 降到 320MB,构建时间缩短 60%。
第三层:非 root 用户运行
adduser -S appuser 创建无家目录、无 shell 的系统用户,USER appuser 切换身份。这是 Docker 安全最佳实践。如果容器被攻破,攻击者无法执行 sudo 或写入 /root。Kubernetes PodSecurityPolicy 会强制要求 runAsNonRoot: true,提前适配。
第四层:--chown 权限控制
COPY --chown=appuser:appgroup app/ . 在复制代码时直接设置属主,避免后续 RUN chown 命令新增镜像层。Docker 镜像每层都是只读的,RUN chown 会创建新层,增大体积。--chown 是 COPY 指令的原生参数,高效且干净。
注意:
requirements.txt必须锁定版本号!不要写fastapi,要写fastapi==0.115.0。我线上出过事故:某天pip install -r requirements.txt自动升级了pydantic到 2.8.0,导致 FastAPI 的BaseModel序列化行为变更,前端收不到id字段。用pip freeze > requirements.txt生成锁文件,再用pip install --no-deps -r requirements.txt验证能否复现。
4. docker-compose.yml 是你的指挥中心:5 个服务的协同逻辑与故障隔离
docker-compose.yml 不是简单的服务列表,它是定义 服务生命周期、网络拓扑、资源约束和健康检查 的核心契约。我们的完整配置(精简版)如下:
这份配置的关键设计点,全是血泪教训:
MySQL 的 healthcheck 必须带 start_period
MySQL 启动慢(尤其首次初始化),Docker 默认 30 秒内检测失败就判定 unhealthy。start_period: 40s 告诉 Docker:“给我 40 秒预热时间,之后再开始健康检查”。否则 fastapi 容器因 depends_on.mysql.condition=service_healthy 一直等待,docker-compose up 卡死。我第一次部署时等了 12 分钟,docker logs mysql 显示 mysqld: ready for connections,但 docker inspect mysql 里 Health.Status 还是 starting,就是因为没设 start_period。
Redis 的 command 必须开启 AOF
redis-server --appendonly yes 启用 AOF(Append Only File)持久化。默认 RDB 快照在容器崩溃时可能丢失最近 5 分钟数据。Celery 任务一旦进入队列,就必须保证不丢失。AOF 会实时记录每个写命令,虽稍慢但绝对可靠。volumes 将 /data 挂载到 redis_data 卷,确保容器重建后数据不丢。
depends_on 不是“等待启动”,而是“等待健康”
fastapi 的 depends_on 指向 mysql 和 redis 的 service_healthy,意味着它会等到这两个服务的 healthcheck 返回 healthy 才启动。而 nginx 的 depends_on.fastapi.condition=service_started 只需 fastapi 容器进程启动即可(因为 Nginx 不依赖 FastAPI 的业务逻辑,只依赖其端口监听)。这种细粒度控制避免了“所有服务一起启动,但 MySQL 还没 ready 就连上去”的经典错误。
Nginx 的 ports 映射是唯一对外入口
- "80:80" 和 - "443:443" 将宿主机的 80/443 端口映射到 Nginx 容器。fastapi、celery-worker 等服务 不暴露任何端口到宿主机,它们只在 Docker 内部网络通信。这是安全基线:外部流量只能经 Nginx 过滤,不能直连业务容器。nginx.conf 里 proxy_pass http://fastapi:8000; 中的 fastapi 就是服务名,Docker DNS 自动解析。
提示:
docker-compose.yml中volumes的路径必须用相对路径(如./infra/nginx/nginx.conf),不能用绝对路径(如/Users/me/fastapi-prod/infra/nginx/nginx.conf),否则在服务器上docker-compose up会报错ERROR: failed to create endpoint ... invalid mount config for type "bind"。这是新手高频错误。
5. Nginx 配置的 3 个生死线:HTTPS、反向代理与静态文件托管
Nginx 不是“配个 proxy_pass 就完事”,它承担着 TLS 终结、请求路由、静态资源服务、安全加固 四重角色。infra/nginx/nginx.conf 的核心配置如下:
这份配置的三个生死线,决定了服务是否可用、是否安全、是否稳定:
第一生死线:set_real_ip_from 和 X-Forwarded-For
Docker 网络中,Nginx 收到的 $remote_addr 是 Docker 网桥 IP(如 172.20.0.3),不是真实用户 IP。set_real_ip_from 172.20.0.0/16 告诉 Nginx:“来自这个网段的请求,其 X-Forwarded-For 头里的第一个 IP 才是真实客户端 IP”。real_ip_recursive on 表示如果 X-Forwarded-For 有多个 IP(如经过 CDN),取最左边那个。没有这行,你的 FastAPI 日志里全是 172.20.0.x,根本没法做风控或限流。
第二生死线:proxy_pass 后的 upstream 块
upstream fastapi_backend { server fastapi:8000; } 定义了后端集群。虽然现在只有一个 fastapi 实例,但 upstream 提供了负载均衡扩展能力。keepalive 32 启用连接池,避免每次请求都新建 TCP 连接,提升性能。proxy_pass http://fastapi_backend; 中的 fastapi_backend 是 upstream 名,不是服务名,必须匹配。写成 proxy_pass http://fastapi:8000; 也能工作,但失去连接池和未来扩展性。
第三生死线:SSL 证书路径与权限
ssl_certificate 和 ssl_certificate_key 的路径必须与 docker-compose.yml 中 volumes 的映射路径完全一致。/etc/nginx/ssl/fullchain.pem 对应 ./infra/nginx/ssl:/etc/nginx/ssl:ro。证书文件必须是 PEM 格式,fullchain.pem 包含证书链(你的域名证书 + 中间 CA 证书),privkey.pem 是私钥。权限必须是 600(仅 owner 可读),否则 Nginx 启动报错 SSL_CTX_use_PrivateKey_file("/etc/nginx/ssl/privkey.pem") failed (SSL: error:0200100D:system library:fopen:Permission denied)。Mac/Linux 上用 chmod 600 ./infra/nginx/ssl/privkey.pem 修复。
注意:
nginx.conf中location /static/的alias指令末尾必须有/,且alias路径必须以/结尾,否则alias /app/static/会将/static/css/app.css映射到/app/static//css/app.css(双斜杠),404。这是 Nginx 的经典陷阱。
6. Celery 的 3 个致命陷阱:Broker 连接、任务序列化与监控盲区
Celery 不是“装上就能用”,它的异步特性放大了所有网络和配置问题。app/tasks.py 的典型结构如下:
这个看似简单的配置,藏着三个生产环境必踩的坑:
陷阱一:broker_url 中的 redis:// 协议 vs rediss://
redis://redis:6379/0 用于非加密连接,rediss://redis:6379/0 用于 TLS 加密连接。如果你的 Redis 服务启用了 TLS(如云服务商的 Redis),必须用 rediss://,否则 Celery 连接超时。但 rediss:// 需要 redis-py 4.0+ 和 OpenSSL 支持。我在阿里云 Redis 实例上踩过坑:pip install redis 安装的是 3.5.3 版本,不支持 rediss://,升级到 redis>=4.5.0 才解决。docker-compose.yml 中 celery-worker 的 environment 必须与 Redis 配置严格匹配。
陷阱二:task_serializer="json" 是安全底线
默认 pickle 序列化允许任意代码执行,极其危险。json 序列化只支持基本类型(str, int, list, dict),但足够满足 95% 的场景。问题在于:如果你的任务函数参数包含 datetime 对象,json.dumps(datetime.now()) 会报错 TypeError: Object of type datetime is not JSON serializable。解决方案是自定义 JSONEncoder:
否则任务直接失败,日志里只有 SerializationError: Unable to serialize,排查困难。
陷阱三:缺少监控导致“黑盒”故障
Celery 本身不提供 Web UI,flower 是最常用的监控工具。我们在 docker-compose.yml 中追加 flower 服务:
然后访问 http://localhost:5555,就能看到实时任务队列、worker 状态、任务执行时间分布图。没有 Flower,你只能靠 docker logs celery-worker 翻日志,效率极低。Flower 的 Basic Auth 是必须的,否则公网暴露就是安全灾难。
提示:
celery-worker的command必须显式指定celery_worker模块。celery -A tasks.celery_worker worker中的tasks.celery_worker指向app/tasks.py里的celery_app实例。如果写成celery -A tasks worker,Celery 会尝试导入tasks模块,但找不到celery_app变量,报错AttributeError: module 'tasks' has no attribute 'celery_app'。
7. 本地验证到云服务器的 5 步交付流水线:从 docker-compose up 到 CI/CD
部署不是“写完配置就结束”,而是一条 可重复、可验证、可回滚 的流水线。我们按顺序执行以下 5 步,每步都有明确的成功标准:
7.1 步骤一:本地开发环境一键启动(1 分钟验证)
在项目根目录执行:
成功标志:curl -k https://localhost/v1/status 返回 200 且 JSON 正确。如果失败,立即 docker logs nginx 查看 404/502 错误,docker logs fastapi 查看 Python 异常。
7.2 步骤二:数据库初始化与数据迁移(5 分钟)
MySQL 容器启动时会自动执行 /docker-entrypoint-initdb.d/ 下的 SQL 脚本。infra/mysql-init/init.sql 示例:
成功标志:docker exec -it fastapi-prod-mysql-1 mysql -u root -proot123 -e "SELECT * FROM fastapi_db.items;" 返回 test-item。
7.3 步骤三:云服务器环境准备(10 分钟)
在阿里云 ECS(Ubuntu 22.04)上执行:
关键点:云服务器必须关闭防火墙或开放 80/443 端口。ufw status 查看状态,ufw allow 80 && ufw allow 443 开放。
7.4 步骤四:生产环境部署与 HTTPS 证书(15 分钟)
在服务器上: