DockerfileのマルチステージビルドとCompose設計|本番イメージの軽量化・セキュリティ・環境分離の実践手順

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Docker > DockerfileのマルチステージビルドとCompose設計|本番イメージの軽量化・セキュリティ・環境分離の実践手順
「Dockerfileを書いてみたけど、イメージが500MBを超えている」「開発環境と本番環境でDockerfileを使い分けたい」
そう感じたことはないでしょうか。

Dockerfileの基本構文は比較的すぐ覚えられますが、本番環境で安全に使えるイメージを作るとなると話が変わります。サイズの肥大化、rootで動くコンテナ、開発用ツールが残ったまま本番に出てしまう——こうした問題は、設計をきちんと理解していないと見過ごしがちです。

この記事では、マルチステージビルドを中心に、本番向けDockerfileの設計と、Docker Composeを使った環境分離の実践手順を解説します。単なるコマンドの紹介ではなく、「なぜそう書くのか」という設計の意図まで踏み込みます。

この記事のポイント

・マルチステージビルドで本番イメージを1/10以下に軽量化できる
・Dockerfile設計の鉄則はCOPY --from でビルド成果物だけを本番に持ち込むこと
・コンテナをroot以外のユーザーで動かすことがセキュリティの最低ライン
・Docker Composeのoverride構成で開発・本番の環境差分をコードで管理する


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

なぜDockerfileの設計が重要なのか

Dockerfileは「動けばいい」で書いてしまうと、後で必ず痛い目に遭います。よくある失敗パターンを3つ挙げます。

1. イメージが巨大になる
`python:3.12` のベースイメージにpipでパッケージを入れると、すぐ1GB超えが起きます。コンパイラやヘッダーファイルなど、ビルドにしか使わないツールが本番イメージに丸ごと含まれるためです。

2. rootでプロセスが動く
デフォルトでコンテナ内のプロセスはrootで動きます。コンテナブレイクアウト(コンテナ脱出)攻撃が成功したとき、rootのままではホストへの影響が甚大です。

3. 開発用の認証情報が残る `.env` ファイルや開発用APIキーをビルドコンテキストに含めてしまい、本番イメージにそのまま焼き込まれてしまうケースです。イメージをDocker Hubにpushした瞬間に情報漏洩が起きます。

この3つを解消するのが、マルチステージビルドと適切なCompose設計です。

動作確認環境: RHEL 9.4 / Ubuntu 24.04 LTS / Docker Engine 27.x

マルチステージビルドの基本構造

1. シングルステージの問題点を確認する

まず、よくあるシングルステージのDockerfileを見てみましょう。

# NG例: ビルドツールが本番イメージに残る FROM python:3.12 WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD ["python", "app.py"]

このDockerfileをビルドすると、python:3.12のベースイメージ(約1GB)+ライブラリが含まれます。アプリ本体は数十MBでも、実行に不要なものが大量についてきます。

2. マルチステージビルドへ書き直す

マルチステージビルドは、1つのDockerfileの中で複数の `FROM` 命令を使い、「ビルド用ステージ」と「実行用ステージ」を分離する技術です。

# Stage 1: ビルドステージ(builder) FROM python:3.12-slim AS builder WORKDIR /build COPY requirements.txt . RUN pip install --no-cache-dir --prefix=/install -r requirements.txt # Stage 2: 実行ステージ(本番用) FROM python:3.12-slim WORKDIR /app # builderステージの成果物だけをコピー COPY --from=builder /install /usr/local # アプリケーションのソースコードをコピー COPY app.py . # rootで動かさない RUN groupadd -r appuser && useradd -r -g appuser appuser USER appuser CMD ["python", "app.py"]

`COPY --from=builder` の一行が核心です。builderステージで作ったPythonパッケージのインストール先(`/install`)だけを本番ステージに持ち込み、コンパイラやpipの本体は捨てます。

実際のビルド結果を確認してみます。

$ docker build -t myapp:prod . $ docker images myapp REPOSITORY TAG IMAGE ID CREATED SIZE myapp prod a3f1b2c4d5e6 2 minutes ago 189MB

ビルドツール込みのシングルステージ版が850MBだったとすると、同じアプリで189MBまで縮小できます。

3. マルチステージビルドのステージ構成を理解する

ステージは2つに限りません。たとえばJavaのアプリケーションでは次のような3ステージ構成が一般的です。

# Stage 1: 依存関係の解決 FROM maven:3.9-eclipse-temurin-21 AS deps WORKDIR /build COPY pom.xml . RUN mvn dependency:go-offline # Stage 2: コンパイル・テスト FROM deps AS builder COPY src ./src RUN mvn package -DskipTests=false # Stage 3: 実行(JREのみ) FROM eclipse-temurin:21-jre WORKDIR /app COPY --from=builder /build/target/app.jar . RUN groupadd -r javauser && useradd -r -g javauser javauser USER javauser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

Maven(ビルドツール)はStage 1・2にしか存在せず、本番ステージはJREだけで動きます。

Dockerfile設計の重要ポイント

1. .dockerignoreで不要ファイルを除外する

Dockerfileと同じディレクトリに `.dockerignore` を置くことで、ビルドコンテキストから除外するファイルを指定できます。これを怠ると、`.git` ディレクトリや `.env` ファイルがイメージに混入するリスクがあります。

# .dockerignore の例 .git .env .env.* *.log __pycache__ .pytest_cache node_modules README.md

特に `.env` ファイルの除外は必須です。開発用の認証情報が含まれていることが多く、イメージ内に残るとDockerイメージ全体が情報漏洩の媒体になります。

2. レイヤーキャッシュを意識した命令の順序

Dockerはレイヤーをキャッシュするため、命令の順序がビルド時間に直結します。

# NG: ソースを先にコピーするとキャッシュが効かない FROM python:3.12-slim WORKDIR /app COPY . . # ソース変更のたびにRUNが走る RUN pip install -r requirements.txt # OK: 依存関係ファイルを先にコピーしてキャッシュを活用 FROM python:3.12-slim WORKDIR /app COPY requirements.txt . # requirements.txtが変わらない限りキャッシュ有効 RUN pip install --no-cache-dir -r requirements.txt COPY . . # ソースは最後

「変更頻度が低いものを先に」が鉄則です。依存関係(requirements.txt、package.json等)は頻繁には変わらないため、先にコピー&インストールしておけば、ソースコードを変更しても `pip install` のレイヤーキャッシュが再利用されます。

3. RUN命令はなるべくまとめる

`RUN` を複数行に分けると、その分だけレイヤーが増えてイメージが肥大化します。

# NG: RUNを分けるとレイヤーが3つ増える RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # OK: &&でまとめてレイヤーを1つに RUN apt-get update && \ apt-get install -y --no-install-recommends curl && \ rm -rf /var/lib/apt/lists/*

`rm -rf /var/lib/apt/lists/*` を同一の `RUN` に入れることも重要です。別のレイヤーで削除しても、先のレイヤーにはデータが残り続けるため、イメージサイズの削減になりません。

4. rootで動かさない(USER命令)

コンテナ内のプロセスをroot以外で動かすことは、最低限のセキュリティ対策です。

# ユーザーとグループを作成 RUN groupadd --gid 1001 appuser && \ useradd --uid 1001 --gid 1001 --no-create-home appuser # ファイルの所有者をappuserに変更 RUN chown -R appuser:appuser /app # 以降の命令はappuserとして実行 USER appuser

rootでないと動かないアプリもありますが、多くのWebアプリやAPIサーバーはroot権限不要です。UID/GIDを固定しておくと、ホスト側でのファイル権限管理も明確になります。

Docker Composeで環境分離する設計

1. docker-compose.ymlの基本構成

開発環境と本番環境で設定を分離するために、Composeでは **override構成** を使うのが定番です。

# ディレクトリ構成 project/ ├── Dockerfile ├── docker-compose.yml # 共通設定 ├── docker-compose.override.yml # 開発環境用(自動適用) └── docker-compose.prod.yml # 本番環境用(明示指定)

まず共通設定ファイルを作ります。

# docker-compose.yml(共通) services: app: image: myapp ports: - "8080:8080" networks: - app-net db: image: postgres:16 environment: POSTGRES_DB: mydb volumes: - db-data:/var/lib/postgresql/data networks: - app-net networks: app-net: volumes: db-data:

2. 開発環境用override設定

# docker-compose.override.yml(開発環境・自動適用) services: app: build: context: . target: builder # マルチステージのbuilderステージで起動 volumes: - .:/app # ホットリロード用のボリュームマウント environment: - DEBUG=true - DATABASE_URL=postgresql://dev:devpass@db:5432/mydb command: ["python", "-m", "uvicorn", "app:app", "--reload"] db: environment: POSTGRES_USER: dev POSTGRES_PASSWORD: devpass ports: - "5432:5432" # 開発時はホストから直接接続できるようにする

3. 本番環境用設定

# docker-compose.prod.yml(本番環境・明示指定) services: app: build: context: . target: production # 本番ステージのみビルド environment: - DEBUG=false - DATABASE_URL=${DATABASE_URL} # 環境変数から取得(ファイルに書かない) restart: unless-stopped deploy: resources: limits: cpus: '1.0' memory: 512M db: environment: POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} # 本番ではポートを外に公開しない(appコンテナからのみアクセス)

本番環境でComposeを起動する際は `-f` オプションで明示します。

# 開発環境での起動(docker-compose.override.yml が自動適用) docker compose up -d # 本番環境での起動(共通設定+本番設定を明示) docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

4. ヘルスチェックとdepends_onの連携

`db` サービスが起動してから `app` サービスを起動させたい場合、`depends_on` だけでは不十分です。コンテナの起動とPostgreSQLの「接続受付準備完了」は別物だからです。

