17 KiB
04-部署运行文档
本文记录真实生产环境的部署架构与运维操作手册(截至 2026-10-18)。 服务器:腾讯云 CVM(14G 内存),已装宝塔面板;系统 Linux。 项目根目录:
/root/project/video,三个业务域名均启用 HTTPS。
1. 总体架构
┌─────────────────────────────────────────┐
用户浏览器/App │ 腾讯云服务器 │
│ │
h5.256kb.top ───────► │ nginx(宝塔) ──► /root/project/video/h5 │
(下载页/邀请落地页) │ ├─ /downloads /videos /uploads │
│ │ → /root/project/video/runtime/* │
│ └─ /r/ /api/v1/invite/ ──┐ │
vm.256kb.top ───────► │ root:/root/project/video/admin/dist │
(管理后台 SPA) │ /api/ /static/ /r/ ──────┤ │
│ ▼ │
vapi.256kb.top ──────► │ 全量反代 ──────────► uvicorn 127.0.0.1:8000
(App API 入口) │ (video-api.service,│
│ 2 workers) │
│ │ │ │
│ ┌───────────┘ └────┐ │
│ ▼ ▼ │
│ docker: mysql8 docker: my-redis
│ 127.0.0.1:3307 127.0.0.1:6379
│ 数据卷 /opt/mysql8-data 配置/数据 /home/redis
└─────────────────────────────────────────┘
视频 ts 分片流量:App → 直连源站 CDN(不经过本服务器,见 02-技术文档)
| 域名 | 用途 | nginx 要点 |
|---|---|---|
h5.256kb.top |
App 下载页 + 邀请落地页 + 静态媒体 | root=/root/project/video/h5;/downloads/ /videos/ /uploads/ alias 到 runtime/;/r/、/api/v1/invite/ 反代 8000 |
vm.256kb.top |
管理后台 | root=/root/project/video/admin/dist(SPA 回退 index.html);/api/ /static/ /r/ 反代 8000 |
vapi.256kb.top |
App 业务 API | 全量反代 127.0.0.1:8000 |
codeserver.256kb.top |
code-server(运维用,默认容器未自启) | 反代 code-server 容器 |
证书:Let's Encrypt ECC,由 acme.sh/宝塔申请到 /www/server/panel/vhost/cert/<域名>/fullchain.pem,crontab 有续期任务(5 4,10,16,22 * * * acme.sh --cron + 宝塔 acme_v2.py),80 端口仅用于 acme 挑战与跳转 HTTPS。
2. 目录规划
| 路径 | 用途 |
|---|---|
/root/project/video/server |
FastAPI 后端源码 + .venv 虚拟环境 + .env + config.yaml |
/root/project/video/admin |
Vue3 后台源码;构建产物 admin/dist/ 即 vm 站点根目录 |
/root/project/video/app |
Flutter App 源码(编译机即本服务器) |
/root/project/video/h5 |
H5 下载页(index.html) |
/root/project/video/runtime |
运行期产出:downloads/(APK)、videos/(转码视频)、uploads/(上传文件) |
/root/project/video/docs |
项目文档(本目录) |
/root/project/video/deploy.sh |
App 一键发版脚本(见 §7) |
/root/project/video/project-ctl.sh |
项目一键启停脚本(见 §8) |
/opt/mysql8-data |
MySQL 8 数据卷(Docker 挂载) |
/home/redis/conf、/home/redis/data |
Redis 配置与数据(Docker 挂载) |
/root/backups/video-app-signing |
App 签名 keystore 备份(极其重要,见 §7.3) |
3. Docker 组件(MySQL / Redis)
两个容器均为 --restart unless-stopped:服务器重启自动拉起;手动 docker stop 后重启不会恢复(project-ctl.sh stop 利用此语义实现"暂停")。
3.1 MySQL 8(容器名 mysql8)
- 镜像
mysql:8(当前 8.4.11),端口映射 宿主机 3307 → 容器 3306 - 数据卷
/opt/mysql8-data:/var/lib/mysql,时区 Asia/Shanghai - 业务库
video,账号root/ 密码256kb.cn - 连接必须加
-h 127.0.0.1:不加时 mysql 客户端走 unix socket 会连到宿主机旧版 MySQL 5.7,曾导致 deploy.sh 把版本号写错库
# 日常连接
mysql -h 127.0.0.1 -uroot -p256kb.cn -P 3307 video
# 或容器内
docker exec -it mysql8 mysql -uroot -p256kb.cn video
# 如需重建容器(数据在 /opt/mysql8-data,不丢)
docker run -d --name mysql8 --restart unless-stopped \
-p 3307:3306 -v /opt/mysql8-data:/var/lib/mysql \
-e TZ=Asia/Shanghai -e MYSQL_ROOT_PASSWORD=256kb.cn mysql:8
3.2 Redis(容器名 my-redis)
- 镜像
redis:latest(7.x),端口 6379,挂载/home/redis/conf与/home/redis/data - 启动命令
redis-server /usr/local/etc/redis/redis.conf - 注意:宿主机曾装过 Redis 5.0.3 并已禁用自启。若它抢占了 6379,新版 redis-py 客户端默认 RESP3 的
HELLO握手会失败 → 后端全接口 500。排查时先确认docker ps里 my-redis 在跑、且 6379 被 docker-proxy 占用 - 后端 redis-py 客户端显式指定
protocol=2(RESP2),新旧服务端都兼容 - 清业务缓存一律容器内执行:
docker exec my-redis redis-cli DEL config:system
# 如需重建容器
docker run -d --name my-redis --restart unless-stopped \
-p 6379:6379 -v /home/redis/conf:/usr/local/etc/redis -v /home/redis/data:/data \
redis:latest redis-server /usr/local/etc/redis/redis.conf
4. 后端服务(video-api.service)
4.1 systemd 单元(/etc/systemd/system/video-api.service)
[Unit]
Description=Video App FastAPI Backend (uvicorn)
After=network.target docker.service redis.service
Requires=docker.service
[Service]
Type=simple
User=root
WorkingDirectory=/root/project/video/server
ExecStart=/root/project/video/server/.venv/bin/uvicorn app.main:app --host 127.0.0.1 --port 8000 --workers 2
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
- 只监听 127.0.0.1:8000,对外全靠 nginx 反代
--workers 2:⚠️ 结算周期任务(推荐激活/创作收益/超时关单)随每个 worker 的 lifespan 各启动一份(app/services/settle_service.py)。当前任务均为条件更新、重复执行无副作用;修改周期任务逻辑时必须保证幂等- 日志走 journald:
journalctl -u video-api -f
常用命令:
systemctl restart video-api # 改后端代码后重启
systemctl enable --now video-api # 启动并恢复开机自启
journalctl -u video-api -n 100 --no-pager
4.2 环境配置(server/.env)
键清单(敏感值不入文档;.env 本身不入 git):
APP_ENV=prod APP_HOST APP_PORT
DATABASE_URL=mysql+aiomysql://root:256kb.cn@127.0.0.1:3307/video?charset=utf8mb4
REDIS_URL=redis://127.0.0.1:6379/0
CELERY_BROKER_URL CELERY_RESULT_BACKEND # 预留,当前未启用
JWT_SECRET ACCESS_TOKEN_TTL REFRESH_TOKEN_TTL ADMIN_TOKEN_TTL
OSS_ENDPOINT OSS_BUCKET OSS_ACCESS_KEY_ID OSS_ACCESS_KEY_SECRET # 未启用则为占位
ALIPAY_APP_ID ALIPAY_PRIVATE_KEY ALIPAY_PUBLIC_KEY # 支付占位→dev_mock
WECHAT_MCHID WECHAT_APP_ID WECHAT_PRIVATE_KEY
TRON_API_KEY # USDT 查账
SMSBAO_USERNAME SMSBAO_API_KEY SMS_SIGN_NAME # 短信宝验证码
SMTP_HOST SMTP_PORT SMTP_USER SMTP_PASSWORD # 邮箱验证码(已启用)
业务参数在 server/config.yaml(顶层键:points / membership / recharge / referral / asset / withdraw / upload),其中白名单项可在后台「系统配置」热改。
4.3 数据库初始化(新环境一次性)
cd /root/project/video/server
python3.11 -m venv .venv && .venv/bin/pip install .
docker exec mysql8 mysql -uroot -p256kb.cn -e \
"CREATE DATABASE IF NOT EXISTS video CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
.venv/bin/alembic upgrade head # 4 个迁移,建 30 张业务表(含 series 补漏迁移 e7a1f3c58d02)
创建首个超管(代码不自动播种):
.venv/bin/python - <<'EOF'
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker
from app.config import settings
from app.core.security import hash_password
from app.models.admin import AdminUser
async def main():
engine = create_async_engine(settings.database_url)
async with async_sessionmaker(engine)() as s:
s.add(AdminUser(username="admin",
password_hash=hash_password("请设置强密码"),
name="超管", role="super"))
await s.commit()
await engine.dispose()
asyncio.run(main())
EOF
后台角色:super(全部权限)、operator(仪表盘/套餐/赠送)、reviewer(视频审核)。
4.4 服务暴露路径(反代依据)
| 路径 | 说明 |
|---|---|
/api/v1/* |
App 业务接口(9 个路由文件 + admin,共 100+ 端点) |
/api/v1/admin/* |
管理后台接口 |
/r/{code} |
邀请落地页 |
/static/* |
本地媒体文件 |
/health |
健康检查 |
5. 管理后台发布(admin/)
cd /root/project/video/admin
npm ci
npm run build # 产物直接覆盖 admin/dist/,即 vm 站点根目录
- 纯静态站点,构建即发布,无需重启任何服务;浏览器强刷(Ctrl+F5)生效
- 前端 API baseURL 为
/api/v1,由 vm 站点的/api/反代到后端 - 菜单按角色显示:仪表盘/套餐/赠送=operator,视频审核=reviewer,其余=super
6. nginx 配置(宝塔管理)
配置文件:/www/server/panel/vhost/nginx/{h5,vm,vapi,codeserver}.256kb.top.conf。
改配置在宝塔面板「网站 → 设置 → 配置文件」编辑,或改文件后 nginx -t && nginx -s reload。
关键规则摘要:
h5.256kb.top
root /root/project/video/h5; # index.html 下载页
location /downloads/ { alias /root/project/video/runtime/downloads/; } # APK
location /videos/ { alias /root/project/video/runtime/videos/; }
location /uploads/ { alias /root/project/video/runtime/uploads/; }
location /r/ { proxy_pass http://127.0.0.1:8000; } # 邀请落地
location /api/v1/invite/ { proxy_pass http://127.0.0.1:8000; }
vm.256kb.top
root /root/project/video/admin/dist;
location / { try_files $uri $uri/ /index.html; } # SPA 回退
location /api/ { proxy_pass http://127.0.0.1:8000; }
location /static/ { proxy_pass http://127.0.0.1:8000; }
location /r/ { proxy_pass http://127.0.0.1:8000; }
vapi.256kb.top:location / { proxy_pass http://127.0.0.1:8000; } 全量反代。
站点日志在 /www/wwwlogs/<域名>.log(排查 App 接口 401/500 时非常管用)。
7. App 编译与发版
7.1 域名发现(不硬编码 API 地址)
启动时优先级:--dart-define=API_BASE > 本地缓存 > DNS TXT(txt.256kb.top) > 兜底域名(md5(yyyyMM)+后缀)。
正式包不带 dart-define 直接构建,走 TXT 发现(当前指向 vapi.256kb.top/api/v1)。
7.2 一键发版 deploy.sh
# 1. 改 app/pubspec.yaml 的 version(如 1.3.7+23,+后为 versionCode)
# 2. 执行
bash /root/project/video/deploy.sh
脚本自动完成:flutter analyze(warning 不阻断)→ flutter build apk --release → gradlew --stop 清理 Gradle Daemon(防内存泄漏)→ 覆盖 runtime/downloads/video-app.apk → 同步 DB system_configs.app.version.android(latest_version/code 更新、min_version_code 保留、连接指定 -h 127.0.0.1)→ docker exec my-redis 清配置缓存。
老用户打开 App 触发 /api/v1/app/version 检查 → 弹更新提示 → 跳 h5 下载页。强制更新:把 min_version_code 调大于用户版本。
7.3 正式签名(2026-10-17 起,v1.3.7+23 为首个正式签名版)
| 项 | 值 |
|---|---|
| keystore | app/android/app/upload-keystore.jks(PKCS12,RSA 2048,有效期 30 年) |
| 密码配置 | app/android/key.properties(alias=video_release) |
| 证书指纹 SHA-256 | 8fb75f6a:3a9f:bc39:...:aef4(完整值见 apksigner verify --print-certs) |
| git | 两者均已 gitignore,不入库 |
| 备份 | /root/backups/video-app-signing/(keystore + key.properties) |
build.gradle.kts 逻辑:key.properties 存在 → release 签名;否则回退 debug(保证新环境可编译)。
⚠️ keystore 丢失 = 永远无法给老用户推更新。除服务器备份外,务必另存一份到本地电脑/网盘。 ⚠️ 首个正式签名版与旧 debug 签名不一致,老用户必须先卸载再安装(版本 notice 已注明)。 侧载安装时小米等厂商会提示"未备案",勾选"已了解风险"即可继续安装;消除该提示需办理 App ICP 备案。
7.4 iOS
flutter build ipa --release(需 macOS + Xcode + 开发者证书,当前仅交付 Android)。上架前确认后台 ios_payment.enabled 开关规避虚拟支付审核。
8. 项目启停 project-ctl.sh
bash /root/project/video/project-ctl.sh start # docker mysql8/my-redis → 等待就绪 → video-api(enable+start) → 健康检查
bash /root/project/video/project-ctl.sh stop # video-api 停止并 disable + 停两个容器(重启服务器也不会拉起)
bash /root/project/video/project-ctl.sh restart # 停 → 启
bash /root/project/video/project-ctl.sh status # 服务/容器/接口探测
- 管理范围:
video-api.service+mysql8+my-redis;不含 nginx(系统组件) - 服务器重启后如项目处于"运行态",docker 与 systemd 会自动拉起全部组件,无需人工干预
- 项目当前(2026-10-18 起)处于暂停态,恢复运行执行
start即可
9. 备份策略
| 对象 | 方式 | 频率建议 |
|---|---|---|
| MySQL 数据 | docker exec mysql8 mysqldump -uroot -p256kb.cn video | gzip > backup-$(date +%F).sql.gz(或宝塔计划任务) |
每日 |
| keystore + key.properties | 已备份 /root/backups/video-app-signing/,另离线保存 |
变更即备 |
runtime/(视频/APK/上传) |
文件级备份(rsync/对象存储) | 每周 |
server/.env、config.yaml |
纳入文件备份 | 变更即备 |
恢复:导入 sql → alembic upgrade head 校验版本 → 启动服务。
10. 日常运维与故障排查
10.1 常用命令
journalctl -u video-api -f # 后端日志
docker logs -f mysql8 / my-redis # 容器日志
tail -f /www/wwwlogs/vapi.256kb.top.log # App 接口访问日志
docker exec my-redis redis-cli DEL config:system # 清系统配置缓存
docker exec my-redis redis-cli KEYS 'auth:*' # 查登录态键
free -h && docker stats --no-stream # 资源占用
10.2 历史故障速查表
| 症状 | 根因 | 处置 |
|---|---|---|
| 后端全接口 500,Redis 报错 | 服务器重启后 my-redis 未自启,宿主机 Redis 5.0.3 抢 6379,RESP3 握手失败 | 停本机 redis 自启;docker start my-redis;客户端已固定 protocol=2 |
| 重启后服务缺失/端口错乱 | Docker 容器未配 restart 策略 | 两容器均已 unless-stopped |
| App 更新提示反复弹 | 硬编码版本号与 APK 不一致 | 已改用 package_info_plus 读真实版本号 |
| 发版后 DB 版本没更新 | deploy.sh mysql 未加 -h 127.0.0.1 写错宿主机库 |
已修复;注意检查 |
| 编译后内存爆满 | Gradle Daemon 常驻 | deploy.sh 已自动 gradlew --stop |
| IDE 里 OpenJDK 吃内存 | Trae 的 fwcd.kotlin 扩展语言服务器 | 禁用该扩展并重载窗口 |
| 编译卡死无输出 | Gradle 进程僵死 | 杀掉重跑即可 |
| 会员/钱包页加载失败 | 僵尸登录态(access 过期+无 refresh) | App 端已修复(401 清态+启动检测);临时:退出重登 |
| 切"我的"仍听到视频声 | 播放器池 open 竞态 | 已修复(suspend/resume) |
| 进度条不走/拖动无效 | 本地代理未支持 Range | 已修复(Accept-Ranges + 206) |
10.3 端到端验证清单(发版/重启后)
curl http://127.0.0.1:8000/health # {"status":"ok"}
curl "http://127.0.0.1:8000/api/v1/videos/feed?type=hot&limit=1" # code:0
curl -I https://h5.256kb.top/downloads/video-app.apk # 200
# 浏览器开 https://vm.256kb.top → 登录 → 仪表盘有数据
# App:启动过版本检查 → Feed 出视频 → 登录 → 播放/计费正常
11. 新环境从零部署速查
- 装 Docker / Python3.11 / Node18+ / Flutter / nginx(或宝塔)
- 建 mysql8、my-redis 容器(§3 命令)
server/建 venv、装依赖、写.env、alembic upgrade head、建超管(§4.3)- 写 systemd unit(§4.1)→
systemctl enable --now video-api admin/构建 dist;配三个 nginx 站点 + 证书(§6)- App:放入 keystore + key.properties →
deploy.sh发版(§7) - 后台「系统配置」:
app.invite_base_url、app.version.android/ios等(见 03 接口文档配置键) - 跑 §10.3 验证清单