Next.jsプロジェクトを開発環境と本番環境のDockerで動かしてみたい【その1】

はじめに

Next.js 公式のサンプル をなぞる形でやってみます。
既に何度か Next.js も Docker も動かしたことはあるので諸々のインストールは端折ります。

Docker Version: 20.10.14

ローカルでプロジェクトを作成

サンプル通りに ターミナルでコマンドを実行。

# 適当なディレクトリに移動して作成
$ cd NextProjects
# with-docker-compose サンプルを基に Next.js プロジェクトを作成
$ npx create-next-app --example with-docker-compose with-docker-compose-app
Ok to proceed? (y) y
Memo

GitHub上のサンプルから作成する場合には、--example Next.js 公式 GitHub のディレクトリ名を指定をすることで、サンプルのプロジェクトを作成することができます。

今回は https://github.com/vercel/next.js/tree/canary/examples/with-docker-compose のサンプルを使いたいので、with-docker-compose を指定しました。

その他のcreate-next-appのオプションについては以下を参照してください。
https://nextjs.org/docs/api-reference/create-next-app

GitHub のサンプルと同じ構成のものが取得できました。

with-docker-compose-app
├── next-app // Next.js のプロジェクトの中身
├── docker-compose.prod.yml // multistage builds を用いて本番サーバーを稼働させる場合
├── docker-compose.prod-without-multistage.yml // multistage builds を用いないで本番サーバーを稼働させる場合
├── docker-compose.dev.yml // 開発環境用
├── README.md
├── .gitignore
├── .env // デフォルトの環境ファイル。実際に API の secret 等は書かないようにする
└── .dockerignore // docker ビルド時に無視したいファイル デフォルトは空
multistage builds って?

Docker のドキュメントによるとビルドするときになるべく Docker イメージサイズを小さく、かつ簡潔にDockerfileを書くための方法らしいです。

イメージをビルドする際に取り組むことといえば、ほとんどがそのイメージサイズを小さく抑えることです。
~~
最終結果として、以前と変わらずに本番環境向けの小さなイメージができあがりました。 しかも複雑さが一切なくなっています。 中間的なイメージを作る必要などありません。 さらに生成した内容をローカルシステムに抽出することも一切不要です。

現在では マルチステージビルドを用いることが主流のようなので、そちらを使うことにしました。
とりあえず Docker の知見が足りてないので、また今度調べる…🐈‍⬛

ローカルの Docker で動かす

サンプル通りに Docker で動かしてみます。
まずは Docker.app を起動しておきます。(起動していない場合は Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?というエラーが表示されます。)

$ docker network create next_sample_network
30c8db3c0ff82a754562a56d3f8390d71a9c58d4558e4bd331b2269da3d88e0b

$ docker compose -f docker-compose.dev.yml build

[+] Building 19.9s (9/13)
=> [internal] load build definition from dev.Dockerfile 0.1s
=> => transferring dockerfile: 530B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:18-alpine 3.2s
=> [auth] library/node:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 22.14kB 0.0s
=> [1/8] FROM docker.io/library/node:18-alpine@sha256:35c22fc0c7b39912a 15.1s
=> => resolve docker.io/library/node:18-alpine@sha256:35c22fc0c7b39912a9 0.0s
=> => sha256:b9d3bddc94b3ffe6274b086651ae71c89467ebe6a3a 2.35MB / 2.35MB 1.7s
=> => sha256:35c22fc0c7b39912a929e5cfe21a29d337268de2b92 1.43kB / 1.43kB 0.0s
=> => sha256:c515e9ddc75410dc7b7519d8b77d2d3dd828b93c383 1.16kB / 1.16kB 0.0s
=> => sha256:16b18c06553724cd05d9c996e0c5d4b831704412352 6.67kB / 6.67kB 0.0s
=> => sha256:530afca65e2ea04227630ae746e0c85b2bd1a179379 2.80MB / 2.80MB 1.5s
=> => sha256:5c829f39f5e84f1a5fc63cdc45372ed7eca43ec1b 46.05MB / 46.05MB 7.7s
=> => extracting sha256:530afca65e2ea04227630ae746e0c85b2bd1a179379cbf2b 1.0s
=> => sha256:f8fce1b8a0b8ad06c963fd5526d15686f34a89579da6925 449B / 449B 1.8s
=> => extracting sha256:5c829f39f5e84f1a5fc63cdc45372ed7eca43ec1b8b556c2 6.5s
=> => extracting sha256:b9d3bddc94b3ffe6274b086651ae71c89467ebe6a3aba31d 0.4s
=> => extracting sha256:f8fce1b8a0b8ad06c963fd5526d15686f34a89579da6925c 0.0s
=> [2/8] WORKDIR /app 0.7s
=> [3/8] COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* 0.0s
=> ERROR [4/8] RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; 0.5s
------
> [4/8] RUN if [ -f yarn.lock ]; then yarn --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:
#0 0.498 Lockfile not found.
------
failed to solve: executor failed running [/bin/sh -c if [ -f yarn.lock ]; then yarn --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]: exit code: 1

