Open7

pnpm workspace + prisma + dockerの構築

tinoue723tinoue723

背景
個人開発でnextjsでフロント・バックを書いていたが、スケジュールジョブ(特定の時間にリマインドするなど)どうしようとなった。
vercelでホストしてたので、vercelのcron jobを使おうと思ったが、hobbyだと制約がきつい。
以前cloud run jobsを使ったことがあったので、dockerfileやらterraformやら作るのめんどいなと思いつつそれでやろうとなった。

tinoue723tinoue723

フォルダ構造変更前(t3-app使用)

.
├── prisma
│   └── schema.prisma
├── public
│   └── favicon.ico
├── src
│   ├── app
│   │   └── layout.tsx
│   ├── server
│   │   ├── auth.ts
│   │   └── db.ts
│   ├── styles
│   │   └── globals.css
│   ├── trpc
│   │   ├── react.tsx
│   │   └── server.ts
│   ├── env.js
│   └── middleware.ts
├── next.config.js
├── package.json
├── package-lock.json
├── postcss.config.cjs
├── prettier.config.js
├── README.md
├── start-database.sh
├── tailwind.config.ts
└── tsconfig.json

変更後

.
├── app
│   ├── job
│   │   ├── dist
│   │   │   └── index.js
│   │   ├── remind-task
│   │   │   └── index.ts
│   │   ├── package.json
│   │   └── tsconfig.json
│   └── web
│       ├── public
│       ├── src
│       │   ├── env.js
│       │   ├── middleware.ts
│       │   └── push-test.ts
│       ├── next.config.js
│       ├── next-env.d.ts
│       ├── package.json
│       ├── postcss.config.cjs
│       ├── prettier.config.js
│       ├── start-database.sh
│       ├── tailwind.config.ts
│       └── tsconfig.json
├── packages
│   └── db
│       ├── prisma
│       │   └── schema.prisma
│       ├── index.js
│       ├── index.ts
│       ├── package.json
│       └── tsconfig.json
├── terraform
│   ├── main.tf
│   └── terraform.tfvars
├── Dockerfile
├── package.json
├── pnpm-lock.yaml
└── pnpm-workspace.yaml
tinoue723tinoue723

nextjsのプロジェクトの中にjobを突っ込むと、Jobのdockerfileで不要なファイルが含まれてしまうかなと思い、分けた。

npmよりもpnpmのほうがworkspaceを扱うには便利そうだった。
Buildの依存関係とかはturborepo使わないとなあと思ってるが、今はそこまで複雑じゃないので妥協。

tinoue723tinoue723

packages/dbはこんな感じ
index.ts(これはt3 appで作られたやつのまんま)

import { PrismaClient } from "@prisma/client";

const createPrismaClient = () =>
  new PrismaClient({
    log:
      process.env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"],
  });

const globalForPrisma = globalThis as unknown as {
  prisma: ReturnType<typeof createPrismaClient> | undefined;
};

export const db = globalForPrisma.prisma ?? createPrismaClient();

if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db;

package.json
buildしないとインポートした側で実行時エラーになるので、buildコマンドを追加。

{
  "name": "@retodo/db",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "tsc"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@tsconfig/node20": "^20.1.4",
    "@types/node": "^20.11.20",
    "prisma": "^5.10.2",
    "typescript": "^5.4.5"
  },
  "dependencies": {
    "@prisma/client": "^5.10.2"
  }
}
tinoue723tinoue723

Dockerfileは https://pnpm.io/docker を参考に

FROM node:20-alpine AS base
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable

FROM base AS build
COPY . /usr/src/app
WORKDIR /usr/src/app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
RUN pnpm -filter job --filter db build
RUN pnpm deploy --filter=job --prod /prod/job

FROM base AS job
COPY --from=build /prod/job /prod/job
WORKDIR /prod/job
CMD [ "pnpm", "start" ]

pnpm -filter job --filter db buildで、必要なパッケージのみbuildする。

pnpm deployという見慣れないコマンドがあるが、どうやらjobの依存パッケージのみに絞ってnode_modulesを再構成してくれるみたい。
これのおかげで他のnextjsなどの不要な依存パッケージを含めずに済む。

tinoue723tinoue723

docker build
docker build -t test1 . --progress=plain

試しにrun
docker run test1

エラー

> @retodo/job@1.0.0 start /prod/job
> node dist/index.js

/prod/job/node_modules/.pnpm/@prisma+client@5.10.2_prisma@5.14.0/node_modules/.prisma/client/default.js:43
    throw new Error(
    ^

Error: @prisma/client did not initialize yet. Please run "prisma generate" and try to import it again.
In case this error is unexpected for you, please report it in https://pris.ly/prisma-prisma-bug-report
    at new PrismaClient (/prod/job/node_modules/.pnpm/@prisma+client@5.10.2_prisma@5.14.0/node_modules/.prisma/client/default.js:43:11)
    at createPrismaClient (/prod/job/node_modules/.pnpm/@retodo+db@file+packages+db_prisma@5.14.0/node_modules/@retodo/db/index.js:5:34)
    at Object.<anonymous> (/prod/job/node_modules/.pnpm/@retodo+db@file+packages+db_prisma@5.14.0/node_modules/@retodo/db/index.js:9:40)
    at Module._compile (node:internal/modules/cjs/loader:1358:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1416:10)
    at Module.load (node:internal/modules/cjs/loader:1208:32)
    at Module._load (node:internal/modules/cjs/loader:1024:12)
    at Module.require (node:internal/modules/cjs/loader:1233:19)
    at require (node:internal/modules/helpers:179:18)
    at Object.<anonymous> (/prod/job/dist/index.js:3:14)

どうやらpnpm deployはpnpm iをし直しているらしく、その際にprisma generateがなぜか実行されないことが問題のようだった。

postinstallフックを試すため、packages/db/package.jsonに以下を追加

    "postinstall": "prisma generate"

pnpm deployで以下のエラーがでた

 > [build 6/6] RUN pnpm deploy --filter=@retodo/job --prod /prod/job:
4.077 .                                        |   +7 +
4.179 Progress: resolved 40, reused 1, downloaded 16, added 7, done
4.188 .../node_modules/@prisma/engines postinstall$ node scripts/postinstall.js
4.406 .../node_modules/@prisma/engines postinstall: Done
4.480 .../node_modules/@prisma/client postinstall$ node scripts/postinstall.js
4.529 .../node_modules/@prisma/client postinstall: warning In order to use "@prisma/client", please install Prisma CLI. You can install it with "npm add -D prisma".
4.533 .../node_modules/@prisma/client postinstall: Done
4.580 .../node_modules/@retodo/db postinstall$ prisma generate
4.588 .../node_modules/@retodo/db postinstall: sh: prisma: not found
4.590  ELIFECYCLE  Command failed.

prismaがないと。。。

最終的には、packages/db/package.jsonに以下を追加して解決した。

    "postinstall": "pnpm dlx prisma generate"

なんか微妙な気もするが、動いたのでおk