一句话简介:一步步教你用 Docker Compose 在群晖 NAS 上自建 Cap——Loom 的开源替代品,实现屏幕录制 + 即时分享。附 5 大真实踩坑解决方案和本地大模型集成方案,零外部 API 依赖。
为什么选择 Cap?
如果你用过 Loom,一定体会过”录屏 → 自动生成链接 → 即时分享”的丝滑体验。但 Loom 的免费版有录制时长限制,付费版每月动辄十几美金,录制内容还存在第三方服务器上——对于注重隐私的团队来说,这是个问题。
Cap 是一个开源的屏幕录制和分享平台,功能对标 Loom,但你可以完全自托管。录制、存储、分享,全在你自己的服务器上完成。
Cap 的主要功能:
- 开源免费,MIT 协议
- 支持屏幕 + 摄像头同步录制
- 自动生成分享链接,无需上传到第三方
- 内置 AI 摘要功能(支持接入本地大模型)
- 提供 macOS / Windows 桌面客户端
这篇文章记录了我在群晖 NAS 上完整部署 Cap 的全过程,包括踩过的 5 个大坑和最终跑通的本地 AI 集成方案。如果你也想自建一个录屏平台,照着做就行。

部署前的准备
你需要什么
| 项目 | 要求 |
|---|---|
| 群晖 NAS | DSM 7.2+,建议 4GB 以上内存 |
| Docker | 已安装 Container Manager(套件中心搜索安装) |
| SSH 访问 | 控制面板 → 终端机和 SNMP → 启用 SSH |
| 域名 + SSL 证书 | 用于 HTTPS 反向代理(可用 Let’s Encrypt 免费申请) |
| Nginx Proxy Manager | Docker 部署的 HTTPS 反代工具 |
为什么需要 HTTPS? 浏览器的屏幕录制 API(
getDisplayMedia)要求安全上下文,HTTP 下无法调用。这是整个部署中最容易忽视的前置条件。
宽带端口说明
很多宽带运营商封锁了 80 和 443 端口。如果你的情况类似,需要选一个替代端口(本文以 183 为例)。这不影响功能,只是访问地址要带上端口号。
整体架构一览
先看全局,理解各组件之间的关系,后面排错时才不会迷路。
互联网
│
Nginx Proxy Manager
(群晖自建,监听 :183)
┌─────────┬─────────┐
│ │ │
HTTPS:183 MinIO 内网直连
│
cap-web:3000
│
┌────────┼────────┬──────────┐
▼ ▼ ▼ ▼
MySQL MinIO media- AI-Proxy
(named (内网 server (自签SSL)
volume) 直连) (FFmpeg) │
本地 AI 服务器
(llama-server)
三条设计原则(踩完坑后总结的):
- 谁需要 HTTPS 就给谁 HTTPS —— Cap Web 走 Nginx Proxy Manager(屏幕录制 API 要求安全上下文),MinIO 走内网 HTTP(简单可靠)
- 能复用镜像就不编译 —— 全部用预构建镜像,不在 NAS 上装 Bun / Rust 工具链
- 能融进 Compose 就不另起炉灶 —— AI 代理容器化在 Compose 里,生命周期和 Cap 统一管理
Step 1:Docker Compose 编排
在 NAS 上创建项目目录,编写 docker-compose.yml。整个方案包含 6 个容器:
| 容器 | 镜像 | 用途 |
|---|---|---|
cap-web | ghcr.io/capsoftware/cap-web:latest | Next.js 主应用 + API + 认证 |
cap-media-server | ghcr.io/capsoftware/cap-media-server:latest | FFmpeg 视频转码 |
cap-mysql | mysql:8.0 | 数据库 |
cap-minio | minio/minio:latest | S3 兼容对象存储 |
cap-minio-setup | minio/mc:latest | 自动创建 bucket(一次性任务) |
cap-ai-proxy | nginx:alpine | OpenAI API → 本地 AI 透明代理 |
docker-compose.yml 关键配置
version: "3.8"
services:
cap-web:
image: ghcr.io/capsoftware/cap-web:latest
container_name: cap-web
ports:
- "3000:3000"
env_file: .env
extra_hosts:
- "api.openai.com:172.21.0.100" # DNS 劫持 → AI 代理
environment:
NODE_TLS_REJECT_UNAUTHORIZED: "0" # 接受自签证书
depends_on:
cap-mysql:
condition: service_healthy
cap-minio:
condition: service_healthy
networks:
- cap-network
cap-media-server:
image: ghcr.io/capsoftware/cap-media-server:latest
container_name: cap-media-server
env_file: .env
networks:
- cap-network
cap-mysql:
image: mysql:8.0
container_name: cap-mysql
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: cap
MYSQL_USER: cap
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- cap-mysql-data:/var/lib/mysql # ⚠️ 必须用 named volume!
command: >
--innodb-buffer-pool-size=256M
--performance-schema=OFF
deploy:
resources:
limits:
memory: 1G
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- cap-network
cap-minio:
image: minio/minio:latest
container_name: cap-minio
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
MINIO_API_CORS_ALLOW_ORIGIN: "https://cap.yourdomain.com:183"
volumes:
- ./minio:/data # MinIO 不受 ACL 影响,可用 bind mount
command: server /data --console-address ":9001"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
networks:
- cap-network
cap-minio-setup:
image: minio/mc:latest
container_name: cap-minio-setup
depends_on:
cap-minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://cap-minio:9000 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
mc mb local/cap --ignore-existing;
mc anonymous set download local/cap;
"
networks:
- cap-network
cap-ai-proxy:
container_name: cap-ai-proxy
image: nginx:alpine
volumes:
- ./ai-proxy.conf:/etc/nginx/conf.d/default.conf:ro
- ./ai-certs:/etc/nginx/certs:ro
networks:
cap-network:
ipv4_address: 172.21.0.100 # 固定 IP,用于 DNS 劫持
volumes:
cap-mysql-data:
networks:
cap-network:
driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16 # 必须指定子网才能分配静态 IP
为什么 MySQL 要用 named volume 而不是 bind mount? 这是第一个大坑,下面详细说。
Step 2:环境变量配置
在项目目录下创建 .env 文件:
# ====== 数据库 ======
MYSQL_ROOT_PASSWORD=你的root密码
MYSQL_PASSWORD=你的cap用户密码
# ====== MinIO 存储 ======
MINIO_ROOT_USER=你的MinIO用户名
MINIO_ROOT_PASSWORD=你的MinIO密码
# ====== Cap 访问地址 ======
CAP_URL=https://cap.yourdomain.com:183
S3_PUBLIC_URL=http://<NAS_IP>:9000 # ⬅ 内网直连,不走反代
# ====== AI 配置 ======
OPENAI_API_KEY=local-llama # 占位值,本地 AI 不需要真 key
GROQ_API_KEY= # ⬅ 留空!否则会优先走 Groq 然后失败
关于
S3_PUBLIC_URL:这里填的是 NAS 的内网 IP,浏览器直接通过内网访问 MinIO 上传视频。这个设计踩了第四个坑才想明白,后面会讲。
Step 3:HTTPS 反向代理
Cap Web 必须通过 HTTPS 访问,否则屏幕录制 API 无法工作,登录验证码也会失败。
配置 Nginx Proxy Manager
- 在 NPM 中添加新的 Proxy Host
- Domain:
cap.yourdomain.com - Scheme:
https - Forward Hostname:
<NAS_IP> - Forward Port:
3000 - SSL:选择你的通配符证书(或申请 Let’s Encrypt)
如果你的宽带封锁了 443 端口,在 NPM 的 Settings → Default Host 中不用管,直接在 Proxy Host 里指定非标准端口即可。
桌面客户端连接
Cap Desktop 客户端也支持连接自托管实例。在客户端设置中填入:
- Cap Server URL:
https://cap.yourdomain.com:183
5 大踩坑实录
这部分是整篇文章最值钱的内容。5 个问题环环相扣,每个都依赖上一个的解决才会暴露。如果你跟着上面的步骤走,大概率也会遇到。
坑 1:MySQL 权限拒绝(群晖 ACL)
现象:MySQL 容器启动失败,日志报错:
mysqld: Can't create/write to file '/var/lib/mysql/is_writable' (OS errno 13 - Permission denied)
排查过程:
第一反应是权限问题。chown 999 改了,chmod 777 也给了,ls -lad 显示 drwxrwxrwx+——注意那个 + 号。这是群晖 btrfs 文件系统的扩展 ACL(synoacl),它在标准 POSIX 权限之上额外拦截了容器的写入操作。
解决方案:把 MySQL 的存储从 bind mount 改为 Docker named volume:
# ❌ 错误:bind mount 会受群晖 ACL 影响
volumes:
- ./mysql:/var/lib/mysql
# ✅ 正确:named volume 不经过宿主机文件系统 ACL
volumes:
- cap-mysql-data:/var/lib/mysql
Named volume 由 Docker 直接管理,不经过宿主机文件系统的 ACL 层,从根上绕过了这个问题。
坑 2:登录验证码失败
现象:邮箱收到了验证码,但在网页输入后提示失败。打开浏览器 DevTools,发现 cookie 根本没设上。
根因:NextAuth 默认开启 secure: true,cookie 带了 Secure 标记,浏览器只在 HTTPS 连接中才会发送。如果你用 HTTP + 内网 IP 访问,cookie 直接被浏览器拒绝。
解决方案:这就是前面 Step 3 配置 Nginx Proxy Manager HTTPS 反代的原因。HTTPS 一通,验证码登录立刻正常。
坑 3:上传永远卡住
现象:录制完成后,浏览器显示 Uploading...,但视频永远传不上去。查 cap-web 日志:
`x-forwarded-host` header with value `cap.yourdomain.com` does not match
`origin` header with value `cap.yourdomain.com:183` from a forwarded
Server Actions request. Aborting the action.
根因:Nginx Proxy Manager 在转发到非标准 HTTPS 端口(比如 :183)时,默认会丢弃 X-Forwarded-Host 中的端口号。而 Next.js 的 Server Actions 会对 forwarded host 和 origin 做严格校验,端口不匹配就直接拒绝请求。
踩坑过程:
- 在 NPM 的 Custom Nginx Configuration 里加
proxy_set_header X-Forwarded-Host $http_host;—— 无效,NPM 的 location 块会覆盖 server 级自定义配置 - 硬编码
proxy_set_header X-Forwarded-Host cap.yourdomain.com:183;—— 还是无效,同上
最终解决方案:在 Compose 中加一个 Nginx 中转容器,强制注入正确的 X-Forwarded-Host 头部。但这个方案后来在坑 4 解决后被移除了——因为最终架构调整后不再需要它。
经验:如果你用的是标准 443 端口,大概率不会遇到这个问题。非标准端口 + NPM + Next.js Server Actions 的组合才会触发。
坑 4:MinIO 上传 403
现象:坑 3 解决后上传还是卡住,但日志里已经没有 X-Forwarded-Host 错误了。打开浏览器 Network 面板,发现对 MinIO 的 PUT 请求返回 403 Forbidden,响应头 Server: openresty——这是 NPM 在响应,不是 MinIO。
排查链路:
Cap 生成预签名 URL
→ 浏览器拿着 URL 通过 NPM 访问 MinIO
→ NPM(openresty)修改了某些头部(如 Host 头)
→ S3 签名校验失败
→ 403 Forbidden
根因:NPM 转发时可能修改了 S3 预签名 URL 中的关键头部,导致 MinIO 的签名校验失败。S3 预签名 URL 对头部非常敏感,任何修改都会导致签名不匹配。
最终解决方案:MinIO 不走 NPM,改为浏览器内网直连。
S3_PUBLIC_URL=http://<NAS_IP>:9000
同时调整 MinIO 的 CORS 配置,允许域名访问:
MINIO_API_CORS_ALLOW_ORIGIN: "https://cap.yourdomain.com:183,http://<NAS_IP>:3000"
这个方案的前提是浏览器和 NAS 在同一内网。如果你的使用场景需要外网访问视频,则需要为 MinIO 单独配置 HTTPS 域名。
坑 5:残留容器端口冲突
现象:坑 4 解决后,移除了中转容器并重建,结果报错:
Bind for 0.0.0.0:3000 failed: port is already allocated
Found orphan containers ([cap-web-nginx]) for this project.
根因:从 Compose 中移除的容器不会自动删除。残留的 cap-web-nginx 仍在占用 3000 端口。
修复:
docker rm -f cap-web-nginx && docker compose up -d
一行命令搞定。但如果你不知道这个机制,可能会在这个报错上浪费不少时间。
踩坑时间线总结
| 顺序 | 问题 | 根因 | 修复方式 |
|---|---|---|---|
| 1 | MySQL 权限拒绝 | 群晖 btrfs 扩展 ACL | 改用 Docker named volume |
| 2 | 验证码登录失败 | NextAuth secure cookie 要求 HTTPS | 配置 NPM HTTPS 反代 |
| 3 | 上传卡住 | NPM 丢失 X-Forwarded-Host 端口 | Nginx 中转注入头部 |
| 4 | MinIO 403 | NPM 干扰 S3 预签名校验 | MinIO 浏览器内网直连 |
| 5 | 端口冲突 | 移除的容器未自动删除 | docker rm -f 清理 |
本地 AI 集成:让录屏自动生成摘要
Cap 内置了 AI 摘要功能,录制完成后自动生成视频文字摘要。官方支持三种 AI Provider:
| Provider | 环境变量 | 用途 |
|---|---|---|
| Groq | GROQ_API_KEY | LLM 摘要 |
| OpenAI | OPENAI_API_KEY | LLM 摘要(fallback) |
| Deepgram | DEEPGRAM_API_KEY | 语音转文字 |
但这里有个问题:Cap 源码把 OpenAI API 地址硬编码为 https://api.openai.com/v1/chat/completions,不支持 OPENAI_BASE_URL 环境变量。也就是说,你不能直接把请求指向本地大模型。
解决思路:DNS 劫持 + 自签证书 + Nginx 代理
在不修改 Cap 源码的前提下,通过三层转发实现请求重定向:
cap-web 容器内:
fetch("https://api.openai.com/v1/chat/completions")
│
▼ extra_hosts 解析(DNS 劫持)
172.21.0.100 (cap-ai-proxy, 固定 IP)
│
▼ nginx 443 自签 SSL → 转发到本地 AI
llama-server (OpenAI 兼容接口)
本地 AI 环境
你需要一台带 GPU 的机器跑 llama.cpp 的 llama-server。它原生提供 OpenAI 兼容的 /v1/chat/completions 端点,不需要 API Key。
推荐模型(12GB 显存可用):
- Gemma 系列 Coding 量化版 —— 64K 上下文,代码理解强
- Qwen 系列 量化版 + 多模态 —— 32K 上下文,支持图片理解
启动命令示例:
llama-server -m your-model.gguf --port 8080 --host 0.0.0.0
AI 代理 Nginx 配置
创建 ai-proxy.conf:
server {
listen 443 ssl;
server_name api.openai.com;
ssl_certificate /etc/nginx/certs/api.openai.com.crt;
ssl_certificate_key /etc/nginx/certs/api.openai.com.key;
proxy_read_timeout 180s; # llama-server 推理较慢,给足超时
proxy_send_timeout 180s;
location / {
proxy_pass http://<AI服务器IP>:8080;
proxy_http_version 1.1;
proxy_buffering off; # 支持 SSE 流式响应
proxy_cache off;
}
}
生成自签证书
mkdir -p ai-certs && cd ai-certs
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout api.openai.com.key \
-out api.openai.com.crt \
-subj "/CN=api.openai.com"
验证连通性
# 在 cap-web 容器内测试能否通过代理到达本地 AI
docker exec cap-web wget -qO- https://api.openai.com/v1/models --no-check-certificate
# 预期返回:llama-server 的模型列表 JSON
如果看到模型列表 JSON,说明整条链路已通。现在 Cap 录制完视频后,会自动调用你的本地大模型生成摘要——零 API 费用,数据完全不出内网。
日常运维速查
常用命令
cd /volume2/docker/cap
# 查看所有容器状态
docker compose ps
# 查看日志
docker compose logs --tail=50 cap-web
docker compose logs --tail=50 cap-media-server
# 搜索登录链接(未配邮件时从日志里找)
docker logs cap-web 2>&1 | grep -i "signin\|callback\|token"
# 检查 AI 连通性
docker exec cap-web wget -qO- https://api.openai.com/v1/models --no-check-certificate
# 更新镜像
docker compose pull && docker compose up -d
# 重启单个服务
docker compose restart cap-web
数据备份
# MySQL 自动备份(可加入群晖任务计划)
docker exec cap-mysql mysqldump -u cap -p<密码> cap > /volume2/backup/cap_$(date +%Y%m%d).sql
# MinIO 视频文件
# 直接备份 ./minio 目录即可
磁盘占用检查
du -sh /volume2/docker/cap/*
docker system df # 查看 Docker 整体占用
常见问题 FAQ
Q1:Cap 和 Loom 相比有什么优势和不足?
说实话,Cap 的功能成熟度还比不上 Loom,视频编辑能力比较弱,移动端也基本没支持。但它的优势也很明确:数据完全在自己手里,没有录制时长限制,还能接本地大模型做摘要,一分钱 API 费用都不用花。如果你对隐私和成本敏感,Cap 值得一试;如果你要的是开箱即用、团队协作顺畅,Loom 目前还是更省心。
Q2:部署 Cap 需要多少内存?
建议 NAS 至少 4GB 内存。MySQL 限制了 1GB,Cap Web 和 Media Server 各需几百 MB,加上 MinIO 和系统开销,4GB 是底线。如果同时跑 AI 模型,AI 服务器建议独立配置。
Q3:MinIO 必须内网直连吗?外网怎么访问视频?
当前方案中 MinIO 走内网直连是为了避免 NPM 干扰 S3 签名。如果需要外网访问视频,可以为 MinIO 单独配置一个 HTTPS 域名(不经过 NPM,直接用 MinIO 自带的 TLS 或独立反代)。
Q4:不用本地 AI,能用云端 API 吗?
可以。直接在 .env 中填入真实的 OPENAI_API_KEY 或 GROQ_API_KEY 即可,不需要配置 AI 代理。如果你同时填了 Groq 和 OpenAI,Cap 会优先用 Groq。
Q5:非标准端口(如 183)会有什么影响?
主要影响 X-Forwarded-Host 头部。如果使用标准 443 端口,坑 3 的问题不会出现。非标准端口需要在反代层正确处理端口信息,Next.js Server Actions 对此校验较严格。
Q6:如何配置邮件发送,不再从日志里找登录链接?
Cap 支持 Resend 邮件服务。在 .env 中配置 RESEND_API_KEY 和发件邮箱即可。具体参数参考 Cap 官方文档。
写在最后
整个部署过程断断续续花了两天,主要时间都耗在排错上。5 个坑里最费神的是坑 3 和坑 4——它们都涉及反向代理对请求头部的修改,而这类问题从日志里很难直接看出原因,需要靠浏览器 DevTools 对比请求头才能定位。
如果你打算部署 Cap,我的建议是:
- 先用标准 443 端口试,跑通后再折腾非标准端口
- MinIO 从一开始就走内网直连,别走反代,省掉坑 4
- MySQL 从一开始就用 named volume,别碰群晖 ACL
- 本地 AI 集成是锦上添花,先把基础功能跑通再说
希望这篇教程能帮你少走弯路。如果你在部署过程中遇到其他问题,欢迎留言交流。
本文基于 Cap 开源项目的实际部署经验整理,转载请注明出处。