Next.js 公式サンプルの Dockerfile を読み解いてみた
今回は、Next.js 公式のサンプルの Dockerfile を読み解いていきます。
Docker も Next.js もほぼ初心者なので間違えているところがあるかもしれません。
全体
まず Dockerfile 全体を眺めます。
https://github.com/vercel/next.js/blob/canary/examples/with-docker/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
# 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
# Rebuild the source code only when needed
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# 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 during the build.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN yarn build
# If using npm comment out above and use below instead
# RUN npm run build
# Production image, copy all the files and run next
FROM node:16-alpine AS runner
WORKDIR /app
ENV NODE_ENV production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# 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
CMD ["node", "server.js"]
大元のイメージ
Next.js 用のイメージを作る上での大元になるイメージをコピーしてきます。
node の公式イメージをコピー元としています。
# Next.jsを動かすには node.js が必要なので公式イメージをコピー元とする
# FROMコマンド イメージ名:タグ
FROM node:16-alpine AS deps
FROM AS
コマンドは、マルチステージビルドの利用にて解説されています。
FROM AS
のFROM イメージ名:タグ AS ステージ名
となっています。AS以降は自由に命名できるようです。
node のイメージのドキュメントによると、イメージのタグに alpine がついているものは、軽量版になります。なるべくイメージのサイズを小さくしたい場合に利用することになります。
本番環境では alpine を指定するのが一般的なようです。
マルチステージビルド
この Dockerfile を読み解くには マルチステージビルド への理解が重要なようです。
Next.jsプロジェクトを開発環境と本番環境のDockerで動かしてみたい【その1】でも一瞬でてきた単語です。
Dockerドキュメントのマルチステージビルドの利用を参考に、更に読み解いていきます。
また、スタンドアロンモードを利用しているので、Next.jsのスタンドアロンモードでビルドしたイメージを Cloud Run へデプロイするの記事も参考にさせていただきました。
マルチステージでは、ビルドをステージごとに分割して行います。
これまでのごくあたりまえの方法として、開発環境向けの Dockerfile を 1 つ用意し、そこにアプリケーションの構築に必要なものをすべて含めます。 そこから本番環境向けとしてスリム化したものをもう 1 つ用意して、アプリケーションそのものとそれを動かすために必要なもののみを含めるようにします。 これは「開発パターン」(builder pattern)と呼ばれてきました。
マルチステージビルド以前の Dockerfile にはFROM
コマンドは1行のみだったようです。
この Dockerfile ではマルチステージビルドが利用されているのでFROM
コマンドを追ってステージごとに分割してみます。
deps
,builder
,production
のステージがそれぞれ用意されています。
deps は dependencies
の略語になります。(知らなかった…😖)
まずはライブラリ等の導入をしていくようです。
##### deps ステージ #####
FROM node:16-alpine AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
# これ以降は /app で実行される
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
本番環境向けに aliphe を指定してる場合には、libc6-compat
を追加することが推奨されているため、RUN apk add --no-cache libc6-compat
を実行しています。
サンプルのコメントにも書いてあるようにhttps://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpineに書かれています。
One common issue that may arise is a missing shared library required for use of process.dlopen. To add the missing shared libraries to your image, adding the libc6-compat package in your Dockerfile is recommended: apk add –no-cache libc6-compat
WORKDIRコマンドで、それ移行のRUN
,COPY
を実行するディレクトリを変更しています。
なるほど。Next.js + Docker のビルド作業とは、app ディレクトリの中にサイトを表示するために必要なものを配置していく作業という理解で良さそう。
次にCOPY
で必要なライブラリの情報を app ディレクトリの中にコピーしています。
COPYコマンドではCOPY コピー元 コピー先
で指定できます。
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
ローカル上にあるライブラリ関係のファイル(package.json yarn.lock* package-lock.json* pnpm-lock.yaml*)を /app
の中にコピーするという指示になります。
コピーをした後にRUN
コマンドでライブラリのインストールを行います。
RUNコマンドではRUN コマンド
で指定できます。
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
if文を用いて、各ファイルが用意されている場合にのみ実行するようなコマンドが指定されています。
yarn.lock
がある場合は、yarn --frozen-lockfile
を実行します。(yarn.lockを生成しない指定付き)
package-lock.json
がある場合は、npm ci
を実行します。(参考: npm installとnpm ciの違いをメモする)
pnpm-lock.yaml
がある場合には、yarn global add pnpm && pnpm i
を実行します。(pnpm。参考:pnpm の特徴
全てのファイルが見つからなかった場合には、exit 1;
により、dockerのビルドごと終了してしまいます。
builder ステージではビルドを行います。
##### builder ステージ #####
FROM node:16-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
deps ステージと同様に作業ディレクトリ /app
に移動してからビルドを行います。
その時に deps ステージの情報を利用しています。
オプションで、これまでの( FROM .. AS <名前> として作成した)構築ステージをコピー元(ソース)の場所として指定するために、 COPY で –from=<名前> フラグを利用できます。これは、ユーザ自身が構築コンテキストを送る作業の替わりとなります。
つまり、COPY --from=deps /app/node_modules ./node_modules
では、deps ステージ上に生成されたnode_modules
ディレクトリを builder ステージ上にコピーしています。
「deps ステージの /app/node_modules」を「builder ステージの /app/node_modules」に持ってきています。
COPY . .
コマンドで、ローカルからDocker環境の/app
へディレクトリをそのままコピーしています。このコマンドにより実際に Next.js サイトを作ったものが Docker 上に丸っと移動しています。
最後にRUN
コマンドで、ビルドを行なっています。
runner ステージでは最終的なイメージに含めたいものだけの抽出を行ないます。
builder ステージではCOPY . .
により、全てを含んだ状態になっていますが、そこから実際に配置するもののみを厳選してイメージ化する作業をしていくようです。
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"]
deps ステージ、builder ステージと同様に作業ディレクトリ/app
を指定します。
ENV
コマンドで環境の指定を行なっています。
ENVコマンドではENV <キー>=<値>
もしくはENV <キー> <値>
で指定ができます。
NODE_ENV production
となっているので、本番環境向けに環境変数が設定されています。
ユーザの作成
次にグループの作成とユーザの作成を行なっています。
これは、公式での説明等が見つからないので推測になってしまいますが、より安全なセキュリティにするためでしょうか…。
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
builder ステージからコピー
builder ステージから public ディレクトリと package.json のみをコピーしています。
public
ディレクトリにはアイコン等の公開ファイルが入っています。
COPY --from=builder /app/public ./public
COPY --from=builder /app/package.json ./package.json
次に builder ステージから node_modules
のパッケージ(/app/.next/standalone
に入っている) と、静的ファイルをコピーしています。
standalone を指定してビルドを行なっているので、.next/standalone
内に必要なパッケージが生成されています。
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
この時に先ほど作成したユーザとグループを所有者として指定しています。
その後 USERコマンドにより、ユーザの変更をおこなっています。
ポートの設定
EXPOSE 3000
ENV PORT 3000
EXPOSEコマンドにて、ポートの指定をおこなっています。
また同時に環境変数のポート番号も変更しています。
起動
最後に起動します。
CMD
コマンドにより、server.js を起動します。
そこにstandaloneディレクトリが追加されます。ここにはアプリを動かすためのファイルが依存関係も含めてすべて入っていて、.next/standalone/server.jsを起動すれば本番アプリが動かせます。
https://zenn.dev/team_zenn/articles/nextjs-standalone-mode-cloudrun
CMDコマンドはCMD ["実行ファイル","パラメータ1","パラメータ2"]
で指定します。
CMD ["node", "server.js"]
では server.js を起動しています。
おわりに
とても長くなってしまいましたが、なんとなく理解ができた気がします。
マルチステージビルドによって必要なもののみを抽出してイメージ化している…という認識で良いはず。
他の方のサンプルでは、ユーザの作成をしていなかったり、deps ステージを用意せずに builder と runner ステージのみで構築されていたりと様々です。
今回のサンプルを利用し「Next.js のプロジェクトを作成して開発環境の Docker で動くようにする【その2】」の記事にて、開発環境の構築をしています。