news 2026/10/19 13:05:14

Turborepo + Next.js + Docker 实战:with-docker 示例中 web 应用的开发与容器化部署

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Turborepo + Next.js + Docker 实战:with-docker 示例中 web 应用的开发与容器化部署
  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】turbo

Build system optimized for JavaScript and TypeScript, written in Rust

项目地址:https://gitcode.com/gh_mirrors/tu/turbo
点击查看免费下载

本指南以 examples/with-docker/apps/web/README.md 为线索,讲解该 Next.js web 应用在 Turborepo monorepo 中的本地开发、前后端联调、生产构建,以及通过turbo prune --docker与多阶段 Dockerfile 完成容器化部署的完整链路。读完本文,你将掌握 yarn workspace 下的开发启动方式、Next.js standalone 输出模式的作用,以及如何用 Turborepo 官方with-docker示例的思路为 monorepo 应用构建体积更小、缓存命中率更高的 Docker 镜像。

示例定位:web 应用在 with-docker monorepo 中的角色

with-docker是 Turborepo 仓库中一个由社区维护的示例(见 examples/with-docker/meta.json),核心演示"用 Docker 部署一个同时包含 Next.js 前端与 Express 后端的 monorepo"。仓库顶层 README 描述其结构(examples/with-docker/README.md):

  • apps/web:一个 Next.js 应用,即本指南的主体;
  • apps/api:一个 Express 服务器,提供后端接口;
  • packages/ui:React 组件库,web 应用直接引用其中的Button;
  • packages/logger:同构日志工具,api 应用使用;
  • packages/eslint-config、packages/typescript-config、packages/jest-presets:共享的工程配置。

