✏️

【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 --from=deps /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 --from=builder /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 --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /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

解決方法①

  1. 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" // 追加!!
}

以降は変更なし
  1. npx prisma generateコマンドでPrismaの定義ファイルを生成する
$ npx prisma generate

application/src/libs/prisma/clientにファイルが生成されればOK

  1. @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