OpenMAIC 本地化部署记录

OpenMAIC 本地化部署记录

__

环境:OrangePi 5 Plus(RK3588,ARM64)/ fnOS(飞牛OS)/ 1Panel + Docker 动机:THU-MAIC 官方托管模式(Access Code)每天限 10 次生成,切到本地部署摆脱额度限制 结果:部署成功,Web 端可正常访问,头像/模型图标正常显示


部署方式

  1. 本地电脑 clone 仓库后,打包上传到 fnOS(因服务器上访问github超时,所以未在服务器上直接 git clone

  2. 在 1Panel 中新建编排,指向仓库文件夹,走仓库自带的 Dockerfile + docker-compose.yml 构建

后面证明,"本地上传"这一步是本次两个坑里其中一个坑的根源。


问题一:1Panel 创建编排时构建超时

现象

#9 [runner 3/7] RUN apk add --no-cache libc6-compat cairo pango jpeg giflib librsvg
#9 1156.0 (33/50) Installing glib (2.88.1-r1)
创建编排 失败 命令超时

前面 pnpm build(Next.js 编译 + TypeScript 检查)已经正常跑完,卡在 runner 阶段装 cairo/pango/jpeg/giflib/librsvg 这几个图形库依赖上——19 分钟才装了 50 个包里的 33 个。

根因

Alpine 默认源 dl-cdn.alpinelinux.org 在本地网络访问慢/丢包,apk add 被拖到十几分钟,而 1Panel「创建编排」这个动作本身有执行超时,构建还没跑完就被杀掉了。跟 Dockerfile 写法、架构(ARM64)都没关系,纯粹是国内访问 Alpine 官方源的网络问题 + 1Panel 面板超时叠加。

修复

  1. Dockerfile 里给 base 阶段加一行换源(deps/runner 阶段继承自 base,不用重复加):

    FROM node:22-alpine AS base
    RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
    
  2. 更重要的一条经验:不要指望 1Panel 面板里的「创建编排」按钮跑完整个 build。改为 SSH 到服务器,在仓库目录下手动执行:

    sudo docker compose build --no-cache
    sudo docker compose up -d
    

    CLI 构建没有面板那层超时限制,构建成功、镜像落地之后,1Panel 面板再去管理这个已存在的编排(启动/停止/重建)就不会再触发这个问题。


问题二:容器崩溃重启循环(EACCES scandir)

现象

✓ Starting...
Error: EACCES: permission denied, scandir '/app/public/vendor/maic-importer/adapter'
▲ Next.js 16.1.2
✓ Starting...
Error: EACCES: permission denied, scandir '/app/public/vendor/maic-importer/adapter'
...(循环往复)

容器不断重启,docker exec 一度报 Container ... is restarting, wait until the container is running,进都进不去。

根因

Dockerfile runner 阶段的三行 COPY

COPY --from=builder /app/public ./public                                    # 漏了 --chown
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

public 这一行唯独没加 --chown=nextjs:nodejs。builder 阶段全程以 root 身份跑,public/vendor/maic-importer/ 下这些文件复制过来后属主还是 root,而容器最后 USER nextjs 切到 uid 1001 运行,对 root 拥有的目录 scandir 自然被拒绝。

排查中踩的坑

  • 第一次 docker exec -it openmaic ...permission denied while trying to connect to the Docker daemon socket —— 当前用户不在 docker 组,需要 sudousermod -aG docker <user>

  • 容器名字猜错:实际是 compose 生成的 openmaic-openmaic-1,不是简单的 openmaic

  • 容器处于 crash-loop 时 完全没法 docker exec 进去,必须先让它能跑起来(改 Dockerfile 重建),再进去做后续验证,无法在崩溃状态下"先進去改权限再重启"这条路走通。

  • 第一次改完 --chown 后在 1Panel 里点「重建」不生效——1Panel 的"重建"很多情况下只是 up -d --force-recreate不等于重新 build 镜像,Dockerfile 的改动根本没进新镜像。改用 CLI docker compose build --no-cache && docker compose up -d 才让改动真正生效。

修复

COPY --from=builder --chown=nextjs:nodejs /app/public ./public

问题三:容器起来了,但网页头像/模型图标 500

现象

容器不再崩溃,Web 端能打开,但页面上老师头像、大模型图标(如 doubao.svg)全部缺失,F12 控制台:

Failed to load resource: the server responded with a status of 500 (Internal Server Error)
:3000/logos/doubao.svg:1

500 而不是 404,说明不是路径错,是服务端读文件时又出错了。

排查

进容器直接看权限:

sudo docker exec -it openmaic-openmaic-1 sh -c "ls -ld /app/public/logos /app/public/avatars; ls -la /app/public/logos | head -5"

结果:

drwxr-xr-x    1 nextjs   nodejs   610 Jul 10 02:28 /app/public/logos
----------    1 nextjs   nodejs  1615 Jul 10 02:28 azure.svg
----------    1 nextjs   nodejs  5380 Jul 10 02:28 baidu.png

属主已经是对的(nextjs:nodejs),但文件权限位是 ----------——连属主自己都没有读权限。

根因

仓库是从本地电脑(Windows/macOS)打包上传到 fnOS 的,上传/打包过程中把这些静态资源文件的权限位传丢了,变成了全 0(chmod 000 效果)。前面 Dockerfile 里加的 --chown 只改了属主,没改权限模式——两者是两件独立的事:chown 决定"这文件归谁",chmod 决定"归属者/同组/其他人能不能读写执行"。属主对了但权限位是 0,照样谁都读不了,包括 nextjs 自己。

修复

先在运行中的容器里现场验证(无需重建即可立即生效):

sudo docker exec -u root -it openmaic-openmaic-1 sh -c "chmod -R a+rX /app/public"

刷新网页,头像和模型图标恢复正常,确认就是这个问题。

固化进 Dockerfile,保证下次重建不再复发:

COPY --from=builder --chown=nextjs:nodejs /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN chown -R nextjs:nodejs ./public && chmod -R a+rX ./public

chmod a+rX(大写 X)只给目录、以及本来就带执行位的文件加 x,不会把普通的 .svg/.png 误标记为可执行文件,是这类"目录+静态文件混合"场景的标准写法。


最终生效的 Dockerfile 改动(相对官方原版的 diff)

# ---- Stage 1: Base ----
FROM node:22-alpine AS base
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories   # 新增:换源,解决 apk 慢/1Panel 构建超时
RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
WORKDIR /app

# ...(deps / builder 阶段不变)...

# ---- Stage 4: Runner ----
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN apk add --no-cache cairo pango jpeg giflib librsvg
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/public ./public          # 修改:补上 --chown
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN chown -R nextjs:nodejs ./public && chmod -R a+rX ./public           # 新增:修正权限模式
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

经验总结 / 下次可以直接跳过的坑

  1. 1Panel「创建编排/重建」≠ 保证重新 build 镜像,Dockerfile 改了之后想确认生效,老老实实 SSH 上去 docker compose build --no-cache,不要依赖面板按钮。

  2. 国内构建 Alpine 系镜像,Dockerfile 里第一步就先换源mirrors.ustc.edu.cn / mirrors.aliyun.com / mirrors.tuna.tsinghua.edu.cn),能省掉一大类"莫名其妙卡住/超时"的排查时间。

  3. chownchmod 是两回事:多阶段构建里 COPY --chown 只保证属主对,不保证权限位对。如果源文件权限本身就有问题(比如从 Windows/macOS 打包上传过来的),属主再对也读不了,两者要一起显式处理。

  4. 容器处于 crash-loop 时无法 docker exec 进去,排查权限类问题得先让容器至少能跑起来(哪怕跑一个 docker run --rm -it --entrypoint sh <image> 一次性容器)才能看到实际现场。

  5. 从本地电脑打包上传项目到 Linux 服务器,是这次两个权限坑的共同源头。以后这类仓库优先直接在 fnOS 上 git clone,避免打包/上传过程中丢权限位这一整类问题;如果已经上传了,记得给整个项目目录跑一遍 chown -R + chmod -R a+rX(或至少对 public/ 这种会被静态直出的目录做一遍)再进 Docker 构建。

  6. 诊断 500 vs 404 是关键分水岭:静态资源 404 → 路径/COPY 范围问题;500 → 服务端读取文件时出错,第一反应查权限(属主 + 权限位)。