web 应用的package.json(examples/with-docker/apps/web/package.json)声明了build、dev、lint、start四个脚本,依赖next、react、react-dom,并通过 workspace 通配符"@repo/ui": "*"直接依赖仓库内的 UI 组件库。仓库根目录的package.json(examples/with-docker/package.json)将apps/*与packages/*声明为 yarn workspaces,根脚本通过turbo run build、turbo run dev统一调度各 workspace 的任务,根目录turbo.json(examples/with-docker/turbo.json)定义了任务的依赖关系、输入与输出缓存规则。

需要特别说明:web 应用 README 是 Next.js 脚手架生成的通用说明,其中提到修改pages/index.js,但当前示例实际采用 Next.js App Router 结构,页面入口是 examples/with-docker/apps/web/src/app/page.tsx。下文均以仓库实际代码为准。

本地开发:启动开发服务器

按照 README 的指引,进入仓库根目录后先安装依赖,再启动开发服务器:

yarn install --frozen-lockfile yarn dev

在with-docker示例中,根目录的dev脚本是turbo run dev,Turborepo 会并行启动apps/web与apps/api两个应用的开发任务,其中 web 应用对应next dev。启动成功后,在浏览器打开 http://localhost:3000 即可看到结果。

几个与开发体验直接相关的要点:

  • 热更新:修改src/app/page.tsx等源码文件后,页面会自动更新,无需手动重启,这正是 README 中 "The page auto-updates as you edit the file" 所描述的行为;
  • 端口约定:web 应用默认运行在 3000 端口,api 应用默认运行在 3001 端口(见 examples/with-docker/apps/api/src/index.ts 中process.env.PORT || 3001);
  • 持久化任务:根目录turbo.json中dev任务被标记为"persistent": true且"cache": false,因为开发服务器是长时间运行的前台进程,不适合做任务缓存;
  • 环境变量:web 应用通过process.env.NEXT_PUBLIC_API_HOST指定后端地址(默认回退到http://localhost:3001),该变量在 examples/with-docker/turbo.json 的build任务中被声明为env输入,参与构建缓存键计算。

前端与后端联调:web 如何调用 api

README 提到 "API routes can be accessed on http://localhost:3000/api/hello",这是 Next.js 脚手架文档对pages/api目录的通用说明。在with-docker示例的实际代码中,web 应用并不自带 API 路由,而是通过 HTTP 请求调用独立的 Express 后端服务。

web 应用的页面逻辑位于 examples/with-docker/apps/web/src/app/page.tsx:用户在表单中输入名字并提交后,前端向`${API_HOST}/message/${name}`发起请求,将后端返回的message渲染到页面;请求失败时显示 "Unable to fetch response",并提供 Reset 按钮重置输入。页面还引用了@repo/ui中的Button组件(见 examples/with-docker/packages/ui/src/button.tsx),这是 monorepo 内部包共享最直观的体现——web 应用通过 workspace 依赖直接复用 UI 组件源码,而next.config.js中配置了transpilePackages: ["@repo/ui"],确保内部包中的 TSX 源码能被 Next.js 正确编译(examples/with-docker/apps/web/next.config.js)。

后端由 Express 实现(examples/with-docker/apps/api/src/server.ts),提供了两个接口:

  • GET /message/:name:返回{ "message": "hello <name>" },供 web 前端调用;
  • GET /status:返回{ "ok": true },用于健康检查。

对应地,examples/with-docker/apps/api/src/tests/server.test.ts 中的 Jest 测试用 supertest 验证了这两个接口的行为:/status返回 200 且body.ok === true,/message/jared返回"hello jared"。这套测试与测试脚本test: jest --detectOpenHandles(配置在 api 的package.json中)相互印证,说明前端联调所依赖的接口契约是有测试保障的。

生产构建:Next.js standalone 输出

web 应用的生产构建脚本是next build。关键在于 examples/with-docker/apps/web/next.config.js 中的两项配置:

const path = require("node:path"); module.exports = { reactStrictMode: true, transpilePackages: ["@repo/ui"], output: "standalone", outputFileTracingRoot: path.join(__dirname, "../../"), };
  • output: "standalone":启用 Next.js 的独立输出模式,构建产物会生成.next/standalone目录,其中包含运行所需的最小化 Node.js 依赖与server.js入口,方便直接拷贝进 Docker 镜像;
  • outputFileTracingRoot:指向 monorepo 根目录(../../),因为 web 应用依赖@repo/ui等 workspace 内部包,文件追踪必须基于仓库根目录才能正确收集依赖文件;
  • transpilePackages: ["@repo/ui"]:让 Next.js 直接编译内部包的 TypeScript/TSX 源码。

在 Turborepo 层面,根目录 examples/with-docker/turbo.json 的build任务声明了"dependsOn": ["^build"](先构建依赖它的 workspace,即@repo/ui),并将输出声明为:

"outputs": [ "dist/**", ".next/**", "!.next/cache/**", "!.next/dev/**", "public/dist/**" ]

这告诉 Turborepo 哪些产物可以被缓存:.next目录整体纳入缓存,但排除其中的cache与dev子目录(它们与构建结果无关且频繁变动)。

Docker 化部署:turbo prune 与多阶段构建

这是with-docker示例的核心价值。直接在 monorepo 上执行docker build有一个经典痛点:根目录的 lockfile 是全局的,任何 workspace 的依赖变动都会导致其他应用的镜像层缓存失效。Turborepo 提供的解决方案是turbo prune <workspace> --docker(详见官方参考文档 apps/docs/content/docs/reference/prune.mdx 与 Docker 指南 apps/docs/content/docs/guides/tools/docker.mdx)。

prune 生成的目录结构

在 monorepo 根目录执行turbo prune web --docker后,输出目录./out中会生成:

  • out/json/:只包含安装依赖所需的package.json(含根 package.json 与相关 workspace 的清单),用于 Dockerfile 中"先装依赖"的缓存层;
  • out/full/:构建目标所需的完整源码与内部包源码,以及根package.json和turbo.json;
  • 一份裁剪后的 lockfile:仅保留构建 web 应用所必需的依赖子集,避免把无关 workspace 的依赖带进镜像。

未加--docker时,所有裁剪后的文件统一放在./out目录;加上--docker后则拆分为json与full两个子目录,这是为了配合 Docker 的分层缓存策略。从源码实现看,--docker标志在crates/turborepo-prune/src/lib.rs中被解析并控制输出布局(包括.npmrc、.yarnrc.yml、bunfig.toml等文件会被复制到 Docker 输出目录),prune 的底层实现由 Rust 编写的turborepo-prunecrate 提供。

web 应用的 Dockerfile 拆解

web 应用的镜像定义见 examples/with-docker/apps/web/Dockerfile,采用三段多阶段构建:

ARG NODE_VERSION=24.21.0 FROM node:${NODE_VERSION}-slim AS base # 阶段一:prepare —— 裁剪 monorepo FROM base AS prepare RUN yarn global add turbo COPY . . RUN turbo prune web --docker # 阶段二:builder —— 先装依赖,再构建 FROM base AS builder COPY --from=prepare /app/out/json/ . RUN yarn install --frozen-lockfile COPY --from=prepare /app/out/full/ . RUN yarn turbo build # 阶段三:runner —— 最小运行时 FROM node:${NODE_VERSION}-alpine AS runner RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs USER nextjs COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./ COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public CMD node apps/web/server.js

这套结构的设计意图清晰:

  • prepare 阶段只做 prune:将整个仓库复制进镜像后立即裁剪,out/json与out/full成为后续阶段的构建原料;
  • builder 阶段先拷贝out/json并执行yarn install:由于package.json与 lockfile 的变动频率远低于源码,这一层可以充分复用 Docker 镜像缓存——只有依赖真正变化时才重装node_modules;随后才拷贝out/full的源码并执行yarn turbo build;
  • runner 阶段最小化:只拷贝 standalone 产物、静态资源与public目录,并以非 root 用户nextjs运行(出于安全考虑),最终通过node apps/web/server.js启动。

用 Docker Compose 编排前后端

仓库根目录的 examples/with-docker/docker-compose.yml 定义了web(3000 端口)与api(3001 端口)两个服务,二者通过外部网络app_network通信,容器之间以容器名作为主机名互相访问。

version: "3" services: web: container_name: web build: context: . dockerfile: ./apps/web/Dockerfile restart: always ports: - 3000:3000 networks: - app_network api: container_name: api build: context: . dockerfile: ./apps/api/Dockerfile restart: always ports: - 3001:3001 networks: - app_network networks: app_network: external: true

按示例 README 的完整操作流程:

# 安装依赖(使用锁定版本) yarn install --frozen-lockfile # 创建容器间通信所需的网络(容器名即主机名) docker network create app_network # 构建生产镜像 docker compose build # 以分离模式启动生产环境 docker compose up -d

启动后访问 http://localhost:3000 即可使用 web 应用。停止并清理所有运行中的容器:

docker compose down

注意:docker compose build的构建上下文是仓库根目录(context: .),两个应用的 Dockerfile 均通过dockerfile: ./apps/web/Dockerfile(或./apps/api/Dockerfile)指定,这与 Dockerfile 内部COPY . .、turbo prune web --docker需要完整仓库上下文的做法一致。api 应用的 Dockerfile(examples/with-docker/apps/api/Dockerfile)与 web 版本结构相同,差异仅在 runner 阶段:以expressjs用户运行,整包拷贝后通过node apps/api/dist/index.js启动。

远程缓存:Docker 构建中的 Remote Cache

示例 README 提供了可选配置:在构建时启用 Turborepo Remote Cache,让容器内的turbo build命中远程缓存,从而即使docker build本身未命中缓存(如--no-cache场景),也能复用构建产物。

实现方式是在 Dockerfile 的 builder 阶段取消注释并传入构建参数(web 与 api 的 Dockerfile 中均已预留位置):

ARG TURBO_TEAM ENV TURBO_TEAM=$TURBO_TEAM ARG TURBO_TOKEN ENV TURBO_TOKEN=$TURBO_TOKEN RUN yarn turbo build

然后通过--build-arg在构建时注入凭据(官方文档中的验证命令,见 apps/docs/content/docs/guides/tools/docker.mdx):

docker build -f apps/web/Dockerfile . --build-arg TURBO_TEAM="your-team-name" --build-arg TURBO_TOKEN="your-token" --no-cache

在 Docker 构建过程中,TURBO_TEAM与TURBO_TOKEN作为构建参数只存在于 builder 阶段,不会进入最终的 runner 镜像层,这也体现了多阶段构建对敏感信息的隔离价值。

工程配置与质量保障

web 应用沿用了 monorepo 共享的工程化配置:

  • ESLint:examples/with-docker/apps/web/eslint.config.js 直接复用@repo/eslint-config/next.js预设,lint 脚本为eslint . --max-warnings 0,任何 warning 都会导致任务失败;
  • TypeScript:TypeScript 版本与共享的@repo/typescript-config预设配合使用;
  • Jest:api 应用通过@repo/jest-presets/node复用统一测试预设,web 应用自身未配置测试脚本,其核心交互逻辑(表单提交、后端响应渲染)依赖 api 的接口测试保障契约。

在 Turborepo 层面,根目录 examples/with-docker/turbo.json 还声明了globalEnv: ["PORT"]与build任务的env: ["NEXT_PUBLIC_API_HOST"]:环境变量被纳入哈希计算,意味着当NEXT_PUBLIC_API_HOST等影响构建结果的环境变量变化时,缓存会自动失效,避免误用旧产物。

常见问题与注意事项

  1. README 与示例代码的差异:web 应用的 README 是 Next.js 通用模板,其中pages/index.js、pages/api/hello.js的说法不适用于当前示例(示例使用 App Router 且后端独立为 api 应用),请以 examples/with-docker/apps/web/src/app/page.tsx 为准;
  2. 构建上下文:Docker 构建必须在 monorepo 根目录执行(docker compose build已自动使用根目录作为 context),否则 Dockerfile 中的COPY . .与turbo prune无法拿到完整仓库;
  3. Node 版本一致性:仓库根package.json声明"node": ">=24.21.0",Dockerfile 中的ARG NODE_VERSION=24.21.0与之保持一致,本地环境建议使用相同或更高版本;
  4. turbo prune跟随的是包依赖而非任务依赖:如参考文档 apps/docs/content/docs/reference/prune.mdx 所述,仅当dependsOn显式指定任务依赖且启用futureFlags.affectedUsingTaskInputs时,任务依赖的 workspace 才会被保留,日常使用中请确认被裁剪的 workspace 确为构建所必需;
  5. .dockerignore:在真实项目中应把node_modules、.next等目录加入.dockerignore,避免它们被复制进 prepare 阶段的镜像层,示例 Dockerfile 本身通过 prune 流程规避了这一问题。

进一步阅读

  • examples/with-docker/README.md:示例整体结构与 Docker Compose 使用说明;
  • apps/docs/content/docs/reference/prune.mdx:turbo prune全部选项(--docker、--out-dir、--production、--use-gitignore等)与输出目录结构说明;
  • apps/docs/content/docs/guides/tools/docker.mdx:monorepo 中 Docker 构建的问题分析、prune 原理与远程缓存配置;
  • crates/turborepo-prune/src/lib.rs:turbo prune的 Rust 底层实现,包括--docker输出布局与附加文件复制逻辑;
  • examples/with-docker/apps/api/Dockerfile:后端应用的镜像构建参考。
  • 构建工具
  • 开发工具
  • CLI

【免费下载链接】turbo

Build system optimized for JavaScript and TypeScript, written in Rust

项目地址:https://gitcode.com/gh_mirrors/tu/turbo
点击查看免费下载
上一篇:fastText语言识别模型的应用案例分享
下一篇:Maple Mono:为开发者量身打造的优雅等宽编程字体

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 13:05:13

智慧校园建设规划指南:从顶层设计到数据中台落地实践

这几年在学校信息化领域干得久了&#xff0c;我发现一个特别普遍的现象&#xff1a;一说要搞智慧校园&#xff0c;校长和信息化主任们最容易陷入两个极端——要么急着上一堆新系统&#xff0c;恨不得一夜之间把学校武装成“未来学校”&#xff1b;要么觉得这事儿太宏大、太烧钱…

作者头像 李华
网站建设 2026/10/16 20:44:38

Java JSON字符串转Map集合:动态数据处理的实用方案与工具选型

写接口的人都知道&#xff0c;最讨厌的不是逻辑复杂&#xff0c;而是接口返回的数据“不讲武德”。我刚开始做后端对接时&#xff0c;接过一个第三方系统的回调&#xff0c;文档上说好固定字段&#xff0c;结果实际参数隔三差五变&#xff0c;今天多一个level&#xff0c;明天把…

作者头像 李华
网站建设 2026/10/14 20:35:37

windows下mysql中binlog日志分析和数据恢复

1.首先查看是否开启了binlog show variables like %log_bin%;看到了是没有开启的。 2.开启binlog日志&#xff0c;并重启mysql服务 不能通过命令的方式去打开&#xff0c;因为会提示说这个参数是只读的。如下图&#xff1a;所以&#xff0c;打开mysql的配置文件&#xff0c;找到…

作者头像 李华