Next.jsアプリケーションのDockerイメージのサイズが大きくて困っていたので調べていたところ、Next.jsの公式ドキュメントにDocker Imageというセクションがあり、おすすめ設定が記載されているのを見つけました。以前はここまで詳細な記述はなかったのですが、つい10日ほど前に追加されたようです。
ドキュメントには一番おすすめの方法だけ書かれているのですが、もともと「What is the best way to use NextJS with docker? · Discussion #16995 · vercel/next.js · GitHub」というdiscussionがあり、ドキュメントの記述はここでの議論が元になっているようです。Discussionではいくつか例が示されているのですが、それぞれさまざまな最適化テクニックが利用されており、どれくらいの容量になるか気になったので、Dockerfileを書いて調べてみました。
検証に使ったコードはこちらに置いてあります。
なお、各テクニックはNext.jsに限らずNode.jsのアプリケーション一般で通用するものだと思います。
まずはベースラインとして最適化なしのDockerfileです。
FROM node WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN yarn install --frozen-lockfile COPY . . RUN yarn build EXPOSE 3000 CMD ["yarn", "start"]
デフォルトのnode
イメージはDebianベースなので元からサイズが大きいです。イメージのサイズを小さくしたい場合は、可能であればまずはAlpine Linuxベースの node:alpine
に乗り換えるのがいいでしょう。
(追記)Alpineでは標準Cライブラリとしてglibcではなくmusl-libcが使われており、Node.jsでもネイティブモジュールを使っている場合に互換性の問題が起きることがあります。2021年時点では、alpineではなく、glibcが採用されておりかつサイズが小さいdistrolessやslimイメージを使うのがおすすめです(ブックマークコメントでの指摘ありがとうございます)。
FROM node:alpine WORKDIR /usr/src/app COPY package.json yarn.lock /usr/src/app RUN yarn install --frozen-lockfile COPY . . RUN yarn build EXPOSE 3000 CMD ["yarn", "start"]
yarn install
はデフォルトではdevDependencies
も含めて全ての依存パッケージをインストールします。アプリケーション実行用のイメージにLinterやテストフレームワークなどの開発用ツールを含める必要はないので、--production
オプションを利用して、devDependencies
をインストールしないようにすると、イメージのサイズを小さくできます。
FROM node:alpine WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN yarn install --production --frozen-lockfile COPY . . RUN yarn build EXPOSE 3000 CMD ["yarn", "start"]
さらに、Yarnのダウンロードキャッシュを削除すると、その分イメージのサイズを小さくできます。先程のdiscussionでは二つ方法が紹介されていました。
一つはyarn cache clean
コマンドを使う方法です。
FROM node:alpine WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN yarn install --production --frozen-lockfile \ && yarn cache clean COPY . . RUN yarn build EXPOSE 3000 CMD ["yarn", "start"]
もう一つは、キャッシュディレクトリを/dev/shm
以下にする方法です。/dev/shm
はLinuxにおけるRAMディスクのマウント先で、キャッシュディレクトリをここに指定すると、手で明示的にキャッシュを消さずとも、イメージのレイヤーにキャッシュが含まれなくなります。
ただし、この方法を利用する場合は、たいてい/dev/shm
のサイズが足りなくなる*1ため、docker build
コマンドの--shm-size
オプションで十分なサイズを指定してやる必要があります。一つめの方法を採用する方が面倒が少なくて楽でしょう。
FROM node:alpine WORKDIR /usr/src/app COPY package.json yarn.lock ./ ENV YARN_CACHE_FOLDER=/dev/shm/yarn_cache RUN yarn install --production --frozen-lockfile COPY . . RUN yarn build EXPOSE 3000 CMD ["yarn", "start"]
$ docker build --shm-size 1gb -f Dockerfile.dev-shm-yarn-cache
さらにイメージサイズを小さくするには、アプリケーションの実行に不要なコードをイメージに含めないようにするとよいです。テストコードは実行に関係ないので削除できますし、Next.jsのようにソースコードをトランスパイル・ビルドしている場合は、ビルド元のコードは不要です。
必要なファイルのみイメージに含める方法はいくつかありますが、ファイルの削除し忘れなどのミスを避けるには、multi-stage buildを使うとよいです。これはNext.jsのドキュメントでおすすめされている方法と同様です。この方法だと、Yarnのパッケージキャッシュを手で削除する必要がないのも楽です。
FROM node:alpine AS deps WORKDIR /usr/src/app COPY package.json yarn.lock ./ RUN yarn install --production --frozen-lockfile FROM node:alpine AS builder WORKDIR /usr/src/app COPY . . COPY --from=deps /usr/src/app/node_modules ./node_modules ENV NODE_ENV=production RUN yarn build FROM node:alpine AS runner WORKDIR /usr/src/app COPY --from=builder /usr/src/app/public ./public COPY --from=builder /usr/src/app/.next ./.next COPY --from=builder /usr/src/app/node_modules ./node_modules EXPOSE 3000 CMD ["yarn", "start"]
では、それぞれのイメージのサイズを比較してみます。
$ docker images nextjs-docker --format "table {{.Tag}}\t{{.Size}}" | sed -e '1d' | sort -k2 -h -r simple 1.17GB # Debianベース alpine 399MB # Alpineベース install-production 315MB # yarn install --production dev-shm-yarn-cache 157MB # キャッシュディレクトリを /dev/shm 以下にする clean-cache 157MB # yarn cache clean staged 156MB # Multi-stage build
結果、Next.jsのドキュメントでおすすめされている方法でビルドしたイメージが一番小さくなりました。全く最適化していないものに比べて1/8以下のサイズです。ベースイメージを変更したのが一番影響がありますが、他の手法もMB単位でサイズが小さくなっています。Multi-stage buildはこの比較だとあまり意味がないように見えますが、実際のアプリケーションでは実行時に不要なファイルがこのサンプルより多いでしょうから、ここで示したよりも効果が出るでしょう。
*1:デフォルトは64MB