YumCut v0.1.0 Docker Compose 部署指南

Ubuntu 24.04 + Docker Compose 从零部署 YumCut v0.1.0 实测指南:双仓库源码构建三个 Dockerfile、MySQL 8.4 四服务编排、RSA 上传签名密钥对、76 个迁移初始化 66 张表、自建账号与 API Key 开通、三次构建/启动排障实录与备份运维。

YumCutYumCutDocker ComposeMySQLNode.js2026/10/25

YumCut v0.1.0 Docker Compose 部署指南

目标:在 Ubuntu 24.04 服务器上用 Docker Compose 从零部署 YumCut v0.1.0(开源 AI 短视频生成工厂)的最小可用集——主应用、MySQL 8.4、存储服务、渲染守护进程四件套。覆盖双仓库源码获取、三个 Dockerfile 编写、RSA 上传签名密钥对生成、编排与环境文件、76 个数据库迁移初始化 66 张表、自建账号与 API Key 开通、健康验证、数据备份与常见问题排障。文中全部命令与回显均在真实部署环境(Ubuntu 24.04、Docker 29.9.2、Docker Compose v5.6.1)实测,取材日期 2026-10-25。项目定位、许可边界与同类选型对比见《YumCut v0.1.0 介绍与选型指南》。

一、环境信息

本篇的实测环境与部署目标如下,读者操作时把 IP、端口与口令换成自己的取值即可:

  • 操作系统:Ubuntu 24.04 LTS,x86_64
  • Docker 引擎:29.9.2(Compose 插件实测 v5.6.1)
  • 应用版本:YumCut v0.1.0(package.json 标记 0.1.0;仓库无 Git 标签,以 master 快照 6401fe6 为准,最近提交 2026-10-24)
  • 存储服务版本:yumcut-storage 快照 c89c35f
  • 运行时:Node.js 24(基础镜像 node:24-bookworm-slim)、Next.js 16.2.6、Prisma 6
  • 数据库:MySQL 8.4.13(官方镜像 mysql:8.4)
  • 渲染依赖:FFmpeg 7.1.1(daemon 启动自检的最低版本)
  • 对外端口:8130(主应用,映射内部 3000)、8129(存储服务,映射内部 3333)
  • 部署目录:/opt/yumcut,源码在 repo/ 与 storage/ 子目录,编排文件在 stack/ 子目录
  • 编排项目名:yumcut-0-1-0-stack(由编排文件 name: 字段固定)
  • 数据卷:yumcut-mysql(MySQL 数据目录)、yumcut-media(存储服务媒体根目录)

版本与宿主机能力取证:

docker --version && docker compose version

实测回显:

Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
git -C /opt/yumcut/repo rev-parse --short HEAD && git -C /opt/yumcut/repo log -1 --format='%ci %s'

实测回显:

6401fe6
2026-10-24 22:08:53 +0300 docs(security): record verified production Node upgrade
grep '"version"' /opt/yumcut/repo/package.json

实测回显:

  "version": "0.1.0",

二、部署形态说明

官方没有发布预构建镜像,全部服务都要从源码构建。两条可行路线对比如下:

形态组成适用场景本文是否实测
Docker Compose(本文主线)app + storage + mysql + daemon 四服务一键编排个人服务器、小团队、希望一条命令拉起是,全文以此为主线
Ubuntu 裸机 + pm2官方 docs/server.md 手册:Node 24 + MySQL 8 + Apache 反代 + pm2 托管不想引入 Docker、需要直接操控进程否,仅指出手册位置

无论哪种形态,都有四条硬约束:

  1. 两仓库缺一不可:主应用仓库 app.yumcut.com 与存储服务仓库 yumcut-storage 是独立代码库,主应用负责界面与 API,存储服务负责媒体上传/下载,二者通过 RSA 签名密钥对建立信任。
  2. 必须用 MySQL 8:主应用通过 Prisma 连接 MySQL,库必须是 utf8mb4 / utf8mb4_0900_ai_ci 字符集;缺库时迁移与启动直接失败。
  3. 必须常驻:daemon 通过轮询数据库领取渲染任务,四个进程任何一个退出,对应链路即停摆。
  4. 渲染链路有外部依赖:daemon 实际出片调用的脚本工作区(video-slides、yumcut-shorts-tools)是作者私有仓库,本文部署能把平台完整跑起来并走通 API,但「自动出片」环节需要作者授权或自行实现渲染脚本——这是选型边界,不影响本篇的部署与验证目标。

三、前置条件

