Docker 部署 - 不只是写个 Dockerfile:一次 FastAPI 项目的“排错”复盘
最近将一个 FastAPI 项目部署到 VMware 虚拟机时,我差点体会到了什么叫“从入门到放弃”。本以为
docker compose up是终点,没想到是噩梦的起点。但坚持就是胜利,最终还是成功通关!!!这篇文章记录了我如何通过分析报错日志,逐一击破 5 大关卡:从 Docker 镜像拉取的“网络迷宫“,到 pip 依赖的“积木难题”,再到容器网络的“巴别塔”混乱。如果你也在部署时感到迷茫,不妨看看这份“血泪史”复盘,或许能帮你少走几小时弯路。
- Docker 部署 - 不只是写个 Dockerfile:一次 FastAPI 项目的“排错”复盘
- 一、项目部署背景
- 二、第一关:Docker 镜像拉取失败,是网络层问题
- 2.1 先判断是不是网络问题
- 2.2 方式一:配置 Docker 镜像加速器
- 2.3 方式二:给 Docker daemon 配代理
- 2.4 这一层的判断标准
- 三、第二关:pip 出现 ResolutionImpossible,是依赖层问题
- 3.1 typing_extensions 版本冲突
- 3.2 async-timeout 版本冲突
- 3.3 conda 导出的 requirements 不适合直接进 Docker
- 3.4 这一关的判断汇总
- 四、第三关:Alembic 连不上 MySQL,是容器网络问题
- 4.1 关键理解:容器里的 127.0.0.1 是它自己
- 4.2 环境变量名也要对得上
- 4.3 推荐配置 DB_URI
- 五、第四关:表建好了但查不到,是表名问题
- 六、第五关:HBuilderX 提示网络连接失败,是前后端地址问题
- 6.1 先看前端 BASE_URL
- 6.2 用浏览器先验证后端地址
- 6.3 验证码接口路径
- 七、速查表:Docker 部署报错“翻译器”
- 八、常用命令速查表
- 8.1 Docker Compose 常用命令
- 8.2 数据库相关命令
- 8.3 Alembic 迁移命令
- 8.4 网络验证命令
- 九、总结
- 参考链接

