はじめに

猫大好きエンジニアの福田です。

皆さんはDockerfileでNode.jsをインストールするとき、どうやってインストールしていますか?

良くみる方法

Node.jsのインストール方法を調べると、こちらのサイトを参考に以下のようにインストールする方法を良く見かけますが、
メジャーバージョンだけを指定してインストールするため、完全なバージョンを指定することができません。
少ないコマンドで済むので簡単ではありますが、指定したメジャーバージョンの最新バージョンがインストールされてしまいます。

FROM ruby:3.2.2-bullseye

RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
 && apt-get update \
 && apt-get install -y --no-install-recommends nodejs \
 && rm -rf /var/lib/apt/lists/*

ENTRYPOINT [ "sh", "-cx" ]
CMD [ "node --version && npm --version && npx --version" ]

# 実行結果
# docker build -t ruby-node-apt:latest . -f Dockerfile.apt
# docker run --rm ruby-node-apt:latest
# + node --version
# v18.16.0
# + npm --version
# 9.5.1
# + npx --version
# 9.5.1

そこで、バージョン指定してインストールする2通りの方法を紹介します。

2つの方法

  1. バイナリからインストール
  2. マルチステージビルドを利用してインストール

※Node.jsのイメージを利用すればバージョンを指定することができますが、本記事では
他のイメージを使いつつNode.jsの固定のバージョンをインストールしたいというような状況を想定しています。

バイナリからインストール

https://nodejs.org/dist/ に各バージョンごとにアーキテクチャのバイナリがありますので、そちらをダウンロードして利用する方法です。

FROM ruby:3.2.2-bullseye
ENV NODE_VERSION 18.16.0

RUN export ARCH=$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x64/') \
 && curl -fsSLO --compressed "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
 && tar -xJf "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
 && rm "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
 && ln -s /usr/local/bin/node /usr/local/bin/nodejs

ENTRYPOINT [ "sh", "-cx" ]
CMD [ "node --version && npm --version && npx --version" ]

# 実行結果
# docker build -t ruby-node-binary:latest . -f Dockerfile.binary
# docker run --rm ruby-node-binary:latest
# + node --version
# v18.16.0
# + npm --version
# 9.5.1
# + npx --version
# 9.5.1

本番等で利用する場合は、GPGで改竄されていないかをチェックし、利用するイメージもslimにして軽量化するなどした方が良いでしょう。

GPG version
FROM ruby:3.2.2-slim-bullseye
ENV NODE_VERSION 18.16.0

RUN export buildDeps='curl xz-utils ca-certificates gnupg2 dirmngr' \
 && apt-get update \
 && apt-get install -y --no-install-recommends ${buildDeps} \
 && rm -rf /var/lib/apt/lists/* \
 && set -ex \
    && for key in \
        # https://github.com/nodejs/node#release-keys
        4ED778F539E3634C779C87C6D7062848A1AB005C \
        141F07595B7B3FFE74309A937405533BE57C7D57 \
        74F12602B6F1C4E913FAA37AD3A89613643B6201 \
        8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \
        C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \
        890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 \
        C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C \
        108F52B48DB57BB0CC439B2997B01419BD92F80A \
    ; do \
        gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "${key}"; \
    done \
 && export ARCH=$(uname -m | sed 's/aarch64/arm64/' | sed 's/x86_64/x64/') \
 && curl -fsSLO --compressed "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" \
 && curl -fsSLO --compressed "https://nodejs.org/dist/v${NODE_VERSION}/SHASUMS256.txt.asc" \
 && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \
 && grep " node-v${NODE_VERSION}-linux-${ARCH}.tar.xz\$" SHASUMS256.txt | sha256sum -c - \
 && tar -xJf "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" -C /usr/local --strip-components=1 --no-same-owner \
 && rm "node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt \
 && ln -s /usr/local/bin/node /usr/local/bin/nodejs \
 && apt-get purge -y --auto-remove ${buildDeps}

ENTRYPOINT [ "sh", "-cx" ]
CMD [ "node --version && npm --version && npx --version" ]

マルチステージビルドを利用してインストール

マルチステージビルドを利用すると以下のように書くことができます。

FROM node:18.16.0-bullseye-slim as node

FROM ruby:3.2.2-slim-bullseye

COPY --from=node /usr/local/bin/node /usr/local/bin/node
COPY --from=node /usr/local/lib/node_modules /usr/local/lib/node_modules

RUN ln -s /usr/local/bin/node /usr/local/bin/nodejs \
 && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm \
 && ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npx

ENTRYPOINT [ "sh", "-cx" ]
CMD [ "node --version && npm --version && npx --version" ]

# 実行結果
# docker build -t ruby-node-multi-stage-build:latest . -f Dockerfile.multi-stage-build
# docker run --rm ruby-node-multi-stage-build:latest
# + node --version
# v18.16.0
# + npm --version
# 9.5.1
# + npx --version
# 9.5.1

まとめ

マルチステージビルドでのインストールについては、同じディストリビューションでないと利用できないと思いますがバイナリからインストールに比べるとビルド時間も早く、ビルド後のイメージの容量も少なくなるようです。

#
# slimを利用しているGPG version版と、マルチステージビルドの容量を比較
#
➜ docker images | grep -e binary-gpg -e multi-stage-build                                                            
ruby-node-binary-gpg          latest                ca1955fa80a4   40 seconds ago   321MB # バイナリ版
ruby-node-multi-stage-build   latest                16c9b4c9d71c   6 minutes ago    269MB # マルチステージ版

Node.jsをバージョン指定でインストールする2つの方法についてご紹介いたしました。
どなたかの参考になれば幸いです。