Next.jsの本番環境・開発環境をdockerで構築する方法

# 動機
開発環境・本番環境でdockerを使ってみよう！と思って試したところ何点か詰まったので備忘録としてNext.jsをdockerで環境構築する方法を記しておきます。

# 開発環境
公式の[テンプレート](https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose/next-app)をもとに作っていきます。

```Dockerfile.dev
FROM node:18-alpine

WORKDIR /app

COPY package.json yarn.lock* ./
RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
    else echo "Warning: Lockfile not found. It is recommended to commit lockfiles to version control." && yarn install; \
    fi

COPY src ./src
COPY public ./public
COPY next.config.js .
COPY tsconfig.json .

CMD ["yarn", "dev"]
```
1行目: v18のnode上で作っていきます
3行目: 作業ディレクトリを指定します。
5行目: package.json, yarn.lockをコンテナ内にコピーします。npmを使う場合はyarn.lockの部分をpackage-lock.jsonとかに変えてください。
6行目: 必要なパッケージをインストール。npmを使う場合は`npm ci`とかに書き換えてください。公式のテンプレートのままif文で条件分岐してもOK
10 ~ 13行目: 起動に必要なコードをコンテナ内にコピー
15行目: `yarn dev`でサーバーを起動。npmを使われる場合は`npm run dev`とかに書き換えてください。

次はdocker-composeを作ります。
`docker-compose.dev.yml`に次のように書いてください。
```docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    image: next-app
    container_name: next-app
    ports:
      - '3001:3001'
    env_file:
      - .env
    volumes:
      - ./src:/app/src
      - ./public:/app/public
      - node_modules:/app/node_modules
    restart: always
    tty: true
    stdin_open: true

volumes:
  node_modules:
```
コンテナの起動は次のコマンドでできます。
```
docker compose -f docker-compose.dev.yml build
docker compose -f docker-compose.dev.yml up -d
```

# 本番環境
Next公式のテンプレートだと上手く行かなかったので、`docker init`で作られるDockerfile・docker-composeをもとに作っていきます。下記のファイルはyarn向けになっているため、npmを使う場合は適宜`yarn run build`や`yarn start`の箇所をnpm向けに書き換えてください。

```Dockerfile.prod
# syntax=docker/dockerfile:1

# Comments are provided throughout this file to help you get started.
# If you need more help, visit the Dockerfile reference guide at
# https://docs.docker.com/engine/reference/builder/

ARG NODE_VERSION=18.16.0

################################################################################
# Use node image for base image for all stages.
FROM node:${NODE_VERSION}-alpine as base

# Set working directory for all build stages.
WORKDIR /usr/src/app


################################################################################
# Create a stage for installing production dependecies.
FROM base as deps

# Download dependencies as a separate step to take advantage of Docker's caching.
# Leverage a cache mount to /root/.yarn to speed up subsequent builds.
# Leverage bind mounts to package.json and yarn.lock to avoid having to copy them
# into this layer.
RUN --mount=type=bind,source=package.json,target=package.json \
    --mount=type=bind,source=yarn.lock,target=yarn.lock \
    --mount=type=cache,target=/root/.yarn \
    yarn install --production --frozen-lockfile

################################################################################
# Create a stage for building the application.
FROM deps as build

# Copy the rest of the source files into the image.
COPY . .
# Run the build script.
RUN yarn run build

################################################################################
# Create a new stage to run the application with minimal runtime dependencies
# where the necessary files are copied from the build stage.
FROM base as final

# Use production node environment by default.
ENV NODE_ENV production

# Run the application as a non-root user.
USER node

# Copy package.json so that package manager commands can be used.
COPY package.json .

# Copy the production dependencies from the deps stage and also
# the built application from the build stage into the image.
COPY --from=deps /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/.next ./.next
COPY ./public ./public

# Run the application.
CMD yarn start
```
```docker-compose.prod.yaml
version: '3'

services:
  next-app:
    container_name: next-app-prod
    image: next-app-prod
    build:
      context: .
      dockerfile: Dockerfile.prod
      args:
        ENV_VARIABLE: ${ENV_VARIABLE}
        NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
    restart: always
    ports:
      - 4400:4400
```
コンテナの起動は次のコマンドでできます。アプリを更新する際も同様のコマンドで更新できます。
参考: https://matsuand.github.io/docs.docker.jp.onthefly/compose/production/
```
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml up -d
```

# おわり
今回は本番環境・開発環境をdockerで構築する方法を記しました。
次回はバックエンドもdockerで作れるようにして、バックエンドとクライアントの間の通信をできるようにしていきます。

# 参考資料
Next.js公式レポジトリ
https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose/next-app

dockerマニュアル
https://matsuand.github.io/docs.docker.jp.onthefly/compose/production/

# 追記
2023/9/27 本番環境でpublicディレクトリ内のファイルが配信されない問題を修正