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

## どうしてやりたかったか？

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](https://nextjs.org/docs/deployment#docker-image)

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

example は、[github](https://github.com/vercel/next.js/tree/canary/examples/with-docker) で見れるので、Dockerfile だけ拝借することにする。

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 アプリを作って、それで検証する。

```bash
$ yarn create next-app app --typescript
```

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

```.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 使うの？という気もするけど。

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

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

```bash
$ 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/standalone``` の ```node_modules``` なので、手元で ```.next/standalone/node_modules/next/dist/server``` を確認してみるも、確かに ```image-optimizer.js``` はない。

なんでー。

## バグだった

[Using <Image /> component with-docker results in missing image-optimizer #32513](https://github.com/vercel/next.js/issues/32513)

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

 [修正内容](https://github.com/vercel/next.js/commit/5d606ebae42469ebd2c3e2bc4f94266525c59315)を見ると image-optimizer.js とそれを正しく動かすための sharp を含めれば良さそうだが、sharp なんてものはインストールされていないし、そう単純でもなさそうな気配がする。

## 対応方針

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

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

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

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

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

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

```typescript: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
```

```yaml:docker-compose.yml
version: '3.8'
services:
  app:
    build:
      context: ./app
      dockerfile: ../Dockerfiles/app
    ports:
      - "3000:3000"
```

```bash
$ 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 に書き換えれば良いんじゃないかと。


おしまい。