📦

Node.js のコンテナイメージを作成するときに気をつけていること

2023/04/05に公開

自分が Node.js を用いたアプリケーションのコンテナイメージを作成するときに気をつけていることをメモとして記載します。

Docker and Node.js Best Practices

node の Docker Image のリポジトリには Best Practices というドキュメントが用意されています。
基本的には、これに従って Dockerfile を書いています。

https://github.com/nodejs/docker-node

Handling Kernel Signals

上記の Best Practices にも記載がありますが、Node.js は PID 1 で動作するように設計されていないようで、そのままコンテナイメージで動かそうとすると SIGINT 等のシグナルを上手く扱ってくれません。
そのため tini 等を PID 1 で動かし、その子プロセスで Node.js を動かします。
https://github.com/krallin/tini

docker コマンドには --init フラグがあるため、これを使うと tini を導入しなくても動作しますが Kubernetes 等のサポートが無い環境もあるため、コンテナイメージを作成するときにあらかじめ tini を導入するようにしています。

FROM node:18 as builder

# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini-static
RUN chmod +x /tini-static

# 略

ENTRYPOINT ["/tini-static", "--"]

distroless

ベースイメージは Node.js のランタイムが入ったイメージが用意されている distroless を採用することが多いです。
shell や package manager も入っていないイメージになっているのでサイズが小さいです。
また、自分がセキュリティや脆弱性周りに詳しくないので distroless 側での対応に頼って、運用コストを下げたいという意図もあります。
https://github.com/GoogleContainerTools/distroless

FROM gcr.io/distroless/nodejs18-debian11:latest

COPY --from=builder /tini-static /tini-static

ENTRYPOINT ["/tini-static", "--", "/nodejs/bin/node"]

distroless ではデフォルトで nodeENTRYPOINT に設定されているため、上記のように tini から distroless 内の node を呼び出すような ENTRYPOINT に変更しています。
distroless は Bazel でイメージを作成しており node の場所を探すのに少々苦労したのですが、この辺り を見て設定しています。

まとめ

ここまで記載したことをまとめると以下のような Dockerfile をベースに要件に応じて作り変えています。
Multi-stage builds を使い、オフィシャルの node イメージでビルドを行い distroless をベースイメージとしてアプリケーションのコンテナイメージを作成しています。

FROM node:18 as builder

# Add Tini
ENV TINI_VERSION v0.19.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-static /tini-static
RUN chmod +x /tini-static

# アプリケーションに必要なビルド等

FROM gcr.io/distroless/nodejs18-debian11:latest

COPY --from=builder /tini-static /tini-static

ENTRYPOINT ["/tini-static", "--", "/nodejs/bin/node"]
# CMD ["server.js"]

Discussion