Files
video/docs/04-部署运行文档.md
T
2026-10-18 21:08:54 +08:00

17 KiB
Raw Blame History

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. 新环境从零部署速查

  1. 装 Docker / Python3.11 / Node18+ / Flutter / nginx(或宝塔)
  2. 建 mysql8、my-redis 容器(§3 命令)
  3. server/ 建 venv、装依赖、写 .env、alembic upgrade head、建超管(§4.3)
  4. 写 systemd unit(§4.1)→ systemctl enable --now video-api
  5. admin/ 构建 dist;配三个 nginx 站点 + 证书(§6)
  6. App:放入 keystore + key.properties → deploy.sh 发版(§7)
  7. 后台「系统配置」:app.invite_base_url、app.version.android/ios 等(见 03 接口文档配置键)
  8. 跑 §10.3 验证清单