完整部署步骤(供下次/换机复用)

1. 获取仓库

推荐直接在服务器上 git clone,不要在本地电脑下载/打包再上传(本次两个坑之一的权限位丢失问题,根源就是本地上传):

cd /vol1/1000    # 换成你想放的目录
git clone https://github.com/THU-MAIC/OpenMAIC.git
cd OpenMAIC

如果这次是已经从本地上传好的目录,先无脑跑一遍权限归一化,防患于未然:

find . -type d -exec chmod 755 {} +
find . -type f -exec chmod 644 {} +

2. 改 Dockerfile(换源 + 权限修正)

对照仓库里的 Dockerfile,确认/补上这几处(本次实际验证有效的版本):

# ---- Stage 1: Base ----
FROM node:22-alpine AS base
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache libc6-compat
RUN corepack enable && corepack prepare pnpm@10.28.0 --activate
WORKDIR /app

# ---- Stage 2: Dependencies ----
FROM base AS deps
RUN apk add --no-cache python3 build-base g++ cairo-dev pango-dev jpeg-dev giflib-dev librsvg-dev
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY packages/ ./packages/
COPY scripts/ ./scripts/
RUN pnpm install --frozen-lockfile

# ---- Stage 3: Builder ----
FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/packages ./packages
COPY . .
COPY --from=deps /app/public/vendor ./public/vendor
RUN pnpm build

