そう感じたことはないでしょうか。
Dockerfileの基本構文は比較的すぐ覚えられますが、本番環境で安全に使えるイメージを作るとなると話が変わります。サイズの肥大化、rootで動くコンテナ、開発用ツールが残ったまま本番に出てしまう——こうした問題は、設計をきちんと理解していないと見過ごしがちです。
この記事では、マルチステージビルドを中心に、本番向けDockerfileの設計と、Docker Composeを使った環境分離の実践手順を解説します。単なるコマンドの紹介ではなく、「なぜそう書くのか」という設計の意図まで踏み込みます。
この記事のポイント
・マルチステージビルドで本番イメージを1/10以下に軽量化できる
・Dockerfile設計の鉄則はCOPY --from でビルド成果物だけを本番に持ち込むこと
・コンテナをroot以外のユーザーで動かすことがセキュリティの最低ライン
・Docker Composeのoverride構成で開発・本番の環境差分をコードで管理する
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜ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"]
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"]
実際のビルド結果を確認してみます。
$ docker build -t myapp:prod . $ docker images myapp REPOSITORY TAG IMAGE ID CREATED SIZE myapp prod a3f1b2c4d5e6 2 minutes ago 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"]
Dockerfile設計の重要ポイント
1. .dockerignoreで不要ファイルを除外する
Dockerfileと同じディレクトリに `.dockerignore` を置くことで、ビルドコンテキストから除外するファイルを指定できます。これを怠ると、`.git` ディレクトリや `.env` ファイルがイメージに混入するリスクがあります。# .dockerignore の例 .git .env .env.* *.log __pycache__ .pytest_cache node_modules README.md
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 . . # ソースは最後
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/*
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
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コンテナからのみアクセス)
# 開発環境での起動(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になるまで待機
本番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"]
$ 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 builder + COPY --from=builder |
| 機密情報をイメージに含めない | .dockerignore で .env 等を除外し、実行時に環境変数で渡す |
| レイヤー数を減らしてサイズを削減する | RUN 命令を && でまとめ、apt キャッシュを同一レイヤーで削除 |
| コンテナをrootで動かさない | USER 命令で非rootユーザーに切り替え、ファイルを --chown で渡す |
| 開発・本番の設定を分離する | docker-compose.override.yml(開発)と docker-compose.prod.yml(本番)を使い分ける |
| DBが起動してからアプリを起動する | healthcheck + depends_on: condition: service_healthy |
コンテナを使う以上、イメージの設計は避けて通れません。今日の記事で紹介したマルチステージとCompose override構成を、ぜひ次のプロジェクトで試してみてください。
次に読む記事
・コンテナとは何か|Dockerで理解する仮想マシンとの違いと利点
・Docker Compose入門|複数コンテナでWordPress環境を構築するハンズオン
・Linux ポート確認の全コマンド|ss・lsof・netstatの使い分け
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、DockerfileのマルチステージビルドからCompose設計まで、実機ハンズオンで学べます。Docker実践講座(linuxmaster.jp)では、コンテナ設計・セキュリティ・本番運用のノウハウを現役エンジニアが丁寧に解説しています。
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら
- 前のページへ:Dockerfileの書き方入門|自分のアプリをイメージ化する基礎と注意点
- この記事の属するカテゴリ:Dockerへ戻る

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます