[Docker]: Dockerのマルチステージビルドについてやってみる
[Docker]: Dockerのマルチステージビルドについてやってみる
なんとなくDockerを使用するのを卒業するという意味も込めて、ざっくりですがマルチステージビルドについて学びました。
初学者が作ったそれっぽいコンテナと思って見ていただければと思ってください。
今回はNext.js・Laravel(API)の構成を想定した以下のようなコンテナをマルチステージビルドで作ってみます。
理由としてはLaravelをDockerで構築するときにはいくつかのパッケージやライブラリ、composerをインストールするのでマルチステージビルドによるイメージサイズの違いがより数値としてより実感しやすいと考えました。
本番環境ではデータベースはAWS RDSやPostgreSQLなどデータベースサービスに, redisはUpstashやElastiCacheに接続することが予想されるので今回は割愛します。
- app(node)
- api(php)
- web(nginx)
- db(mysql)
- cache(redis)
リポジトリ
マルチステージビルドとは
- Dockerイメージを複数のステージに分けて構築することができる
-
FROM
を使用してイメージを利用 - ステージに分けることでそのステージごとに必要なファイルやライブラリを選択、コピーできるため不要な物を除外した軽量なイメージを作成できる
- 不要なライブラリやツールは除外することで脆弱性を減らし、セキュリティを強化することができる。→攻撃対象を減らす
主な構文
FROM
:
イメージやステージを指定する
AS
:
ステージに名前をつける
COPY --from=<stage>
: 指定したステージからファイルをコピーする
マルチステージビルドなしで実装
Dockerファイルのみのサイズ
test-multistage-build-app latest 09ac8427dcf1 2 months ago 1.12GB
test-multistage-build-api latest 582c9eb1939a 2 weeks ago 503MB
test-multistage-build-web latest c907445d05b9 10 days ago 192MB
test-multistage-build-db latest 4b7ca4355544 12 days ago 776MB
test-multistage-build-cache latest 9ad1c98c04f5 3 months ago 117MB
プロジェクトを作成した時点でのサイズ
test-multistage-build-app latest 6e086bad9911 8 minutes ago 1.12GB
test-multistage-build-api latest 4cfe67e869f5 5 minutes ago 623MB
test-multistage-build-web latest c907445d05b9 10 days ago 192MB
test-multistage-build-db latest 4b7ca4355544 12 days ago 776MB
test-multistage-build-cache latest 9ad1c98c04f5 3 months ago 117MB
その後playwrightなどなんやかんやインストールした最終サイズ
test-multistage-build-app latest 1b6cf9fce580 44 seconds ago 4.3GB
test-multistage-build-api latest 71184510e6c3 13 hours ago 623MB
test-multistage-build-web latest c907445d05b9 11 days ago 192MB
test-multistage-build-db latest 4b7ca4355544 12 days ago 776MB
test-multistage-build-cache latest 9ad1c98c04f5 3 months ago 117MB
マルチステージビルドで実装
nodeコンテナ
まず下記を参考にしつつ、コンテナを作成
###################################
#
# 開発環境
#
###################################
FROM node:22.14.0-slim AS dev
# 一時的にルートユーザーを使用
USER root
# グローバルインストール
RUN npm install -g npm@latest vercel@latest
# playwrightのインストール
RUN npx playwright install-deps
RUN npx playwright install
# 非ルートユーザーに切り替え
USER node
# ディレクトリの指定
WORKDIR /home/node/app
###################################
#
# ビルド環境
#
###################################
FROM dev AS build
# 非ルートユーザーに切り替え
USER node
# ディレクトリの指定
WORKDIR /home/node/app
COPY ./app/package*.json /home/node/app/
COPY ./app /home/node/app/
RUN npm ci
# ビルドを実行
RUN npm run build
# 不必要なパッケージを削除
RUN npm prune --production
###################################
#
# 本番環境
#
###################################
FROM gcr.io/distroless/nodejs22-debian12:nonroot AS production
# ディレクトリの指定
WORKDIR /app
# ビルドステージからファイルをコピー
COPY /home/node/package*.json ./
COPY /home/node/app/next.config.ts ./
COPY /home/node/app/public ./public
COPY /home/node/app/.next ./.next
COPY /home/node/app/node_modules ./node_modules
# 環境変数を設定
ENV NODE_ENV production
# Next.jsアプリケーションを起動
CMD ["node_modules/.bin/next", "start"]
結果
大体1/6にできました。このうちnode_modulesのサイズは482MB
です。まだ余分なパッケージがあるとは思うのですが、なかなか上手くいきませんでした。
test-multistage-build-app latest fc6def45b2f2 8 minutes ago 675MB
phpコンテナ
###################################
#
# 開発環境
#
###################################
FROM php:8.4-fpm AS dev
WORKDIR /home/php/api
RUN apt-get update \
&& apt-get install -y \
build-essential \
git \
zip \
unzip \
vim \
curl \
libfreetype6-dev \
libjpeg62-turbo-dev \
libmcrypt-dev \
libpng-dev && \
pecl install redis && \
docker-php-ext-enable redis
RUN docker-php-ext-install bcmath
RUN docker-php-ext-install pdo_mysql mysqli exif
RUN cd /usr/bin && curl -s http://getcomposer.org/installer | php && ln -s /usr/bin/composer.phar /usr/bin/composer
COPY ./container/api/php.ini /usr/local/etc/php/php.ini
# 非ルートユーザーに切り替え
RUN useradd -ms /bin/bash php && \
echo "php:php" | chpasswd
USER php
###################################
#
# 本番環境
#
###################################
FROM php:8.4-fpm-alpine AS production
WORKDIR /home/php/api
COPY ./api /home/php/api
# COPY --from=dev --chown=php:php composer*.json ./
COPY /usr/bin/composer /usr/bin/composer
COPY /usr/local/etc/php/php.ini /usr/local/etc/php/php.ini
RUN composer install --no-scripts --no-autoloader
# 非ルートユーザーに切り替え
RUN addgroup -S -g "1000" "php" && \
adduser -u "1000" -G "php" -D "php"
USER php
VOLUME ["/var/run/php-fpm"]
CMD ["php-fpm", "-R"]
zend.exception_ignore_args = off
expose_php = on
max_execution_time = 30
max_input_vars = 1000
upload_max_filesize = 64M
post_max_size = 128M
memory_limit = 256M
error_reporting = E_ALL
display_errors = on
display_startup_errors = on
log_errors = on
error_log = /var/log/php/php-error.log
default_charset = UTF-8
[Date]
date.timezone = Asia/Tokyo
[mysqlnd]
mysqlnd.collect_memory_statistics = on
[Assertion]
zend.assertions = 1
[mbstring]
mbstring.language = Japanese
結果
大体1/6となりました。本番用の環境をalpineベースのイメージを使用してるため、当たり前と言われたら当たり前なのですが。(-_-;)
test-multistage-build-api latest 2fb32a6411af 18 seconds ago 148MB
念のため、Trivyを使用して脆弱性をチェック
参考記事より
Trivy は、Aqua Security社が提供するオープンソースの脆弱性スキャナです。コンテナイメージ、ファイルシステム、Kubernetes、リポジトリなどに潜む脆弱性を迅速かつ簡単に検出することができます。
Trivyの特徴としては、以下が挙げられます:
- 多彩なスキャン対象: Dockerイメージやファイルシステム、Kubernetesクラスタ、IaC(Infrastructure as Code)などに対応
- 高精度な脆弱性検出: 詳細な脆弱性データベース(NVDやRed Hat、Ubuntuなど)を活用し、精度の高いスキャンを実現
- 簡単なセットアップ: 初期設定が非常に簡単で、短時間でスキャンを実行可能
実行方法
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock aquasec/trivy image イメージ名
結果
本来は表のような出力結果が表示されますが、端折ってます。
nodejsコンテナ
- @babel/runtime (package.json)│CVE-2025-27789│MEDIUM│fixed│7.22.5
phpコンテナ
- sqlite-libs│CVE-2025-29087│MEDIUM│fixed│3.48.0-r0│ 3.48.0-r1
今回の構築を通して、Linuxの特にユーザーや権限等、今回の環境であるnodejs, phpなどの理解が必要であると再認識できました。やはりLinuxがないと今後Dockerが活かせないので今後本等を読んで知識を深めていきたい。
参考
最後に
間違っていることやセキュリティ上のベストプラクティスでこういう書き方の方が良いよなどあればコメントに書いていただけると幸いです。
よろしくお願いいたします。
Discussion