在文章中,笔者打算使用pnpm的workspaces功能实现monorepo架构。接着使用docker compose来部署Nextjs项目。
monorepo项目搭建
安装pnpm
npm i pnpm -g
创建新项目
mkdir nextjs-monorepo
cd nextjs-monorepo
pnpm init
在根目录创建pnpm-workspace.yaml。参考如下内容:
packages:
# 存放应用的文件夹
- 'apps/*'
# 存放包的文件夹
- 'packages/*'
创建nextjs应用
进入apps目录,创建nextjs应用:
pnpx create-next-app@latest
//根据自己的需求选择
What is your project named? # web (change to whatever you want)
Would you like to use TypeScript with this project? # Yes
Would you like to use ESLint with this project? # Yes
Would you like to use Tailwind CSS with this project? Yes # Yes
Would you like to use `src/` directory with this project? # Yes
Use App Router (recommended)? # Yes
Would you like to customize the default import alias? # No
这里创建了一个name为web的nextjs应用。接着就可以通过下面命令运行起来了:
cd web
pnpm dev
//或者在根目录
pnpm --filter web dev
创建package
进入packages文件夹,创建一个core文件夹存放一些核心或者公共的代码:
cd packages
mkdir core
cd core
pnpm init
在core/src/utils下创建一个common.ts文件,存放一些公共的函数:
export const sayHello = ()=>{
console.log('hello');
}
由于使用typescript,需要在core目录下创建tsconfig.json文件:
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Language and Environment */
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"ES6",
"DOM"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
/* Modules */
"module": "ESNext" /* Specify what module code is generated. */,
"rootDir": "./src" /* Specify the root folder within your source files. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"resolveJsonModule": true /* Enable importing .json files. */,
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
"declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
"outDir": "build" /* Specify an output folder for all emitted files. */,
"declarationDir": "./types" /* Specify the output directory for generated declaration files. */,
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
/* Completeness */
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"jsx": "react"
},
"include": ["src/**/*"]
}
然后安装typescript来编译ts文件:
pnpm add typescript -F core
接着在package.json文件添加相关的script命令:
{
"name": "core",
"version": "1.0.0",
"description": "core code",
"files": [
"build"
],
"main": "./build/index.js",
"types": "./build/index.d.ts",
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"typescript": "^5"
}
}
运行pnpm build就可以编译ts文件到build目录。
web使用core的包
在web项目的package.json文件中添加对core的依赖:
dependencies:{
//...
"core":"workspace:*"
}
然后运行pnpm install把core软连接到web中。
最后就可以直接引入使用:
import { sayName } from "core/build/utils/common";
console.log(sayName("world"));
docker compose
安装docker compose
直接去[官网]下载对应系统的安装包即可。
编写yml文件
在项目根目录下新建一个docker-compose.yml文件:
services:
web:
container_name: web
build:
context: ./
dockerfile: ./apps/web/Dockerfile
ports:
- 3002:3002
编写Dockerfile 文件
FROM node:18-alpine AS base
# apps/web/Dockerfile
# 1. 安装所需要的基础环境
FROM base AS deps-and-builder
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache g++ make py3-pip libc6-compat
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
# 通过package和lock文件来安装依赖
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
COPY ./apps/web ./apps/web
COPY ./packages ./packages
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN echo "pnpm install completed"
# 打包
RUN pnpm --filter web build
# 3. 运行阶段,copy所需要的文件
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
# 这里需要保持standalone的目录结构
COPY --from=deps-and-builder --chown=nextjs:nodejs /app/apps/web/.next/standalone ./
COPY --from=deps-and-builder /app/apps/web/public ./apps/web/public
COPY --from=deps-and-builder --chown=nextjs:nodejs /app/apps/web/.next/static ./apps/web/.next/static
#可通过该命令查看是否copy成功
RUN ls -la /app/apps/web
USER nextjs
EXPOSE 3002
ENV PORT=3002
ENV HOSTNAME="0.0.0.0"
CMD ["node", "apps/web/server.js"]
上面的Dockerfile有几点值得注意:
- 有时候COPY文件的时候会报错,请确保目录的结构是否保持一致,可能copy的时候放错地方,或者文件路径不存在。可以通过
RUN ls -la your path来调试(可以在docker的可视化客户端查看打印日志)。 - 在COPY整个项目或者目录的时候,可以通过
.dockerignore文件来排除不想COPY的文件或目录。 - nextjs应用需要在
next.config.mjs文件中配置output类型为standalone。建议先运行build命令然后查看standalone的目录结构,后面COPY public和static的文件夹需要跟standalone的目录结构保持一致。
const nextConfig = {
output: "standalone",
}
运行docker compose
可以通过下面的命令来启动docker compose:
# 启动
docker compose -f docker-compose.yml up -d --build
# 停止
docker compose -f docker-compose.yml down
或者创建一个Makefile文件,编写以下内容:
.PHONY: docker-start
docker-start:
docker compose -f docker-compose.yml up -d --build
.PHONY: docker-stop
docker-stop:
docker compose -f docker-compose.yml down
# 启动
make docker-start
# 停止
make docker-stop
启动后可以通过docker ps来查看是否成功