# ---- Stage 4: Runner ----
FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV HOSTNAME=0.0.0.0
ENV PORT=3000
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
RUN apk add --no-cache cairo pango jpeg giflib librsvg
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN chown -R nextjs:nodejs ./public && chmod -R a+rX ./public
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]

3. 配置 LLM Provider

cp .env.example .env.local

编辑 .env.local,至少配一个核心 LLM Provider Key,DEFAULT_MODEL 记得带 provider 前缀(否则默认按 OpenAI 解析):

ANTHROPIC_API_KEY=sk-ant-...
DEFAULT_MODEL=anthropic:claude-3-5-haiku-20241022

或者用 Google(速度/成本更均衡):

GOOGLE_API_KEY=...
DEFAULT_MODEL=google:gemini-3-flash-preview

联网搜索/图片/视频/TTS 等都是可选功能,按需追加对应 Key(TAVILY_API_KEY / IMAGE_* / VIDEO_* / TTS_*),不配也能跑基础的课堂生成。

4. 构建镜像(一定走 CLI,不要依赖 1Panel 面板按钮)

sudo docker compose build --no-cache

--no-cache 保证 Dockerfile 改动一定生效,不会被层缓存跳过。这一步在 1Panel 面板里点「创建编排/重建」大概率会因为面板自身的超时被打断,尤其是第一次拉依赖、装 cairo/pango 这些图形库的时候,SSH 上去手动跑不受这个限制。

5. 启动

sudo docker compose up -d

之后就可以回 1Panel 面板里正常管理这个编排(启动/停止/看日志),因为镜像已经构建好了,面板只是拉起已有镜像,不会再触发构建超时。

6. 验证

# 看容器是不是稳定运行,而不是在重启
sudo docker compose ps

# 确认服务端健康
curl -fsS http://localhost:3000/api/health

# 权限问题排查专用:确认 public 目录属主+权限位都正常
sudo docker exec -it <容器名> sh -c "ls -ld /app/public/logos /app/public/avatars /app/public/vendor/maic-importer"

正常应该是属主 nextjs:nodejs,目录权限 755drwxr-xr-x),文件权限 644-rw-r--r--)左右,不应该出现 ---------- 这种全零权限。

浏览器打开 http://<fnOS-IP>:3000,确认页面上老师头像、模型图标都能正常显示,再算部署完成。

7.(可选)NPM 反代对外

如果需要局域网外访问,走你已有的 Nginx Proxy Manager 配置一个转发到 <fnOS-IP>:3000 即可,跟你现有 Gitea/Halo 等服务的做法一致。


TODO / 后续

  • 给 Hermes 写调用本地 OpenMAIC 的 skill(对接方式定为直接 HTTP 调用本地 http://<fnOS-IP>:3000/api/...,不需要 Access Code 鉴权)

  • 确认本地部署用哪个 LLM Provider 写入 .env.localDEFAULT_MODEL 记得带 provider 前缀,如 anthropic:claude-3-5-haiku-20241022

  • 视情况通过 NPM(Nginx Proxy Manager)把本地 OpenMAIC 反代出去,方便非本机访问

fnOS 1.2.0xx 升级踩坑记录 2026-07-02

评论区