services: db: image: postgres:16 healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-dev}"] interval: 10s timeout: 5s retries: 5 app: depends_on: db: condition: service_healthy # dbがhealthyになるまで待機

`condition: service_healthy` を使うことで、DBのヘルスチェックが通過してから `app` が起動します。本番環境でアプリが「DB接続エラーで即死」するのを防げます。

本番Dockerfileの完成形

ここまでの要素をまとめた、Pythonアプリ向けの本番Dockerfileです。実際のサーバー(Rocky Linux 9.4)で動作確認しています。

# syntax=docker/dockerfile:1 # === Stage 1: 依存関係のインストール === FROM python:3.12-slim AS builder WORKDIR /build # 依存関係ファイルをコピー(ソースより先) COPY requirements.txt . # ビルド用ツールをインストール(builderステージのみ) RUN apt-get update && \ apt-get install -y --no-install-recommends gcc libpq-dev && \ rm -rf /var/lib/apt/lists/* && \ pip install --no-cache-dir --prefix=/install -r requirements.txt # === Stage 2: 本番イメージ === FROM python:3.12-slim AS production WORKDIR /app # ランタイム依存のみインストール RUN apt-get update && \ apt-get install -y --no-install-recommends libpq5 && \ rm -rf /var/lib/apt/lists/* # builderステージの成果物をコピー(ビルドツールは含まれない) COPY --from=builder /install /usr/local # 非rootユーザーを作成 RUN groupadd --gid 1001 appuser && \ useradd --uid 1001 --gid 1001 --no-create-home appuser # アプリをコピーして所有者を変更 COPY --chown=appuser:appuser app/ ./app/ # 非rootで実行 USER appuser # ヘルスチェック HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8080/health')" EXPOSE 8080 CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

このDockerfileのビルド後イメージサイズを確認します。

$ docker build --target production -t myapp:prod . # サイズを確認 $ docker images myapp:prod REPOSITORY TAG IMAGE ID CREATED SIZE myapp prod d4e7f8a9b0c1 30 seconds ago 203MB # 本番ステージのみビルドしているため、builderステージのgcc等は含まれない $ docker history myapp:prod | head -10 IMAGE CREATED CREATED BY SIZE d4e7f8a9b0c1 30 seconds ago CMD ["python" "-m" "uvicorn" "app.main:app"… 0B 30 seconds ago EXPOSE 8080 0B 30 seconds ago HEALTHCHECK ... 0B 30 seconds ago USER appuser 0B

トラブルシュート・よくあるエラー対処

「COPY --from が失敗する」場合

# エラー例 failed to solve: failed to read dockerfile: COPY --from requires --from to be a stage name # 原因: AS で名前をつけていない FROM python:3.12-slim # NGの例(AS builderが抜けている) # 修正: AS で必ずステージ名をつける FROM python:3.12-slim AS builder

「non-root userでPermission denied」が出る場合

# エラー例 PermissionError: [Errno 13] Permission denied: '/app/logs/app.log' # 原因: ログディレクトリの所有者がrootのまま # 修正: USER切替前にchownしておく RUN mkdir -p /app/logs && \ chown -R appuser:appuser /app/logs USER appuser

「docker compose upでdbより先にappが起動してしまう」場合

# 原因: depends_onにconditionを指定していない depends_on: - db # コンテナ起動を待つだけで、DB接続受付は待たない # 修正: service_healthyを使う(dbにhealthcheckを設定した上で) depends_on: db: condition: service_healthy

本記事のまとめ

やりたいこと 方法
ビルドツールを本番イメージから除外する FROM ... AS builderCOPY --from=builder
機密情報をイメージに含めない .dockerignore で .env 等を除外し、実行時に環境変数で渡す
レイヤー数を減らしてサイズを削減する RUN 命令を && でまとめ、apt キャッシュを同一レイヤーで削除
コンテナをrootで動かさない USER 命令で非rootユーザーに切り替え、ファイルを --chown で渡す
開発・本番の設定を分離する docker-compose.override.yml(開発)と docker-compose.prod.yml(本番)を使い分ける
DBが起動してからアプリを起動する healthcheckdepends_on: condition: service_healthy
マルチステージビルドは、一度書いてしまえばCI/CDパイプラインにそのまま組み込めます。開発者がローカルで `docker build` するときも、本番と同じDockerfileを使えるので、「開発環境では動くのに本番で動かない」問題を根本から断てます。

コンテナを使う以上、イメージの設計は避けて通れません。今日の記事で紹介したマルチステージとCompose override構成を、ぜひ次のプロジェクトで試してみてください。

現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、DockerfileのマルチステージビルドからCompose設計まで、実機ハンズオンで学べます。Docker実践講座(linuxmaster.jp)では、コンテナ設計・セキュリティ・本番運用のノウハウを現役エンジニアが丁寧に解説しています。

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。