# 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`。 如果这里显示宿主机 `mysql/mysqldump` 是 5.7,而 Docker 容器是 MySQL 8.0.35,不需要卸载旧客户端。宿主机 5.7 客户端只用于简单连通性测试; 生产自动备份必须配置 `MYSQL_CONTAINER`,使用容器内部的 MySQL 8 `mysqldump`,避免跨版本备份兼容问题。 ## 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 WITH mysql_native_password 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`。 这里使用 `mysql_native_password` 是为了兼容服务器现有 MySQL 5.7 命令行客户端 和系统 `mysqlclient` 动态库。数据库端口只绑定 `127.0.0.1:3307`,该账号不会 暴露公网。 从宿主机验证: ```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 MYSQL_CONTAINER=mysql_o0fbdmmf8q ``` `MYSQL_CONTAINER` 必须填写 `docker ps` 显示的 MySQL 8 容器名。你的服务器 当前应填写 `mysql_o0fbdmmf8q`。部署脚本会把临时凭据复制进容器,调用容器 内部的 MySQL 8 `mysqldump`,完成后立即删除。 锁定权限: ```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 天。 人工备份建议同样使用容器内的 MySQL 8 客户端。以下命令会提示输入应用数据库密码: ```bash docker exec -i mysql_o0fbdmmf8q mysqldump \ -u hulumath -p \ --single-transaction --quick \ --triggers --no-tablespaces \ --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 docker exec mysql_o0fbdmmf8q mysqldump --version ``` 容器内应显示 MySQL 8。然后在 `.env.production` 设置: ```dotenv MYSQL_CONTAINER=mysql_o0fbdmmf8q ``` ### 字符集或 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 ```