Docker with Multi Stage Buildを利用したNext.jsのDockerイメージを作る

tl;dr

  • Next.jsのSSG(Static Site Generator)を利用したDocker Imageを作る際にMulti Stage BuildでImageを作ると、利用しない場合に比べて453MB変わる

Dockerfile

以前とは違い、Production環境でも利用出来るように構築をした。SSGを利用する前提で書いている。 ただ、実際にはSSGをProductionで利用する場合はNetlifyやVercel、S3など静的コンテンツを置くのに適したプラットフォームを利用すると思うので、あまり意味はないかもしれない。

gist.github.com

それぞれbase, builder, productionとステージを分けている。

base

依存関係にあるnpm packageをインストールする。開発環境ではbase stageを利用し、docker build時の引数でnext コマンドを指定するだけで良い。

$ docker build -t next-app:base --target base .
Sending build context to Docker daemon  177.8MB
Step 1/7 : FROM node:14-alpine as base
 ---> fa2fa5d4e6f4
Step 2/7 : WORKDIR /app/js/base
 ---> Using cache
 ---> 3bcd585979e7
Step 3/7 : COPY package.json .
 ---> Using cache
 ---> 73e62066ac85
Step 4/7 : COPY yarn.lock .
 ---> Using cache
 ---> d6ed6b01b3ed
Step 5/7 : COPY tsconfig.json .
 ---> Using cache
 ---> 7a8ae367da60
Step 6/7 : RUN yarn install
 ---> Using cache
 ---> dd874aced5fe
Step 7/7 : COPY . .
 ---> Using cache
 ---> 5ad130f7577a
Successfully built 5ad130f7577a
Successfully tagged next-app:base

$ docker run -it --rm -p 3000:3000 next-app:base yarn next
yarn run v1.22.5
$ /app/js/base/node_modules/.bin/next
ready - started server on http://localhost:3000
info  - ready on http://localhost:3000
event - build page: /
wait  - compiling...
A codemod is available to fix the most common cases: https://nextjs.link/codemod-ndc
info  - ready on http://localhost:3000

builder

アプリケーションをbuildする層。この層でアプリケーションbuildしproduction stageへは不要なもの(node_modules, ソースコード etc)を持ち込まないようにする。

production

Production環境に適応したイメージを作る層。ここで不要なファイルが作成されるとその分Imageサイズが大きくなるので不要なものは入れてはいけない。 このstageでbuildをしてしまうとソースコードなど不要なものが必要になるので、一つ前のbuilder stageから必要なものをコピーする。

最後のyarn install --productionでproductionに必要な依存関係のみをインストールする。

結果

Multi Stage Buildを利用した場合と利用しない場合で比較をしてみる。

Multi Stage Buildを利用する

$ docker build -t next-app:base . --target base
Sending build context to Docker daemon  194.2MB
Step 1/7 : FROM node:14-alpine as base
 ---> fa2fa5d4e6f4
Step 2/7 : WORKDIR /app/js/base
 ---> Using cache
 ---> 7a04d844b125
Step 3/7 : COPY package.json .
 ---> Using cache
 ---> ab5127932992
Step 4/7 : COPY yarn.lock .
 ---> Using cache
 ---> e8ba5b6cb1ab
Step 5/7 : COPY tsconfig.json .
 ---> Using cache
 ---> 10b359198a18
Step 6/7 : RUN yarn install
 ---> Using cache
 ---> affbace5d646
Step 7/7 : COPY . .
 ---> Using cache
 ---> ce9e93809dc2
Successfully built ce9e93809dc2
Successfully tagged next-app:base

builder stage

$ docker build -t next-app:builder . --target builder
Sending build context to Docker daemon  194.2MB
Step 1/12 : FROM node:14-alpine as base
 ---> fa2fa5d4e6f4
Step 2/12 : WORKDIR /app/js/base
 ---> Running in ad2141be97fb
Removing intermediate container ad2141be97fb
 ---> 6d182a28bf59
Step 3/12 : COPY package.json .
 ---> 4a44d0936af9
Step 4/12 : COPY yarn.lock .
 ---> 489f697cba57
Step 5/12 : COPY tsconfig.json .
 ---> b155046ac03c
Step 6/12 : RUN yarn install
 ---> Running in 937d5fb38b0e
yarn install v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@2.1.3: The platform "linux" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[4/4] Building fresh packages...
Done in 49.13s.
Removing intermediate container 937d5fb38b0e
 ---> 27a2d7a30d34
Step 7/12 : COPY . .
 ---> c19dc009a6a6
Step 8/12 : FROM node:14-alpine as builder
 ---> fa2fa5d4e6f4
Step 9/12 : ENV NODE_ENV=production
 ---> Running in 3bd3364115b5
