npm run buildで生成したdistやpipで入れたパッケージは、ローカルPCに作られるものだと思っていました。実際にはビルド中のステージ内に作られます。この違いからCOPY --fromの役割を整理します。
FROMごとに新しいステージが始まる
マルチステージビルドでは、1つのDockerfileに複数のFROMを書きます。
FROM node:24 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine AS production
COPY --from=builder /app/dist /usr/share/nginx/html
/app/distはbuilder内にあります。次のFROMで始まるproductionには存在しないため、COPY --from=builderでコピー元を明示します。
最終イメージへ渡るのはdistだけです。node_modules、src、Node.js、npmは別途コピーしない限り入りません。distはそれらの複製ではなく、生成されたHTML、JavaScript、CSSなどの配信用成果物です。
Pythonでは仮想環境を成果物として渡せる
Pythonパッケージも同じ考え方です。次は、ビルド用・開発用・本番用を分けた例です。
# syntax=docker/dockerfile:1
FROM python:3.12-slim-bookworm AS base
ENV VIRTUAL_ENV=/opt/venv \
PATH="/opt/venv/bin:$PATH"
WORKDIR /app
FROM base AS builder
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential libpq-dev \
&& rm -rf /var/lib/apt/lists/*
RUN python -m venv "$VIRTUAL_ENV"
COPY requirements.txt .
RUN python -m pip install --no-cache-dir -r requirements.txt
FROM builder AS development
COPY requirements-dev.txt .
RUN python -m pip install --no-cache-dir -r requirements-dev.txt
COPY . .
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]
FROM base AS production
RUN apt-get update \
&& apt-get install -y --no-install-recommends libpq5 \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --system --gid 10001 app \
&& useradd --system --uid 10001 --gid app \
--no-create-home --no-log-init app
COPY --from=builder /opt/venv /opt/venv
COPY --chown=app:app . .
USER app
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "config.wsgi:application"]
builderでは/opt/venvへ依存パッケージを入れ、productionには仮想環境だけをコピーしています。
site-packagesと/usr/local/binを個別にコピーする方法もありますが、ライブラリ本体とコマンドをまとめた専用の仮想環境の方が追いやすくなります。
Pythonパッケージだけでは動かない場合がある
Pythonパッケージをコピーしても、OS側の依存関係は移動しません。例ではbuilderのlibpq-devをコンパイル時に使い、本番側にはlibpq5だけを入れています。C拡張がある場合は、次も確認します。
- ビルド側と本番側のPythonバージョンやCPUアーキテクチャに互換性がある
- 必要な共有ライブラリが本番側に存在する
- Debian系とAlpine系など、互換性のない環境を安易にまたがない
どのステージをイメージにするか
ターゲットを指定しなければ、最後のproductionが完成イメージになります。開発用は明示します。
docker build --target development -t myapp-dev .
docker build --target production -t myapp-prod .
2イメージが自動作成されるわけではありません。BuildKitは、ターゲットが依存しないステージを基本的に処理しません。
本番プロセスを非rootで動かす
useraddでユーザーを作り、COPY --chownで所有者を合わせ、USER appへ切り替えています。以降のCMDはapp権限です。
非rootなら、脆弱性を突かれた場合のコンテナ内の権限を制限できます。ただし完全な隔離ではなく、マウントやLinux capabilitiesなども別途管理が必要です。
まとめ
-
FROMごとに新しいビルドステージとファイルシステムが始まる - ステージ間で必要な成果物だけを渡すのが
COPY --from - Pythonでは仮想環境をまとめてコピーできるが、OSの実行時依存関係も必要
-
--targetで開発用と本番用を個別にビルドする - 本番では所有権を整え、
USERで非rootユーザーへ切り替える