【Node.js/Next.js】Cloud Runで動作する軽量なDockerを構築してみた
概要
本記事では、Next.jsをコンテナをサーバーレスで実行するサービスであるCloud Runで動作する軽量のDocker環境構築について紹介します。
ネットにある様々な記事を見てきましたが、動作目的でDockerイメージが大きくなっており、パフォーマンスとセキュリティに課題がありました。そこでDockerの軽量化および最適化を試みました。
Docker環境
シングルステージビルド(slim)
シングルステージビルドしたDockerイメージです。本番環境に必要無いデータを多く含んでいるため、イメージサイズも大きくなっています。構成はシンプルのため、理解しやすいですがパフォーマンス/セキュリティ面には課題があります。
FROM node:14.17.0-slim
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
COPY . .
RUN yarn build:next
EXPOSE 8080
CMD ["yarn", "start:next"]
package.json
"scripts": {
"start:next": "next start -p 8080",
"build:next": "rm -rf .next && next build"
}
マルチステージビルド(slim)
Dockerのマルチステージビルドを使用したイメージです。マルチビルドステージを使用することで、ランタイムに必要なデータだけになり、シングルステージビルドに比べて大幅にイメージサイズを削減できました。個人的にはこれでも十分だったのですが、まだ軽量化の余地があったので、もうひと踏ん張りしてみました。(楽しくなってきた)
#==================================================
# Base Layer
FROM node:14.17.0-slim AS base
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
COPY . .
#==================================================
# Build Layer
FROM base AS build
ENV NODE_ENV=production
WORKDIR /build
COPY /app ./
RUN yarn build:next
# ==================================================
# Package install Layer
FROM node:14.17.0-slim AS node_modules
WORKDIR /modules
COPY package.json yarn.lock ./
RUN yarn install --non-interactive --frozen-lockfile --production
# ==================================================
# Production Run Layer
FROM node:14.17.0-slim
ENV NODE_ENV=production
WORKDIR /app
COPY package.json yarn.lock next.config.js ./
COPY /build/public ./public
COPY /build/.next ./.next
COPY /modules/node_modules ./node_modules
EXPOSE 8080
CMD ["yarn", "start:next"]
package.json
"scripts": {
"start:next": "next start -p 8080",
"build:next": "rm -rf .next && next build"
}
マルチステージビルド(distroless)
前述と同様、Dockerのマルチステージビルドを使用しています。構成もほとんど変わりませんが、RunLayerをslimからdistrolessに変更しています。distrolessにはyarn
が含まれていないため、カスタムサーバーから立ち上げる方法に変更し、yarn
に依存しないようにしています。
Distrolessとは?
Distrolessは、Googleが提供しているDockerイメージです。動作に最低限な依存のみ含まれているため、非常に軽量なイメージです。最小の構成になっているため、本番環境向きでパフォーマンスとセキュリティにも優れています。
#==================================================
# Base Layer
FROM node:14.17.0-slim AS base
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn --frozen-lockfile
COPY . .
#==================================================
# Build Layer
FROM base AS build
ENV NODE_ENV=production
WORKDIR /build
COPY /app ./
RUN yarn build
# ==================================================
# Package install Layer
FROM node:14.17.0-slim AS node_modules
WORKDIR /modules
COPY package.json yarn.lock ./
RUN yarn install --non-interactive --frozen-lockfile --production
# ==================================================
# Production Run Layer
FROM gcr.io/distroless/nodejs:14
ENV NODE_ENV=production
WORKDIR /app
COPY package.json yarn.lock next.config.js ./
COPY /build/public ./public
COPY /build/.next ./.next
COPY /build/dist ./dist
COPY /modules/node_modules ./node_modules
EXPOSE 8080
CMD ["dist/index.js"]
package.json
"scripts": {
"start": "node dist/index.js",
"build": "npm run build:next && npm run build:server",
"build:server": "rm -rf dist && tsc --project tsconfig.server.json",
"build:next": "rm -rf .next && next build"
}
server/index.ts
import express, { Request, Response } from 'express'
import next from 'next'
const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()
const port = process.env.PORT || 8080
app.prepare().then(() => {
const server = express()
server.use((req: Request, res: Response) => handle(req, res))
server.listen(port, (err?: any) => {
if (err) throw err
console.log(`> Ready on http://localhost:${port}`)
})
})
追記(2021/06/07)
internetkunさんよりご指摘頂きました。ありがとうございます。以下にサーバー立ち上げの別方法を追記します。node_modules
にはnext
が内包されているため、node_modules/.bin/next start
で起動することができます。よって、パフォーマンスに影響がでるカスタムサーバーは特別な理由がない限りは、使用を控えると良さそうです。
Before deciding to use a custom server please keep in mind that it should only be used when the integrated router of Next.js can't meet your app requirements. A custom server will remove important performance optimizations, like serverless functions and Automatic Static Optimization.
+ RUN yarn build:next
- RUN yarn build
- COPY /build/dist ./dist
+ CMD ["node_modules/.bin/next start"]
- CMD ["dist/index.js"]
軽量化の結果
まず、今回の軽量化でどの程度イメージサイズを削減できたかですが、以下の通りです。(プラグイン入れているので、元の容量がやや大きくなってます)
イメージ | サイズ | 削減率 |
---|---|---|
シングルステージビルド(slim) | 917.52 MB | - |
マルチステージビルド(slim) | 297.91 MB | 67.5% |
マルチステージビルド(distroless) | 251.02 MB | 72.6% |
シングルステージビルドからマルチステージビルドへの変更、distrolessの活用により軽量化前と比べてイメージサイズを約70%削減でき、デプロイ時間の短縮とセキュリティの向上を実現できました。
まとめ
今回、軽量化を実施してみてシングルステージビルドには必要無いデータが多く含まれていることが分かりました。今後はマルチステージビルドを活用してパフォーマンスとセキュリティに優れた軽量なDocker環境を構築できればと思います。
少々長くなりましたが、本記事を最後まで読んで頂き、ありがとうございました!
まだまだエンジニアとしては経験が浅いので、ご助言等ありましたらコメントいただけると幸いです。
更新履歴
- マルチステージビルド(distroless)の一部を更新(2021/06/07)
関連記事
参考記事
Discussion