1Panel 服务器 Docker 与 new-api 迁移 MySQL 全记录
1Panel 服务器 Docker 与 new-api 迁移 MySQL 全记录
这次迁移主要做了两件事:先修复 Debian 12 上 Docker 端口映射失败的问题,再把 new-api 从 SQLite 迁移到 MySQL。过程里踩到的坑不少,尤其是 iptables/nftables 兼容性和 SQLite 到 MySQL 的语法差异。
背景
new-api 原本运行在 Docker 中,默认使用 SQLite 数据库。随着日志、用户和渠道数据越来越多,SQLite 数据库体积已经接近 200MB,继续放在容器数据卷里不太适合长期维护。
我的目标是:
- 在 1Panel 管理的服务器上启动 MySQL 容器
- 把 new-api 的 SQLite 数据完整导入 MySQL
- 使用新容器先测试,再切换反向代理流量
- 尽量不影响线上服务
生产迁移里最重要的一点是:不要直接停旧服务。先把新环境跑起来,确认没问题以后再切流量。
iptables 与 Docker 端口映射问题
报错现象
在 1Panel 安装 MySQL 容器时,容器可以创建成功,但启动阶段失败,错误类似:
iptables: No chain/target/match by that name.进一步看 Docker 相关日志,可以看到 nftables 后端不兼容的问题:
iptables v1.8.9 (nf_tables): chain `DOCKER' in table `filter' is incompatible, use 'nft' tool.根因
Debian 12 bookworm 默认使用 iptables-nft,也就是 nftables 后端。Docker 需要维护自己的 DOCKER 链做端口映射,但当前环境下 filter 表里的链状态和 nft 后端不兼容。
这个问题有个容易误判的地方:已有容器的端口映射可能仍然正常,因为 NAT 表没有立即受影响;真正失败的是新建容器或新增端口映射。
修复方式
先备份当前 iptables 规则:
mkdir -p /root/backup-iptables-$(date +%Y%m%d)
iptables-save > /root/backup-iptables-$(date +%Y%m%d)/iptables-all.txt然后把 iptables 和 ip6tables 切换到 legacy 模式:
update-alternatives --set iptables /usr/sbin/iptables-legacy
update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy最后重启 Docker:
systemctl restart docker如果容器设置了 restart: always 或 unless-stopped,Docker 重启后容器会自动恢复。使用 host 网络模式的容器,例如 openresty,一般不受这个端口映射问题影响。
SQLite 到 MySQL 的迁移难点
new-api 使用 GORM,首次连接 MySQL 时可以自动建表,但这不等于会自动迁移 SQLite 数据。真实迁移还需要把 SQLite dump 转成 MySQL 可导入的 SQL。
直接转换会遇到几类问题:
- SQLite 允许
TEXT字段参与主键或索引,MySQL 会报ERROR 1170。 - SQLite 支持部分索引,例如
WHERE deleted_at IS NULL,MySQL 不能直接照搬。 - SQLite 的 BLOB 字节串可能写成
X'...',MySQL 的 JSON 列无法直接接受。 - MySQL 不允许
TEXT或BLOB字段设置默认值。 - SQLite 默认大小写敏感,MySQL 默认排序规则通常大小写不敏感,唯一键可能产生冲突。
所以这一步不能只靠简单的搜索替换,更适合写转换脚本处理结构和数据。
转换脚本处理策略
我最后使用脚本生成了一份 MySQL 可导入 SQL,主要做了这些处理:
- 索引、主键、唯一键涉及的
TEXT字段改成VARCHAR(191) - 非索引大文本字段改成
LONGTEXT - SQLite 部分索引转换为 MySQL 可接受的表达式索引
- BLOB JSON 字节串转换为 UTF-8 JSON 字符串
- 带时区的 SQLite 时间转换为 MySQL
DATETIME(6)字面量 - 全部使用
utf8mb4_bin排序规则,尽量保持 SQLite 的大小写敏感行为
迁移时不要把真实数据库密码、root 密码和 DSN 明文写进公开文章或仓库。下面命令里的敏感字段都用占位符代替。
蓝绿部署流程
整体流程是先导入数据,再启动新容器测试,最后切换反向代理。
1. 导入 MySQL 数据
先创建目标数据库和用户,并授予权限:
CREATE DATABASE `one-api` CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;
CREATE USER 'one-api'@'%' IDENTIFIED BY '<mysql-user-password>';
GRANT ALL PRIVILEGES ON `one-api`.* TO 'one-api'@'%';
FLUSH PRIVILEGES;然后导入转换后的 SQL:
mysql -u one-api -p one-api < /root/one-api-mysql-importable.sql如果已经让 new-api 连接过空 MySQL,GORM 可能已经插入默认数据。这种情况下需要先清空默认数据,避免导入时出现主键或唯一键冲突。
2. 启动 MySQL 版 new-api 容器
新容器先监听 3001,旧容器继续监听 3000:
docker run -d --name new-api-mysql \
--network 1panel-network \
--restart unless-stopped \
-p 3001:3000 \
-e SQL_DSN="one-api:<mysql-user-password>@tcp(<mysql-host>:3306)/one-api?charset=utf8mb4&parseTime=True&loc=Local" \
-e TZ=Asia/Shanghai \
-e ERROR_LOG_ENABLED=true \
-e BATCH_UPDATE_ENABLED=true \
-v /home/ubuntu/data/new-api-mysql:/app/data \
calciumion/new-api:latest --log-dir /app/logs这里要注意两点:
- new-api 容器必须加入能访问 MySQL 的 Docker 网络
SQL_DSN里建议带上charset=utf8mb4&parseTime=True&loc=Local
3. 测试新容器
先用接口确认服务状态:
curl http://localhost:3001/api/status正常情况下会返回 success: true。然后再检查用户、渠道、配置项、日志等核心表的数据量是否和 SQLite 对得上。
我这次迁移后的数据量大致如下:
| 表 | 行数 |
|---|---|
| abilities | 10,143 |
| channels | 61 |
| logs | 434,906 |
| users | 7,146 |
| tokens | 253 |
| options | 60 |
| vendors | 12 |
| midjourneys | 137 |
| quota_data | 4,033 |
| top_ups | 304 |
| redemptions | 9 |
| setups | 1 |
4. 切换反向代理
确认新容器工作正常以后,再修改 openresty/nginx 反向代理,把 upstream 从旧容器端口切到新容器端口:
proxy_pass http://127.0.0.1:3001;然后 reload:
nginx -s reload切换后观察登录、渠道调用、日志写入和额度扣减是否正常。
5. 下线旧容器
确认没有问题后,再停掉旧的 SQLite 版容器:
docker stop new-api
docker rm new-api旧 SQLite 数据卷不要马上删除,至少保留一段时间,方便回滚或对账。
关键检查点
迁移完成后,我建议至少检查这些内容:
- MySQL 用户是否只拥有目标库权限
- new-api 容器是否加入了正确的 Docker 网络
SQL_DSN是否使用了正确的库名、字符集和时区参数- 用户数、渠道数、令牌数、配置项是否和迁移前一致
- 日志表是否还能继续写入
- 反向代理切换后前台和管理后台是否都能访问
- 旧 SQLite 数据库是否已经备份
总结
这次迁移最大的坑不是 new-api 本身,而是 Docker 网络、iptables 后端和数据库方言差异叠在了一起。
比较稳的做法是:
- 先修复 Docker 端口映射环境
- 写脚本把 SQLite dump 转成 MySQL 可导入 SQL
- 用新容器连接 MySQL 单独测试
- 通过反向代理切流量
- 保留旧数据库和旧容器一段时间
这样即使新环境出问题,也能快速把流量切回旧容器,避免一次迁移把生产服务直接打停。
