这篇文章记录的是个人技术博客上线过程中一组真实排障问题。它们看起来分别属于 Nginx、FastAPI、MySQL、Docker Compose 和服务器环境,但本质上都指向同一件事:一个全栈项目能不能稳定上线,不只取决于代码能不能运行,还取决于配置、网络、启动顺序和验证方式是否闭环。
问题背景
博客项目包含 Next.js 前端、FastAPI 后端、MySQL、Redis 和 Nginx。前端主要服务公开博客和简历作品展示,后端提供文章、AI 草稿和发布管理接口。服务器配置不高,所以部署时不能依赖很重的本地工具链,也不能让所有服务无节制占用内存。
部署过程中遇到过几类典型问题:
| 现象 | 直接表现 | 核心原因 |
|---|---|---|
| Nginx 反复重启 | host not found in upstream | upstream 名称和 compose 服务名不一致 |
| 后端容器重启 | CORS_ORIGINS 解析失败 | 环境变量格式和配置类型不匹配 |
| 数据库迁移失败 | ERROR 1045 Access denied | 脚本使用的账号密码和容器实际配置不一致 |
| 本机 curl 正常,外网不通 | 服务器 127.0.0.1 可访问,公网 IP 不行 | 80 端口映射、防火墙或旧容器仍占用入口 |
| 服务器无法 npm smoke | npm not found | 测试方式不适合生产服务器环境 |
Nginx upstream 找不到后端
一次部署时,Nginx 日志里出现类似 host not found in upstream "backend:8080" 的错误。这个问题不是后端代码挂了,而是 Nginx 容器启动时解析不到配置里的 upstream 主机名。
排查顺序是:
- 先看 docker compose ps,确认后端容器是否真的在同一个 compose 项目里。
- 再看 Nginx 配置里的 upstream 名称,例如 backend:8080。
- 对照 compose service 名、container name 和网络。
- 保证 Nginx、frontend、backend 在同一个 Docker 网络里。
这里最容易混淆的是 container name 和 service name。Docker Compose 网络里更推荐使用 service 名作为内部 DNS 名称。如果 Nginx 写的是 backend:8080,compose 里也应该存在对应的 backend 服务,或者统一改成实际可解析的服务名。
CORS 环境变量解析失败
后端曾经因为 cors_origins 配置启动失败。日志里能看到 pydantic settings 在解析环境变量时抛出 JSON decode 错误。
这个问题的根源是:配置字段希望拿到数组,但服务器环境变量里可能写成普通字符串。比如:
示例:CORS_ORIGINS=http://127.0.0.1,http://服务器IP
如果配置层直接把它当 JSON 解析,就会失败。更稳的方式是让代码支持两种输入:
- JSON 数组:["http://127.0.0.1","http://服务器IP"]
- 逗号分隔字符串:http://127.0.0.1,http://服务器IP
对于低配置服务器上的一键部署脚本来说,逗号分隔更容易维护,也更不容易因为 shell 转义出错。这个问题让我意识到,生产配置不只是“类型正确”,还要考虑部署人员实际会怎么填写。
MySQL 迁移时 Access denied
数据库容器 healthy 后,迁移脚本仍然可能失败。比如执行 migration 时出现:
示例:ERROR 1045 (28000): Access denied for user 'root'@'localhost'
这个问题通常不是 SQL 文件本身错了,而是迁移脚本使用的账号、密码、host 和容器环境变量不一致。排查时要分清几件事:
- MYSQL_ROOT_PASSWORD 是容器初始化时使用的密码。
- 数据卷已经存在后,修改 env 不会自动重置数据库密码。
- 在容器内连接 localhost 和从其他容器连接 mysql 是不同场景。
- init SQL 只在空数据卷首次初始化时执行,不能替代后续 migration。
最终处理思路是让部署脚本从同一份 env 读取数据库密码,迁移连接明确使用 MySQL service host,并且把“初始化”和“迁移”分开处理。
本机可访问但公网 IP 不通
还有一种情况是服务器上执行:
示例:
- curl http://127.0.0.1
- curl http://127.0.0.1/api/v1/ping
都能返回正常结果,但外网访问公网 IP 失败。这时问题一般不在 Next.js 或 FastAPI,而在入口层。
我会按这个顺序排查:
- docker ps 看 Nginx 是否映射了 0.0.0.0:80->80/tcp。
- ss -lntp | grep ':80' 看 80 端口是不是被旧容器占用。
- 检查服务器安全组或防火墙是否开放 80。
- 确认当前服务的 Nginx 正在代理新 frontend,而不是旧的 my-blog_web 容器。
这类问题很适合用最小验证链路:先本机 curl,再容器日志,再端口监听,最后查外网防火墙。
服务器没有 npm 怎么测试
部署时我一开始尝试用 Node 版 smoke test,但服务器提示 npm not found。这说明测试方式不应该依赖生产服务器安装完整前端工具链。
后来我把服务器 smoke test 收敛成 bash + curl:
示例:bash scripts/smoke-test.sh http://127.0.0.1
它只验证真实 Nginx 暴露出来的页面和接口,包括首页、博客列表、文章详情、隐藏后台、robots、sitemap 和可选后端 ping。这样测试更贴近生产,也更适合低配置服务器。
收获
这次排障让我更清楚地理解:上线不是把容器跑起来就结束了。一个真正可演示的全栈作品,需要具备这些能力:
- 能通过日志定位服务失败点。
- 能理解 Docker Compose 网络和服务名解析。
- 能把环境变量设计成更适合部署的形式。
- 能区分数据库初始化和后续迁移。
- 能用轻量 smoke test 验证真实入口。
这些问题虽然细碎,但很接近真实业务部署。对个人博客来说,它们让项目从“页面作品”变成了一个能上线、能验证、能维护的全栈系统。