最適な Node.js Docker イメージを選択する
2022年9月30日
0 分で読めます編集者注:
2022 年 10 月 21 日: この記事は、Node.js Alpine Linux イメージと本番環境で使用する他の Node.js コンテナイメージの比較をわかりやすくするために更新しました。
Node.js Docker イメージの選択は些細なことのように思えるかもしれませんが、イメージサイズと潜在的な脆弱性は、CI/CD パイプラインとセキュリティ体制に劇的な影響を与える可能性があります。では、最適な Node.js Docker イメージを選択するにはどうすればいいでしょうか?
FROM node:latest
、または単に FROM node
(前者のエイリアス) を使用することの潜在的なリスクは見落とされがちです。CI/CD パイプライン全体のセキュリティリスクやファイルサイズを認識していない場合はなおさらです。
以下は、Node.js Docker イメージのチュートリアルやブログ記事で一般的に参照されている Node.js Dockerfile の一例です。ただし、この Dockerfile には大きな欠陥があり、推奨されません。
FROM node
WORKDIR /usr/src/app
COPY . /usr/src/app
RUN npm install
CMD "npm" "start"
以前、Docker で Node.js ウェブアプリケーションをコンテナ化する 10 のベストプラクティスについての記事を投稿し、ステップバイステップのガイドを用意したことがありますが、今回はその例を土台に改善を加え、本番環境で使用できる Node.js Docker イメージを作成してみます。
この記事では、理想的な Node.js Docker イメージについて理解するために、Dockerfile
の内容としてあえて上記の例を使用しています。
Node.js Docker イメージのオプション
Node.js イメージのビルドで選択できるオプションは、実際にはかなり多くあります。Node.js のコアチームが管理している公式の Node.js Docker イメージから、特定の Docker ベースイメージから選択できる特定の Node.js イメージタグ、さらには Google の distroless
プロジェクトや Docker チームが提供する必要最低限の scratch
イメージに Node.js アプリケーションをビルドするという他のオプションまで、さまざまなものがあります。
これらすべてのオプションのうち、どの Node.js Docker イメージが理想的なのでしょうか。
1 つずつ見て、利点と潜在的なリスクについて詳しく学びましょう。
著者注: この記事では、2022 年 6 月頃に最後に Node.js 18.2.0 としてリリースされた特定の Node.js バージョンを比較しています。
デフォルトの node イメージ
まずは管理されている node
イメージから見ていきましょう。Node.js Docker チーム
によって公式に管理されており、いくつかの Docker ベースのイメージタグが含まれています。これらは各種基盤ディストリビューション (Debian、Ubuntu、Alpine) や Node.js ランタイム自体の各種バージョンにマッピングされています。amd64
や arm64x8
(新しい Apple M1) などの CPU アーキテクチャをターゲットとする特定のバージョンタグもあります。
Debian ディストリビューションで最も一般的な node
イメージタグの bullseye
や buster
は、buildpack-deps
をベースにしており、それ自体は別のチームによって管理されています。
このデフォルトの node
イメージをベースに、fastify
npm の依存関係のみを含めて、Node.js Docker イメージをビルドするとどうなるでしょうか?
FROM node
WORKDIR /app
RUN npm install fastify
docker build --no-cache -f Dockerfile1 -t dockerfile1
を使用してイメージをビルドすると、以下のようになります。
Node.js ランタイムバージョンを指定していないため、
node
はnode:latest
のエイリアスとなり、現在は Node.js バージョン 18.2.0 を指しています。Node.js Docker イメージのサイズは 952 MB になります。
この最新の Node.js イメージの依存関係とセキュリティ脆弱性のフットプリントはどうなっているでしょうか?Snyk を使用したコンテナスキャンを docker scan dockerfile1
で実行すると、以下のことがわかります。
依存関係は合計 409 件。これらは、
curl/libcurl4
やgit/git-man
、imagemagick/imagemagick-6-common
など、オペレーティングシステムのパッケージマネージャーで検出されたすべてのオープンソースライブラリになります。これらの依存関係では、バッファオーバーフロー、Use After Free エラー、Out-of-bounds Write など、合計 289 件のセキュリティ問題が発見されています。
Node.js 18.2.0 ランタイムバージョンには、DNS リバインディング、HTTP リクエストスマグリング、設定ハイジャックなど、7 つのセキュリティ問題の脆弱性が存在します。
アプリケーションの Node.js イメージで wget
、git
、curl
を使用する必要は本当にあるのでしょうか?まず、全体的に見栄えがよくありません。Node.js Docker イメージには何百件もの依存関係とツールがあり、何百件もの脆弱性が存在するため、7 つの異なるセキュリティ脆弱性が含まれる Node.js ランタイムには、潜在的な攻撃の余地がたくさんあります。
Node.js Docker Hub のオプション node:buster 対 node:bullseye
Node.js Docker Hub リポジトリで利用可能なタグを参照すると、代替 Node.js イメージタグとして node:buster
と node:bullseye
の 2 つのオプションが見つかります。
これらの Docker イメージタグはいずれも Debian ディストリビューションバージョンをベースにしています。buster
イメージタグは、2022 年 8 月から 2024 年にかけてサポート終了を迎える Debian 10 に対応しているため、あまり良い選択ではありません。bullseye
イメージタグは Debian 11 に対応し、Debian の現在の安定版リリースとされていて、2026 年 6 月にサポート終了が予定されています。
著者注: このため、新規および既存のすべての Node.js Docker イメージを node:buster
イメージタグから node:bullseye
または他の適切な代替タグに移行することを強くお勧めします。
以下をベースに新しい Node.js Docker イメージをビルドしてみましょう。
FROM node:bookworm
この Node.js の Docker イメージタグをビルドして、上記の結果と比較すると、サイズ、依存関係の数、検出された脆弱性の数がまったく同じであることがわかります。node
、node:latest
、node:bullseye
はすべてビルドされた同じ Node.js のイメージタグを指しているためです。
Node.js イメージタグをスリムにする
公式の Node.js Docker チームも、動作する Node.js 環境に必要なツールだけを明示的にターゲットとするイメージタグを管理しています。
これらの Node.js イメージタグは、node:bullseye-slim
などの slim
イメージタグバリアント、または node:14.19.2-slim
などの Node.js バージョン固有として参照されます。
Debian の現在の安定版リリースである bullseye
をベースに Node.js の slim
イメージをビルドしてみましょう。
FROM node:bookworm-slim
これでイメージサイズは劇的に減少し、1 GB 近くのコンテナイメージからイメージサイズは 246 MB になりました。その内容をスキャンしてみると、ソフトウェア全体のフットプリントが大幅に減少しており、依存関係は 97 件、脆弱性は 56 件にとどまっています。
node:bullseye-slim
は、コンテナイメージのサイズとセキュリティ体制の観点から優れた出発点となります。
LTS Node.js Docker イメージ
これまで、Node.js Docker イメージは、Node.js の現行バージョンの Node.js 18 をベースにしていました。ただし、Node.js のリリーススケジュールによると、このバージョンが公式に Active LTS
ステータスになったのは 2022 年 10 月です。
もし、ビルドする Node.js Docker イメージで常に長期サポート (LTS) バージョンに依存していたとしたらどうでしょうか?Docker のイメージタグは随時更新して新しい Node.js イメージをビルドしましょう。
FROM node:lts-bookworm-slim
スリムな Node.js LTS バージョン (16.15.0) では、イメージの依存関係とセキュリティの脆弱性の数が同程度で、イメージサイズは 188 MB とわずかに小さくなっています。
そこで、LTS
と Current
の Node.js ランタイムバージョンのどちらを選択するかについて何らかの要件があるかもしれませんが、いずれにしても Node.js イメージのソフトウェアフットプリントに大きな影響を与えるものではありません。
node:alpine は Node.js Docker イメージの優れた選択となりますか?
Node.js Docker チームは、node:alpine
イメージタグとそのバリアントを管理して、Alpine Linux ディストリビューションの特定のバージョンを Node.js ランタイムのバージョンと対応させています。
Alpine Linux プロジェクトは、信じられないほど小さいイメージサイズでよく引き合いに出されますが、ソフトウェアのフットプリントが小さいことにより、参照される脆弱性サーフェスが小さくなるため、優れた選択肢となります。以下のコマンドは、Dockerfile に node をビルドするように指示するもので、その時点で、圧縮されていないイメージのサイズが大きくなります。
FROM node:alpine
...
これにより、Docker イメージのサイズは 178 MB となり、slim
Node.js イメージの 188 MB と比較的同じですが、Alpine イメージタグでは、OS 依存関係が計 16 件、セキュリティの脆弱性は 2 件のみ検出されています。これにより、alpine
イメージタグがイメージサイズを小さくし、脆弱性の数を少なくする点で良い選択肢であることがわかります。
node:alpine
は Node.js Docker イメージの選択肢としてより優れていますか?
Alpine の Node.js イメージバリアントは、全体的なイメージサイズが小さく、脆弱性の数はさらに少なくなるかもしれません。ただし、Alpine プロジェクトでは、C 標準ライブラリの実装として musl を使用するのに対して、Debian の bullseye
や slim
などの Node.js イメージタグは glibc
実装に依存していることを認識することが重要です。これらの違いにより、パフォーマンスの問題や機能のバグが生じ、異なる C ライブラリを使用することでアプリケーションクラッシュの原因となる可能性があります。イタマール・ターナー=トラウリング氏 (Itamar Turner-Trauring) は、Python Docker イメージの Alpine イメージタグに関連する想定外のランタイムの問題が生じた経験についても投稿しています。
Node.js alpine
イメージタグを選択するということは、事実上、非公式の Node.js ランタイムを選択していることになります。Node.js Docker チームは、Alpine ベースのコンテナイメージのビルドを公式にはサポートしていません。そのため、Alpine ベースのイメージタグは実験的であり、一貫性がない可能性があるとし、非公式ビルド
から利用できるようにしています。ここで非公式ビルドイメージタグのレポジトリから引用します。
非公式ビルドは、Node.js でサポートされていない、あるいは部分的にしかサポートされていないいくつかのプラットフォーム向けに、基本的な Node.js バイナリの提供を試みるものです。このプロジェクトは、いかなる保証も行わず、その結果も厳密には検証されていません。nodejs.org で入手できるビルドには、コードの品質、関連するプラットフォームでのサポート、配信のタイミングや方法に関して非常に高い品質基準があります。非公式ビルドから入手できるビルドのテストは最小限か、まったく行われていません。プラットフォームが公式の Node.js テストインフラには含まれていない可能性があります。これらのビルドは、ユーザーコミュニティの便宜のために提供されており、コミュニティにはメンテナンスを支援することが期待されています。
Node.js alpine
イメージタグの互換性で注目すべき点は以下のとおりです。
Yarn に互換性がない (issue #1716)。
ネイティブ C バインディングのクロスコンパイルに
node-gyp
が必要な場合、プロセスの依存関係にある Python は Alpine イメージでは利用できないため、自分で解決する必要がある (issue #1706)。
Alpine ベースの Node.js Docker イメージを使用する場合、Docker セキュリティツール (Trivy や Snyk など) では現在、Alpine ベースイメージでランタイム関連の脆弱性を検出できないことに注意してください。将来的には変更されるかもしれませんが、現在のところ、Node.js 18.2.0 のランタイム自体に脆弱性があり、Node.js 18.2.0 alpine
ベースイメージタグのセキュリティ脆弱性を発見することはできません。これはセキュリティツールそのものに関わることで、Alpine ベースイメージには関連しませんが、それでも考慮すべき点ではあります。
Node.js のディストロレス Docker イメージ
ベンチマークの最後の比較項目は、Google のディストロレスコンテナイメージになります。
ディストロレス Docker イメージとは何ですか?
これらのイメージは、アプリケーションとランタイムの依存関係のみをターゲットとしているため、slim
Node.js イメージタグよりもさらにスリムです。そのため、ディストロレス Docker イメージには、コンテナパッケージマネージャー、シェル、その他の汎用ツールの依存関係がないため、サイズと脆弱性のフットプリントが小さくなります。
幸いなことに、Distroless プロジェクトでは Node.js のランタイム固有のディストロレス Docker イメージが管理されており、完全な名前空間によって gcr.io/distroless/nodejs-debian11
として識別され、Google のコンテナレジストリ (gcr.io
部分) で利用です。
Distroless コンテナイメージにはソフトウェアがないため、Docker マルチステージワークフローを使用して、コンテナの依存関係をインストールし、ディストロレスイメージにコピーできます。
FROM node:22-bookworm-slim AS build
WORKDIR /app
COPY . /app
RUN npm install
FROM gcr.io/distroless/nodejs22-debian12
COPY --from=build /app /usr/src/app
WORKDIR /usr/src/app
CMD ["server.js"]
このディストロレス Docker イメージをビルドすると、112 MB のファイルになり、slim
イメージタグや alpine
イメージタグのバリアントに比べてファイルサイズが大幅に縮小されます。
ディストロレス Docker イメージの使用を検討する場合は、重要な考慮事項がいくつかあります。
ディストロレス Docker イメージは、現在の安定した Debian リリースバージョンをベースにしています。つまり、最新版で「使用期限」がかなり先になることを意味すため、非常に良いことです。
ディストロレス Docker イメージは Debian ベースのため、glibc の実装に依存し、本番環境で想定外の問題が発生することはほとんどありません。
使い始めるとまもなく、Distroless チームが Node.js ランタイムバージョンをさほどきめ細かく管理していないことに気づくことでしょう。これは、頻繁に更新される汎用の
nodejs:16
タグに依存するか、特定の時点でイメージの SHA256 ハッシュを元にインストールする必要があることを意味します。
Node.js Docker イメージタグの比較
Node.js Docker の各種イメージタグの比較については、以下の表を参照してください。
イメージタグ | Node.js ランタイムバージョン | OS 依存関係 | OS セキュリティの脆弱性 | 高いおよび重大な脆弱性 | 中程度の脆弱性 | 低い脆弱性 | Node.js ランタイムの脆弱性 | イメージサイズ | Yarn 利用可能 |
---|---|---|---|---|---|---|---|---|---|
ノード | 18.2.0 | 409 | 289 | 54 | 18 | 217 | 7 | 952MB | Yes |
node:bullseye | 18.2.0 | 409 | 289 | 54 | 18 | 217 | 7 | 952MB | Yes |
node:bullseye-slim | 18.2.0 | 97 | 56 | 4 | 8 | 44 | 7 | 246MB | Yes |
node:lts-bullseye-slim | 16.15.0 | 97 | 55 | 4 | 7 | 44 | 6 | 188MB | Yes |
node:alpine | 18.2.0 | 16 | 2 | 2 | 0 | 0 | 0 | 178MB | Yes |
gcr.io/distroless/nodejs:16 | 16.17.0 | 9 | 11 | 0 | 0 | 11 | 0 | 112MB | No |
Node.js のさまざまなイメージタグを通して学んだデータと洞察を調べ、どれが最も理想的かを判断してみましょう。
開発の同等性
Node.js イメージタグの選択が開発の一貫性に帰着する、つまり開発環境と本番環境がまったく同じになるように最適化したいという状況の場合、これはもうすでに負け戦になっているかもしれません。ほとんどの場合、3 大オペレーティングシステムはすべて異なる C ライブラリの実装を使用しています。Linux は glibc に、Alpine は musl に、macOS は独自の BSD libc の実装に依存しています。
Docker イメージサイズ
ときにはサイズも重要です。ただし、もっと正確に言うと、単にサイズの最小化ではなく、全体的なソフトウェアフットプリントの最小化が目標になります。その場合、slim
イメージタグは alpine
と比較して、あまりサイズが変わらず、どちらもコンテナイメージの平均が 200 MB 程度になっています。確かに、slim
イメージのソフトウェアフットプリントは依然としてかなり大きく (97 件に対して alpine
は 16 件)、脆弱性サーフェス (`slim` の 56 件に対して `alpine` の `2` 件) も大きくなっています。
セキュリティの脆弱性
脆弱性は重要な問題で、コンテナイメージのサイズを小さくすべき理由について説明する多くの記事のテーマとなってきました。ただし、セキュリティ問題をどう扱うかが非常に重要です。
node
および node:bullseye
のイメージについては、ソフトウェアのフットプリントが大きく、セキュリティの脆弱性が増すために取り除き、小さなイメージタイプの作成に取り組むことができます。slim
、alpine
、distroless
の 3 つを比較すると、高から重大のセキュリティ脆弱性のバリエーションは絶対数としては大きくなく、0 件から 4 件の間で、アプリケーションのユースケースとしてはそれほど問題のない、管理可能なリスクです。
サポートおよびレジリエンス
Node.js Docker チームがコンテナイメージのビルドの問題に優先順位を設定して対応し、問題をタイムリーに解決できるようにすることには、大きな意味があります。公式の Debian ベースのイメージタグでないものに関しては、基本的にチェックリストから外すことはできません。
node
または node:bullseye-slim
のイメージタグを使用すると、完全なオペレーティングシステムのイメージを選択する場合でも、スリムなバージョンの依存関係を使用する場合でも、最新バージョンの Node.js ランタイムを取得できます。偶数バージョン (Node.js 18.2.0) ですが、この記事の執筆時点ではまだ長期サポートのライフサイクルに含まれていないため、npm
自体の最新バージョン (新しい動作にバグがあり、安定するまで時間がかかる) など他の依存コンポーネントの新バージョンがバンドルされることになると思われます。
結論
最も理想的な Node.js Docker イメージは、最新の Debian OS をベースに、安定かつアクティブな長期サポート版の Node.js を搭載した、スリムなバージョンのオペレーティングシステムでしょう。
その場合、node:lts-bullseye-slim
Node.js イメージタグを選択することになります。私は、決定的なイメージタグを使用するのが良いと考えており、lts
エイリアスの代わりに、ベースとなる実際のバージョン番号という若干の変更で使用するようにしています。
最も理想的な Node.js Docker イメージタグは、node:16.17.0-bullseye-slim
となります。
もし、経験豊富な DevOps チームで働いていて、カスタムベースイメージをサポートできるのであれば、次にお勧めするのは Googleの distrolessイメージタグです。これにより、公式の Node.js ランタイムバージョンの glibc 互換性が確保されるためです。ただし、このワークフローではある程度管理が求められるため、対応できる場合にのみお勧めします。