3.1 宿主机资源

  • 磁盘:三个构建产物合计约 10 GB(实测 app 3.41 GB、storage 1.69 GB、daemon 4.63 GB),加上源码与构建缓存,建议预留 25 GB 以上。
  • 内存:建议至少 4 GiB。Next.js 生产构建阶段最吃内存,运行期四服务合计约 1.5 GiB。
  • CPU:2 核可跑,构建阶段会明显吃满;daemon 渲染时 FFmpeg 转码会吃满可用核。

3.2 端口检查

先确认 8130 与 8129 未被占用:

ss -ltn | grep -E ':(8129|8130)' || echo "8129/8130 空闲"

部署前实测回显:

8129/8130 空闲

部署成功后同一命令应能看到四条 LISTEN 行(见 5.1)。

3.3 基础工具

git、curl、openssl 与 Docker 引擎需可用:openssl 用于生成上传签名密钥对与会话密钥,curl 用于后续全部验证。若服务器访问 GitHub 或 npm 源缓慢,先为 Docker 引擎配置代理再构建,不要改源码绕过依赖安装。

四、部署步骤

4.1 获取源码

创建部署目录并克隆两个仓库(主应用与存储服务):

mkdir -p /opt/yumcut && cd /opt/yumcut
git clone https://github.com/IgorShadurin/app.yumcut.com.git repo
git clone https://github.com/IgorShadurin/yumcut-storage.git storage

检出后核验两个快照,必须与本文一致或更新:

git -C /opt/yumcut/repo rev-parse --short HEAD
git -C /opt/yumcut/storage rev-parse --short HEAD

实测回显:

6401fe6
c89c35f

4.2 生成上传签名密钥对

主应用给浏览器签发「上传授权」时用 RSA 私钥签名,存储服务用对应公钥验签——两个服务靠这对密钥建立信任。在部署目录下生成(OpenSSL 3.x 直接输出可用格式):

mkdir -p /opt/yumcut/stack/keys && cd /opt/yumcut/stack
openssl genrsa -out keys/upload_private.pem 4096
openssl rsa -in keys/upload_private.pem -RSAPublicKey_out -out keys/upload_public.pem
chmod 600 keys/upload_private.pem keys/upload_public.pem
head -1 keys/upload_private.pem keys/upload_public.pem

实测回显:

==> keys/upload_private.pem <==
-----BEGIN PRIVATE KEY-----

==> keys/upload_public.pem <==
-----BEGIN RSA PUBLIC KEY-----