开始闯关!!![████████░░] 80% 环境准备中...
一、项目部署背景
这次部署的项目结构大概是这样:
| 组件 | 作用 |
|---|---|
| FastAPI | 后端接口服务 |
| MySQL 8.0 | 业务数据库 |
| Redis | 邮箱验证码缓存 |
| Alembic | 数据库结构迁移 |
| Nginx | 对外转发 HTTP 请求 |
| HBuilderX / uni-app | 本地运行前端页面 |
| VMware Ubuntu | Docker 部署环境 |
整体访问链路可以理解为:
Windows 本地浏览器 / HBuilderX 前端↓
虚拟机 IP,例如 http://192.168.x.x↓
Docker 中的 nginx↓
FastAPI web 容器↓
MySQL db 容器 / Redis 容器
也就是说,前端不在 Docker 里面,而是在 Windows 本地跑;后端、数据库、Redis 在 Ubuntu 虚拟机的 Docker 里面跑。
这就自然带来几个容易混淆的点:
Windows 的 127.0.0.1
Ubuntu 虚拟机的 127.0.0.1
web 容器里的 127.0.0.1
MySQL 容器里的 127.0.0.1
它们看起来都叫 127.0.0.1,但指向的根本不是同一台“机器”。
二、第一关:Docker 镜像拉取失败,是网络层问题
刚开始执行:
docker compose up -d --build
可能会遇到类似报错:
failed to resolve reference "docker.io/library/nginx:alpine"
failed to do request: Head "https://registry-1.docker.io/v2/..."
connect: connection refused
这个错误发生在拉取基础镜像阶段,比如:
image: nginx:alpine
image: mysql:8.0
image: redis:alpine
这类问题通常和代码没关系,属于 Docker 访问镜像仓库失败。
2.1 先判断是不是网络问题
在虚拟机里可以这样检查:
ping baidu.com
curl -I https://registry-1.docker.io/v2/
docker info | grep -i proxy
如果 Docker Hub 无法访问,可以考虑两种方式。
2.2 方式一:配置 Docker 镜像加速器
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<EOF
{"registry-mirrors": ["https://registry.cn-hangzhou.aliyuncs.com","https://hub-mirror.c.163.com","https://docker.mirrors.ustc.edu.cn"]
}
EOFsudo systemctl daemon-reload
sudo systemctl restart docker
验证:
docker info | grep "Registry Mirrors"
2.3 方式二:给 Docker daemon 配代理
如果使用 VMware NAT 模式,虚拟机访问 Windows 上的代理时,不能写 Windows 的 127.0.0.1,而要写 VMnet8 网关 IP。
Windows 上查看:
ipconfig
找到类似:
VMware Network Adapter VMnet8
IPv4 地址: 192.168.xxx.1
在虚拟机中配置 Docker 代理:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf <<EOF
[Service]
Environment="HTTP_PROXY=http://192.168.xxx.1:7897"
Environment="HTTPS_PROXY=http://192.168.xxx.1:7897"
Environment="NO_PROXY=localhost,127.0.0.1"
EOFsudo systemctl daemon-reload
sudo systemctl restart docker
同时要注意,Windows 代理软件里需要开启类似:
Allow LAN
Bind Address = 0.0.0.0
否则虚拟机访问不到 Windows 上的代理端口。
2.4 这一层的判断标准
如果报错出现在:
registry-1.docker.io
nginx:alpine
mysql:8.0
python:3.10-slim
优先考虑 Docker 镜像拉取问题。
如果日志已经开始出现:
pip install -r requirements.txt
Downloading https://pypi.tuna.tsinghua.edu.cn/...
说明已经进入下一层了,不要还一直纠结 Docker Hub。
三、第二关:pip 出现 ResolutionImpossible,是依赖层问题