Removing intermediate container 3bd3364115b5
 ---> 0cfcd40518b3
Step 10/12 : WORKDIR /app/js/builder
 ---> Running in 0e20dd40600f
Removing intermediate container 0e20dd40600f
 ---> 9dac22ae0e11
Step 11/12 : COPY --from=base /app/js/base /app/js/builder
 ---> 208200318cf4
Step 12/12 : RUN ["yarn", "build"]
 ---> Running in ef55ba375411
yarn run v1.22.5
$ next build && next export
Creating an optimized production build...

Compiled successfully.

Automatically optimizing pages...

Page                                                           Size     First Load JS
┌ ● /                                                          9.31 kB        90.9 kB
├   /_app                                                      1.9 kB           60 kB
├ ○ /404                                                       3.25 kB        63.2 kB
└ ● /articles/[id]                                             344 B          81.9 kB
    ├ /articles/1
    └ /articles/2
+ First Load JS shared by all                                  60 kB
  ├ static/pages/_app.js                                       1.9 kB
  ├ chunks/444ecc389d1b2833cd1c90b9f6d2e962a7bb15f6.c2e7b8.js  10.7 kB
  ├ chunks/62e1b0f2.e05b9c.js                                  64 B
  ├ chunks/framework.4dd100.js                                 40.3 kB
  ├ runtime/main.9f98e5.js                                     6.28 kB
  ├ runtime/webpack.c21266.js                                  746 B
  ├ css/7ed58071346528c353bf.css                               97.4 kB
  └ css/cfb837a2694c3ad390b0.css                               108 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)(SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

> using build directory: /app/js/builder/.next
  copying "static build" directory
> No "exportPathMap" found in "next.config.js". Generating map from "./pages"
  launching 1 workers
Exporting (0/2)
  copying "public" directory
Exporting (1/2)
Exporting (2/2)

Export successful
Done in 42.64s.
Removing intermediate container ef55ba375411
 ---> ef487ea233e3
Successfully built ef487ea233e3
Successfully tagged next-app:builder

production stage

$ docker build -t next-app:production .
Sending build context to Docker daemon  194.2MB
Step 1/18 : FROM node:14-alpine as base
 ---> fa2fa5d4e6f4
Step 2/18 : WORKDIR /app/js/base
 ---> Running in 141707b10d1b
Removing intermediate container 141707b10d1b
 ---> 7a04d844b125
Step 3/18 : COPY package.json .
 ---> ab5127932992
Step 4/18 : COPY yarn.lock .
 ---> e8ba5b6cb1ab
Step 5/18 : COPY tsconfig.json .
 ---> 10b359198a18
Step 6/18 : RUN yarn install
 ---> Running in 25c5e101426f
yarn install v1.22.5
[1/4] Resolving packages...
[2/4] Fetching packages...
info fsevents@1.2.13: The platform "linux" is incompatible with this module.
info "fsevents@1.2.13" is an optional dependency and failed compatibility check. Excluding it from installation.
info fsevents@2.1.3: The platform "linux" is incompatible with this module.
info "fsevents@2.1.3" is an optional dependency and failed compatibility check. Excluding it from installation.
[3/4] Linking dependencies...
warning " > file-loader@6.0.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
warning " > url-loader@4.1.0" has unmet peer dependency "webpack@^4.0.0 || ^5.0.0".
[4/4] Building fresh packages...
Done in 78.52s.
Removing intermediate container 25c5e101426f
 ---> affbace5d646
Step 7/18 : COPY . .
 ---> ce9e93809dc2
Step 8/18 : FROM node:14-alpine as builder
 ---> fa2fa5d4e6f4
Step 9/18 : ENV NODE_ENV=production
 ---> Running in 05b08f765682
Removing intermediate container 05b08f765682
 ---> 43348a423c2a
Step 10/18 : WORKDIR /app/js/builder
 ---> Running in e924b7124b7e
Removing intermediate container e924b7124b7e
 ---> d872eca49f96
Step 11/18 : COPY --from=base /app/js/base /app/js/builder
 ---> 5aa288be27aa
Step 12/18 : RUN ["yarn", "build"]
 ---> Running in c1762e58a430
yarn run v1.22.5
$ next build && next export
Creating an optimized production build...

Compiled successfully.

Automatically optimizing pages...

Page                                                           Size     First Load JS
┌ ● /                                                          9.31 kB        90.9 kB
├   /_app                                                      1.9 kB           60 kB
├ ○ /404                                                       3.25 kB        63.2 kB
└ ● /articles/[id]                                             344 B          81.9 kB
    ├ /articles/1
    └ /articles/2
+ First Load JS shared by all                                  60 kB
  ├ static/pages/_app.js                                       1.9 kB
  ├ chunks/62e1b0f2.e05b9c.js                                  64 B
  ├ chunks/9a9e7c821a17e6790c99026b07c1c46382c721c2.c2e7b8.js  10.7 kB
  ├ chunks/framework.4dd100.js                                 40.3 kB
  ├ runtime/main.9f98e5.js                                     6.28 kB
  ├ runtime/webpack.c21266.js                                  746 B
  ├ css/7ed58071346528c353bf.css                               97.4 kB
  └ css/cfb837a2694c3ad390b0.css                               108 B

λ  (Server)  server-side renders at runtime (uses getInitialProps or getServerSideProps)(Static)  automatically rendered as static HTML (uses no initial props)(SSG)     automatically generated as static HTML + JSON (uses getStaticProps)

> using build directory: /app/js/builder/.next
  copying "static build" directory
> No "exportPathMap" found in "next.config.js". Generating map from "./pages"
  launching 1 workers
Exporting (0/2)
  copying "public" directory
Exporting (1/2)
Exporting (2/2)

Export successful
Done in 51.92s.
Removing intermediate container c1762e58a430
 ---> d264d497a0a5
Step 13/18 : FROM nginx:latest as production
 ---> f35646e83998
Step 14/18 : WORKDIR /app/js/src
 ---> Running in 89ce96f55a15
Removing intermediate container 89ce96f55a15
 ---> fd0fb921aa75
Step 15/18 : RUN rm -rf /etc/nginx/conf.d
 ---> Running in d73698e7006d
Removing intermediate container d73698e7006d
 ---> 85cc19c895e4
Step 16/18 : ADD conf/nginx/conf.d /etc/nginx/conf.d
 ---> 27ee1c3cfac8
Step 17/18 : COPY --from=builder /app/js/builder/out ./out
 ---> 5d4d1bd4881e
Step 18/18 : EXPOSE 80
 ---> Running in ef6862c2ccd5
Removing intermediate container ef6862c2ccd5
 ---> 46fc04190def
Successfully built 46fc04190def
Successfully tagged next-app:production

Multi Stage Buildを利用しない

$ cat Dockerfile_not_msb
FROM alpine:latest

RUN apk update
RUN apk add --update nodejs nodejs-npm yarn

WORKDIR /app/js

COPY package.json .
COPY yarn.lock .
COPY tsconfig.json .

RUN yarn install
COPY . .

RUN ["yarn", "build"]

RUN apk add nginx openrc
RUN rm -rf /etc/nginx/conf.d
ADD conf/nginx/conf.d /etc/nginx/conf.d

EXPOSE 80

$ docker build -t next-app:not_msb -f Dockerfile_not_msb .
Sending build context to Docker daemon    224MB
Step 1/14 : FROM alpine:latest
 ---> a24bb4013296
Step 2/14 : RUN apk update
 ---> Using cache
 ---> 5a65f08f5db7
Step 3/14 : RUN apk add --update nodejs nodejs-npm yarn
 ---> Using cache
 ---> c97ad0a3e846
Step 4/14 : WORKDIR /app/js
 ---> Using cache
 ---> a37951715a8d
Step 5/14 : COPY package.json .
 ---> Using cache
 ---> 6faddfc45d20
Step 6/14 : COPY yarn.lock .
 ---> Using cache
 ---> bd166cc22fbc
Step 7/14 : COPY tsconfig.json .
 ---> Using cache
 ---> f827439198ba
Step 8/14 : RUN yarn install
 ---> Using cache
 ---> 46c450c588da
Step 9/14 : COPY . .
 ---> Using cache
 ---> 7b18d341a97a
Step 10/14 : RUN ["yarn", "build"]
 ---> Using cache
 ---> 578c525c3ce1
Step 11/14 : RUN apk add nginx openrc
 ---> Using cache
 ---> 94434ea28089
Step 12/14 : RUN rm -rf /etc/nginx/conf.d
 ---> Using cache
 ---> 571eedb7cbb5
Step 13/14 : ADD conf/nginx/conf.d /etc/nginx/conf.d
 ---> Using cache
 ---> 1a765adec9ca
Step 14/14 : EXPOSE 80
 ---> Using cache
 ---> 5df8f8cc2b74
Successfully built 5df8f8cc2b74
Successfully tagged next-app:not_msb

結果

$ docker images | grep next-app
next-app                           not_msb                 5df8f8cc2b74        7 minutes ago       614MB
next-app                           builder                 ef487ea233e3        About an hour ago   327MB
next-app                           production              46fc04190def        About an hour ago   161MB
next-app                           base                    ce9e93809dc2        About an hour ago   599MB

まとめ

  • Multi Stage Buildを利用することで一つのDockerfileで開発環境・Production環境をすぐに作ることが出来る。
  • Multi Stage Buildを利用した場合、利用しない場合に比べて453MB変わる。