🐳

Node.js on Dockerでスリムかつセキュアなコンテナイメージを作る - 2025年版

に公開1

はじめに

みなさんはDockerfileを書いていますか?
近年はAIによる支援でDockerfileをスクラッチで書く機会が減っていると考えられます。
しかしながら、AIに書かせる場合でも良い書き方を知っていることで、その後の修正が容易になります。

この記事では、Node.jsアプリケーションのDockerfileを作成する際に、私ならこのように書くという提案をします。

作成するDockerfileとディレクトリ構造

初めに、完成形のDockerfileとディレクトリ構造を示します。
以降のセクションで実装のポイントを解説していきます。

実行可能なサンプルコードはGitHubで公開しているので、必要に応じてこちらも参照してください。
https://github.com/hosht/node-docker-example-for-zenn-article

Dockerfile

各種バージョンは記事執筆時点での最新版です。

# syntax=docker/dockerfile:1.20.0
FROM node:24.11.1-bookworm-slim AS builder
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable pnpm
WORKDIR /app
COPY pnpm-lock.yaml /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch
COPY . /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --offline
RUN pnpm run build
RUN pnpm prune --prod --ignore-scripts

FROM gcr.io/distroless/nodejs24-debian12:nonroot AS app
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/dist /app/dist
COPY --from=builder --chown=nonroot:nonroot /app/node_modules /app/node_modules
COPY --from=builder --chown=nonroot:nonroot /app/package.json /app/package.json
ENV NODE_OPTIONS="--enable-source-maps"
CMD ["dist/index.js"]

ディレクトリ構造

.
├── Dockerfile
├── LICENSE
├── package.json
├── pnpm-lock.yaml
├── src
│   └── index.ts
└── tsconfig.json

Dockerfile全行解説

# syntax=docker/dockerfile:1.20.0

Dockerfileの構文を指定します。
バージョンの固定にこだわりがなければdocker/dockerfile:1にします。

FROM node:24.11.1-bookworm-slim AS builder

Node.jsイメージのバージョン指定は24.11.1-bookworm-slimにしていますが、お好みでハッシュ値にします。

ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"

pnpmが今回の構成で中心的役割を果たします。

RUN corepack enable pnpm

corepackを使ってpnpmを有効化します。
package.jsonのpackageManagerフィールドでpnpmのバージョンを指定しておきましょう。

WORKDIR /app
COPY pnpm-lock.yaml /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm fetch

先行してpnpm-lock.yamlをコピーしてから、pnpm fetchの準備をします。
pnpm fetchはpnpm-lock.yamlの内容に基づいて仮想ストアにパッケージをダウンロードします。
package.jsonに依存しないため、package.jsonに変更がない場合にキャッシュが効きやすくなります。
公式ドキュメントでも言及されているように、Dockerイメージの作成に役立つコマンドです。
https://pnpm.io/cli/fetch
--mount=type=cache,id=pnpm,target=/pnpm/storeを指定することで、実行環境によってはビルド毎にキャッシュを再利用可能です。

COPY . /app
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --offline

pnpm fetchで先行してダウンロードした依存パッケージをインストールします。
--offlineオプションを付与することで、ネットワークアクセスを行わず高速に実行可能です。

RUN pnpm run build
RUN pnpm prune --prod --ignore-scripts

ビルドステージのnode_modulesは実行環境にコピーするのでpnpm prune --prodでdevDependenciesを削除します。

FROM gcr.io/distroless/nodejs24-debian12:nonroot AS app

実行環境にはDistrolessイメージを使用します。
Distrolessはnonrootイメージが提供されており、利用可能な場面では積極的に採用します。

ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder --chown=nonroot:nonroot /app/dist /app/dist
COPY --from=builder --chown=nonroot:nonroot /app/node_modules /app/node_modules
COPY --from=builder --chown=nonroot:nonroot /app/package.json /app/package.json

ビルドステージからアプリケーションの実行に必要なファイルのみをコピーします。
--chown=nonroot:nonrootを指定して、ファイルの所有者をnonrootユーザーに変更します。

ENV NODE_OPTIONS="--enable-source-maps"
CMD ["dist/index.js"]

--enable-source-mapsオプションを指定して、スタックトレースにソースコードの情報を含めます。
CMDはpackage.jsonのmainフィールドを記載している場合は["."]でカレントディレクトリのみを指定可能です。

おわりに

この記事はpnpm公式のWorking with Dockerを参考にDockerfileを作成したところ、細かい改善ポイントが見つかったため、改修を加えた内容になります。
pnpmは機能追加が頻繁に行われているためリリースを継続的にウォッチすることをお勧めします。
https://pnpm.io/docker

この記事はNode.js、TypeScript、pnpmを前提とした内容になりますが、他のパッケージマネージャーやランタイムでも取り入れられる考え方や手法があるので参考にしてみてください。

Appendix

GitHubで編集を提案
Bitkey Developers

Discussion