Docker 部署踩坑记录:从"构建失败"到"服务跑通",以及为什么数据被清空了
这篇文章记录一次把本项目(FastAPI + MySQL)用 Docker Compose 在 Windows 本地跑通、再准备迁移到远程 Linux 的过程中遇到的典型问题与解决方案。
对应仓库关键文件:
- 后端镜像构建:Dockerfile
- 生产编排:docker-compose.prod.yml
- 环境变量模板:.env.example
- 部署教程:部署.md
背景:我想要的目标很简单
- 本地 Windows 先用 Docker 跑通(可访问
/docs) - 然后把同一套
docker-compose.prod.yml + .env拿到远程 Linux 一键复用
实际过程里,一共踩了 4 个坑:
- 构建阶段拉取
ghcr.io失败(EOF) - 后端容器一直重启:MySQL 认证失败(1045)
- MySQL 8 认证插件导致 PyMySQL 需要
cryptography - 为了让新账号/密码生效,我删除了数据卷,导致历史数据被清空
下面按时间线复盘。
坑 1:Docker build 拉 ghcr.io/astral-sh/uv:latest 失败(EOF)
现象
执行:
bash
docker compose --env-file .env -f docker-compose.prod.yml up -d --build
报错类似:
text
failed to resolve source metadata for ghcr.io/astral-sh/uv:latest ... EOF
根因
原始 Dockerfile 使用了:
dockerfile
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
这要求构建机能稳定访问 ghcr.io(GitHub Container Registry)。在部分网络环境下,访问 GHCR 会不稳定或被拦截,从而导致构建失败。
解决
把"从 GHCR 拷 uv 二进制"的方式,改成"从 PyPI 安装 uv":
dockerfile
RUN python -m pip install --no-cache-dir -U pip && \
python -m pip install --no-cache-dir uv
这样构建阶段只依赖 PyPI(通常更好访问),构建成功率大幅提升。
坑 2:构建成功但访问不了,原因是后端容器在重启
现象
构建已经完成,但访问 http://127.0.0.1:9006/docs 失败。
第一反应是"端口没映射/防火墙",但正确排查顺序是先看容器状态:
bash
docker compose --env-file .env -f docker-compose.prod.yml ps
会看到类似:
text
nongzidian_api ... Restarting (1) ...
也就是说:不是"访问不了",而是"服务根本没跑起来"。
根因
查看日志:
bash
docker logs --tail 200 nongzidian_api
出现:
text
sqlalchemy.exc.OperationalError: (pymysql.err.OperationalError) (1045, "Access denied for user 'root'@'...' (using password: YES)")
这是典型的 MySQL 认证失败。
当时 DATABASE_URL 使用了 root 账号连接:
env
DATABASE_URL=mysql+pymysql://root:密码@db:3306/nongzidian
但 MySQL 官方镜像中,root 的远程登录策略、插件策略可能与你预期不同(尤其是容器间连接场景),导致被拒绝。
解决
改成使用业务账号连接 MySQL(更推荐的生产做法):
docker-compose.prod.yml中让 MySQL 初始化创建业务用户:MYSQL_USERMYSQL_PASSWORD
.env中改用业务用户拼DATABASE_URL
示例:
env
MYSQL_USER=nongzidian
MYSQL_PASSWORD=你的强密码
DATABASE_URL=mysql+pymysql://nongzidian:你的强密码@db:3306/nongzidian
坑 3:MySQL 8 认证方式导致 PyMySQL 报 cryptography 缺失
现象
认证失败的报错从 1045 变成了:
text
RuntimeError: 'cryptography' package is required for sha256_password or caching_sha2_password auth methods
根因
MySQL 8 常见的默认认证插件是 caching_sha2_password。PyMySQL 在使用某些认证方式时需要 cryptography 包参与加密过程,否则会直接报错。
即便你加了:
yaml
command: --default-authentication-plugin=mysql_native_password
也不能保证所有用户/场景都一定不会走到 caching_sha2_password(历史数据卷里旧用户、初始化顺序、镜像行为差异等都会影响)。
解决(推荐)
在项目依赖里显式加入 cryptography,让后端在任何 MySQL 8 认证方式下都能稳定连接:
- 依赖文件:pyproject.toml
并且更新 uv.lock,否则 Dockerfile 里的:
bash
uv sync --frozen ...
会因锁文件不匹配而失败。
坑 4:为什么"跑通了",但历史数据全没了?
现象
容器最终确实跑通了,/docs 能访问,但发现之前 MySQL 里已有的数据不见了。
根因
MySQL 数据存在 Docker 的命名卷里(本项目是 mysql_data)。
当你修改这类 MySQL 初始化参数时:
MYSQL_ROOT_PASSWORDMYSQL_USERMYSQL_PASSWORDMYSQL_DATABASE
如果数据卷已经存在,MySQL 容器启动时通常会跳过初始化逻辑(因为它认为已经初始化过了)。
为了让"新账号/新密码"生效,常见做法是删除数据卷:
bash
docker compose --env-file .env -f docker-compose.prod.yml down -v
注意这个 -v 的含义:删除卷 = 删除数据库文件 = 数据清空。
解决与正确姿势
如果你只是本地测试,删卷没问题;但如果你在正式环境已经有数据了,必须先备份。
方案 A:删卷前先备份(推荐)
bash
docker exec nongzidian_mysql mysqldump -uroot -p你的root密码 nongzidian > backup.sql
然后再删卷重建。
方案 B:不删卷,手动在 MySQL 里创建用户(生产更常见)
进入 MySQL 容器执行 SQL(思路示例):
sql
CREATE USER IF NOT EXISTS 'nongzidian'@'%' IDENTIFIED BY '你的强密码';
GRANT ALL PRIVILEGES ON nongzidian.* TO 'nongzidian'@'%';
FLUSH PRIVILEGES;
这样既能让后端用业务账号登录,也不会丢数据。
额外小坑:Windows PowerShell 的 curl
在 Windows PowerShell 里,curl 常常是 Invoke-WebRequest 的别名,行为和 Linux 的 curl 不一样。
如果你想用真正的 curl,可用:
bash
curl.exe -I http://127.0.0.1:9006/docs
或者直接用浏览器打开 /docs。
最终我得到的一套"更稳"的结论
- Docker 构建阶段尽量少依赖不稳定的镜像源(例如 GHCR),否则"能跑/不能跑"全看网络脸色
- MySQL 连接不要用 root,推荐用业务账号(权限最小化、行为更可控)
- MySQL 8 + PyMySQL 场景,显式加
cryptography可以显著降低认证相关坑 - 修改 MySQL 初始化环境变量时要牢记:已有数据卷时这些变量不一定生效
down -v是大杀器:它解决问题也会清空数据,上线前必须备份
如果你准备把这套部署搬到远程 Linux,建议顺序是:
- 本地跑通并固定配置
- 把
.env里的密码全部换成生产强密码 - 远程上首次部署可以用"删卷初始化",但一旦有数据就改用"备份 + 迁移/手动建用户"的方式