v0.2 Preview: Construction Plan.
CI / test (push) Failing after 1m31s

This commit is contained in:
2026-08-08 21:25:27 +08:00
parent b88d15698a
commit 1f98f7152d
822 changed files with 96959 additions and 8 deletions
+598
View File
@@ -0,0 +1,598 @@
# Ubuntu + 宝塔 + MySQL 8.0.35 从零部署
本方案允许宝塔原生 MySQL 5.7 与 Docker MySQL 8.0.35 并存:
```text
旧应用(如仍需保留) → MySQL 5.7 :3306
葫芦数学 Django → MySQL 8.0.35 :3307
```
生产拓扑:
```text
用户
↓ HTTPS / WSS
宝塔 Nginx
↓ 127.0.0.1:8000
systemd: Gunicorn + UvicornWorker
├── Django HTTP API
└── Django Channels WebSocket
├── Docker MySQL 8.0.35 :3307
└── Redis :6379
```
约定:
- Ubuntu 22.04 或 24.04。
- Gitea 仓库:`http://117.72.28.96:8765/Jacky/Hulumath-Web.git`
- 项目目录:`/www/wwwroot/Hulumath-Web`
- 应用监听:`127.0.0.1:8000`
- MySQL 8 宿主机端口:`3307`
- systemd 服务:`hulumath-web`
- 示例域名:`math.example.com`,操作时替换为真实域名。
## 1. 服务器资源
8GB 内存服务器建议:
| 服务 | 建议限制 |
| --- | ---: |
| MySQL 8.0.35 | 1024 MB |
| Django 2 workers | 约 200-500 MB |
| Redis | 50-200 MB |
| Nginx | 通常小于 100 MB |
首发负载下压力不大。不要同时为 MySQL 5.7 和 8.0 分配数 GB Buffer Pool。
防火墙只放行:
```text
22 / 80 / 443
```
不要向公网开放:
```text
3306 / 3307 / 6379 / 8000
```
## 2. 备份旧站
```bash
mkdir -p /www/backup/hulumath-legacy
cp -a /www/wwwroot/Hulumath-Web \
"/www/backup/hulumath-legacy/Hulumath-Web.$(date +%Y%m%d_%H%M%S)"
```
旧 SQLite 数据不会自动导入 MySQL。若旧站已有真实用户,需要另做一次性迁移。
## 3. 安装系统依赖
宝塔软件商店保留或安装:
- Nginx
- Redis 7
- Docker 管理器
- Python 项目管理器及 Python 3.12.13
SSH 执行:
```bash
apt update
apt install -y git curl openssl build-essential pkg-config \
default-libmysqlclient-dev mysql-client-8.0 redis-tools
```
若 Ubuntu 软件源没有 `mysql-client-8.0`
```bash
apt install -y default-mysql-client
```
验证:
```bash
redis-cli -h 127.0.0.1 ping
mysql --version
mysqldump --version
ls -l /www/server/pyporject_evn/versions/3.12.13/bin/python*
```
Redis 应返回 `PONG`
## 4. 配置 Docker MySQL 8.0.35
宝塔 `MySQL 多版本管理(Docker 应用)` 中设置:
```text
版本:mysql:8.0.35
数据库名称:mysql_hulumath
MySQL 端口:3307
MySQL root 密码:生成强密码并离线保存
数据目录:保持宝塔默认持久化目录
内存限制:1024 MB
```
`compose 文件` 中确认或补充:
```yaml
services:
mysql:
image: mysql:8.0.35
ports:
- "127.0.0.1:3307:3306"
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_0900_ai_ci
- --default-time-zone=+08:00
- --sql-mode=STRICT_TRANS_TABLES,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION
environment:
MYSQL_ROOT_PASSWORD: 替换为强密码
volumes:
- /www/dk_project/dk_mm_datas:/var/lib/mysql
deploy:
resources:
limits:
memory: 1024M
```
关键点:
- 最好绑定 `127.0.0.1:3307`,而不是 `0.0.0.0:3307`
- 数据目录必须是持久化目录。
- 不要复用旧 MySQL 5.7 的数据目录。
- 不要直接把 5.7 数据目录挂载给 8.0。
查看容器:
```bash
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'
```
记录 MySQL 8 容器名:
```bash
MYSQL8_CONTAINER=实际容器名
```
## 5. 创建应用数据库与用户
进入 MySQL 8 容器:
```bash
docker exec -it "$MYSQL8_CONTAINER" mysql -uroot -p
```
执行:
```sql
CREATE DATABASE hulumath
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
CREATE USER 'hulumath'@'%' IDENTIFIED BY 'ReplaceWithStrongPassword';
GRANT ALL PRIVILEGES ON hulumath.* TO 'hulumath'@'%';
FLUSH PRIVILEGES;
SELECT VERSION();
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
SHOW VARIABLES LIKE 'sql_mode';
```
版本必须是 `8.0.35`,字符集应为 `utf8mb4`
从宿主机验证:
```bash
mysql -h 127.0.0.1 -P 3307 \
-u hulumath -p hulumath \
-e 'SELECT VERSION(), DATABASE();'
```
数据库密码若含有 `@``#``:``/` 等字符,写入 `DATABASE_URL` 前必须 URL 编码。
## 6. 验证服务器可以读取 Gitea 仓库
```bash
git ls-remote \
http://117.72.28.96:8765/Jacky/Hulumath-Web.git \
refs/heads/main
```
该仓库当前允许匿名读取,因此生产服务器无需保存 Gitea 账号密码。若以后改为私有仓库,应使用只读 Deploy Token,并通过 Git credential helper 配置,不要把 Token 写进 workflow 或仓库 URL。
## 7. 重新创建项目目录
```bash
cd /www/wwwroot
if [ -d Hulumath-Web ]; then
mv Hulumath-Web \
"Hulumath-Web.flask.$(date +%Y%m%d_%H%M%S)"
fi
git clone \
http://117.72.28.96:8765/Jacky/Hulumath-Web.git \
Hulumath-Web
cd Hulumath-Web
git checkout main
```
确认:
```bash
test -f backend/manage.py
test -f scripts/deploy_production.sh
test -f deploy/hulumath-web.service
```
## 8. 创建生产环境变量
```bash
cd /www/wwwroot/Hulumath-Web
cp .env.production.example .env.production
openssl rand -hex 48
nano .env.production
```
示例:
```dotenv
DJANGO_SETTINGS_MODULE=config.settings
DJANGO_SECRET_KEY='替换为 openssl 生成的密钥'
DJANGO_DEBUG=false
DJANGO_ALLOWED_HOSTS=math.example.com
DATABASE_URL='mysql://hulumath:ReplaceWithStrongPassword@127.0.0.1:3307/hulumath'
REDIS_URL='redis://127.0.0.1:6379/0'
CORS_ALLOWED_ORIGINS=https://math.example.com
CSRF_TRUSTED_ORIGINS=https://math.example.com
API_ANON_RATE=120/minute
API_USER_RATE=600/minute
SECURE_HSTS_SECONDS=31536000
DEPLOY_HEALTH_HOST=math.example.com
MYSQLDUMP_BIN=/usr/bin/mysqldump
```
如果 `command -v mysqldump` 返回其他路径,修改 `MYSQLDUMP_BIN`
锁定权限:
```bash
chown root:root .env.production
chmod 600 .env.production
```
## 9. 在宝塔创建网站
宝塔 `网站``添加站点`
```text
域名:math.example.com
根目录:/www/wwwroot/Hulumath-Web
PHP:纯静态
数据库:不创建
FTP:不创建
```
站点配置加入:
```nginx
client_max_body_size 10m;
location /ws/ {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
location / {
proxy_pass http://127.0.0.1:8000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 10s;
proxy_read_timeout 120s;
}
```
检查并重载:
```bash
nginx -t
/etc/init.d/nginx reload
```
## 10. 配置 SSL
在宝塔站点 `SSL` 页面:
1. 申请 Let's Encrypt。
2. 开启强制 HTTPS。
3. 确认证书域名与 `DJANGO_ALLOWED_HOSTS` 一致。
## 11. 停止旧 Flask
在宝塔 Python 项目管理器停止旧 Flask。
```bash
ss -ltnp | grep ':8000' || true
```
首次 Django 部署前,8000 不应被旧进程占用。
## 12. 首次手动部署
确保系统安装了 mysqlclient 编译依赖:
```bash
apt install -y build-essential pkg-config default-libmysqlclient-dev
```
执行:
```bash
cd /www/wwwroot/Hulumath-Web
chmod +x scripts/deploy_production.sh
PROJECT_DIR=/www/wwwroot/Hulumath-Web \
PYTHON_BIN=/www/server/pyporject_evn/versions/3.12.13/bin/python3 \
bash scripts/deploy_production.sh
```
脚本顺序:
```text
创建虚拟环境
→ 安装 mysqlclient 等依赖
→ mysqldump --single-transaction
→ Django MySQL 检查
→ migrate
→ collectstatic
→ systemd restart
→ HTTP health
→ WebSocket/Redis health
```
查看:
```bash
systemctl status hulumath-web --no-pager
journalctl -u hulumath-web -n 200 --no-pager
```
## 13. 首次初始化业务数据
只在空数据库执行:
```bash
cd /www/wwwroot/Hulumath-Web
set -a
source .env.production
set +a
.venv-production/bin/python backend/manage.py seed_initial_content
.venv-production/bin/python backend/manage.py seed_contests
.venv-production/bin/python backend/manage.py createsuperuser
```
不要把种子命令加入每次自动部署。
## 14. 验证 MySQL 与网站
```bash
cd /www/wwwroot/Hulumath-Web
set -a
source .env.production
set +a
.venv-production/bin/python backend/manage.py check --database default
.venv-production/bin/python backend/manage.py showmigrations
```
检查表引擎与字符集:
```bash
mysql -h 127.0.0.1 -P 3307 -u hulumath -p hulumath -e "
SELECT TABLE_NAME, ENGINE, TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA='hulumath'
ORDER BY TABLE_NAME;"
```
所有业务表应使用 `InnoDB``utf8mb4_0900_ai_ci`
健康检查:
```bash
curl -H 'Host: math.example.com' \
-H 'X-Forwarded-Proto: https' \
http://127.0.0.1:8000/health/
curl https://math.example.com/health/
```
## 15. 配置 Gitea Actions
生成 Actions 到服务器的 SSH 密钥:
```bash
ssh-keygen -t ed25519 -C 'gitea-actions-hulumath' \
-f ./hulumath_actions_deploy -N ''
```
把公钥加入生产服务器:
```bash
cat hulumath_actions_deploy.pub >> /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys
```
在 Gitea 仓库 `Settings → Actions → Secrets` 添加:
| Secret | 值 |
| --- | --- |
| `DEPLOY_HOST` | 服务器公网 IP 或 SSH 域名 |
| `DEPLOY_USER` | 推荐首次使用 `root` |
| `DEPLOY_SSH_KEY` | 私钥全文 |
确保有能匹配 `ubuntu-latest` 的在线 Runner。
PR 合并到 `main` 后,workflow 会:
```text
SQLite 快速测试
→ 启动 MySQL 8.0.35 临时容器
→ MySQL 全量迁移和测试
→ SSH 生产服务器
→ MySQL 备份
→ migrate
→ 重启和健康检查
```
## 16. 备份与恢复
自动备份目录:
```text
/www/backup/hulumath/
```
文件格式:
```text
hulumath_YYYYMMDD_HHMMSS.sql.gz
```
保留 14 天。
人工备份:
```bash
mysqldump \
-h 127.0.0.1 -P 3307 \
-u hulumath -p \
--single-transaction --quick \
--routines --triggers --events \
--hex-blob --set-gtid-purged=OFF \
hulumath | gzip > /www/backup/hulumath/manual.sql.gz
```
恢复前必须停止应用并先备份当前库:
```bash
systemctl stop hulumath-web
gunzip -c /www/backup/hulumath/具体备份.sql.gz | \
mysql -h 127.0.0.1 -P 3307 -u hulumath -p hulumath
systemctl start hulumath-web
```
## 17. 旧数据迁移边界
不能直接执行:
```text
复制 SQLite 文件到 MySQL
复制 MySQL 5.7 数据目录给 MySQL 8
把 PostgreSQL dump 导入 MySQL
```
如果新 MySQL 8 还是空库,直接迁移 Django schema 并导入种子数据即可。
如果需要保留旧 SQLite 或其他数据库中的业务数据,应通过 Django `dumpdata/loaddata` 或专用 ETL 脚本迁移,并在 Staging 核对:
- 用户数
- MathBTI 结果数
- 剧情存档数
- 比赛记录数
- 卡牌和成长记录数
## 18. 常见问题
### mysqlclient 安装失败
```bash
apt install -y build-essential pkg-config default-libmysqlclient-dev
rm -rf /www/wwwroot/Hulumath-Web/.venv-production
bash scripts/deploy_production.sh
```
### Access denied for user
确认用户允许 Docker 外部宿主机连接:
```sql
SELECT user, host FROM mysql.user WHERE user='hulumath';
SHOW GRANTS FOR 'hulumath'@'%';
```
### Can't connect to MySQL server
```bash
docker ps
ss -ltnp | grep 3307
mysql -h 127.0.0.1 -P 3307 -u hulumath -p hulumath
```
### 部署找不到 mysqldump
```bash
command -v mysqldump
find /www/server/mysql /usr -type f -name mysqldump 2>/dev/null
```
然后设置:
```dotenv
MYSQLDUMP_BIN=/实际路径/mysqldump
```
### 字符集或 Emoji 写入失败
确认:
```sql
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
```
应为 `utf8mb4``utf8mb4_0900_ai_ci`
### 实时匹配出现锁等待
确认使用 MySQL 8.0.35 而不是 5.7,并检查索引:
```sql
SHOW INDEX FROM contest_realtimematch;
```
应存在 `matchmaking_lookup_idx`
### 502 Bad Gateway
```bash
systemctl status hulumath-web
journalctl -u hulumath-web -n 200 --no-pager
ss -ltnp | grep 8000
```
### WebSocket 失败
检查 Nginx `/ws/``Upgrade`/`Connection` 头,并确认:
```bash
redis-cli ping
journalctl -u hulumath-web -n 200 --no-pager
```