エラー…😶

公式サンプルのdocker network create my_network 部分をdocker network create next_sample_networkと勝手にネットワーク名を変えてしまっていたので、とりあえずそれを直します。

my_networkから名称変更したいので、docker-compose.dev.ymlの方を書き換えました。

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

services:
  next-app:
    container_name: next-app
    build:
      context: ./next-app
      dockerfile: dev.Dockerfile
    environment:
      ENV_VARIABLE: ${ENV_VARIABLE}
      NEXT_PUBLIC_ENV_VARIABLE: ${NEXT_PUBLIC_ENV_VARIABLE}
    volumes:
      - ./next-app/src:/app/src
      - ./next-app/public:/app/public
    restart: always
    ports:
      - 3000:3000
    networks:
      - next_sample_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:
  next_sample_network:
    external: true

再度実行するもやはり同じエラー😶‍

検索して調べてみました。

検索ワード
Lockfile not found nextjs docker

検索結果

解決してみてから改めて上記を見てみると解決策書いてありましたね。

Just change nextjs version in package.json file to latest and run yarn install. Now docker build work fine.

Dockerfile をみてみる

上記ディレクトリ構成からnext-appの中身を見てみると Dockerfile が見つかったのでそれを調査してみます。(GitHub)

next-app
├── public
├── src
├── .gitignore
├── dev.Dockerfile // 開発環境用のDockerfile
├── next-env.d.ts
├── next.config.js
├── package.json
├── prod-without-multistage.Dockerfile // multistage builds を使わないで本番サーバーを稼働させる用
├── prod.Dockerfile // multistage builds を用いて本番サーバーを稼働させる用
└── tsconfig.json
dev.Dockerfile
FROM node:18-alpine

WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --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

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

CMD yarn dev

Lockfile について書いてありました。この部分の if 文が全て通ってないのでLockfile not found.で処理が終了してしまっているようです。

Memo

この記事を書いているつい7日前に Dockerfile変更がされているのがわかりました。(PR)

依存性パッケージのインストールをする

上記より、package-lock.jsonが存在していないのがないのが問題っぽいので、一度実行してみます。

$ cd ~/NextProjects/with-docker-compose-app/next-app
$ npm install

package-lock.jsonnode_modulesが追加されました。

改めて以下のコマンドを実行

$ cd ~/NextProjects/with-docker-compose-app
$ docker compose -f docker-compose.dev.yml build

