MO Gallery 官网部署耗时分析与 Docker 迁移镜像优化
背景
MO Gallery 官网 mo-gallery-offical 使用 GitHub Actions 通过 SSH 将源码同步到服务器,然后在服务器执行:
docker compose up -d --build一次部署虽然最终成功,但 GitHub Actions 运行时间达到 8 分 42 秒:
部署流程本身没有卡死,主要问题是 Docker 在服务器上进行了完整的应用构建,并且把只用于数据库迁移的构建阶段也导出成了一个完整镜像。
日志分段
根据 Actions 日志,各阶段耗时如下:
| 阶段 | 耗时 |
|---|---|
| Checkout、SSH、源码同步 | 约 6 秒 |
pnpm install --frozen-lockfile | 约 68 秒 |
prisma generate + next build | 约 191 秒 |
导出 migrate 镜像 | 约 230 秒 |
| 数据库迁移、容器重建和启动 | 约 17 秒 |
真正需要关注的是下面这段:
[migrate] exporting layers 155.0s[migrate] unpacking ... 74.5s仅 migrate 镜像的导出和解包就占用了接近四分钟。
原因分析
原来的 Compose 配置如下:
services: migrate: build: context: . target: builder command: sh -c "node_modules/.bin/prisma migrate deploy"而 Dockerfile 的 builder 阶段负责完整的前端构建:
FROM base AS builderCOPY . .RUN pnpm install --frozen-lockfileRUN pnpm run build因此,builder 阶段包含了很多数据库迁移根本不需要的内容:
- 完整的 workspace 依赖
- 全部源码
.next构建产物- Next.js、Milkdown、Tiptap、图片处理等前端依赖
- Prisma CLI 以及应用的其他开发依赖
但迁移服务实际上只执行:
prisma migrate deploy它只需要读取 prisma/schema.prisma、prisma/migrations 和 Prisma CLI。让 Compose 直接使用 target: builder,等于先构建完整应用,再把整个构建阶段导出成一个迁移镜像。
方案调整
将 Dockerfile 拆成三个职责明确的阶段:
builder -> 完整安装依赖并构建 Next.js 应用migrate -> 只安装 Prisma CLI,执行数据库迁移runner -> 只运行 Next.js standalone 产物1. 新增迁移依赖清单
在 prisma/migrate-package.json 中只声明迁移所需的两个直接依赖:
{ "name": "mo-gallery-official-migrate", "private": true, "dependencies": { "dotenv": "17.4.2", "prisma": "7.10.0" }}这里使用精确版本,避免迁移镜像在构建时意外升级 Prisma 主版本。升级根项目 Prisma 版本时,需要同步更新这份清单。
2. 新增独立 migrate 阶段
# ---- 迁移阶段:只保留 Prisma CLI、dotenv 与迁移文件 ----FROM base AS migrateENV NODE_ENV=production
COPY prisma/migrate-package.json ./package.jsonRUN npm install --omit=dev --no-package-lock
COPY prisma.config.ts ./COPY prisma ./prisma
CMD ["node_modules/.bin/prisma", "migrate", "deploy"]这个阶段不会复制应用源码,也不会复制 .next 和完整 workspace。prisma.config.ts 中使用了:
import 'dotenv/config'import { defineConfig } from 'prisma/config'所以迁移镜像需要 dotenv 和 Prisma CLI,其他应用依赖都不需要。
3. Compose 使用 migrate 阶段
services: migrate: build: context: . target: migrate command: ["node_modules/.bin/prisma", "migrate", "deploy"] environment: DATABASE_URL: ${DATABASE_URL:?请在 .env 中设置 DATABASE_URL}应用服务仍然使用默认的 Dockerfile 最终阶段,也就是只包含 Next.js standalone 运行产物:
services: app: build: . depends_on: migrate: condition: service_completed_successfully这样可以保持原有的启动顺序:数据库迁移成功后,应用容器才启动。
为什么这样能减少耗时
之前的迁移镜像继承了完整 builder 阶段。即使迁移命令只运行几秒,Docker 仍需要:
- 导出包含完整依赖和构建产物的镜像层;
- 将这些镜像层写入 Docker 镜像存储;
- 解包迁移镜像;
- 再创建迁移容器。
调整后,migrate 阶段只包含 Prisma 运行所需内容,完整应用构建仍然只服务于 app 镜像,不再被重复作为迁移镜像导出。
本次日志中旧迁移镜像的导出和解包约耗时 230 秒,因此这部分是最明确的优化目标。实际第一次切换到独立阶段后,新的迁移镜像导出和解包约耗时 74 秒,但同时新增了约 68 秒的精简 Prisma 依赖安装;由于两个镜像阶段并行构建,最终部署从约 8 分 42 秒降至约 6 分 36 秒,实际减少约 2 分 06 秒。
迁移依赖清单只复制 prisma/migrate-package.json,后续源码变化不会使 npm install 层失效。只要 Prisma 版本和迁移依赖没有变化,后续部署应能复用这一层,迁移阶段还会比本次更快。
实测结果
优化前的部署运行时间约为 8 分 42 秒,优化后的本次运行时间约为 6 分 36 秒,减少约 2 分 06 秒。
本次运行的迁移阶段日志如下:
npm install --omit=dev --no-package-lock 约 68 秒migrate 镜像导出与解包 约 74 秒因此不能简单地把原来的 230 秒全部视为净收益:精简镜像仍然需要首次安装 Prisma CLI 及其传递依赖。不过迁移镜像已经从“完整应用构建镜像”变成了独立小阶段,后续部署可以复用稳定的 npm 安装层。
当前剩余耗时
拆分迁移镜像后,部署仍会有两个主要耗时来源:
pnpm install --frozen-lockfile:最新运行中应用构建阶段用了约 108 秒。当前 Dockerfile 为了解决 workspace 包复制顺序问题,将完整源码复制后再安装依赖,普通源码变化会导致依赖安装层失效。next build:最新运行中用了约 233 秒,服务器需要重新执行 Turbopack 编译和 TypeScript 检查。
后续还可以继续优化:
- 使用 BuildKit cache mount 持久化 pnpm store;
- 将根目录和 workspace 的
package.json与源码分层复制,尽可能复用依赖安装层; - 使用更轻量的构建缓存策略;
- 评估在 CI 构建镜像后推送到镜像仓库,服务器只拉取并重启,减少服务器本地构建时间。
不过这些优化涉及构建缓存和发布链路调整,而独立 migrate 阶段不改变现有发布流程,优先级更高、风险更低。
结论
部署耗时长并不是 SSH 同步慢,也不是 Prisma 迁移本身慢,而是数据库迁移服务错误地复用了完整应用构建阶段:
target: builder将其调整为:
target: migrate并为迁移服务提供只包含 Prisma CLI、dotenv 和迁移文件的独立镜像,可以避免导出完整 builder 镜像,同时保持“迁移成功后再启动应用”的部署语义不变。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!