最適なNode.js Dockerイメージの選び方
2022年9月30日
0 分で読めます
編集者注:
Node.js向けのChainguard Distroless Imageを追加しました。
(2023年8月31日)最新のNode.js LTSリリースに合わせて、推奨するNode.jsのバージョン、例、脆弱性スキャンの結果を更新しました。
Node.js Dockerイメージの選択は些細なことに思えるかもしれません。しかし、イメージのサイズや潜在的な脆弱性は、CI/CDパイプラインやセキュリティ態勢に大きな影響を及ぼす可能性があります。では、最適なNode.js Dockerイメージはどのように選べばよいのでしょうか?
FROM node:latestや、単にFROM node (前者のエイリアス)を使うリスクは見落としがちです。CI/CDパイプラインに持ち込まれるセキュリティ上のリスクや膨大なファイルサイズを把握していなければ、なおさらです。
以下は、Node.js Dockerイメージのチュートリアルやブログ記事でよく参考例として紹介されているDockerfileです。しかし、このDockerfileには重大な問題があり、おすすめできません。
以前、DockerでNode.jsウェブアプリケーションをコンテナ化するための10のベストプラクティスを、手順を追って解説しました。この例をさらに改良し、本番環境で使えるNode.js Dockerイメージを作成する方法を紹介しています。
この記事では、理想的なNode.js Dockerイメージを見つけるため、上記の簡略化した例をDockerfileの内容として使用します。
Node.js Dockerイメージの選択肢
Node.jsイメージをビルドする際には、実に多くの選択肢があります。Node.jsコアチームが管理する公式Node.js Dockerイメージや、そのベースイメージで選べる個別のNode.jsイメージタグのほか、GoogleやChainguardのdistrolessイメージ、Dockerチームが提供する最小構成のscratchイメージをベースにNode.jsアプリケーションをビルドする方法もあります。
これらの選択肢のうち、どのNode.js Dockerイメージが最適なのでしょうか?
それぞれを詳しく見て、メリットと潜在的なリスクを確認しましょう。
著者注:この記事では、2024年4月頃にリリースされた時点のNode.jsバージョン(Node.js 22.1.0)を比較します。
デフォルトのnodeイメージ
まず、メンテナンスされているnodeイメージから見ていきましょう。これはNode.js Dockerチームが公式に管理しており、複数のDockerベースイメージタグが含まれています。タグは、基盤となるディストリビューション(Debian、Ubuntu、Alpine)やNode.jsランタイムのバージョンに対応しています。また、amd64やarm64x8(新しいApple M1)など、CPUアーキテクチャを指定するタグもあります。
Debianディストリビューション向けの一般的なnodeイメージタグ(bullseyeやbookwormなど)は、それ自体が別のチームによって管理されるbuildpack-depsをベースにしています。
このデフォルトのnodeイメージをベースに、npm依存関係としてfastifyだけを含むNode.js Dockerイメージをビルドすると、どうなるでしょうか?ここでは簡単にするため、アプリケーション全体をデプロイする代わりに、この例を使います。
同じディレクトリからdocker build --no-cache -t mynode . — を実行してイメージをビルドします。サイズの違いを示すため、この例ではnode:latestイメージも取得しました。結果は次のとおりです。
Node.jsランタイムのバージョンを指定していないため、
nodeはnode:latestのエイリアスです。これは現在、Node.js 22.1.0を指しています。Dockerイメージのサイズは1.13GBです。
そのうち
node:latestベースイメージが1.11GBを占めています。
この最新のNode.jsイメージに含まれる依存関係やセキュリティ脆弱性は、どの程度でしょうか?コンテナをスキャンして確認できます。ビルドに対して同じスキャンを実行するには、無料のSnykアカウントを作成し、Snyk CLIをインストールしてください。ドキュメントの手順に従うか、簡単な`npm install -g snyk`コマンドを実行します。
snyk container test mynode --file=Dockerfile --exclude-app-vulnsを実行すると、次の結果が得られます。
依存関係は合計413件です。これらは、
curl/libcurl4、git/git-man、imagemagick/imagemagick-6-commonなど、OSのパッケージマネージャーで検出されたオープンソースライブラリです。これらの依存関係から、バッファオーバーフロー、解放後使用エラー、範囲外書き込みなど、合計179件のセキュリティ問題が見つかりました。
Node.js 20.5.1などの古いNode.jsイメージには、DNSリバインディング、HTTPリクエストスマグリング、設定の乗っ取りなど、さまざまなセキュリティ問題が見つかりました。
アプリケーションのNode.jsイメージにwget、git、curlを含める必要が本当にあるでしょうか?何百もの依存関係やツールをNode.js Dockerイメージに含め、それらに何百もの脆弱性が見つかる状況は、決して好ましいものではありません。攻撃を受ける余地が大きくなります。
Node.js Docker Hubの選択肢:node:buster、node:bullseye、node:bookworm
Node.js Docker Hubリポジトリで利用可能なタグを見ると、node:buster、node:bullseye、node:bookwormなど、Node.jsイメージタグの選択肢が複数あります。
これら3つのDockerイメージタグは、いずれもDebianディストリビューションの各バージョンをベースにしています。busterイメージタグはDebian 10に対応しており、2022年8月から2024年にかけてサポート終了を迎えたため、あまりおすすめできません。bullseyeイメージタグはDebian 11に対応し、Debianの現行「旧安定版」と位置付けられており、サポート終了は2026年6月の予定です。最後に、bookwormは現行の「安定版」です。この記事の執筆時点ではサポート終了日は未定ですが、これまでの傾向から、2028年頃になると考えてよいでしょう。
著者注:そのため、新規・既存を問わず、すべてのNode.js Dockerイメージでnode:busterタグからnode:bullseye、node:bookworm,またはその他の適切な選択肢への移行を強くおすすめします。
次のタグをベースに、新しいNode.js Dockerイメージをビルドしましょう。
このNode.js Dockerイメージタグをビルドして上記の結果と比較すると、サイズ、依存関係の数、脆弱性の検出数はまったく同じです。これは、node、node:latest、node:bookwormがすべて同じNode.jsイメージタグを指しているためです。
イメージを軽量化するNode.jsタグ
Node.js Docker公式チームは、Node.js環境を機能させるために必要なツールだけを明示的に対象としたイメージタグも管理しています。
これらのNode.jsイメージタグはslimイメージタグのバリエーションとして提供されており、node:bookworm-slimのようなタグや、node:20-slimのようにNode.jsのバージョンを指定するタグがあります。
Debianの現行安定版bookwormをベースに、Node.jsのslimイメージをビルドしましょう。
イメージサイズは大幅に減少し、約1GBから231MBになりました。内容をスキャンすると、ソフトウェア全体のフットプリントも大きく減少しています。依存関係は89=8件、脆弱性はわずか37件です。
コンテナイメージのサイズとセキュリティ態勢の観点から、node:bookworm-slimはすでに優れた出発点です。
LTS版Node.js Dockerイメージ
ここまでのNode.js Dockerイメージは、現行バージョンのNode.js 22をベースにしてきました。しかし、Node.jsのリリーススケジュールによると、このバージョンが正式にActive LTSになるのは2024年10月です。
ビルドするNode.js Dockerイメージでは、常に長期サポート(LTS)版を使うとどうなるでしょうか?Dockerイメージタグを適宜変更し、新しいNode.jsイメージをビルドしてみましょう。
より軽量なNode.js LTS版(20.13.1)では、依存関係とセキュリティ脆弱性の数はほぼ同じで、イメージサイズは219MBとわずかに小さくなります。
つまり、LTS版とCurrent版のNode.jsランタイムのどちらを選ぶかについて個別の要件があっても、Node.jsイメージのソフトウェアフットプリントに大きな影響はありません。
Node.jsイメージにはnode:alpineのほうが適している?
Node.js Dockerチームはnode:alpineイメージタグとそのバリエーションを管理しており、Alpine Linuxの各ディストリビューションとNode.jsランタイムの各バージョンを組み合わせられます。
Alpine Linuxプロジェクトは、非常に小さなイメージサイズでよく知られています。これはソフトウェアフットプリントが小さくなり、結果として脆弱性の対象領域も小さくなるため、大きなメリットです。
Dockerイメージのサイズは167MBとなり、Node.jsのslimイメージより64MB小さくなります。また、この記事の執筆時点では、Alpineイメージタグで検出されたOS依存関係は17件、セキュリティ脆弱性は1件だけでした。このことから、alpineイメージタグは、イメージサイズと脆弱性の数の両方を抑えるうえで有力な選択肢と言えそうです。
Node.js Dockerイメージにはnode:alpineが最適なのでしょうか?
Node.js用Alpineイメージは、全体のイメージサイズと脆弱性の数を小さくできる可能性があります。ただし、AlpineプロジェクトではC標準ライブラリの実装にmuslを使っている一方、bullseyeやslimなどのDebianベースのNode.jsイメージタグではglibcを使っている点に注意が必要です。この違いにより、パフォーマンスの問題や機能上のバグが起きたり、基盤となるCライブラリの差異によってアプリケーションがクラッシュしたりする可能性があります。Itamar Turner-Trauringも、Python DockerイメージのAlpineタグに関連する予期しないランタイムの問題について記事に書いています。
Node.jsのalpineイメージタグを選ぶということは、実質的に非公式のNode.jsランタイムを選ぶことになります。Node.js DockerチームはAlpineベースのコンテナイメージビルドを公式にはサポートしていません。そのため、Alpineベースのイメージタグは実験的で、安定性が保証されないと明記されており、次の非公式ビルドから提供されています。非公式ビルドのイメージタグリポジトリには、次のように記載されています。
Unofficial-buildsは、Node.jsでサポートされていない、または一部しかサポートされていないプラットフォーム向けに、基本的なNode.jsバイナリの提供を試みています。このプロジェクトは保証を提供しておらず、成果物も厳格なテストを受けていません。nodejs.orgで公開されるビルドは、コード品質、対象プラットフォームのサポート、提供時期や方法について非常に高い品質基準を満たしています。一方、unofficial-buildsで公開されるビルドは、テストが最低限、またはまったく行われておらず、プラットフォームによっては公式のNode.jsテスト基盤に含まれていない場合があります。これらのビルドはユーザーコミュニティの利便性のために提供されていますが、保守にはコミュニティの協力が期待されています。
Node.jsのalpineイメージタグの互換性に関して、特に注目すべき点は次のとおりです。
Yarnとの互換性がない(issue #1716)。
ネイティブCバインディングのクロスコンパイルに
node-gypが必要な場合、そのプロセスの依存関係であるPythonはAlpineイメージに含まれていません。自分で解決する必要があります(issue #1706)。
Node.js向けDistroless Dockerイメージ
ベンチマークで最後に比較するのはDistrolessイメージです。主な選択肢は、従来からあるGoogleのdistrolessコンテナイメージと、新しいChainguardのdistrolessコンテナイメージの2つです。
Distroless Dockerイメージとは?
これらのイメージは、アプリケーションとそのランタイム依存関係だけを対象にしているため、slim Node.jsイメージタグよりもさらに軽量です。そのため、distroless Dockerイメージにはパッケージマネージャー、シェル、その他の汎用ツールが含まれず、サイズと脆弱性の対象領域を小さくできます。
Google Distrolessイメージ
Google Distrolessプロジェクトは、Node.js専用のランタイム向けdistroless Dockerイメージを管理しています。完全な名前空間ではgcr.io/distroless/nodejs22-debian12と表記され、Googleのコンテナレジストリ(gcr.ioの部分)から利用できます。
注: DebianとNode.jsの古い組み合わせも存在しますが、現在もメンテナンスされているバージョンを選ぶよう注意してください。サポート対象の最新イメージについては、Distroless GitHubリポジトリの表をご覧ください。
Distrolessコンテナイメージにはソフトウェアが含まれていないため、Dockerのマルチステージワークフローを使ってコンテナの依存関係をインストールし、distrolessイメージにコピーできます。
このdistroless Dockerイメージをビルドすると、177MBのファイルになります。slimとalpine のイメージタグと比べて、ファイルサイズを削減できます。
distroless Dockerイメージの使用を検討している場合は、次の重要な点に注意してください。
現在の安定版Debianリリースをベースとしているため、サポート終了まで十分な期間があり、最新の状態に保たれています。これは大きなメリットです。
Debianベースであるため、glibc実装に依存しており、本番環境で問題が発生する可能性が低くなります。
DistrolessチームがNode.jsランタイムの細かなバージョンごとにメンテナンスしていないことは、すぐに分かるでしょう。そのため、頻繁に更新される汎用の
latestタグを使うか、特定時点のイメージのSHA256ハッシュを指定してインストールする必要があります。
Chainguard Distrolessイメージ
distrolessイメージのもう1つの選択肢はChainguardです。Chainguardは、古いNodeバージョン向けやFIPS対応版を含む複数のイメージを提供しています。無料の開発者向けプランでは2つのイメージが利用できます。執筆時点でNode 22.1.0を実行する「latest」と、開発やデバッグ用にパッケージマネージャーとシェルを追加した「latest-dev」です。
先ほどのGoogle distrolessの例とよく似た方法を使えます。
この方法で生成されるイメージは142MBで、Google Distrolessイメージと同程度です。報告された脆弱性はありません。
すべてのChainguard Imagesは、ChainguardのWolfi Linuxディストリビューションをベースにビルドされています。Wolfiはglibcに対してコンパイルされるため、muslとの互換性に関する問題はありません。
Node.js Dockerイメージタグの比較
最終更新日の2024年5月15日時点で、さまざまなNode.js Dockerイメージタグを比較した結果を、以下の表にまとめました。
イメージタグ | Node.jsランタイムのバージョン | OSの依存関係 | OSのセキュリティ脆弱性 | 重大度「高」および「クリティカル」の脆弱性 | 中程度の脆弱性 | 低程度の脆弱性 | Node.jsランタイムの脆弱性 | イメージサイズ | Yarnの利用可否 |
|---|---|---|---|---|---|---|---|---|---|
node:latest | 22.1.0 | 413 | 179 | 3 | 0 | 176 | 0 | 1135MB | はい |
node:bookworm | 22.1.0 | 413 | 179 | 3 | 0 | 176 | 0 | 1135MB | はい |
node:bookworm-slim | 22.1.0 | 88 | 37 | 2 | 0 | 35 | 0 | 233MB | はい |
node:lts-bookworm-slim | 20.13.1 | 88 | 37 | 2 | 0 | 35 | 0 | 219MB | はい |
node:alpine | 22.1.0 | 17 | 1 | 0 | 0 | 1 | 0 | 145MB | はい |
22.1.0 | 8 | 16 | 0 | 0 | 16 | 0 | 186MB | いいえ | |
cgr.dev/chainguard/node:latest | 22.1.0 | 25 | 0 | 0 | 0 | 0 | 0 | 134MB | いいえ |
cgr.dev/chainguard/node:latest-dev | 22.1.0 | 66 | 0 | 0 | 0 | 0 | 0 | 651MB | はい |
それぞれのNode.jsイメージタグのデータと分かったことを確認し、どれが最適かを検討しましょう。
開発環境との一致度
Node.jsイメージタグを選ぶ基準が開発環境との一貫性、つまり開発環境と本番環境をまったく同じにすることなら、すでに実現は難しいかもしれません。多くの場合、主要な3つのOSはそれぞれ異なるCライブラリ実装を使用しています。Linuxはglibc、Alpineはmusl、macOSは独自のBSD libc実装に依存しています。
Dockerイメージのサイズ
サイズが重要な場合もあります。ただし、より正確には、目指すべきなのは単に最小サイズではなく、ソフトウェア全体のフットプリントを最小限に抑えることです。その観点では、slimイメージタグはalpineの各イメージとサイズに大きな差がなく、コンテナイメージの平均サイズはいずれも約211MBです。一方で、slimイメージのソフトウェアフットプリントは依然として大きく(alpineの17に対して89)、その分、脆弱性の対象範囲も広くなります(slimは28、0はalpine)。
セキュリティ脆弱性
脆弱性は重要な懸念事項であり、コンテナイメージのサイズを小さくすべき理由を説明する記事でも、たびたび取り上げられています。ただし、セキュリティ上の問題の意味合いは非常に重要です。
ソフトウェアフットプリントが大きく、セキュリティ脆弱性も多いnodeとnode:bullseyeのイメージを除外し、より小規模なイメージ群に注目しましょう。slim、alpine、distrolessを比べると、重大度「高」および「クリティカル」の脆弱性の絶対数には大きな差がなく、0から2の範囲です。対処可能なリスクであり、アプリケーションの用途によっては問題にならない可能性もあります。
サポートと安定性
Node.js Dockerチームがコンテナイメージのビルドに関する懸念を優先して対処し、問題をタイムリーに解決できることは大きなメリットです。Debianベースの公式イメージタグ以外を使う場合、この項目をチェックリストから外すことは基本的にできません。
nodeまたはnode:22.1.0-bookworm-slimのイメージタグを使用する場合、フルOSイメージを選んでも、依存関係を絞った軽量版を使用しても、最新のNode.jsランタイムが含まれます。執筆時点では、これは偶数のバージョン(Node.js 22.1.0)ですが、まだ長期サポート(LTS)の対象にはなっていません。そのため、npm自体の最新バージョンなど、依存する他のコンポーネントの新しいバージョンも含まれます(npmは新しい挙動に不具合が生じることがあり、安定するまで時間がかかることで知られています)。
結論は?
理想的なNode.js Dockerイメージは、最新のDebian OSをベースにした軽量なOSイメージで、安定した現行の長期サポート版Node.jsを搭載しているものです。
その条件に合うのはnode:lts-bookworm-slimのNode.jsイメージタグです。私はタグを固定して使うことを推奨しているので、少し変更して、ltsエイリアスの代わりに実際の基盤となるバージョン番号を指定します。
最適なNode.js Dockerイメージタグは node:20.13.1-bookworm-slimです。
カスタムベースイメージをサポートできる成熟したDevOpsチームであれば、次点のおすすめはGoogleのdistrolessイメージタグです。公式Node.jsランタイムバージョンとのglibc互換性が維持されるためです。ただし、このワークフローにはメンテナンスが必要なので、それに対応できる場合に限っておすすめします。
DevSecOpsのためのコンテナセキュリティ
Snykでコンテナの脆弱性を無料で検出・修正しましょう。



