640 lines
14 KiB
Markdown
640 lines
14 KiB
Markdown
# 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_USE_HTTPS=true
|
||
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
|
||
```
|
||
|
||
仅使用 IP、尚未配置域名和证书时,可临时改为:
|
||
|
||
```dotenv
|
||
DJANGO_USE_HTTPS=false
|
||
DJANGO_ALLOWED_HOSTS=117.72.28.96,127.0.0.1,localhost
|
||
CORS_ALLOWED_ORIGINS=http://117.72.28.96
|
||
CSRF_TRUSTED_ORIGINS=http://117.72.28.96
|
||
SECURE_HSTS_SECONDS=0
|
||
DEPLOY_HEALTH_HOST=117.72.28.96
|
||
```
|
||
|
||
绑定域名并申请 SSL 后,必须把 `DJANGO_USE_HTTPS` 改回 `true`,并将
|
||
Allowed Hosts、CORS、CSRF 和健康检查 Host 全部替换为正式域名。
|
||
|
||
## 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。
|
||
|
||
如果 Runner 和生产环境在同一 Docker 宿主机,且公网 IP 不支持 NAT 回环,
|
||
`DEPLOY_HOST` 可使用 Docker 网桥宿主机地址 `172.17.0.1`。
|
||
|
||
先在 Gitea 保护 `main` 分支,禁止直接推送,并要求 `CI / test` 通过后才能合并。
|
||
|
||
PR 阶段会执行:
|
||
|
||
```text
|
||
Ruff 静态检查
|
||
→ SQLite 测试和 75% 覆盖率门槛
|
||
→ MySQL 8.0.35 全量迁移和测试
|
||
```
|
||
|
||
PR 合并到 `main` 后会执行:
|
||
|
||
```text
|
||
最终 main 发布文件与脚本语法检查
|
||
→ SSH 生产服务器
|
||
→ MySQL 备份
|
||
→ collectstatic
|
||
→ migrate
|
||
→ 重启
|
||
→ 应用与 Nginx HTTP/WebSocket 冒烟检查
|
||
```
|
||
|
||
合并后的轻量发布检查不再重复创建虚拟环境、启动临时 MySQL 或下载依赖;
|
||
完整 Django、MySQL 和静态资源检查由合并前必须通过的 `CI / test` 负责。
|
||
|
||
## 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
|
||
```
|