[+] Building 14.6s (14/14) FINISHED
=> [internal] load build definition from dev.Dockerfile 0.0s
=> => transferring dockerfile: 36B 0.0s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 2B 0.0s
=> [internal] load metadata for docker.io/library/node:18-alpine 2.4s
=> [auth] library/node:pull token for registry-1.docker.io 0.0s
=> [internal] load build context 0.2s
=> => transferring context: 28.32kB 0.2s
=> [1/8] FROM docker.io/library/node:18-alpine@sha256:8da3d86c911f91712b 0.0s
=> => resolve docker.io/library/node:18-alpine@sha256:8da3d86c911f91712b 0.0s
=> CACHED [2/8] WORKDIR /app 0.0s
=> [3/8] COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* 0.1s
=> [4/8] RUN if [ -f yarn.lock ]; then yarn --frozen-lockfile; elif 9.2s
=> [5/8] COPY src ./src 0.0s
=> [6/8] COPY public ./public 0.0s
=> [7/8] COPY next.config.js . 0.0s
=> [8/8] COPY tsconfig.json . 0.0s
=> exporting to image 2.5s
=> => exporting layers 2.5s
=> => writing image sha256:4a2951eeb0944f4629b70fc9379c16b325a19af509ec3 0.0s
=> => naming to docker.io/library/with-docker-compose-app_next-app 0.0s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them

無事に成功しました。

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
with-docker-compose-app_next-app latest 4a2951eeb094 21 seconds ago 416MB

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
30c8db3c0ff8 next_sample_network bridge local

コンテナを作成し起動する

最後に Docker コンテナを作成して起動してみます。サンプル通りです。

$ docker compose -f docker-compose.dev.yml up

[+] Running 1/1
⠿ Container next-app Created 0.1s
Attaching to next-app
next-app | yarn run v1.22.19
next-app | $ next dev
next-app | ready - started server on 0.0.0.0:3000, url: http://localhost:3000
next-app | event - compiled client and server successfully in 4s (177 modules)
next-app | Attention: Next.js now collects completely anonymous telemetry regarding usage.
next-app | This information is used to shape Next.js' roadmap and prioritize features.
next-app | You can learn more, including how to opt-out if you'd not like to participate in this anonymous program, by visiting the following URL:
next-app | https://nextjs.org/telemetry
next-app |

http://localhost:3000にアクセスしてみます。

無事にアクセスできました。

control+cで一度終了しておきます。

あとしまつ

手順をまとめるために、最初からやり直したいので、一度全て削除しておきます。

ここまでの Docker の状態

$ docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE
with-docker-compose-app_next-app latest 4a2951eeb094 21 seconds ago 416MB

$ docker network ls
NETWORK ID NAME DRIVER SCOPE
30c8db3c0ff8 next_sample_network bridge local

$ docker container ls -a
CONTAINER ID   IMAGE                              COMMAND                  CREATED          STATUS                     PORTS                                         NAMES
124ea52e6b74   with-docker-compose-app_next-app   "docker-entrypoint.s…"   19 minutes ago   Exited (1) 3 minutes ago                                                 next-app

Docker の後始末をします。

# docker container の削除
$ docker container rm next-app
next-app

# docker network の削除
$ docker network rm next_sample_network
next_sample_network

# docoker image の削除
$ docker image rm with-docker-compose-app_next-app

~/NextProjects/with-docker-compose-appディレクトリを削除します。

ローカル環境構築まとめ

最後に、本記事のハマりどころも踏まえて、もう一度ローカル環境を構築してみます。

# 配置したいディレクトリへ移動
$ cd ~/NextProjects
# with-docker-compose サンプルを基に Next.js プロジェクトを作成
$ npx create-next-app --example with-docker-compose with-docker-compose-app
# できたディレクトリに移動
$ cd with-docker-compose-app
# さらに下の階層の next-app に移動
$ cd next-app
# パッケージのインストール
$ npm install
# docker-compose.dev.yml ファイルがある階層に移動
$ ~/NextProjects/with-docker-compose-app/
# docker network の作成
$ docker network create my_network
# コンテナ用のイメージを構築する
$ docker compose -f docker-compose.dev.yml build
# コンテナを起動する
$ docker compose -f docker-compose.dev.yml up

これでローカルの Docker で無事に起動することができました。

つづく。