Skip to main content

最適な Node.js Docker イメージを選択する

wordpress-sync/hero-docker-secrets

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 ランタイム自体の各種バージョンにマッピングされています。amd64arm64x8 (新しい Apple M1) などの CPU アーキテクチャをターゲットとする特定のバージョンタグもあります。

Debian ディストリビューションで最も一般的な node イメージタグの bullseyebuster は、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 ランタイムバージョンを指定していないため、nodenode:latest のエイリアスとなり、現在は Node.js バージョン 18.2.0 を指しています。

  • Node.js Docker イメージのサイズは 952 MB になります。

この最新の Node.js イメージの依存関係とセキュリティ脆弱性のフットプリントはどうなっているでしょうか?Snyk を使用したコンテナスキャンを docker scan dockerfile1 で実行すると、以下のことがわかります。

  • 依存関係は合計 409 件。これらは、curl/libcurl4 git/git-manimagemagick/imagemagick-6-common など、オペレーティングシステムのパッケージマネージャーで検出されたすべてのオープンソースライブラリになります。

  • これらの依存関係では、バッファオーバーフロー、Use After Free エラー、Out-of-bounds Write など、合計 289 件のセキュリティ問題が発見されています。

  • Node.js 18.2.0 ランタイムバージョンには、DNS リバインディングHTTP リクエストスマグリング設定ハイジャックなど、7 つのセキュリティ問題の脆弱性が存在します。

アプリケーションの Node.js イメージで wgetgitcurl を使用する必要は本当にあるのでしょうか?まず、全体的に見栄えがよくありません。Node.js Docker イメージには何百件もの依存関係とツールがあり、何百件もの脆弱性が存在するため、7 つの異なるセキュリティ脆弱性が含まれる Node.js ランタイムには、潜在的な攻撃の余地がたくさんあります。

Node.js Docker Hub のオプション node:buster 対 node:bullseye

Node.js Docker Hub リポジトリで利用可能なタグを参照すると、代替 Node.js イメージタグとして node:busternode: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 イメージタグをビルドして、上記の結果と比較すると、サイズ、依存関係の数、検出された脆弱性の数がまったく同じであることがわかります。nodenode:latestnode: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 とわずかに小さくなっています。

そこで、LTSCurrent の 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 の bullseyeslim などの 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 のイメージについては、ソフトウェアのフットプリントが大きく、セキュリティの脆弱性が増すために取り除き、小さなイメージタイプの作成に取り組むことができます。slimalpinedistroless の 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 互換性が確保されるためです。ただし、このワークフローではある程度管理が求められるため、対応できる場合にのみお勧めします。