LoginSignup
12
6

More than 1 year has passed since last update.

Next.js の Product Build を docker-compose で動かしたい

Posted at

どうしてやりたかったか?

Next.js + typescript + Prisma2 + PostgreSQL でアプリケーションを作っていて、PostgreSQL は Docker で動かしていたので、合わせて docker-compose したかった。で、Deploy も docker で動かしたかった。
Production の PostgreSQL は、Docker 使わないけど、staging サーバ等では、Docker でも構わなかったので、docker-compose で、そのまま行くことにした。

ネットで調べたところ、開発環境用のものはいくつか見つかったが、Product Build 用のものは見当たらなかった。みんな vercel なの??

検証環境

  • OS: MacOS Catalina Version 10.15.7(19H1615)
  • node: v16.13.1
  • yarn: 1.22.15
  • docker: Docker version 20.10.11, build dea9396
  • docker-compose: Docker Compose version v2.2.1

公式ドキュメントによると

Deployment - Docker Image

with-docker の example を clone して build/run しろとある。気持ちは分かるけど、そうじゃない。

example は、github で見れるので、Dockerfile だけ拝借することにする。

Dockerfile は、こうなっている

# Install dependencies only when needed
FROM node:16-alpine 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
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile

# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build

# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup -g 1001 -S nodejs
RUN adduser -S nextjs -u 1001

# You only need to copy next.config.js if you are NOT using the default configuration
# COPY --from=builder /app/next.config.js ./
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

# 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

EXPOSE 3000

ENV PORT 3000

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry.
# ENV NEXT_TELEMETRY_DISABLED 1

CMD ["node", "server.js"]
  • yarn install する
  • build する
  • 必要なものだけコピーして node server.js する

シンプル。特別なことはやってなさそう。

準備

まずは適当な Next.js アプリを作って、それで検証する。

$ yarn create next-app app --typescript

yarn dev, yarn build, yarn start 全部 OK。
ひとまず、Dockerfile で動かしてみたいので、先程の Dockerfile を置く。node_modules とかはコピーする必要がない(コピーしても無駄な上時間がかかる)ので、.dockerignore に。

Dockerfile
.dockerignore
node_modules
README.md
.next

とりあえず実行、そして失敗

"/app/.next/standalone" not found: not found

と言われてしまう。なんでやねん。
yarn build したときに出来た .next の中に standalone がないらしい。
手元で build したときに出来た .next の中にも確かにない。

対応、だがしかし

standalone は、experimental feature のようなので、next.config.js に追加すれば良い。Product Build でだけ experimental feature 使うの?という気もするけど。

next.config.js
/** @type {import('next').NextConfig} */
module.exports = {
  reactStrictMode: true,
  experimental: {
    outputStandalone: true,
  },
}

再度 docker build すると、成功する。そして、docker run もできる。Welcome to Next.js! も表示される。でも、エラーが出る。

$ docker run -p 3000:3000 nextjs-docker
Listening on port 3000
Error: Cannot find module './image-optimizer'
Require stack:
- /app/node_modules/next/dist/server/next-server.js
- /app/server.js
    at Function.Module._resolveFilename (node:internal/modules/cjs/loader:933:15)
    at Function.Module._load (node:internal/modules/cjs/loader:778:27)
    at Module.require (node:internal/modules/cjs/loader:1005:19)
    at require (node:internal/modules/cjs/helpers:94:18)
    at Object.fn (/app/node_modules/next/dist/server/next-server.js:609:49)
    at Router.execute (/app/node_modules/next/dist/server/router.js:222:48)
    at processTicksAndRejections (node:internal/process/task_queues:96:5)
    at async Server.run (/app/node_modules/next/dist/server/next-server.js:1135:29)
    at async Server.handleRequest (/app/node_modules/next/dist/server/next-server.js:325:20)
    at async Server.<anonymous> (/app/server.js:18:5) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [
    '/app/node_modules/next/dist/server/next-server.js',
    '/app/server.js'
  ]
}

image-optimizer がないとのこと。最終的に実行されているのは、.next/standalonenode_modules なので、手元で .next/standalone/node_modules/next/dist/server を確認してみるも、確かに image-optimizer.js はない。

なんでー。

バグだった

Using component with-docker results in missing image-optimizer #32513

v12.0.8-canary.7 以上で対応済みとのこと。
現在の stable version は、 12.0.7 なので、しばし待て。ということか。

修正内容を見ると image-optimizer.js とそれを正しく動かすための sharp を含めれば良さそうだが、sharp なんてものはインストールされていないし、そう単純でもなさそうな気配がする。

対応方針

image-optimizer.js は、next/image API を利用する際に呼び出される。
考えられる方針は、以下3つくらい?

  1. v12.0.8-canary.7 以上を使う(本日時点の最新は、v12.0.8-canary.19
  2. next/image API の利用をやめる
  3. image-optimizer.js の dependencies をひたすらコピーする

3つ目は、後で Dockerfile を編集するの嫌だし、途中まで試みたけど、切りがないのでやめる。

v12.0.8-canary.7 以上を使う

マイナーバージョンアップの canary だし、別に良っか。という方針。next/image API を利用している場合は、こっち。v12.0.8 が出たら忘れずに直すこと。

package.json
{
  ...
  "dependencies": {
    "next": "12.0.8-canary.19",
    ...
  },
  ...
}

next/image API の利用をやめる

潜在的エラーなので、微妙だけど、next/image 使わないよって場合は、こっち。今回の例では、app/pages/index.tsx 内で使っているので、そこを消してしまう。

app/pages/index.tsx
import type { NextPage } from 'next'
import Head from 'next/head'
//import Image from 'next/image'
import styles from '../styles/Home.module.css'

const Home: NextPage = () => {
  return (
    <div className={styles.container}>
      ...
      <footer className={styles.footer}>
        <a
          href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
          target="_blank"
          rel="noopener noreferrer"
        >
          Powered by{' '}
          {/*<span className={styles.logo}>
            <Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
          </span>*/}
        </a>
      </footer>
    </div>
  )
}

export default Home

docker-compose に組み込む

Next.js のアプリの中に、Dockerfile を置いておくのは個人的に気持ち悪いので、以下のような配置とする。Dockerfile は他でも書くかも知れないので、名前を app に変更。.dockerignore は…ま、いっか。

.
├── Dockerfiles
│   └── app
├── app (Next.js アプリ)
├── README.md
└── docker-compose.yml
docker-compose.yml
version: '3.8'
services:
  app:
    build:
      context: ./app
      dockerfile: ../Dockerfiles/app
    ports:
      - "3000:3000"
$ docker-compose build
$ docker-compose up -d

OK!

まとめ

本日時点で Next.js の Product Build を docker-compose で動かす手順をまとめると、
いくつか微妙な点はあるが(experimental feature 使ったり、canary 使ったり)

  • with-docker example の Dockerfile を拝借
  • next.config.js に standalone を追加
  • next.js のバージョンを v12.0.8-canary.8 以上にするか、next/image API の使用をやめる
  • docker-compose に組み込む

となる。Next.js のバージョンも変更できないし、next/image API も使ってる!という場合は、build して yarn start するような Dockerfile に書き換えれば良いんじゃないかと。

おしまい。

12
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
6