distrolessイメージでもENTRYPOINTを使いたい!
はじめに
この記事は、production環境のDockerイメージをalpineからdistrolessに変更した際、CMDではなくENTRYPOINTでアプリケーションを起動するようにした話を個人的な備忘録としてまとめたものです。
なお、Dockerのアップデートおよび仕様変更によって将来的にこの手順が使用できなくなる可能性があります。(記.2024/3/11)
distrolessって?
Dockerでアプリケーションの実行環境を構築する際、node:20
のように実行環境が含まれたイメージを引っ張ってくることがほとんどになると思います。ただ、そのままではdebianやubuntuに入っているアプリケーションの実行には関係ない (言ってしまえば余計な) パッケージが入っており、イメージサイズがとても大きくなってしまいます。
そこで使用するのがdistrolessで、これは本当に必要最低限のパッケージしか入っていない軽量なものになります。同じような軽量化イメージはalpineなど他にもありますが、巷ではalpineは脆弱性が放置されているためdistrolessを使用するのがベストプラクティスだと言われているようです。distrolessはGoogleが管理しており、シェルすら入っていない (RCEのリスクが激減) ため信頼性は高いと思います。
CMDとENTRYPOINTの違い
一般的には、CMDもENTRYPOINTもDockerfileの最後の行でアプリケーションを立ち上げるコマンドを実行するために使用されます。
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 8080
ENTRYPOINT [ "npm", "run", "start" ]
この2つの違いはCMDはデフォルト値のような扱い (上書き可能) なのに対し、ENTRYPOINTは必ずそのコマンドが実行されるという点にあります。CMDはコマンドラインでコンテナを実行するときに引数で実行コマンドの指定があるとそれに上書きされますが、ENTRYPOINTは必ず固定されます。厳密にはENTRYPOINTの後にCMDを書くとCMDの内容はENTRYPOINTの引数として機能したりといった仕様がありますが、この記事ではこれだけの認識で問題ないと思います。(参考記事)
実際のDockerfile
実際に使用しているDockerfileはこのようになっています。distrolessイメージではENTRYPOINTで実行しようとするとnode
のパスが通っていないため、/nodejs/bin/node
で直接ビルドしたJSファイルを実行しています。
ついでにステージを分けたりnonrootのユーザーで実行するようにしたりしているので、他にも何か参考になれば幸いです。
FROM node:20-alpine AS deps
ENV TZ=Asia/Tokyo
ENV NODE_ENV=production
WORKDIR /opt/app
COPY package.json .
COPY pnpm-lock.yaml .
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
#--------------------------------------------------
FROM node:20-alpine AS build
ENV TZ=Asia/Tokyo
WORKDIR /opt/app
COPY package.json .
COPY pnpm-lock.yaml .
RUN npm install -g pnpm
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build
#--------------------------------------------------
FROM gcr.io/distroless/nodejs20-debian12:nonroot AS runner
ENV TZ=Asia/Tokyo
ENV NODE_ENV=production
WORKDIR /opt/app
COPY /opt/app/node_modules ./node_modules
COPY /opt/app/build ./build
USER nonroot
ENTRYPOINT ["/nodejs/bin/node", "build/src/main.js"]
Discussion