Next.js環境をDocker上に作成する dev & build対応

[[公式]: Next.js × Docker 本番環境サンプル](https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile)
## Next.jsをDocker上で動かす
こちらの記事ではNext.js開発環境をDocker上に作成するための手順を紹介いたします。

[GitHub: 今回のサンプルコードはこちら](https://github.com/yuichi-star-alchemist/next-docker)

:::note
要点は以下の通りです。

- Dockerとローカルそれぞれでnode_modulesなど必要モジュールを用意する
- 開発時はsrcディレクトリをバインドマウント → ホットリロードを可能にする
- 同環境で本番ビルドと実行ができる

問題点があればぜひご指摘ください
:::

### 1. プロジェクト作成
まずはテンプレートの作成です。
```sh
npx create-next-app@latest
```

選択肢は全てYesにしました。
今回の構成と同じにする場合は、srcを使用する選択をしてください。

![スクリーンショット 2024-12-21 022244.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3735501/3330d7c5-2db0-619c-219e-352434423a75.png)

### 2. 構成ファイルの追加

今回は追加でDockerの設定ファイルを作成します。
頻繁に使用する開発(デフォルト)用と、
たまに使用する本番環境用とで、
それぞれのDockerfileとcompose.yamlを用意します。
共通になりますが、.dockerignoreも作成します。

## 共通ファイル

#### .dockerignore
build時、コンテクスト(構築用ファイル群)に含めたくないものを指定します。
ここに含めたものは、COPYなどの対象になりません。
必要に応じて追加します。
```
.git
.gitignore
.dockerignore
Dockerfile*
*.yaml
.md

.next
.swc
node_modules

```

---
## 開発環境実行用
#### develop.sh
**./develop.sh**として実行すると起動します。
ファイルの実行権限が必要です。
ない場合は**chmod u+x ./develop.sh**などで追加します。

-fオプションによってcomposeファイルが組み合わされて実行されます。
**./develop.sh b**と半角空白+bをつけて実行すると再ビルドが走ります。
本番環境実行直後、開発に戻る時はそのように実行するとよいです。

```sh
#!/bin/bash

if [ "$1" = "b" ]; then
  docker compose -f compose.yaml build
fi
docker compose -f compose.yaml up
```

#### Dockerfile

```dockerfile
FROM node:22.11.0-slim
ENV NODE_ENV=development
USER node
WORKDIR /app
COPY package*.json ./
RUN npm ci

COPY --chown=node:node ./*.*js* ./*.ts ./
COPY --chown=node:node ./public ./public

ENV HOSTNAME="0.0.0.0"

CMD ["npm", "run", "dev"]
```
<details><summary>コードの補足</summary>

- npm ci
node_modulesを一度消してからpackage-lock.jsonを書き換えずに依存関係をインストールします。
package-lock.jsonの通りに再現するためのコマンドです。

- COPY
./にある.js, .json, .mjs, .tsをコピーします。
./publicも同様です。他のディレクトリを作成した場合は追記してください。
</details>

:::note warn
補足事項

追加のインストールが必要な時は、ローカルでインストールしてから、Dockerの再ビルドを行います。

```sh
npm install  <モジュール名> <モジュール名> ...
↓
↓
./develop.sh b
```
これによってローカルのモジュール更新 + package-lock.jsonの更新によりDockerのビルド時に更新が入ります。

また、「/」配下の各設定ファイルはマウント非対象のため、**これらを変更した場合も再ビルドが必要**になります。
:::

#### compose.yaml
dev用の項目を書きます。
prodと共通の項目があってもどちらにも書きます。
他のコンテナなどを追加する場合も同様です。

```yaml
services:
  nextjs:
    build:
      context: .
      dockerfile: Dockerfile
    container_name:
      nextjs-dev
    ports:
      - "3000:3000"
    volumes:
      - ./src/:/app/src/
```
---
## 本番環境実行用
#### production.sh
**./production.sh**として実行すると起動します。
ファイルの実行権限が必要です。
ない場合は**chmod u+x ./production.sh**などで追加します。

```sh
#!/bin/bash

if [ "$1" = "b" ]; then
  docker compose -f compose.prod.yaml build
fi
docker compose -f compose.prod.yaml up

```

#### Dockerfile.prod
本番実行の時はcompose.prod.yaml内の指定でこちらが選択されます。
公式のものを多少削って利用しております。

```dockerfile
FROM node:22.11.0-slim AS base

FROM base AS deps
ENV NODE_ENV=production
WORKDIR /app
COPY package*.json ./
RUN npm ci --include=dev

FROM base AS builder
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM base AS runner
ENV NODE_ENV=production
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
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
ENV HOSTNAME="0.0.0.0"

CMD ["node", "server.js"]
```

#### compose.prod.yaml
prod用の項目を書きます。
devと共通の項目があってもどちらにも書きます。
他のコンテナなどを追加する場合も同様です。

```yaml
services:
  nextjs:
    build:
      context: .
      dockerfile: Dockerfile.prod
    container_name:
      nextjs-prod
    ports:
      - "3000:3000"

```

#### next.config.ts
1行追加します。
本番環境に最適化されたコードが出力されるそうです。
```diff_typescript
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
+  output: "standalone",
};

export default nextConfig;
```

### 以上です！
最後まで見て頂き、ありがとうございます！

:::note
再ビルドが必要なタイミング
- モジュール追加後
- develop ↔ production環境を切り替える時
- 設定ファイルに変更を加えた時

:::
今回はDockerに関する部分を紹介させていただきましたが、他にもESLintの設定などもありますね。

Dockerのドキュメントを眺めながら作ってみました。
一応やりたいことができて満足しましたが、もっといいやり方があるなどあればぜひ教えて頂きたいです！

:::note warn
この方法では、AWSのEC2インスタンス無料枠:t2.micro内でのビルドはcpu使用量が厳しくて難しいようでした。
(ソースコードの量や使用モジュールの量が少なければ大丈夫です)
今回のサンプルコードの状態でcpu最大使用率68%のようです。(メトリクス参照)
:::
ビルドができれば接続可能です！
![スクリーンショット 2024-12-22 214249.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3735501/404df86c-873f-8b47-3442-8c5fc209d43a.png)


#### シェルスクリプト作成の参考にさせて頂きました！
https://qiita.com/osw_nuco/items/a5d7173c1e443030875f