镜像拉下来之后,Dockerfile 里通常会执行:
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt \-i https://pypi.tuna.tsinghua.edu.cn/simple --prefer-binary --timeout 300
这时的报错就变成了“Python 依赖安装失败”。
3.1 typing_extensions 版本冲突
日志里出现过:
ERROR: Cannot install ... and typing_extensions==4.12.2 because these package versions have conflicting dependencies.The conflict is caused by:The user requested typing_extensions==4.12.2cryptography 47.0.0 depends on typing-extensions>=4.13.2
这说明依赖版本不兼容。翻译过来就是:“我没法同时满足这两个包的要求。”
比如,cryptography 说它要 typing_extensions 的 4.13.2 以上版本,而你的 requirements.txt 却死死锁定了 4.12.2 版本。
3.2 async-timeout 版本冲突
日志可能又出现:
ERROR: Cannot install ... and async-timeout==5.0.1 because these package versions have conflicting dependencies.The conflict is caused by:The user requested async-timeout==5.0.1langchain-classic 1.0.7 depends on async-timeout<5.0.0 and >=4.0.0; python_version < "3.11"
这个问题的关键点在:Python 3.10 下,langchain-classic 要求:
async-timeout>=4.0.0,<5.0.0
而当前写死成了:
async-timeout==5.0.1
这就好比你在搭积木:
- 你手里有一块旧积木(比如
langchain-classic),它的卡扣设计只认特定形状的连接件(async-timeout<5.0.0)。 - 但你的工具箱里只有新积木(
async-timeout==5.0.1),形状对不上。 - 这时候,
pip这个“拼装工”就罢工了,这俩完全拼不到一块去!
既然是积木冲突,解决方案无非:
- 换积木:升级那个“挑剔”的包(比如升级
langchain),让它兼容新版本。 - 削足适履:降级新包,满足旧包的依赖(修改
requirements.txt)。 - 引入“翻译官”:使用
pip-tools或poetry这种高级依赖管理工具,它们能自动帮你找到一套“大家都兼容”的版本组合。
如果你使用的是新版 LangChain,请检查其对应版本的依赖要求,原理相同。
3.3 conda 导出的 requirements 不适合直接进 Docker
这次还发现了类似本地路径依赖:
greenlet @ file:///D:/某位置
SQLAlchemy @ file:///D:/某位置
typing_extensions @ file:///某位置...
这些路径只在原来的本机环境里存在,Docker 容器里当然找不到。
应该改成正常 PyPI 依赖:
greenlet>=3.0.0
SQLAlchemy>=2.0.0
typing_extensions>=4.13.2,<5
比如使用 pip list --format=freeze > requirements.txt 重新导出,或者使用 conda env export --no-builds 来去除平台特定信息。
3.4 这一关的判断汇总
如果日志里有:
ResolutionImpossible
conflicting dependencies
The conflict is caused by
就不要先怀疑代理。pip 已经能下载包了,只是版本解不出来。
重建命令:
docker compose build --no-cache web
docker compose up -d
四、第三关:Alembic 连不上 MySQL,是容器网络问题
构建成功后,执行数据库迁移:
docker compose exec web alembic upgrade head
结果报错:
ConnectionRefusedError: [Errno 111] Connect call failed ('127.0.0.1', 3306)pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on '127.0.0.1'")
但是进入 MySQL 容器却正常:
docker compose exec db mysql -uroot -p你的密码
说明 MySQL 本身没坏。
4.1 关键理解:容器里的 127.0.0.1 是它自己
在 Docker Compose 中,每个容器都有自己的网络命名空间。
web 容器里的 127.0.0.1 = web 容器自己
db 容器里的 127.0.0.1 = db 容器自己
在 Compose 网络中,容器之间应该用服务名访问:
web -> db:3306
web -> redis:6379
web 容器死活连不上 db 容器,报错 ConnectionRefusedError。但是,在 db 容器里检查过,MySQL 明明跑得好好的啊?
这里的核心误区在于对 127.0.0.1 的理解。
- 在容器的世界里,
127.0.0.1意思是“本国领土”。 - 当
web容器找12idot0.0.1:3306时,它其实是在问自己:“我家里有 MySQL 吗?” 没有它就懵了。
这就好比两个不同国家的人(容器),说着不同的母语(网络命名空间)。
这就需要你建立一条“跨国专线”**。
- 起个外号(服务名):在
docker-compose.yml里,我们给数据库服务起名叫db。 - 指名道姓:告诉
web容器,你要找的不是“本国的 127.0.0.1”,而是隔壁“叫 db 的那个国家”。 - 修改连接串:把数据库地址从
127.0.0.1改为db。
这样,web 容器就能通过 Docker 内置的 DNS 翻译官,顺利找到 db 容器了。
4.2 环境变量名也要对得上
本项目后端读取的是:
DB_HOST
DB_USER
DB_PASSWORD
DB_NAME
DB_URI
但 Compose 中如果写成:
environment:- MYSQL_HOST=db- MYSQL_USER=root- MYSQL_PASSWORD=你的密码- MYSQL_DB=你的数据库名称
后端并不会读取这些变量,于是会退回默认配置:
127.0.0.1
这就是为什么 MySQL 明明在运行,Alembic 却连不上。
4.3 推荐配置 DB_URI
最直接的方式是给 web 服务配置完整连接串:
services:web:environment:- REDIS_HOST=redis- DB_URI=mysql+aiomysql://root:你的密码@db:3306/你的数据库名称?charset=utf8mb4
或者拆开写:
services:web:environment:- REDIS_HOST=redis- DB_HOST=db- DB_PORT=3306- DB_USER=root- DB_PASSWORD=你的密码- DB_NAME=你的数据库名称
修改后重启 web 容器:
docker compose up -d --force-recreate web
再次执行迁移:
docker compose exec web alembic upgrade head
成功时会看到:
INFO [alembic.runtime.migration] Context impl MySQLImpl.
INFO [alembic.runtime.migration] Will assume non-transactional DDL.
INFO [alembic.runtime.migration] Running upgrade -> d71e18fcb326, add user email_code model
这时数据库迁移就完成了。
五、第四关:表建好了但查不到,是表名问题
进入 MySQL 后:
docker compose exec db mysql -uroot -p你的密码
查看数据库:
show databases;
use 你的数据库名称;
show tables;
结果:
+------------------------+
| Tables_in_你的数据库名称 |
+------------------------+
| alembic_version |
| email_code |
| user |
+------------------------+
但是执行:
select * from users;
报错:
ERROR 1146 (42S02): Table '你的数据库名称.users' doesn't exist
原因很简单:实际表名是单数:
user
不是:
users
正确查询方式:
select * from `user`;
desc `user`;
select * from email_code;
select * from alembic_version;
这里建议给 user 加反引号,因为 user 在 MySQL 中容易和系统概念混淆。
如果 user 表为空,也不一定是错的。还没有注册用户时,表就是空的。
更推荐通过网页注册用户,而不是直接 SQL 插入。因为注册流程通常会做这些事:
邮箱格式校验
验证码校验
密码哈希
重复用户校验
写入 user 表
删除 Redis 中的验证码
如果手动插入,很容易把明文密码写进去,后面登录会失败。
六、第五关:HBuilderX 提示网络连接失败,是前后端地址问题
后端和数据库都跑起来后,用 HBuilderX 打开前端,在注册页点击“获取验证码”,出现提示:
网络连接失败,请检查服务地址
这个提示来自前端 uni.request 的 fail 回调。它说明:请求没有成功到达后端。
6.1 先看前端 BASE_URL
前端请求配置一般在:
你的项目名称/http/http.js
类似:
const BASE_URL = "http://192.168.x.x:8080";
这里最容易混淆:8080 可能只是 HBuilderX 本地前端预览端口,不一定是后端端口。(通常前端开发服务器是 8080,但生产环境通常不是)。
请确保这里的端口与 docker compose ps 显示的对外暴露端口一致。
如果 Docker Compose 中 nginx 对外暴露的是:
nginx:ports:- "80:80"
那么前端应该访问:
const BASE_URL = "http://192.168.x.x";
如果直接暴露 FastAPI 的 8000 端口,才写:
const BASE_URL = "http://192.168.x.x:8000";
具体用哪个端口,以 docker compose ps 的端口映射为准。
6.2 用浏览器先验证后端地址
在虚拟机中查看 IP:
hostname -I
在 Windows 浏览器中访问:
http://虚拟机IP/
如果后端正常,可能看到:
{"message":"Hello World"}
只有浏览器能访问这个地址,HBuilderX 前端才可能访问成功。
6.3 验证码接口路径
后端验证码接口:
GET /auth/code?email=xxx@xxx.com
前端封装:
getEmailCode: (email) => request("/auth/code?email=" + encodeURIComponent(email), { method: "GET" })
如果前端提示“网络连接失败”,优先查:
BASE_URL 是否正确
虚拟机 IP 是否正确
Docker 服务是否启动
Nginx 或 web 端口是否暴露
Windows 是否能访问虚拟机 IP
如果请求已经到达后端,但邮件发送失败,才继续查 SMTP 配置,比如:
MAIL_USERNAME
MAIL_PASSWORD
MAIL_FROM
MAIL_PORT
MAIL_SERVER
MAIL_STARTTLS
MAIL_SSL_TLS
七、速查表:Docker 部署报错“翻译器”
为了方便大家排查问题,我把这次复盘的“线索”整理成了一张表。下次遇到报错,直接对号入座:
| 报错关键词 / 现象 | 可能层级 | 通俗解释 (Lyn_Li版) | 解决方向 |
|---|---|---|---|
registry-1.docker.io / 超时 |
网络层 | “海关”堵车了 | 配置镜像加速器 / Docker 代理 |
ResolutionImpossible / 冲突 |
依赖层 | “乐高积木”形状不匹配 | 修改 requirements.txt 版本范围 |
Can't connect to MySQL / 127.0.0.1 |
网络层 | “认错亲爹” (容器内 127.0.0.1 指的是自己) | 改用服务名访问 (如 db:3306) |
Table doesn't exist |
数据层 | “眼花看错名” | 执行 show tables; 确认真实表名 (如 user vs users) |
网络连接失败 (前端) |
联调层 | “地址写错收不到货” | 检查 BASE_URL 是否指向虚拟机 IP 和正确端口 |
这张图比单独背命令更有用。部署失败时,就不再会是盲目把所有问题都归因成“网络不好”或者“Docker 程序问题”。
八、常用命令速查表
8.1 Docker Compose 常用命令
| 操作 | 命令 |
|---|---|
| 启动服务 | docker compose up -d |
| 构建并启动 | docker compose up -d --build |
| 只重建 web 镜像 | docker compose build --no-cache web |
| 强制重建 web 容器 | docker compose up -d --force-recreate web |
| 查看容器状态 | docker compose ps |
| 查看 web 日志 | docker compose logs -f web |
| 停止容器但保留数据 | docker compose down |
| 停止并删除数据卷 | docker compose down -v |
⚠️ docker compose down -v 会删除数据卷,MySQL 数据可能一起没了,谨慎使用。
8.2 数据库相关命令
# 进入 MySQL 容器
docker compose exec db mysql -uroot -p你的密码
show databases;
use 你的数据库名称;
show tables;
select * from `user`; # 特殊表名
select * from alembic_version; # 普通表名
8.3 Alembic 迁移命令
# 执行迁移到最新版本
docker compose exec web alembic upgrade head# 查看当前迁移版本
docker compose exec web alembic current
8.4 网络验证命令
# 虚拟机中查看 IP
hostname -I# 查看 Docker 端口映射
docker compose ps# 查看 Docker 代理配置
docker info | grep -i proxy
Windows 浏览器中访问:
http://虚拟机IP/
九、总结
这次部署让我对 Docker 的理解更具体了一点:Docker 不是把项目“打包一下”这么简单,它其实把任务拆成了几层。
镜像层:基础镜像能不能拉下来
依赖层:requirements 能不能解出来
容器层:服务之间能不能互相访问
数据库层:迁移有没有真正执行
前端层:浏览器能不能访问到后端入口
每一层都有自己的错误信号。
看到 Docker 部署报错,不要急着重装,也不要所有问题都归因给代理。先看错误发生在哪一层:拉镜像、装依赖、连数据库、跑迁移,还是前端访问。层次分清了,问题通常就小了一半。
| 问题 | 核心判断 |
|---|---|
| 拉镜像失败 | 查 Docker Hub、镜像源、daemon 代理 |
ResolutionImpossible |
查 requirements 版本冲突 |
连不上 127.0.0.1:3306 |
容器内 localhost 指向当前容器,改用 db:3306 |
| 所查询的表不存在 | 先 show tables;,查看实际表名 |
| HBuilderX 网络连接失败 | 先检查 BASE_URL 和虚拟机后端地址 |
部署跑通后,再回头看这些报错,会发现它们其实都在提示同一件事:不要只看命令,要看命令运行在哪个环境里。
参考链接
- Docker Compose Networking: https://docs.docker.com/compose/how-tos/networking/
- Docker daemon proxy: https://docs.docker.com/engine/daemon/proxy/
- pip dependency resolution: https://pip.pypa.io/en/latest/topics/dependency-resolution/
- FastAPI Docker Deployment: https://fastapi.tiangolo.com/deployment/docker/
声明:本文借助 AI 辅助工具进行资料整理与初稿生成,所有内容均经过作者本人的核对、修改与编排,文责自负。
