Files
Hulumath-Web/docs/BAOTA_UBUNTU_FROM_ZERO.md
T
Jacky 1f98f7152d
CI / test (push) Failing after 1m31s
v0.2 Preview: Construction Plan.
2026-08-08 21:25:27 +08:00

12 KiB
Raw Blame History

Ubuntu + 宝塔 + MySQL 8.0.35 从零部署

本方案允许宝塔原生 MySQL 5.7 与 Docker MySQL 8.0.35 并存:

旧应用(如仍需保留) → MySQL 5.7 :3306
葫芦数学 Django     → MySQL 8.0.35 :3307

生产拓扑:

用户
  ↓ 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。

防火墙只放行:

22 / 80 / 443

不要向公网开放:

3306 / 3307 / 6379 / 8000

2. 备份旧站

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 执行:

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

apt install -y default-mysql-client

验证:

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 应用) 中设置:

版本:mysql:8.0.35
数据库名称:mysql_hulumath
MySQL 端口:3307
MySQL root 密码:生成强密码并离线保存
数据目录:保持宝塔默认持久化目录
内存限制:1024 MB

compose 文件 中确认或补充:

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。

查看容器:

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}'

记录 MySQL 8 容器名:

MYSQL8_CONTAINER=实际容器名

5. 创建应用数据库与用户

进入 MySQL 8 容器:

docker exec -it "$MYSQL8_CONTAINER" mysql -uroot -p

执行:

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

从宿主机验证:

mysql -h 127.0.0.1 -P 3307 \
  -u hulumath -p hulumath \
  -e 'SELECT VERSION(), DATABASE();'

数据库密码若含有 @#:/ 等字符,写入 DATABASE_URL 前必须 URL 编码。

6. 验证服务器可以读取 Gitea 仓库

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. 重新创建项目目录

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

确认:

test -f backend/manage.py
test -f scripts/deploy_production.sh
test -f deploy/hulumath-web.service

8. 创建生产环境变量

cd /www/wwwroot/Hulumath-Web
cp .env.production.example .env.production
openssl rand -hex 48
nano .env.production

示例:

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

锁定权限:

chown root:root .env.production
chmod 600 .env.production

9. 在宝塔创建网站

宝塔 网站添加站点

域名:math.example.com
根目录:/www/wwwroot/Hulumath-Web
PHP:纯静态
数据库:不创建
FTP:不创建

站点配置加入:

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;
}

检查并重载:

nginx -t
/etc/init.d/nginx reload

10. 配置 SSL

在宝塔站点 SSL 页面:

  1. 申请 Let's Encrypt。
  2. 开启强制 HTTPS。
  3. 确认证书域名与 DJANGO_ALLOWED_HOSTS 一致。

11. 停止旧 Flask

在宝塔 Python 项目管理器停止旧 Flask。

ss -ltnp | grep ':8000' || true

首次 Django 部署前,8000 不应被旧进程占用。

12. 首次手动部署

确保系统安装了 mysqlclient 编译依赖:

apt install -y build-essential pkg-config default-libmysqlclient-dev

执行:

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

脚本顺序:

创建虚拟环境
→ 安装 mysqlclient 等依赖
→ mysqldump --single-transaction
→ Django MySQL 检查
→ migrate
→ collectstatic
→ systemd restart
→ HTTP health
→ WebSocket/Redis health

查看:

systemctl status hulumath-web --no-pager
journalctl -u hulumath-web -n 200 --no-pager

13. 首次初始化业务数据

只在空数据库执行:

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 与网站

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

检查表引擎与字符集:

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;"

所有业务表应使用 InnoDButf8mb4_0900_ai_ci

健康检查:

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 密钥:

ssh-keygen -t ed25519 -C 'gitea-actions-hulumath' \
  -f ./hulumath_actions_deploy -N ''

把公钥加入生产服务器:

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 会:

SQLite 快速测试
→ 启动 MySQL 8.0.35 临时容器
→ MySQL 全量迁移和测试
→ SSH 生产服务器
→ MySQL 备份
→ migrate
→ 重启和健康检查

16. 备份与恢复

自动备份目录:

/www/backup/hulumath/

文件格式:

hulumath_YYYYMMDD_HHMMSS.sql.gz

保留 14 天。

人工备份:

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

恢复前必须停止应用并先备份当前库:

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. 旧数据迁移边界

不能直接执行:

复制 SQLite 文件到 MySQL
复制 MySQL 5.7 数据目录给 MySQL 8
把 PostgreSQL dump 导入 MySQL

如果新 MySQL 8 还是空库,直接迁移 Django schema 并导入种子数据即可。

如果需要保留旧 SQLite 或其他数据库中的业务数据,应通过 Django dumpdata/loaddata 或专用 ETL 脚本迁移,并在 Staging 核对:

  • 用户数
  • MathBTI 结果数
  • 剧情存档数
  • 比赛记录数
  • 卡牌和成长记录数

18. 常见问题

mysqlclient 安装失败

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 外部宿主机连接:

SELECT user, host FROM mysql.user WHERE user='hulumath';
SHOW GRANTS FOR 'hulumath'@'%';

Can't connect to MySQL server

docker ps
ss -ltnp | grep 3307
mysql -h 127.0.0.1 -P 3307 -u hulumath -p hulumath

部署找不到 mysqldump

command -v mysqldump
find /www/server/mysql /usr -type f -name mysqldump 2>/dev/null

然后设置:

MYSQLDUMP_BIN=/实际路径/mysqldump

字符集或 Emoji 写入失败

确认:

SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';

应为 utf8mb4utf8mb4_0900_ai_ci

实时匹配出现锁等待

确认使用 MySQL 8.0.35 而不是 5.7,并检查索引:

SHOW INDEX FROM contest_realtimematch;

应存在 matchmaking_lookup_idx

502 Bad Gateway

systemctl status hulumath-web
journalctl -u hulumath-web -n 200 --no-pager
ss -ltnp | grep 8000

WebSocket 失败

检查 Nginx /ws/Upgrade/Connection 头,并确认:

redis-cli ping
journalctl -u hulumath-web -n 200 --no-pager