YumCut v0.1.0 Docker Compose 部署指南
Ubuntu 24.04 + Docker Compose 从零部署 YumCut v0.1.0 实测指南:双仓库源码构建三个 Dockerfile、MySQL 8.4 四服务编排、RSA 上传签名密钥对、76 个迁移初始化 66 张表、自建账号与 API Key 开通、三次构建/启动排障实录与备份运维。
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、需要直接操控进程 | 否,仅指出手册位置 |
无论哪种形态,都有四条硬约束:
- 两仓库缺一不可:主应用仓库
app.yumcut.com与存储服务仓库yumcut-storage是独立代码库,主应用负责界面与 API,存储服务负责媒体上传/下载,二者通过 RSA 签名密钥对建立信任。 - 必须用 MySQL 8:主应用通过 Prisma 连接 MySQL,库必须是
utf8mb4/utf8mb4_0900_ai_ci字符集;缺库时迁移与启动直接失败。 - 必须常驻:daemon 通过轮询数据库领取渲染任务,四个进程任何一个退出,对应链路即停摆。
- 渲染链路有外部依赖: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-tokenCookie临时调试 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 只在数据卷为空时初始化账号密码,改环境变量不会改已有库的密码。
九、参考链接
- 主应用仓库(查询日期 2026-10-25):https://github.com/IgorShadurin/app.yumcut.com
- 存储服务仓库(查询日期 2026-10-25):https://github.com/IgorShadurin/yumcut-storage
- 仓库内裸机部署手册:
docs/server.md、架构文档docs/tech.md、渲染依赖说明docs/daemon-ffmpeg.md(随源码分发) - FFmpeg 7 PPA(查询日期 2026-10-25):https://launchpad.net/~ubuntuhandbook1/+archive/ubuntu/ffmpeg7
- MySQL 8.4 官方镜像(查询日期 2026-10-25):https://hub.docker.com/_/mysql