Next.js のプロジェクトを VPS 上の Docker にデプロイする【その2】

Next.js のプロジェクトを VPS 上の Docker にデプロイする【その1】」の続きです。

前回は、本番環境用の Dockerfile の作成まで行いました。
今回は、本番環境でも Docker Compose 利用できるようにしていこうと思います。

本番環境用と開発環境用の違い

まずは、開発環境用に作成した docker-compose.yml と 公式サンプルの本番環境用の docker-compose.yml ファイルを見比べて、本番環境と開発環境のちがいを調査してみます。

開発環境用の docker-compose.yml

前回のシリーズの「Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その3】」で開発環境用に作成したdocker-compose.dev.ymlは以下の通りです。

docker-compose.dev.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: dev.Dockerfile
    ports:
      - 3000:3000
    volumes:
      - ./public:/app/public
      - ./pages:/app/pages
      - ./styles:/app/styles

公式サンプルの本番環境用の docker-compose.yml

Next.js 公式の docker-compose サンプルでの本番環境用のdocker-compose.dev.ymlをみてみます。

https://github.com/vercel/next.js/blob/canary/examples/with-docker-compose/docker-compose.prod.yml

docker-compose.prod.yml
version: '3'

services:
  next-app:
    container_name: next-app
    build:
      context: ./next-app
      dockerfile: prod.Dockerfile
      args:
        ENV_VARIABLE: ${ENV_VARIABLE}
        NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
    restart: always
    ports:
      - 3000:3000
    networks:
      - my_network

  # Add more containers below (nginx, postgres, etc.)

# Define a network, which allows containers to communicate
# with each other, by using their container name as a hostname
networks:
  my_network:
    external: true

それぞれの違い

まず1つ目に networksの定義です。
networksの定義は、コンテナ同士が通信できるようにするための定義です。
現在外部のコンテナとの通信を行う予定がないので、今回は開発環境と同様に定義しないでおきます。(外部コンテナとの通信が必要なケースになったときにまた考えたいので)

2つ目はbuildargsの有無です。
前回の記事で作成したdocker-compose.dev.ymlでは定義していませんが、公式サンプルの開発環境のファイルではenvironmentでこれらの環境変数の設定を行なっています。

args と environment の違い

argsはbuildの配下に設定しますが、environmentはサービス名の配下に設定しています。

docker-compose.yml
version: '3'

services:
  next-app:
    container_name: next-app
    build:
      context: ./next-app
      dockerfile: prod.Dockerfile
      # args は build 配下
      args:
        ENV_VARIABLE: ${ENV_VARIABLE}
        NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
    # build の外に定義
    environment:
      ENV_VARIABLE: ${ENV_VARIABLE}
      NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}

environment はドキュメントによるとコンテナ内での環境変数を指定することができます。

一方 args はドキュメントによると構築時に build のオプション(args)を追加することができます。

args は build用変数で、environment は環境変数ということになります。

開発環境と本番環境で同様の値を定義しているのでわかりづらいですが、args はdocker buildでイメージを作成するときに使われる変数で、environment はイメージからdocker runでコンテナを起動するときに利用される変数になります。

参照: docker-compose コマンドでの args:, environment:, env_file: 及び .env ファイルの使い方

3つ目はvolumesの有無です。
本番環境を更新する場合には、イメージを作成してビルドし直してデプロイします。
開発環境のように変更が即時に反映する必要もないので、利用しません。

最後にrestartの有無です。
これは、コンテナが停止したときに自動でリスタートをするかどうかの設定になります。

本番環境ではサーバーが停止したなどの理由により、Docker 内のコンテナが停止してしまう可能性があります。そのときにサーバが復帰して Docker Engine も再起動した際に、自動でコンテナを再起動するかどうかの設定ができます。

再起動の条件についてはドキュメントを確認してください。

開発環境と本番環境の違いまとめ

開発環境

  • 変更の即時反映が必要(volumes の利用)

本番環境

  • 変更の即時反映が不要(volumes 不要)
  • ビルド時に本番用のビルド変数の設定が必要(args の利用)
  • コンテナの自動再起動(restart の利用)

