軽量なDockerfileとシェルエントリポイントの作成
マルチステージビルドスクリプトと、シグナル処理や設定テンプレート機能を備えた堅牢なエントリポイントラッパーを作成します。
「軽量なDockerfileとシェルエントリポイントの作成」はCoddyKit上の無料Linux Command Line & Bash Scripting Masteryレッスンです。 これはレッスン1/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはLinux Command Line & Bash Scripting Mastery学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
DevOpsでLeanなDockerfileが重要な理由
本番環境では、Dockerイメージのメガバイト単位の容量すべてにコストが伴います。pullの遅延、攻撃対象領域の拡大、レジストリストレージの浪費につながるためです。堅牢なシェルエントリポイントと組み合わせたLeanなDockerfileは、成熟したDevOpsの実践を示す特徴です。
- マルチステージビルドでは、ビルド時のツールと最終的な実行イメージを分離するため、サイズを大幅に削減できます。
- エントリポイントシムは、コンテナの起動処理を行う小さなシェルスクリプトです。設定ファイルのテンプレート展開、環境変数の検証、シグナル処理を行い、最後にメインプロセスをexecします。
- 両者を組み合わせることで、Kubernetes、ECS、ベアメタル環境における、信頼性と移植性に優れたコンテナワークロードの基盤になります。
このレッスンでは、両方の手法を最初から最後まで扱い、パイプラインにそのまま導入できる本番品質のパターンを紹介します。
マルチステージDockerfileの構成
マルチステージDockerfileでは、複数のFROM命令を使用します。各ステージは分離されたレイヤーの集合であり、必要なアーティファクトだけを次のステージにコピーします。
- ステージ0(builder):コンパイラ、テストランナー、ビルド依存関係をインストールします。
- ステージ1(runtime):最小構成のベース(例:
alpine、distroless)から開始し、コンパイル済みバイナリやアプリケーションバンドルだけをコピーします。 - 明示的にコピーしない限り、最終イメージに
gcc、make、ソースコードが含まれることはありません。
COPYで--from=<stage>を使用すると、ステージ間の境界を越えてファイルを取得できます。ステージにはAS <name>で名前を付けると可読性が高まり、docker build --targetによる対象の選択も容易になります。
# ---- Stage 0: builder ----
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /app/server ./cmd/server
# ---- Stage 1: runtime ----
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]レイヤーの最小化とキャッシュの無効化
RUN、COPY、ADDの各命令は、新しいレイヤーを作成します。命令の順序が適切でないと、ビルドキャッシュが不必要に無効化され、CIパイプラインが遅くなります。
- 依存関係マニフェスト(
package.json、go.mod、requirements.txt)を先にコピーし、その後でソースコードをコピーします。これにより、依存関係のインストールをソースコードとは独立してキャッシュできます。 - 関連するコマンドを
&&で連結し、パッケージキャッシュが中間レイヤーに残らないよう、同じRUNレイヤー内でクリーンアップします。 - パッケージマネージャーでは
--no-cacheを使用し、インストール後にリストファイルを削除します。
FROM python:3.12-slim AS builder
WORKDIR /app
# 1. Install deps first (cached until requirements change)
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 2. Copy source (cache busted only when code changes)
COPY src/ ./src/
FROM python:3.12-slim
COPY --from=builder /install /usr/local
COPY --from=builder /app/src /app/src
WORKDIR /app
CMD ["python", "-m", "src.main"]BuildKitのシークレットとSSH転送
プライベートレジストリの認証情報、SSHキー、APIトークンをイメージレイヤーに決して含めてはいけません。Docker BuildKitには、安全に扱うための2つの仕組みがあります。
--secret:単一のRUNステップ内にシークレットファイルをマウントします。レイヤーに組み込まれることはありません。/run/secrets/<id>からアクセスできます。--ssh:ホストのSSHエージェントソケットをビルドに転送します。これにより、秘密鍵を埋め込むことなくgit cloneで認証できます。
DOCKER_BUILDKIT=1またはdocker buildx buildでBuildKitを有効にします。# syntax=docker/dockerfile:1ディレクティブにより、これらの機能が使用可能になります。
# syntax=docker/dockerfile:1
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
# Mount NPM token as a secret — never stored in the image
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci --prefer-offline
# Usage at build time:
# DOCKER_BUILDKIT=1 docker build \
# --secret id=npm_token,src=~/.npmrc-token \
# -t myapp:latest .堅牢なエントリポイントシムの作成
エントリポイントシムは、DockerfileでENTRYPOINTに設定するシェルスクリプトです。メインプロセスに制御を渡す前に、実行環境を準備する役割を担います。
適切に構成されたシムは、次の順序で処理します。
- ステップ1:
set -euo pipefailを設定し、どれかの処理が失敗したら早期に中断するようにします。 - ステップ2:必須の環境変数を検証し、役立つメッセージを表示して即座に失敗させます。
- ステップ3:環境変数から設定ファイルをテンプレート展開します。
- ステップ4:正常終了のためのシグナルハンドラーを登録します。
- ステップ5:
exec "$@"— シェルをメインプロセスに置き換え、PID 1がシムではなくアプリケーションになるようにします。
最後のexecは重要です。これがないと、KubernetesやDockerランタイムから送られたシグナルが子プロセスに転送されません。
#!/usr/bin/env bash
set -euo pipefail
# Step 2: validate required env vars
REQUIRED_VARS=(DATABASE_URL APP_SECRET PORT)
for var in "${REQUIRED_VARS[@]}"; do
if [[ -z "${!var:-}" ]]; then
echo "[entrypoint] ERROR: required env var '$var' is not set" >&2
exit 1
fi
done
# Step 3: template config (see next scene)
# Step 4: signal handling (see scene after that)
# Step 5: hand off to CMD
exec "$@"envsubstによる設定テンプレートの展開
envsubst(GNU gettextパッケージに含まれ、Alpineではgettextとして提供されています)は、テンプレートファイル内の${VAR}プレースホルダーを、現在の環境変数の値に置き換えます。
- イメージに
*.tmpl形式の設定テンプレートを含め、起動時にエントリポイントで展開します。 envsubstには変数のリストを明示的に渡します。これにより、nginxの正規表現などに含まれる無関係なドル記号まで誤って展開されるのを防げます。- 展開後のファイルは、
/tmpや専用の設定ボリュームなど、書き込み可能なパスに保存します。
#!/usr/bin/env bash
# Template: /etc/nginx/conf.d/app.conf.tmpl contains:
# server { listen ${NGINX_PORT}; server_name ${SERVER_NAME}; ... }
export NGINX_PORT=${NGINX_PORT:-8080}
export SERVER_NAME=${SERVER_NAME:-localhost}
envsubst '${NGINX_PORT} ${SERVER_NAME}' \
< /etc/nginx/conf.d/app.conf.tmpl \
> /etc/nginx/conf.d/app.conf
echo "[entrypoint] nginx config rendered:"
grep -E 'listen|server_name' /etc/nginx/conf.d/app.conf
exec "$@"シグナル処理と正常なシャットダウン
Kubernetes、ECS、またはdocker stopによって停止されると、コンテナはSIGTERMを受信します。エントリポイントシムがPID 1であり、シグナルを転送しない場合、猶予期間の後にメインプロセスはSIGKILLで強制終了され、リクエストの取りこぼしやデータ破損につながる可能性があります。
trapを使用して、シム内でSIGTERMとSIGINTを捕捉します。kill -TERM "$child"を使用して、子プロセスのPIDにシグナルを転送します。wait "$child"で子プロセスの終了まで待機し、その終了コードを引き継ぎます。- または、
execでシェル全体を置き換える方法もあります。この場合、OSが子プロセスに直接シグナルを配信するため、trapは不要です。単純なケースでは、このパターンが推奨されます。
#!/usr/bin/env bash
set -euo pipefail
# Start main process in background
"$@" &
child=$!
# Forward SIGTERM and SIGINT to the child
trap 'echo "[entrypoint] caught SIGTERM, forwarding..."; kill -TERM "$child"' TERM
trap 'echo "[entrypoint] caught SIGINT, forwarding..."; kill -INT "$child"' INT
# Wait for child to exit and capture its exit code
wait "$child"
exit $?最小限のInitプロセスとしてtiniを使用する
コンテナが子プロセスを生成する場合(例:ワーカーをforkするシェル)、ゾンビプロセスを回収するために本物のinitが必要です。tiniは、コンテナ向けに作られた非常に小さなinitバイナリです。
- イメージに
tiniを追加し、エントリポイントのラッパーとして設定します。 - ゾンビ状態の子プロセスを回収し、シグナルを正しく転送して、子プロセスのステータスコードで終了します。
- Dockerには
docker run --initで有効になる組み込みのtiniがありますが、イメージに組み込めば、ランタイム(Kubernetes、ECSなど)が変わっても動作を一貫させられます。
FROM node:20-alpine
# Install tini for proper signal handling and zombie reaping
RUN apk add --no-cache tini
WORKDIR /app
COPY --chown=node:node . .
RUN npm ci --omit=dev
USER node
# tini wraps CMD; forwards SIGTERM and reaps zombies
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "server.js"]非rootユーザーとして実行する
root(UID 0)で実行するコンテナは重大なセキュリティリスクになります。コンテナエスケープが発生すると、ホスト全体へのアクセスを許してしまうためです。メインプロセスを実行する前に、必ず権限のないユーザーへ切り替えてください。
- Dockerfileで専用のシステムユーザーとグループを作成します。Alpineでは
addgroup/adduser、Debianではgroupadd/useraddを使用します。 COPY --chown=appuser:appgroupでアプリケーションファイルの所有者を変更します。別のRUN chownレイヤーを作成するより効率的です。USER命令でそのユーザーに切り替えます。エントリポイントとCMDはこのユーザーを引き継ぎます。- Kubernetesの
securityContext.runAsNonRoot: trueを設定すると、rootのまま実行するイメージの起動を拒否します。
FROM python:3.12-slim
# Create non-root user
RUN groupadd --gid 1001 appgroup && \
useradd --uid 1001 --gid appgroup --shell /bin/bash --create-home appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy app files with correct ownership in a single layer
COPY --chown=appuser:appgroup src/ ./src/
COPY --chown=appuser:appgroup entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
USER appuser
ENTRYPOINT ["/entrypoint.sh"]
CMD ["python", "-m", "src.main"]イメージでのヘルスチェックとReadiness Probe
KubernetesのLiveness ProbeとReadiness Probeはマニフェストで定義しますが、単独のdocker runやDocker Compose環境向けに、DockerfileへHEALTHCHECKを組み込むこともできます。
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 CMD ...- HTTPサービスには
curl --failまたはwget -qO-を使用します。HTTP以外のデーモンでは、/dev/tcp/localhost/PORTでソケットを検査します。 - 必要なものだけをインストールします。distrolessイメージでは、ヘルスチェックだけのために
curlを追加せず、専用のProbeバイナリまたはアプリケーション独自のヘルスチェック用バイナリを使用します。
FROM nginx:1.27-alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY dist/ /usr/share/nginx/html/
# Lightweight health check using bash TCP pseudo-device
# (no curl needed — works on any image with bash)
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
CMD bash -c 'exec 3<>/dev/tcp/localhost/80 && echo -e "GET /health HTTP/1.0\r\n" >&3 && cat <&3 | grep -q "200 OK"' || exit 1
EXPOSE 80すべてを組み合わせる:本番用エントリポイント
以下は、このレッスンで扱ったすべてのパターン(環境変数の検証、設定テンプレートの展開、シグナル転送、execによる引き継ぎ)を組み合わせた、本番品質の完全なエントリポイントシムです。このパターンは、Kubernetesにデプロイされる実際のNode.js、Python、Goのマイクロサービスで使用されています。
- 各セクションにはコメントがあり、自己文書化されたテンプレートとして利用できます。
- シムは50行未満に抑えています。エントリポイントはシンプルで監査しやすいものにすべきです。
- 最後の
exec "$@"に注目してください。すべての準備が完了すると、シェルがアプリケーションプロセスに置き換わるため、アプリケーションがPID 1になり、すべてのOSシグナルを直接受信します。
#!/usr/bin/env bash
# entrypoint.sh — production-grade container entrypoint shim
set -euo pipefail
# ── 1. Validate required environment variables ──────────────────
REQUIRED=(DATABASE_URL APP_SECRET PORT LOG_LEVEL)
for var in "${REQUIRED[@]}"; do
[[ -n "${!var:-}" ]] || { echo "[entrypoint] FATAL: $var is not set" >&2; exit 1; }
done
# ── 2. Set safe defaults for optional variables ─────────────────
export HOST=${HOST:-0.0.0.0}
export WORKERS=${WORKERS:-2}
# ── 3. Render config template ───────────────────────────────────
if [[ -f /etc/app/app.conf.tmpl ]]; then
envsubst '${DATABASE_URL} ${PORT} ${LOG_LEVEL} ${HOST}' \
< /etc/app/app.conf.tmpl \
> /etc/app/app.conf
echo "[entrypoint] config rendered at /etc/app/app.conf"
fi
# ── 4. Wait for dependent services (optional, fast) ─────────────
if [[ -n "${WAIT_FOR_HOST:-}" ]]; then
echo "[entrypoint] waiting for ${WAIT_FOR_HOST}:${WAIT_FOR_PORT:-5432}..."
until bash -c "exec 3<>/dev/tcp/${WAIT_FOR_HOST}/${WAIT_FOR_PORT:-5432}" 2>/dev/null; do
sleep 1
done
echo "[entrypoint] dependency ready"
fi
# ── 5. Hand off to CMD (PID 1 becomes the application) ──────────
echo "[entrypoint] starting: $*"
exec "$@"理解度チェック:エントリポイントでのシグナル処理
コンテナのエントリポイントスクリプトにおけるシグナル処理の理解度を確認しましょう。
まとめ:LeanなDockerfileとシェルエントリポイント
本番環境向けのコンテナ作成について、必要な要素を一通り学びました。
- マルチステージビルドでは複数の
FROM命令を使用し、コンパイラやビルドツールを最終イメージから排除して、Leanで最小限の実行レイヤーを作成します。 - レイヤーの順序では、ソースコードより前に依存関係マニフェストをコピーすることで、キャッシュヒット率を高め、CIパイプラインを高速化します。
- BuildKitのシークレットとSSHマウントにより、認証が必要なビルドを壊すことなく、認証情報をイメージ履歴から除外できます。
- エントリポイントシムは環境変数を検証し、
envsubstで設定ファイルをテンプレート展開して、exec "$@"でアプリケーションに処理を引き渡します。 - シグナル処理では、
exec(アプリケーションをPID 1にする)を使うか、バックグラウンドジョブを使用する場合は明示的なtrap+kill+waitパターンを使う必要があります。 - tiniを使うと、コンテナが複数のプロセスを生成する場合に、ゾンビプロセスの回収と正しいシグナル転送を実現できます。
- 非rootユーザーと
HEALTHCHECK命令を加えることで、Kubernetesの本番ワークロードに対応した、安全で可観測性の高いイメージが完成します。
これらのパターンを一貫して組み合わせることで、イメージはより小さく、デプロイはより高速になり、本番環境での堅牢性も大幅に向上します。
よくある質問
「軽量なDockerfileとシェルエントリポイントの作成」レッスンは無料ですか?
はい。「軽量なDockerfileとシェルエントリポイントの作成」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Linux Command Line & Bash Scripting Masteryコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Linux Command Line & Bash Scripting Masteryコースには全4レッスンが含まれています。
「軽量なDockerfileとシェルエントリポイントの作成」で何を学びますか?
マルチステージビルドスクリプトと、シグナル処理や設定テンプレート機能を備えた堅牢なエントリポイントラッパーを作成します。 ブラウザで直接実行するハンズオンコードでLinux Command Line & Bash Scripting Masteryを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。
Linux Command Line & Bash Scripting Masteryを始めるのに経験は必要ですか?
事前経験は必要ありません。CoddyKitのLinux Command Line & Bash Scripting Masteryは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン1/4です。
「軽量なDockerfileとシェルエントリポイントの作成」レッスンにはどのくらい時間がかかりますか?
ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。
このLinux Command Line & Bash Scripting Masteryレッスンでコードを書いて実行できますか?
はい。すべてのLinux Command Line & Bash Scripting Masteryレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。
このコースのすべてのレッスン
- 軽量なDockerfileとシェルエントリポイントの作成
- envsubstとheredocによる設定のテンプレート化
- CLIとjqによるクラウドリソースのスクリプト操作
- ヘルスプローブ、準備完了ゲート、待機ループ