✏️
【NextJS ver15 × Prisma ver6】Prismaを今までのノリで使ってたらbuild時にコケ倒した話
ゴール
DockerでNextJSのフロントエンド
とMySQL
のイメージを構築して、アプリを起動する
環境構築
以下の設定でプロジェクトを作成
.env
MYSQL_ROOT_PASSWORD=password
MYSQL_DATABASE=test-db
DATABASE_URL="mysql://root:password@db:3306/test-db"
compose.yaml
services:
application:
container_name: test
build:
context: ./application
dockerfile: Dockerfile
env_file:
- .env
volumes:
- ./application/src:/app/src
- ./application/public:/app/public
restart: always
ports:
- 3000:3000
networks:
- test_network
db:
image: mysql:8.0
platform: linux/amd64 # M1 Macの場合はplatformをlinux/amd64に指定する
container_name: test-db
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
env_file:
- .env
volumes:
- mysql:/var/lib/mysql
ports:
- "3306:3306"
networks:
- test_network
networks:
test_network:
driver: bridge # プロジェクト専用の隔離されたネットワーク環境を作成するための設定(デフォルトではbridgeネットワークを使用するが、明示的に指定)
volumes:
mysql:
Dockerfileは公式に記載のある設定ファイルを参考
application/Dockerfile
FROM node:22.14.0-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* ./
RUN \
if [ -f package-lock.json ]; then npm ci; \
else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && npm install; \
fi
# Set timezone to JST -> https://qiita.com/y_i1029/items/07f179c758857b78edd4 の記事になっている症状と同じだったため↓を追記
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
apk del tzdata
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
# buildするタイミングでCloudRunの環境変数で定義しているDATABASE_URLとNEXTAUTH_URLの値を渡す
ARG DATABASE_URL
ENV DATABASE_URL ${DATABASE_URL}
RUN npx prisma generate && \
npm run build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY /app/.next/standalone ./
COPY /app/.next/static ./.next/static
USER nextjs
CMD ["node", "server.js"]
application/package.json
{
"name": "test",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@auth/prisma-adapter": "^2.7.2",
"@hookform/resolvers": "^5.0.1",
"@prisma/client": "^6.5.0",
"bcryptjs": "^3.0.2",
"next": "15.3.0",
"next-auth": "^5.0.0-beta.25",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.55.0",
"zod": "^3.24.2"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@tailwindcss/postcss": "^4",
"@types/bcryptjs": "^3.0.0",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"eslint": "^9",
"eslint-config-next": "15.3.0",
"prisma": "^6.5.0",
"tailwindcss": "^4",
"typescript": "^5"
}
}
プロジェクトの設定
PrismaのSchema設定ファイル
application/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// アカウント情報
model Account {
id String @id @default(uuid())
email String @unique // ユーザーが設定するメールアドレス
hashedPassword String @map("hashed_password") // ハッシュ化されたパスワード
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at") // 削除された場合はここに日時が入る
// リレーション
user User? // Account:User = 1:1
}
// ユーザー基本情報
model User {
id String @id @default(uuid())
accountId String @unique @map("account_id") // アカウントID
nickname String
bio String @db.Text
image String // メイン画像のURL
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// リレーション
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
}
PrismaClientの初期設定ファイル
application/src/libs/prisma/db/index.ts
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
AuthJSの設定ファイル
application/src/libs/authjs/index.ts
import { PrismaAdapter } from '@auth/prisma-adapter'
import NextAuth from 'next-auth'
import { prisma } from '@/libs/prisma/db'
import authConfig from './auth.config'
export const { handlers, auth, signIn, signOut } = NextAuth({
callbacks: {
async jwt({ token }) {
return token
},
async session({ session, token }) {
if (token) {
if (token.sub && session.user) {
session.user.id = token.sub
}
return session
}
return session
},
},
adapter: PrismaAdapter(prisma),
session: { strategy: 'jwt' },
...authConfig,
})
application/src/libs/authjs/auth.config.ts
import { compare } from 'bcryptjs'
import type { NextAuthConfig } from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import { loginSchema } from '@/schemas'
import { getAccountByEmail } from '@/actions'
export default {
providers: [
Credentials({
name: 'Credentials',
async authorize(credentials) {
const validated = loginSchema.safeParse(credentials)
if (validated.success) {
const { email, password } = validated.data
const account = await getAccountByEmail(email)
if (!account || !(await compare(password, account.hashedPassword))) {
return null
}
return account
}
return null
},
}),
],
} satisfies NextAuthConfig
バリデーションの定義とActionsのファイル
application/src/schemas/login-schema.ts
import { z } from 'zod'
export const loginSchema = z.object({
email: z.string().email(),
password: z.string().min(8, 'パスワードは8文字以上で入力してください'),
})
export type LoginSchema = z.infer<typeof loginSchema>
application/src/actions/auth.ts
'use server'
import { Account } from '@prisma/client'
import { prisma } from '@/libs/prisma/db'
export async function getAccountByEmail(email: string): Promise<Account | null> {
return prisma.account.findUnique({
where: { email },
})
}
エラーと解決方法
やっと本題!!
エラー①
Dockerを起動するためにターミナルから下記コマンドを実行
$ docker compose up -d --build
下記のような'"@prisma/client"' has no exported member
のエラーになる
./src/actions/auth.ts:3:10
Type error: Module '"@prisma/client"' has no exported member 'Account'.
1 | 'use server'
2 |
> 3 | import { Account } from '@prisma/client'
| ^
4 | import { prisma } from '@/libs/prisma/db'
5 |
6 | export async function getAccountByEmail(email: string): Promise<Account | null> {
Next.js build worker exited with code: 1 and signal: null
解決方法①
- schema.prismaファイルに
output
を指定する- 指定する場所はどこでもOK(Prisma公式では"app/generated/prisma/client"を指定している)
- 今回は
libs/prisma配下
に設置したいため"../src/libs/prisma/client"と指定
application/prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/libs/prisma/client" // 追加!!
}
以降は変更なし
-
npx prisma generate
コマンドでPrismaの定義ファイルを生成する
$ npx prisma generate
application/src/libs/prisma/client
にファイルが生成されればOK
- @prisma/client と記載している箇所を書き換える
application/src/libs/prisma/db/index.ts
import { PrismaClient } from '@/libs/prisma/client' // 変更!!
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const prisma = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma
application/src/actions/auth.ts
'use server'
import { Account } from '@/libs/prisma/client' // 変更!!
import { prisma } from '@/libs/prisma/db'
export async function getAccountByEmail(email: string): Promise<Account | null> {
return prisma.account.findUnique({
where: { email },
})
}
エラー②
改めてDockerを起動するためにターミナルから下記コマンドを実行
$ docker compose up -d --build
下記のようなESlintエラー
になる
---この前にもエラーは出力されている---
24.04 30:1206 Error: 'o' is assigned a value but never used. @typescript-eslint/no-unused-vars
24.04 30:2052 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 30:2124 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 30:3972 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 30:8067 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 30:8433 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 30:9603 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04 31:1006 Error: Expected an assignment or function call and instead saw an expression. @typescript-eslint/no-unused-expressions
24.04
24.04 ./src/libs/prisma/client/wasm.js
24.04 10:3 Error: 'skip' is assigned a value but never used. @typescript-eslint/no-unused-vars
24.04 11:5 Error: A `require()` style import is forbidden. @typescript-eslint/no-require-imports
24.04 228:11 Error: 'target' is defined but never used. @typescript-eslint/no-unused-vars
24.04 228:19 Error: 'prop' is defined but never used. @typescript-eslint/no-unused-vars
24.04
24.04 info - Need to disable some ESLint rules? Learn more here: https://nextjs.org/docs/app/api-reference/config/eslint#disabling-rules
------
failed to solve: process "/bin/sh -c npx prisma generate && npm run build" did not complete successfully: exit code: 1
解決方法②
eslint.config.mjs
の設定に生成されたファイルを除外するように記述する
eslint.config.mjs
import { dirname } from 'path'
import { fileURLToPath } from 'url'
import { FlatCompat } from '@eslint/eslintrc'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const compat = new FlatCompat({
baseDirectory: __dirname,
})
const eslintConfig = [
{
ignores: ['src/libs/prisma/client/**/*'],
}, // 追加!!
...compat.extends('next/core-web-vitals', 'next/typescript'),
]
export default eslintConfig
Discussion