本番用のdocker-compose.yml

開発環境と本番環境の違いを踏まえて、本番用にdocker-compose.prod.ymlを作成します。

# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site/

# 本番用の docker-compose.yml を作成
$ touch docker-compose.prod.yml
docker-compose.prod.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: prod.Dockerfile
      args:
        ENV_VARIABLE: ${ENV_VARIABLE}
        NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
    restart: always
    ports:
      - 3000:3000

dockerfileには前回作った本番用の Dockerfile を指定しました。

docker compose で動作を確認

上記ファイルの動作確認を行います。

# 作業ディレクトリに移動
$ cd ~/NextProjects/donuts-site/

# ビルドを行う docker compose -f docker-compose.yml ファイルの指定 build
$ docker compose -f docker-compose.prod.yml build

# 起動する docker compose -f docker-compose.yml ファイルの指定 up -d(バックグラウンドで起動)
$ docker compose -f docker-compose.prod.yml up -d

http://localhost:3000 にてアクセスできることを確認します。

あとしまつ

確認できたらコンテナとイメージの後始末をしておきます。

# コンテナを止める
$ docker compose -f docker-compose.prod.yml down

# コンテナが削除されていることを確認
$ docker container ls -a

# イメージは残っていることを確認
$ docker image ls
REPOSITORY                TAG       IMAGE ID       CREATED         SIZE
donuts-site_donuts-site   latest    70d97c5956fd   2 minutes ago   121MB

# イメージも削除しておく
$ docker image rm donuts-site_donuts-site

.env

無事に起動できましたが、docker compose upの際に、以下の Warning が出ています。

WARN[0000] The "ENV_VARIABLE" variable is not set. Defaulting to a blank string. 
WARN[0000] The "NEXT_PUBLIC_ENV_VARIABLE" variable is not set. Defaulting to a blank string. 

これらのビルド変数は、.envファイルにて定義を行います。

公式サンプルの.envファイルをみてみます。

.env
# DO NOT ADD SECRETS TO THIS FILE. This is a good place for defaults.
# If you want to add secrets use `.env.production.local` instead, which is automatically detected by docker-compose.

ENV_VARIABLE=production_server_only_variable
NEXT_PUBLIC_ENV_VARIABLE=production_public_variable

今更ですが、これらの環境変数はサンプルであり、現状では不要なものですね。
もし.env や .env.local 等の環境ファイルに変数を追加した場合には、docker-compose.yml の args にも忘れずに追加が必要という認識でよさそうです。

これらの話は今後必須にはなりますが、今回は一度保留にして先に進もうと思います。

最終的な本番環境のファイル

args はサンプルでの指定だったので削除しておきました。必要になった際に追記していきます。

docker-compose.prod.yml
version: '3'

services:
  donuts-site:
    container_name: donuts-site
    build:
      context: .
      dockerfile: prod.Dockerfile
    restart: always
    ports:
      - 3000:3000

Dockerfile は前回の記事(その1)で作成した時と変更点はありません。

prod.Dockerfile
##### deps ステージ #####
FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
  if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \
  elif [ -f package-lock.json ]; then npm ci; \
  elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i; \
  else echo "Lockfile not found." && exit 1; \
  fi

##### builder ステージ #####
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

RUN yarn build

##### runner ステージ #####
FROM node:16-alpine AS runner
WORKDIR /app

ENV NODE_ENV=production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json

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

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

これらの変更は GitHub のリポジトリにプッシュしておきます。

おわりに

本番環境用の Dockerfile と Docker Compose ファイルの用意ができました。

次回はこれらを利用して、実際に本番環境の Docker にデプロイを行っていこうと思います。

本番環境へのデプロイは、イメージの作成を外部で行ってから VPS に持っていくのか、VPS 上でイメージの作成から行うのか…前者が正解だと思っているのですが、もう少し調査が必要です。

前者の場合は「ConoHa で Docker 自動デプロイしたかったから、webhook サーバーを立てた。」の記事のように DockerHub を利用するかどうかも要検討です。

まだまだ先は長いですが頑張っていきます。