マルチステージビルド

この講座では、Dockerイメージの軽量化技術であるマルチステージビルドについて学びます。

  • マルチステージビルドの概念とメリット
  • ビルドステージと実行ステージの分離
  • 新しいDockerfileの命令(RUN・WORKDIR・EXPOSE)
  • 新しいDockerコマンド(-d・-p・logs・exec)
  • 本番想定でのDockerfileの工夫(軽量イメージ・非rootユーザ・.dockerignore

1. マルチステージビルドとは

これまでのDockerイメージ作成では、1つのDockerfileに「開発に必要なツール」と「実行に必要な環境」をすべて詰め込んでいました。しかし、これには大きな問題があります。

例えば、JavaやGo言語などでアプリケーションを開発する場合、ソースコードをコンパイル(実行可能な形式に変換)するための「コンパイラ」や「ビルドツール」が必要です。しかし、実際にアプリケーションを動かす時には、これらの道具は不要です。

不要な道具がイメージ内に残ったままだと、以下のようなデメリットが発生します。イメージのファイルが大きくなり、ダウンロードやアップロードに時間がかかるというリスクがあります。

また、不要なツールが入っている分、脆弱性が含まれる可能性も高まります。

それを解決する方法として、マルチステージビルドがあります。

「ビルド(調理)をする段階」と「実行(提供)をする段階」を分けることで、最終的なイメージには「完成した料理(実行ファイル)」だけを乗せることができます。これにより、イメージサイズを劇的に小さく(軽量化)することができます。

2. マルチステージビルドの定義方法

マルチステージビルドにおけるビルドステージと実行ステージの関係を以下に示します。

flowchart LR
  subgraph build["ビルドステージ"]
    SRC[ソースコード] --> DEPS[依存関係インストール]
    DEPS --> COMPILE[コンパイル/ビルド]
    COMPILE --> BIN[実行バイナリ]
  end

  BIN -- "成果物のみコピー" --> RUN_BIN[実行バイナリ]

  subgraph run["実行ステージ(最終イメージ)"]
    RUN_BIN --> EXEC[アプリケーション実行]
  end

  DEPS -. "ビルドツールは破棄" .-> DISCARD[不要なツール類]

  style DISCARD fill:#fdd,stroke:#c00

マルチステージビルドを行うには、1つのDockerfileの中に FROM 命令を複数回記述します。Docker公式のMulti-stage buildsでも「With multi-stage builds, you use multiple FROM statements in your Dockerfile. Each FROM instruction can use a different base, and each of them begins a new stage of the build.」と説明されています。

それぞれの FROM が新しいステージ(段階)の始まりを意味します。また、特定のステージに名前をつけることで、後のステージから参照しやすくします。

つまり「FROMの命令の数だけ、複数のステージを用意できる」というものです。

なお、実際にイメージに含まれるのは「最後のステージ(FROM)に含まれる内容」のみとなります。そのため、他のステージで使用したファイルやツールは、実際のイメージには含まれません。

実際の定義方法を見ていきます。

# 第1ステージ:ビルド用(名前を 'builder' とする)
FROM <イメージA> AS builder
# ...ここでビルド処理を行う...

# 第2ステージ:実行用(ここが最終的なイメージになる)
FROM <イメージB>
# ...必要なものだけをコピーして起動...

FROMを2つ記載し、1つ目はビルド用、2つ目は実行用として使用します。 1つ目のFROMには、AS builder という記述で、ステージにbuilderという名前をつけています。 これ自体がなくてもマルチステージビルドは定義できますが、2つ目のステージにファイルを受け渡す際に、名前があった方が「どこからコピーするか」を明確に指定できるため、このように名前をつけるのが一般的です。

2.1 ファイルの受け渡し(COPY --from)

ステージを跨いでファイルの受け渡しを行う方法を説明します。

まず重要な点として、異なるステージ間のファイルは自動的には引き継がれません。 そのため、ビルド用のステージで作成した成果物(実行ファイルなど)を、実行用のステージへ明示的にコピーする必要があります。

COPYコマンドに、以下のように --from=ステージ名 を記述することで、別のステージにあるファイルをコピーしてくることができます。

COPY --from=builder /app/main /app/main

この記述は、「builderステージにある /app/main というファイル(コピー元)」を、「現在の実行ステージの /app/main (コピー先)」にコピーする、という意味になります。 これにより、ビルド用ステージで作られた成果物だけを、最終的なイメージに取り込むことができます。

3. 新しく登場するDockerfileの命令

3.1 RUN

RUNは、Dockerイメージを作成(ビルド)する段階で実行したいコマンドを指定するための命令です。

RUN touch test.txt

この記述は、イメージを作成する途中で touch test.txt というコマンドを実行し、test.txt という空のファイルを作成することを意味しています。

その他にも、アプリケーションに必要なツールをインストールしたり、フォルダ構成を整えたりする際によく使用されます。

CMDとの違い

RUNとCMDはどちらもコマンドを実行する命令ですが、その実行されるタイミングが明確に異なります。

RUNは「イメージを作る時(ビルド時)」に実行されます。一方でCMDは「コンテナが動く時(起動時)」に実行されます。

つまり、コンテナを実行する前処理として事前に実行されるのがRUNで、実際にコンテナを起動するときに実行されるのがCMDです。

また、RUNは実行したい処理の数だけ何度でも記述して積み重ねることができますが、CMDは「起動時に実行するデフォルトのコマンド」という性質上、原則としてDockerfileの最後に1つだけ記述するという点も大きな違いです。

3.2 WORKDIR

WORKDIRは、コンテナ内でコマンドを実行する際の「作業場所(カレントディレクトリ)」を指定するための命令です。Linuxコマンドの cd(ディレクトリ移動)と同じような役割を果たします。

WORKDIR /app

この記述を行うと、これ以降に記述された RUN、COPY、CMD などの命令は、すべてここで指定されたディレクトリ(例:/app)を起点として実行されるようになります。

この命令を使用することで、Dockerfileの記述がシンプルになるというメリットがあります。例えば COPY . . のように記述した際、コピー先が自動的に WORKDIR で指定した場所になるため、毎回 /app/index.html のように長いパスを書く必要がなくなります。

また、指定したディレクトリがコンテナ内に存在しない場合、Dockerが自動的にフォルダを作成してくれるため、事前に mkdir コマンドなどでフォルダを用意しておく必要もありません。

WORKDIR /app
COPY . .

上記のような記述の場合、ホスト側のすべてのファイルは、コンテナ内の /app ディレクトリの中にコピーされることになります。

3.3 EXPOSE

EXPOSEは、コンテナが実行時に「どのポート番号で通信を待ち受けるか」をDockerに知らせるための命令です。

EXPOSE 8080

この記述は、このコンテナ内のアプリケーションが8080番ポートを使用することを宣言しています。 これにより、Dockerfileを読む人や、このイメージを使用するユーザに対して「このコンテナを使うときは、8080番ポートを開けてくださいね」という意図を伝える(ドキュメント化する)役割があります。

ただし、非常に重要な注意点があります。EXPOSEと書いただけでは、実際に自分のPC(ホスト)からアクセスできるようにはなりません。 あくまで「ポート番号の宣言」としての意味合いが強いため、実際にアクセス可能にするには、コンテナ起動時に -p オプションを使ってポートを繋ぐ必要があります。

なぜ前回のハンズオン(Nginx)では書かなかったのか? 前回のハンズオンで作成したDockerfileには EXPOSE を記述しませんでしたが、それでも正常に動作しました。これには理由があります。

それは、ベースイメージとして使用した nginx:alpine の中に、すでに EXPOSE 80 が記述されていたからです。

Dockerでは、FROM で指定したベースイメージの設定の多くを引き継ぐことができます。Nginxの公式イメージ側ですでに「80番ポートを使います」という宣言がなされていたため、私たちが改めて書く必要はなかったのです。

自分でゼロからアプリケーションを作る場合(GoやNode.jsなどでアプリを開発する場合など)は、この EXPOSE を記述して、どのポートを使うかを明示するのがベストプラクティスとなります。

3.4 まとめ(命令一覧)

最後に、今回登場したDockerfileの新しい記載方法を下記表にまとめておきます。

命令 説明
RUN <コマンド> イメージビルド時にコマンドを実行
WORKDIR <パス> 作業ディレクトリを指定
EXPOSE <ポート番号> コンテナが使用するポートを宣言

4. 新しく登場するDockerコマンド

4.1 docker container runの-dオプション

今回、docker container runで実行する際に、-dオプションを使用してバックグラウンドで起動します。

-dは、Detached(切り離された)モードの略で、コンテナをバックグラウンドで実行し、ターミナルの操作権を即座にユーザに戻すためのオプションです。

docker container run -d my-image

この記述は、コンテナを裏側(バックグラウンド)で起動し続けることを意味しています。

通常、このオプションをつけずに起動すると、コンテナのログが画面に流れ続け、そのコンテナが終了するまでターミナルで他のコマンドを入力することができなくなります。また、その状態でターミナルを閉じてしまうと、コンテナも道連れになって停止してしまいます。

Webサーバやデータベースのように、常駐して動き続ける必要のあるアプリケーションを動かす際は、この-dオプションを使って「コンテナの実行プロセスをターミナルから切り離す」という設定を行うのが一般的です。

4.2 docker container runの-pオプション

-pは、コンテナ内の特定ポートに、自分のPC(ホスト)からアクセスできるようにポートを接続(公開)して起動するためのオプションです。

docker container run -p 8080:80 my-image

-p8080:80と記述することで、「自分のPCの8080番ポート(左側)」に来たアクセスを、「コンテナ内の80番ポート(右側)」に転送する設定になります。

今回のハンズオンでは、Goアプリケーションが8080番ポートで起動するため、-p 8080:8080と記述します。

docker container run -p 8080:8080 my-image

これは「ホストの8080番ポート」と「コンテナの8080番ポート」を同じ番号で繋ぐ設定です。ホストとコンテナで同じポート番号を使用する場合は、このように左右を同じ値にします。

コンテナ内のアプリケーションは独自のポートで動作していますが、そのままでは外部からアクセスできません。このオプションを使ってホストとコンテナのポートを繋ぐことで、ブラウザやcurlコマンドからアクセス可能になります。

4.3 docker container logs

今回、コンテナの内部で何が起きているかを確認するために、docker container logsコマンドを使用します。

これは、実行中のコンテナが出力したメッセージ(標準出力・標準エラー出力)を、後から確認するためのコマンドです。

docker container logs my-go-container

この記述は、指定したコンテナ(ここではmy-go-container)がこれまでに書き出したログ情報を、現在のターミナルに表示させることを意味しています。

先ほどの-dオプションを使用してバックグラウンドで起動した場合、Webサーバの起動メッセージやエラーログなどは画面に一切表示されず、裏側で隠れてしまいます。

「起動したはずなのにアクセスできない」といったトラブルの際には、まずこのコマンドを実行します。そうすることで、「実はポート番号が間違っていた」「プログラムがエラーで落ちていた」といった内部の状況を、あたかもその場で実行したかのように確認することができます。

4.4 docker container exec

これは、すでにバックグラウンドで動いているコンテナに対して、外から別のコマンドを割り込ませて実行するためのコマンドです。

単発コマンドの実行

まずは、起動中のコンテナに対して、コマンドを一つだけ実行したい場合の記述です。

docker container exec my-go-container ls -l /app

この記述は、「my-go-containerの中で ls -l /app コマンドを実行して、その結果を表示しろ」という命令になります。

この使い方は、コンテナの中に入り込む(ログインする)わけではありません。外から命令を投げ込んで、その実行結果だけを受け取るイメージです。「ファイルがちゃんと作られたか確認したい」「設定ファイルの中身をcatコマンドでちらっと見たい」といった、一瞬の確認作業を行う際に便利です。

-itオプション(対話モードでの実行)

一方で、コンテナの中に入って、連続してコマンドを実行したい場合には -it オプションを使用します。

docker container exec -it my-go-container sh

この記述は、コンテナの中で sh(シェル)を起動し、その操作を自分のターミナルと繋ぐことを意味しています。ここで登場する -it は、2つの重要な役割を持っています。

  • -i(interactive)は「双方向」という意味で、自分のキーボードからの入力をコンテナに届けるための設定です。これがないと、コマンドを打ってもコンテナは反応しません。
  • -t(tty)は「端末」という意味で、コンテナからの出力を手元の画面で見やすく表示するための設定です。

これらを組み合わせることで、あたかもコンテナの中にログインしたかのように、対話形式で次々とコマンドを打ちながら調査を行うことができるようになります。

4.5 まとめ(コマンド一覧)

最後に、今回登場したコマンドを下記表にまとめておきます。

コマンド 説明
docker container run -d コンテナをバックグラウンドで実行
docker container run -p <ホスト>:<コンテナ> ポートを接続してコンテナを起動
docker container logs <コンテナ名> コンテナのログを確認
docker container exec <コンテナ名> <コマンド> 起動中のコンテナでコマンドを実行
docker container exec -it <コンテナ名> sh コンテナ内に入って対話的に操作

5. ハンズオン:GoのAPIを実行する(マルチステージビルド)

それでは、マルチステージビルドや、応用的なDockerfileの書き方を理解するために、Go言語の簡単なAPIを起動するDockerfileを作成し、実行するハンズオンを行います。

5.1 Dockerfileを用意する

まず、任意の場所に作業用のフォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成したフォルダを開きます。

任意のフォルダ  ← このフォルダを作成

Dockerfileを作成します。Visual Studio Codeのエクスプローラーでフォルダを右クリックし、「新しいファイル」を選択してDockerfileという名前で作成してください。

任意のフォルダ
└── Dockerfile  ← このファイルを作成

作成したファイルに以下の内容を記述して保存します。

# ビルドステージ
FROM golang:1.21-alpine AS builder

WORKDIR /app

COPY main.go .

RUN go build -o myapp main.go

# 実行ステージ
FROM alpine:latest

WORKDIR /app

COPY --from=builder /app/myapp .

EXPOSE 8080

CMD ["./myapp"]

コードを説明します。

5.2 ビルドステージ

まずはビルドステージの内容です。

FROM golang:1.21-alpine AS builder

今回はAlpine Linuxに、Go言語の1.21のバージョンが含まれたベースイメージを使用していきます。AS builderにより、builderというステージ名をつけています。

WORKDIR /app

続いて、WORKDIRにより、標準で使用するフォルダに/appを指定します。これにより、今後実行されるコマンドは/appフォルダに対して実行されます。

COPY main.go .

続いて、COPYコマンドで、ローカルに作成したmain.goを、コンテナにコピーします。事前にWORKDIR /appを指定しているので、/appの中にmain.goがコピーされます。

RUN go build -o myapp main.go

このコマンドは、Go言語のソースコードをコンパイルして、実行可能なファイルを作成するための命令です。

コマンドの前半にあるgo buildは、Go言語のプログラムをビルドするための標準的なコマンドです。これによって、人間が書いたプログラムコードを、コンピュータが直接実行できる形式に変換します。

続いて記述されている-o myappという部分は、出来上がった実行ファイルに名前をつけるためのオプションです。-oはOutput(出力)の略であり、ここではmyappという名前を指定しています。これにより、ビルドが完了するとmyappという名前の実行ファイルが作成されます。

最後のmain.goは、ビルドの対象となるソースコードのファイル名を指定しています。

結果として、このコマンドが実行されるとコンテナ内にmyappというファイルが生成されます。このファイルは、次の実行ステージにコピーして動かすための重要な成果物となります。

5.3 実行ステージ

続いて、実行ステージの内容を見ていきます。

FROM alpine:latest

ここでは、実行ステージで使用するベースイメージとして、Alpine Linuxの最新版を指定しています。Go言語の実行ファイルはすでに作成済みのため、実行環境としてはAlpine Linuxのような最小限のOSがあれば問題ありません。

WORKDIR /app

ビルドステージと同様、/appフォルダを標準のコマンド実行場所として定義しています。

COPY --from=builder /app/myapp .

この記述は、マルチステージビルドにおいて最も重要な「成果物の受け渡し」を行うための命令です。

通常のCOPYコマンドは自分のPCにあるファイルをコンテナにコピーしますが、ここに--from=builderというオプションをつけることで、コピー元が「自分のPC」から「builderと名付けたビルド用ステージ」に切り替わります。

具体的には、ビルド用ステージの中に作られた/app/myappという場所にある実行ファイルを、現在のステージの作業ディレクトリ(末尾のドットが表す場所)にコピーしています。

この命令によって、ビルドに使用した重たいツール類はすべて置き去りにし、動作に必要な完成したファイルだけを新しい環境に持ってくることができます。

EXPOSE 8080

この命令は、コンテナ内のアプリケーションが通信に使用するポート番号を宣言するための命令です。

ここでは8080を指定していますが、これは今回作成したGo言語のアプリケーションが、8080番ポートで待ち受けるように作られているためです。この記述をしておくことで、このDockerイメージを利用する人に対して「実行時には8080番ポートへの接続が必要ですよ」という情報を伝えることができます。

ただし、以前のハンズオンでも触れた通り、この記述だけでは外部(ホスト)からのアクセスは許可されません。実際にブラウザなどからアクセスするためには、コンテナ起動時に-pオプションを使用してポートを接続する必要があります。

CMD ["./myapp"]

この命令は、コンテナが起動したタイミングで実行される「デフォルトのコマンド」を指定するものです。

記述されている ./myapp は、「現在のディレクトリにある myapp というファイルを実行する」という意味になります。この myapp は、先ほど COPY コマンドを使ってビルド用ステージから持ってきた、Go言語の実行ファイルそのものです。

つまり、この記述によって「Dockerコンテナが立ち上がると同時に、完成したGoアプリケーションが起動する」という最終的な動作が定義されます。

5.4 Go言語のファイルを用意する

Go言語のファイルを用意します。Visual Studio Codeのエクスプローラーでフォルダを右クリックし、「新しいファイル」を選択してmain.goという名前で作成してください。

任意のフォルダ
├── Dockerfile
└── main.go  ← このファイルを作成

作成したファイルに以下の内容を記述して保存します。

package main

import (
    "fmt"
    "net/http"
    "os" // 追加
)

func handler(w http.ResponseWriter, r *http.Request) {
    // ログ確認用:アクセスが来たことをコンソールに出力
    fmt.Println("Access received!")
    fmt.Fprintf(w, "Hello from Go Container!")
}

func main() {
    http.HandleFunc("/", handler)
    // ログ確認用:起動したことをコンソールに出力
    fmt.Println("Server starting on :8080...")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

このプログラムは、エンドポイントにアクセスしたら「Hello from Go Container!」というメッセージを返すシンプルなAPIとなります。

Go言語の文法については、ここでは割愛させていただきます。

5.5 Dockerイメージのビルド

まずは、作成したDockerfileをもとにDockerイメージを作成(ビルド)します。ターミナルで以下のコマンドを実行してください。

docker build -t go-app .

5.6 コンテナの起動

作成したイメージを使ってコンテナを起動します。以下のコマンドを実行してください。

docker run -p 8080:8080 -d --name my-go-container go-app

このコマンドでは、-p 8080:8080 というオプションを使って、自分のPC(ホスト)の8080番ポートと、コンテナ側の8080番ポートを接続しています。Dockerfileに EXPOSE 8080 と記述しましたが、実際に通信を通すためにはこのオプションが必須となります。

そのほか、-d はコンテナをバックグラウンドで動かし続けるための指定です。

5.7 動作確認

ターミナルで、以下のコマンドを実行します。

curl http://localhost:8080

「Hello from Go Container!」というコードが帰ってくれば、ここまでの操作は成功です。

5.8 APIにアクセスしてログの変化を見る

今回は、もう少しコンテナの中身をみていきます。

まずは、コンテナの実行ログを確認していきます。docker container logsコマンドを実行します。

docker container logs my-go-container

このコマンドは、my-go-containerのコンテナで、コンソールに出力されたログを表示させます。

コンテナ起動時に表示させている

Server starting on :8080...

と、先程APIを呼び出したときに出力している

Access received!

の2つのログが書き出されていれば、無事にログが参照できています。

5.9 コンテナに入り中身を確認

コンテナの中に入って「マルチステージビルドの結果」を検証します。

docker container exec -it my-go-container sh

コンテナの中に入ったら、lsコマンドでファイルを確認します。

ls -l

以下のように、実行ファイルにコピーされたmyappのファイルが存在していることが確認できます。

total 6200
-rwxr-xr-x    1 root     root       6345234 Jan 10 10:00 myapp

ポイントとして、ビルドステージで作成したmain.go(ソースコード)が存在せず、ビルドされた myapp(バイナリ)だけがあることを確認できます。

つまり、実行ステージで明示的に作成やコピーしたものだけが実行コンテナに含まれるため、ファイルを軽量にすることができます。

最後にexitコマンドで、コンテナから抜けることができます。

exit

5.10 不要リソースの削除

ハンズオンが終わったら、作成したリソースを削除(お片付け)します。

コンテナの停止

まずは動いているコンテナを停止します。

docker container stop my-go-container

停止したことを確認します。

docker container ls -a

ステータスがExitedとなっていれば、無事に停止しています(プロセスが終了しています)。

コンテナの削除

停止したコンテナを完全に削除します。

docker container rm my-go-container

コンテナの一覧に表示されなくなれば、無事に削除されています。

docker container ls -a

イメージの削除

最後に、作成したイメージも削除します。

docker image rm go-app

イメージの一覧に表示されなくなれば、無事削除されています。

docker image ls

6. 本番想定でのDockerfileの工夫

ここまでで、マルチステージビルドを使って最終イメージから不要なビルドツールを取り除き、軽量化する方法を学びました。本番運用を意識すると、これに加えてDockerfileに盛り込んでおきたい工夫がいくつかあります。ここでは代表的な3つを紹介します。

6.1 軽量なベースイメージを選ぶ

マルチステージビルドで最終イメージから不要なツールを除いても、ベースイメージ自体に不要なファイルが多く含まれていると、イメージはなかなか小さくなりません。そこで、ベースイメージを選ぶ段階から軽量なものを意識する、というのが本番想定の工夫の1つ目です。

公式イメージには、用途に応じていくつかのバリエーションが用意されていることが多く、Pythonの場合は次のような種類があります。

イメージ 特徴
python:3.13 標準イメージ。Debianベースで、開発に必要なツールが一通り含まれる。サイズは大きめ
python:3.13-slim 標準イメージから不要なパッケージを削った軽量版。多くのアプリケーションでまず候補になる
python:3.13-alpine Alpine Linuxベースで非常に軽量。ただしOSのライブラリ(musl libc)が標準的なものと異なるため、依存ライブラリのビルドで問題が出ることがある
gcr.io/distroless/python3 アプリ実行に必要な最小構成だけのイメージ。シェルすら含まれず、攻撃面が最小化される

迷ったら、まず -slim を選ぶと失敗が少ないです。よりサイズや攻撃面を絞りたい場合は distroless を検討する、という選び方が一般的です。各バリエーションの詳細はPython公式イメージ(Docker Hub)、distrolessはGoogleContainerTools/distroless(GitHub)に記載があります。

たとえば、これまで FROM python:3.13 と書いていた箇所を、以下のように書き換えるだけでイメージサイズを大きく削減できます。

FROM python:3.13-slim

6.2 実行ユーザを非rootユーザに切り替える

Dockerコンテナは、デフォルトでは root ユーザでアプリケーションが実行されます。動かすだけなら問題なく動きますが、本番運用ではこれを 一般ユーザ(非rootユーザ)に切り替えておく のがセオリーです。

理由は、アプリケーションに脆弱性が見つかったときの被害を小さく抑えるためです。コンテナ内が root のままだと、攻撃者が任意のコマンドを実行できる脆弱性を突いた際に、システムファイルを書き換えたりパッケージをインストールしたりといった広い操作が可能になってしまいます。一般ユーザで動かしておけば、そのユーザの権限の範囲内に被害を抑え込めます。Docker公式のBuilding best practicesでも「If a service can run without privileges, use USER to change to a non-root user.」と明記されており、特権が不要なサービスはUSER命令で非rootユーザに切り替えることが推奨されています。

Dockerfileで実行ユーザを切り替えるには、USER 命令を使います。一般的な書き方は次のとおりです。

RUN useradd -m appuser
USER appuser

RUN useradd -m appuserappuser という一般ユーザを作成し、USER appuser でそれ以降の命令とコンテナ起動時の実行ユーザを appuser に切り替えています。USER 命令は、依存関係のインストールなどrootでないと実行できない処理が終わった Dockerfileの末尾近くに書く のがポイントです。

正しく切り替わっているかは、コンテナを起動した状態で次のコマンドを実行して確認できます。

docker container exec <コンテナ名> whoami

以下のような実行結果が表示されます。

appuser

appuser と表示されれば、コンテナ内のプロセスが非rootユーザで動いていることを確認できます。

💡 ポイント
非rootユーザで動くコンテナは、Linuxの仕様上 1024未満のポート(80・443など)をbindできません。本番でHTTPS(443)やHTTP(80)で待ち受けたい場合は、コンテナ自身は8000・8080などの番号で待ち受け、外向きの80・443はALB(ロードバランサー)で終端する、という構成を取るのが一般的です。

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

docker build を実行すると、Dockerはまずビルドを実行する作業フォルダ(ビルドコンテキスト)の中身をすべてDockerデーモンに送信します。このとき、ビルドに不要なファイルまで送信されると、ビルドが遅くなったり、本来含めたくないファイル(.git 内の履歴、.env の機密情報など)がイメージに紛れ込むリスクがあります。

これを防ぐために使用するのが .dockerignore ファイルです。.dockerignore を作業フォルダの直下に置き、除外したいファイルやフォルダを記述しておくと、docker build 時にそれらは送信されなくなります。

書き方は .gitignore と同じ感覚で、1行に1パターンずつ記述します。

.git
.env
__pycache__/
*.log
node_modules/
.vscode/

それぞれの行の意味は次のとおりです。

  • .git — Gitの履歴フォルダ。コンテナ内では不要
  • .env — 機密情報を含む環境変数ファイル。イメージに含めてはいけない
  • __pycache__/ — Pythonのキャッシュフォルダ
  • *.log — ログファイル全般
  • node_modules/ — Node.jsの依存ライブラリ。コンテナ内で改めてインストールするため不要
  • .vscode/ — エディタ設定。コンテナ実行には不要

.dockerignore を置いた状態で docker build を実行すると、これらのファイルがビルドコンテキストから除外され、ビルドが高速化し、イメージにも含まれなくなります。

📝 .env.dockerignore に書き忘れた場合のリスク
.env には、データベースのパスワードやAPIキーなどの機密情報が書かれていることが多くあります。これを .dockerignore に書き忘れたままイメージをビルドし、ECRなどの公開リポジトリにPushすると、誰でも docker pull してイメージから機密情報を取り出せる状態になってしまいます。.env.dockerignore 登録は、本番運用では必須の設定と考えてください。

7. まとめ

この講座では、マルチステージビルドと応用的なDockerfileの書き方について学びました。

  • マルチステージビルドは、ビルド用と実行用のステージを分けてイメージを軽量化する手法
  • 複数のFROM命令を使って複数のステージを定義できる
  • COPY --from=ステージ名でステージ間のファイル受け渡しを行う
  • RUNはイメージビルド時に、CMDはコンテナ起動時に実行される
  • WORKDIRで作業ディレクトリを指定する
  • EXPOSEでコンテナが使用するポートを宣言する
  • -dオプションでコンテナをバックグラウンドで実行できる
  • docker container logsでコンテナのログを確認できる
  • docker container execで起動中のコンテナ内でコマンドを実行できる
  • 本番想定では、-slim-alpinedistroless などの軽量なベースイメージを選ぶ
  • USER 命令で実行ユーザを非rootユーザに切り替えることで、脆弱性が出たときの被害を抑えられる
  • .dockerignore でビルドコンテキストから不要ファイルや機密情報を除外する