私钥必须是 BEGIN PRIVATE KEY(PKCS#8)、公钥必须是 BEGIN RSA PUBLIC KEY(PKCS#1),与主应用/存储服务的加载逻辑一致。

💡 技术拓展:签名密钥不止 RSA 一种。常见非对称签名算法对比——

算法特点适用场景
RSA-4096兼容性最好、密钥与签名体积大通用系统间签名,YumCut 所用
ECDSA P-256密钥短、验签快移动端、带宽敏感场景
Ed25519更短更快、确定性签名新系统首选,需双方库支持

YumCut 的密钥生成脚本(npm run upload-keys:generate)固定生成 RSA-4096 密钥对,本文用等价的 openssl 命令生成,免去宿主机安装 Node。

官方脚本方式(需要宿主机有 Node 环境,在 repo/ 目录执行)实测回显如下,两种产物可互换:

✔ Generated RSA key pair for upload signing.
  Private key : /app/keys/upload_private.pem
  Public key  : /app/keys/upload_public.pem

Next steps:
  • App deployment (APP_MODE=full):
      export UPLOAD_SIGNING_PRIVATE_KEY="$(cat /app/keys/upload_private.pem)"
      export UPLOAD_SIGNING_PUBLIC_KEY="$(cat /app/keys/upload_public.pem)"
  • Storage deployment (APP_MODE=storage):
      export UPLOAD_SIGNING_PUBLIC_KEY="$(cat /app/keys/upload_public.pem)"

Keep the private key secret; commit neither file.

4.3 编写三个 Dockerfile

上游两个仓库都没有 Dockerfile,以下三个文件是本文实测使用的自写件,分别放在 repo/Dockerfile、repo/Dockerfile.daemon 与 storage/Dockerfile。

主应用 /opt/yumcut/repo/Dockerfile(构建期需要 NEXTAUTH_URL 与 NEXT_PUBLIC_STORAGE_BASE_URL 两个构建参数,原因见第八章 Q1/Q2):

FROM node:24-bookworm-slim

RUN apt-get update \
  && apt-get install -y --no-install-recommends openssl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
COPY prisma ./prisma

RUN npm ci --ignore-scripts

COPY . .

ARG NEXT_PUBLIC_STORAGE_BASE_URL
ARG NEXTAUTH_URL=http://localhost:3000

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    ALLOW_NO_OAUTH=1 \
    ALLOW_NO_REVIEW_AUTH=1 \
    NEXT_PUBLIC_STORAGE_BASE_URL=$NEXT_PUBLIC_STORAGE_BASE_URL \
    NEXTAUTH_URL=$NEXTAUTH_URL \
    NEXTAUTH_SECRET=build-time-placeholder-secret \
    DATABASE_URL=mysql://build:build@127.0.0.1:3306/build

RUN npx prisma generate && npm run build

EXPOSE 3000
CMD ["sh", "-c", "npx prisma migrate deploy && npm start"]

要点说明:

  • npm ci --ignore-scripts 与源码分两层:依赖层在源码变化时可复用缓存,重建快一个数量级。
  • ALLOW_NO_OAUTH=1:自托管没有配置 Google/Apple OAuth 时,允许构建通过(官方内置逃生门,生产勿开);登录体系的后果见 5.3。
  • ALLOW_NO_REVIEW_AUTH=1:允许在缺少审核账号配置时构建移动端审核接口(见第八章 Q2)。
  • 构建期 NEXTAUTH_SECRET/DATABASE_URL 是占位值:仅满足构建期校验,运行期由编排注入真实值。
  • 启动命令先 prisma migrate deploy 再起服务:数据库结构随应用启动自动迁移(见 4.6)。

存储服务 /opt/yumcut/storage/Dockerfile:

FROM node:24-bookworm-slim

RUN apt-get update \
  && apt-get install -y --no-install-recommends openssl ca-certificates \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --ignore-scripts

COPY . .

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1

RUN npm run build

EXPOSE 3333
CMD ["sh", "-c", "npm run start -- -p 3333"]

渲染守护进程 /opt/yumcut/repo/Dockerfile.daemon(Ubuntu 24.04 基底便于安装 FFmpeg 7.x,Node 运行时应从官方镜像整树拷贝):

FROM node:24-bookworm-slim AS nodedist

FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive

COPY --from=nodedist /usr/local /usr/local

RUN apt-get update \
  && apt-get install -y --no-install-recommends software-properties-common ca-certificates curl python3 python3-pip fonts-dejavu-core \
  && add-apt-repository -y ppa:ubuntuhandbook1/ffmpeg7 \
  && apt-get update \
  && apt-get install -y --no-install-recommends ffmpeg \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY package.json package-lock.json ./
COPY prisma ./prisma

RUN npm ci --ignore-scripts

COPY . .

ENV NODE_ENV=production \
    NEXT_TELEMETRY_DISABLED=1 \
    ALLOW_NO_OAUTH=1 \
    DATABASE_URL=mysql://build:build@127.0.0.1:3306/build

RUN npx prisma generate

CMD ["npm", "run", "daemon"]

⚠️ PPA 版本漂移风险:ppa:ubuntuhandbook1/ffmpeg7 是第三方维护源,同一 Dockerfile 在不同时间构建可能装到不同形态(本文两次构建分别装到 git-master 开发快照与 7.1.1 正式构建),由此引发的启动故障与解法见第八章 Q3。

4.4 编写编排文件与环境文件

/opt/yumcut/stack/compose.yml 全文(与本文实测运行文件一致):

name: yumcut-0-1-0-stack

services:
  mysql:
    image: mysql:8.4
    command: --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_ai_ci --log-bin-trust-function-creators=1
    environment:
      - MYSQL_ROOT_PASSWORD=q1w2e3r4
      - MYSQL_DATABASE=yumcut
      - MYSQL_USER=yumcut
      - MYSQL_PASSWORD=q1w2e3r4
    healthcheck:
      test: ['CMD-SHELL', 'mysqladmin ping -h 127.0.0.1 -uroot -pq1w2e3r4 --silent']
      interval: 5s
      timeout: 5s
      retries: 20
      start_period: 20s
    volumes:
      - yumcut-mysql:/var/lib/mysql
    restart: unless-stopped

  app:
    build:
      context: ../repo
      dockerfile: Dockerfile
      args:
        NEXT_PUBLIC_STORAGE_BASE_URL: http://192.168.146.128:8129
        NEXTAUTH_URL: http://192.168.146.128:8130
    ports:
      - '8130:3000'
    env_file:
      - ./.env.app
    depends_on:
      mysql:
        condition: service_healthy
    volumes:
      - ./keys/upload_private.pem:/run/secrets/upload_private.pem:ro
      - ./keys/upload_public.pem:/run/secrets/upload_public.pem:ro
    restart: unless-stopped

  storage:
    build:
      context: ../storage
      dockerfile: Dockerfile
    ports:
      - '8129:3333'
    environment:
      - MEDIA_ROOT=/data/media
      - STORAGE_PUBLIC_URL=http://192.168.146.128:8129
      - STORAGE_ALLOWED_ORIGINS=http://192.168.146.128:8130
      - MEDIA_CORS_ALLOWLIST=http://192.168.146.128:8130
      - UPLOAD_SIGNING_PUBLIC_KEY=/run/secrets/upload_public.pem
      - DAEMON_API_PASSWORD=q1w2e3r4
      - PORT=3333
    volumes:
      - yumcut-media:/data/media
      - ./keys/upload_public.pem:/run/secrets/upload_public.pem:ro
    restart: unless-stopped

  daemon:
    build:
      context: ../repo
      dockerfile: Dockerfile.daemon
    env_file:
      - ./.env.daemon
    depends_on:
      - app
      - storage
    volumes:
      - ../video-slides:/workspaces/video-slides
      - ../yumcut-shorts-tools:/workspaces/shorts-tools
      - ./.env.daemon:/app/.daemon.env:ro
      - ../workspaces/characters:/workspaces/characters
      - ../workspaces/captions:/workspaces/captions
      - ../workspaces/projects:/workspaces/projects
    restart: unless-stopped

networks:
  default:
    ipam:
      config:
        - subnet: 10.89.59.0/24

volumes:
  yumcut-mysql:
  yumcut-media:

编排要点逐项说明:

  • mysql 服务:command 固定 utf8mb4 字符集(Prisma 迁移对 emoji 等四字节字符的硬性要求);healthcheck 用 mysqladmin ping 探活,主应用通过 depends_on.condition: service_healthy 等数据库就绪后再启动。
  • app 构建参数:NEXT_PUBLIC_STORAGE_BASE_URL 是浏览器直连存储服务的地址,必须是用户浏览器可达的地址(本文是宿主机 IP 加 8129 端口),它在构建期被编译进前端代码,改地址必须重建;NEXTAUTH_URL 同理。
  • 密钥对挂载:私钥只挂给 app(签名),公钥同时挂给 app 与 storage(验签),均为只读。
  • storage 环境:STORAGE_ALLOWED_ORIGINS 控制允许调用上传接口的浏览器来源,MEDIA_CORS_ALLOWLIST 控制允许拉取媒体 /api/media 的来源,本文都只放行主应用来源;STORAGE_PUBLIC_URL 用于拼接公开媒体链接。
  • daemon 工作区挂载:video-slides 与 yumcut-shorts-tools 两个渲染脚本工作区是私有仓库,本文用空目录占位挂载,daemon 调度框架正常启动,渲染任务会因缺少脚本而无法执行(预期行为);characters/captions/projects 三个工作区目录存放角色、字幕与项目中间产物。
  • 固定子网 10.89.59.0/24:避免与宿主机其他编排项目的默认网段冲突;多项目共存时按段错开。

主应用环境文件 /opt/yumcut/stack/.env.app 全文:

DATABASE_URL=mysql://yumcut:q1w2e3r4@mysql:3306/yumcut
NEXTAUTH_URL=http://192.168.146.128:8130
NEXTAUTH_SECRET=a8d29c23ff77bec8e99ab6255a1c22327f75722a42524898b2905eae84e504ee
NEXT_PUBLIC_STORAGE_BASE_URL=http://192.168.146.128:8129
STORAGE_PUBLIC_URL=http://192.168.146.128:8129
UPLOAD_SIGNING_PRIVATE_KEY=/run/secrets/upload_private.pem
UPLOAD_SIGNING_PUBLIC_KEY=/run/secrets/upload_public.pem
SERVICE_API_PASSWORD=q1w2e3r4
DAEMON_API_PASSWORD=q1w2e3r4
ENABLE_PUBLISH_SCHEDULER=0
ALLOW_NO_OAUTH=1
ALLOW_NO_REVIEW_AUTH=1
MEDIA_ROOT=/app/media

其中 NEXTAUTH_SECRET 用以下命令随机生成(本文落的是实测生成的值,读者请重新生成自己的):

openssl rand -hex 32

实测回显:

a8d29c23ff77bec8e99ab6255a1c22327f75722a42524898b2905eae84e504ee

daemon 环境文件 /opt/yumcut/stack/.env.daemon 全文:

DAEMON_ID=daemon-local-1
DAEMON_API_BASE_URL=http://app:3000
DAEMON_STORAGE_BASE_URL=http://storage:3333
DAEMON_STORAGE_HEALTH_PATH=/api/storage/health
DAEMON_HEALTH_PATH=/api/daemon/health
DAEMON_SCRIPT_WORKSPACE=/workspaces/video-slides/ai-agent
DAEMON_SCRIPT_WORKSPACE_V2=/workspaces/shorts-tools
DAEMON_CHARACTERS_WORKSPACE=/workspaces/characters
DAEMON_SCRIPT_CAPTION=/workspaces/captions
DAEMON_SCRIPT_MODE=fast
DAEMON_PROJECTS_WORKSPACE=/workspaces/projects
DAEMON_AUDIO_DEFAULT_VOICE=Kore
DAEMON_API_PASSWORD=q1w2e3r4
DAEMON_INTERVAL_MS=1000
DAEMON_MAX_CONCURRENCY=2
DAEMON_TASK_TIMEOUT_SECONDS=3600
DAEMON_REQUEST_TIMEOUT_MS=15000
DAEMON_SKIP_FFMPEG_CHECK=1

关键项说明:DAEMON_API_BASE_URL/DAEMON_STORAGE_BASE_URL 走编排内网服务名;DAEMON_API_PASSWORD 必须与主应用、存储服务三处一致,否则 daemon 调接口一律 403;DAEMON_SKIP_FFMPEG_CHECK=1 是跳过 FFmpeg 版本自检的逃生门,设置背景见第八章 Q3。

4.5 构建镜像

按「存储服务 → 主应用 → daemon」顺序构建(首次构建需拉取基础层与全部 npm 依赖,耗时较长):

cd /opt/yumcut/stack
docker compose build storage
docker compose build app
docker compose build daemon

构建成功标志(实测回显节选):

#13 7.526 ▲ Next.js 16.2.6 (Turbopack)
#13 7.553   Creating an optimized production build ...
#13 13.91 ✓ Compiled successfully in 6.2s
...(输出节选,实测首尾)
 Image yumcut-0-1-0-stack-app Built

确认三个构建产物:

docker images --filter reference='yumcut-0-1-0-stack-*' --format '{{.Repository}}:{{.Tag}} {{.ID}} {{.Size}}'

实测回显:

yumcut-0-1-0-stack-daemon:latest d37ea83f5b60 4.63GB
yumcut-0-1-0-stack-app:latest 9bb10d669c65 3.41GB
yumcut-0-1-0-stack-storage:latest 533294f6f1bb 1.69GB

⚠️ 主应用构建不是一次通过的,两次典型失败与修法见第八章 Q1(缺 NEXTAUTH_URL 构建参数)与 Q2(缺审核账号配置)。本文 4.3 的 Dockerfile 已是修完后的最终版。

4.6 启动服务与数据库迁移

cd /opt/yumcut/stack
docker compose up -d

首次启动时,主应用会先执行 prisma migrate deploy 再拉起 Next.js。实测启动日志(输出节选,实测首尾):

app-1  | Prisma schema loaded from prisma/schema.prisma
app-1  | 76 migrations found in prisma/migrations
app-1  | Applying migration `20250912160330_init`
app-1  | Applying migration `20250912160331_telegram_integration`
...
app-1  | Applying migration `20260813114500_add_token_top_up_purchases`
app-1  | The following migration(s) have been applied:

迁移完成后确认表数量:

docker exec yumcut-0-1-0-stack-mysql-1 mysql -uyumcut -pq1w2e3r4 yumcut \
  -N -e "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema='yumcut';"

实测回显:

66

即 65 张业务表加 1 张 _prisma_migrations 迁移记录表,76 个迁移文件全部落库。主应用就绪日志:

app-1  | ▲ Next.js 16.2.6
app-1  | - Local:         http://localhost:3000
app-1  | - Network:       http://10.89.59.4:3000
app-1  | ✓ Ready in 85ms

五、部署验证

5.1 服务状态与端口

cd /opt/yumcut/stack
docker compose ps

实测回显:

NAME                           IMAGE                        COMMAND                  SERVICE   CREATED          STATUS                    PORTS
yumcut-0-1-0-stack-app-1       yumcut-0-1-0-stack-app       "docker-entrypoint.s…"   app       28 minutes ago   Up 28 minutes             0.0.0.0:8130->3000/tcp, [::]:8130->3000/tcp
yumcut-0-1-0-stack-daemon-1    yumcut-0-1-0-stack-daemon    "npm run daemon"         daemon    13 minutes ago   Up 13 minutes
yumcut-0-1-0-stack-mysql-1     mysql:8.4                    "docker-entrypoint.s…"   mysql     28 minutes ago   Up 28 minutes (healthy)   3306/tcp, 33060/tcp
yumcut-0-1-0-stack-storage-1   yumcut-0-1-0-stack-storage   "docker-entrypoint.s…"   storage   28 minutes ago   Up 28 minutes             0.0.0.0:8129->3333/tcp, [::]:8129->3333/tcp

四个服务均为 Up、数据库为 healthy 即启动成功。再看端口监听:

ss -ltn | grep -E ':(8129|8130)'

实测回显:

LISTEN 0      4096         0.0.0.0:8129       0.0.0.0:*
LISTEN 0      4096         0.0.0.0:8130       0.0.0.0:*
LISTEN 0      4096            [::]:8129          [::]:*
LISTEN 0      4096            [::]:8130          [::]:*

5.2 页面与健康接口

主应用首页返回 200:

curl -s -o /dev/null -w 'HTTP %{http_code} type=%{content_type}\n' http://192.168.146.128:8130/

实测回显:

HTTP 200 type=text/html; charset=utf-8

存储服务的健康接口由 x-daemon-password 头保护。不带口令直接访问应被拒绝:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://192.168.146.128:8129/api/storage/health

实测回显:

HTTP 403

携带编排里配置的共享口令(本文示例 q1w2e3r4):

curl -s -H 'x-daemon-password: q1w2e3r4' http://192.168.146.128:8129/api/storage/health

实测回显:

{"ok":true,"time":"2026-10-25T10:34:22.606Z"}

主应用侧的 daemon 健康接口同样受口令保护,对照验证:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://192.168.146.128:8130/api/daemon/health
curl -s -H 'x-daemon-password: q1w2e3r4' -H 'x-daemon-id: probe-1' http://192.168.146.128:8130/api/daemon/health

实测回显:

HTTP 403
{"ok":true,"time":"2026-10-25T10:39:32.197Z"}

这组 403/200 对照同时证明:口令鉴权链路正常、主应用到存储服务的共享密钥配置一致。

5.3 创建用户与 API Key

自托管部署有一个必须知道的现实:主应用的注册登录走 Google/Apple OAuth;本文按最小依赖构建(ALLOW_NO_OAUTH=1),没有配置任何 OAuth 服务商,界面上没有可用的注册入口。自用场景下直接在数据库里建用户、再发 API Key,是官方代码支持的正规路径(用户表与 API Key 表均为 Prisma 模型,字段约束见下)。

💡 技术拓展:自建账号不止「写库」一种。常见方案对比——

方案特点适用场景
配置 Google/Apple OAuth官方原设计,界面自助注册登录有 OAuth 应用资质的正式运营
直连数据库建用户零外部依赖,即建即用个人自用、内网部署,本文所用
手工签发会话 JWT用 NEXTAUTH_SECRET 签发 next-auth.session-token Cookie临时调试 Web 界面(仓库 docs/debug-web-admin-login.md)

本文目标是把 API 链路跑通,采用数据库建用户 + API Key 方案;OAuth 配置属运营化改造,不在本篇范围。

在主应用服务内用 Prisma 客户端创建用户(邮箱 demo@yumcut.local 为本文实测账号):

cd /opt/yumcut/stack
docker compose exec -T app node -e "const {PrismaClient}=require('@prisma/client');const p=new PrismaClient();p.user.create({data:{email:'demo@yumcut.local',name:'demo'}}).then(u=>{console.log('id='+u.id);console.log('email='+u.email);return p.\$disconnect();}).catch(e=>{console.error(e.message);process.exit(1);})"

实测回显:

id=0633c05f-f5e3-4a4d-a464-5cf69fc7381e
email=demo@yumcut.local

再为该用户签发 API Key。Key 的明文只在创建时可见,库里只存 SHA-256 摘要与前 12 位前缀(UserApiKey 模型的 tokenHash/tokenPrefix 字段),格式为 ycu_ 加 32 字节随机串:

RAW="ycu_$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')"
echo "生成的明文 Key: $RAW"
HASH=$(printf '%s' "$RAW" | sha256sum | awk '{print $1}')
docker compose exec -T app node -e "const {PrismaClient}=require('@prisma/client');const p=new PrismaClient();p.userApiKey.create({data:{userId:'0633c05f-f5e3-4a4d-a464-5cf69fc7381e',name:'ci-demo',tokenHash:'$HASH',tokenPrefix:'${RAW:0:12}',scopes:['read','write']}}).then(k=>{console.log('keyId='+k.id);return p.\$disconnect();}).catch(e=>{console.error(e.message);process.exit(1);})"

实测回显:

生成的明文 Key: ycu_5nv1_SQAyFzDaQKwnFnIledCX4RI9d9cko_G4oazHzA
keyId=f11b1065-224a-42ed-a885-44fa9a5cad77

⚠️ 明文 Key 只出现这一次,务必当场保存;丢失只能作废重建。scopes 支持 read/write 两档,缺省为两者全开。

5.4 User API 调用验证

用 5.3 签发的 Key 调项目列表接口(Bearer 认证,base 路径 /api/user/v1):

curl -s -H "Authorization: Bearer ycu_5nv1_SQAyFzDaQKwnFnIledCX4RI9d9cko_G4oazHzA" \
  http://192.168.146.128:8130/api/user/v1/projects

实测回显:

{"items":[],"limit":50,"nextCursor":null}

空列表说明认证通过、读写链路正常(新用户没有项目)。对照验证:不带 Key 或带错误 Key 一律 401:

curl -s -o /dev/null -w 'HTTP %{http_code}\n' http://192.168.146.128:8130/api/user/v1/projects
curl -s -o /dev/null -w 'HTTP %{http_code}\n' -H "Authorization: Bearer wrong-key" http://192.168.146.128:8130/api/user/v1/projects

实测回显:

HTTP 401
HTTP 401

5.5 daemon 启动自检

daemon 启动时会依次检查 FFmpeg 版本、主应用与存储服务的健康接口,全部通过才进入轮询循环:

cd /opt/yumcut/stack
docker compose logs daemon --since 30m | grep -E 'Skipping|startup|FFmpeg'

实测回显:

daemon-1  | [2026-10-25T10:24:48.164Z] INFO  Daemon performing startup checks
daemon-1  | [2026-10-25T10:24:48.164Z] WARN  Skipping FFmpeg version check due to DAEMON_SKIP_FFMPEG_CHECK=1
daemon-1  | [2026-10-25T10:24:48.232Z] INFO  Daemon startup checks passed

startup checks passed 表示 daemon 到主应用、到存储服务的链路全部打通。版本自检被跳过的原因与正式验证方法见第八章 Q3;当前环境内的 FFmpeg 版本实测:

docker exec yumcut-0-1-0-stack-daemon-1 ffmpeg -version | head -1

实测回显:

ffmpeg version 7.1.1 Copyright (c) 2000-2025 the FFmpeg developers

满足 daemon 要求的 ≥7.1.1。

六、数据持久化与备份

6.1 持久化对象

数据位置说明
用户、项目、脚本、配音、任务、代币流水yumcut-mysql 卷(/var/lib/mysql)全部业务状态,升级与重启都不丢
媒体文件(上传图片、成片等)yumcut-media 卷(/data/media)由存储服务写入,经 /api/media 提供访问
上传签名密钥对stack/keys/ 目录与数据库同等重要,丢失后旧签名授权无法验签
项目中间产物workspaces/ 各挂载目录渲染过程文件,可由 daemon 重建

6.2 数据库备份

用 mysqldump 导出业务库(--no-tablespaces 避免对 PROCESS 权限的额外要求):

docker exec yumcut-0-1-0-stack-mysql-1 sh -c \
  'mysqldump -uyumcut -pq1w2e3r4 --no-tablespaces yumcut' > backup-20261025.sql
head -8 backup-20261025.sql && wc -l backup-20261025.sql

实测回显:

-- MySQL dump 10.13  Distrib 8.4.13, for Linux (x86_64)
--
-- Host: localhost    Database: yumcut
-- ------------------------------------------------------
-- Server version	8.4.13

/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
2370 backup-20261025.sql

转储头为标准 MySQL 8.4 格式,全库 2370 行 SQL。恢复时把转储喂给新库即可:

# 恢复操作(先建空库再导入,正式执行前请自行备份确认):
docker exec yumcut-0-1-0-stack-mysql-1 mysql -uroot -pq1w2e3r4 \
  -e "CREATE DATABASE yumcut_restore CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;"
docker exec -i yumcut-0-1-0-stack-mysql-1 mysql -uroot -pq1w2e3r4 yumcut_restore < backup-20261025.sql

恢复完成后用 4.6 的表数核对法确认 66 张表全部回来,再删除临时库。

6.3 媒体数据备份

yumcut-media 卷的内容是纯文件树,用一次性任务打包到宿主机即可:

docker run --rm -v yumcut-0-1-0-stack_yumcut-media:/data/media:ro -v /opt/yumcut/stack:/backup \
  ubuntu:24.04 tar -czf /backup/media-20261025.tar.gz -C /data media

七、常用运维命令

cd /opt/yumcut/stack
docker compose ps                 # 查看四服务状态,mysql 应显示 healthy
docker compose logs -f app        # 跟踪主应用日志(迁移、API 报错都在这里)
docker compose logs -f daemon     # 跟踪渲染守护日志(启动自检、任务领取)
docker compose restart daemon     # 只重启 daemon,不重建数据卷
docker compose up -d              # 改了 .env.app/.env.daemon 后用此命令应用
docker compose down               # 停止全部服务(默认保留数据卷,加 -v 才删数据)

7.1 升级方法

仓库没有标签与 Release,升级就是跟随 master:先备份数据库(6.2),再拉新代码、记新旧两个 commit,重建并启动:

cd /opt/yumcut
git -C repo pull --ff-only && git -C storage pull --ff-only
cd stack && docker compose build storage app daemon && docker compose up -d
docker compose logs app | grep -c 'Applying migration'   # 有新增迁移会自动落库

回滚时 git checkout <旧commit> 后重建即可;若升级带入了新迁移,回滚代码前必须先按 6.2 备份,迁移本身不可逆。

八、常见问题

Q1:主应用构建失败,报 NEXTAUTH_URL is required for production builds

实测报错原文:

#13 33.79 Error: [env] NEXTAUTH_URL is required for production builds so NextAuth does not fall back to http://localhost:3000.

原因:生产构建强制校验 NEXTAUTH_URL,防止会话回调地址回落到 localhost。解法:在 repo/Dockerfile 中声明 ARG NEXTAUTH_URL 并注入 ENV,同时在编排 build.args 里传入真实对外地址(本文 4.3/4.4 已内置该修法)。

Q2:修完 Q1 后构建仍失败,报 Failed to collect page data for /api/mobile/auth/review

实测报错原文:

#13 25.53 Error: REVIEW_LOGIN_EMAIL and REVIEW_LOGIN_PASSWORD must be configured for reviewer auth.
#13 25.53     at Object.<anonymous> (.next/server/app/api/mobile/auth/review/route.js:15:3)
#13 25.53 Error: Failed to collect page data for /api/mobile/auth/review

原因:移动端「审核账号」接口在构建期收集页面数据时强制要求审核账号环境变量。自用部署不需要该功能,官方内置逃生门:在 Dockerfile 构建环境中加 ALLOW_NO_REVIEW_AUTH=1 即可跳过(本文 4.3 已内置)。

Q3:daemon 崩溃循环,日志报 Failed to detect FFmpeg version

定位过程:docker compose ps 显示 daemon 处于 Restarting,日志中启动自检在 FFmpeg 一步失败。进入服务手工执行 ffmpeg -version,第一行输出为 ffmpeg version N-128013-g6b05263b44-20261024——PPA 当时装到的是 git-master 开发快照,版本号不是 x.y.z 形态,daemon 的解析正则 ffmpeg version\s+([0-9]+(\.[0-9]+){1,2}) 匹配不到,直接抛错退出。

解法:daemon 内置逃生门,在 .env.daemon 追加 DAEMON_SKIP_FFMPEG_CHECK=1 后重启,自检日志出现 Skipping FFmpeg version check → startup checks passed,进程恢复稳定(回显见 5.5)。治本方案是安装正式发行版 FFmpeg ≥7.1.1(本文复核时同一 PPA 已回落到 7.1.1 正式构建,ffmpeg -version 实测见 5.5);若用 git-master 快照渲染,须自行承担兼容性风险。

Q4:浏览器能打开首页,但点上传图片一直失败

原因:NEXT_PUBLIC_STORAGE_BASE_URL 是构建期编译进前端的地址,若填了编排内网地址(如 http://storage:3333)或 localhost,浏览器解析不到。该值必须填浏览器可达的地址,改动后需要重新构建主应用(docker compose build app && docker compose up -d),只改环境变量不重建不生效。

Q5:daemon 日志反复出现 Invalid daemon credentials 或 403

原因:三处共享口令不一致——主应用 .env.app 的 DAEMON_API_PASSWORD、存储服务编排 environment 的同名项、.env.daemon 的同名项必须完全相等。改齐后 docker compose up -d 重建生效,再用 5.2 的 403/200 对照验证。

Q6:执行数据库迁移后主应用起不来,日志显示连接被拒

原因:DATABASE_URL 中的库名/用户名/密码与 mysql 服务的 MYSQL_DATABASE/MYSQL_USER/MYSQL_PASSWORD 三处不一致,或主应用在 MySQL 就绪前启动。本文编排用 condition: service_healthy 规避启动竞态;手工改动口令后需注意 MySQL 只在数据卷为空时初始化账号密码,改环境变量不会改已有库的密码。

九、参考链接