前置き
Dockerにはベストプラクティスなるものが存在している。
ベストプラクティスのポイントは大きく以下2点が中心になる。おそらく最優先で取り入れるべきはマルチステージビルドになると思われる
- セキュリティ対策
- イメージサイズ最適化
ベースイメージはalpineなどの軽量なものを選択する
- そもそもベースイメージが大きいと、最終のイメージサイズが大きくなる可能性が高い
- Ubuntu、CentOSなどの既にパッケージ化されたOSイメージは、サイズが大きい
- ただし、軽量なものを選択しても、ビルド時にライブラリなどを入れすぎるとかえってサイズが大きくなる可能性があるので注意(この場合、必要なライブラリが最初から入っているイメージを使う方が後からapt/apkでインストールするよりも高速)
- 以下はNodeの例
// bad
FROM node:17.0.1
// good
FROM node:17.0.1-alpine
公式のベースイメージを使用する
- 公式の検証済みイメージをベースイメージとしてFROMに指定する
・例えば、Nodeを使用する際にUbuntuのイメージを取得してそこにapt等でnodeを入れるのではなく、公式のnodeイメージをビルドしてバイナリをUbuntu等のOSイメージに取り込む(下部 マルチステージビルドを活用する を参照) - イメージの発行元の改ざんを検知する仕組みを取り入れる
・.bashrcや.zshrcに以下設定を追加すると、push, build, create, pull, run のコマンド実行時に自動で改ざんを検知することができるexport DOCKER_CONTENT_TRUST=1
- 脆弱性を定期的にスキャン
docker scan コマンドで実行できる
latestタグは使用しない
- イメージを指定する際、latestタグは使用せず具体的なバージョンを記載する
・意図しないバージョンでビルドしてしまう危険性があるため
// bad
FROM node:latest
// good
FROM node:17.0.1
イメージサイズは可能な限り小さく
- ビルドされた最終的なイメージは、サイズが大きくなればなるほど脆弱性も高まる
- イメージサイズは、docker imagesコマンドもしくは、Docker Desktopなどで都度確認する
- 後述のマルチステージビルドを活用する
Dockerfileについて
ファイルの管理
- DockerfileはWebサーバー用とAPサーバー用に分けて管理する
- 例えば、php-fpm、nginxという一般的な構成であれば、それぞれDockerfileを分けてビルドする
イメージレイヤを最小限に
- イメージはレイヤー(層)の蓄積で構成されている(下図はDockerHubの公式Nodeイメージ)
- レイヤーが増えるDockerfileのコマンドを理解する
・レイヤ―の増加は、最終イメージサイズの増加。
・run、copy、addがレイヤ数を増加させる - RUNコマンドは出来るだけ繋げる
・1行ずつを書くとその分だけレイヤーができてしまう
・ただし、むやみに繋げればいいというものではない。
・イメージサイズの減少が期待できるのは、何かを追加するコマンドと後から削除するコマンドをつなげた場合のみ(単に追加するだけのコマンドの連結は、繋げていない場合とイメージサイズは変わらない)
// これは、RUNを繋げていない場合よりもイメージサイズが小さくなる
RUN apt-get update \
&& apt-get install -y systemd \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
- レイヤ数とレイヤの各サイズはビルドの際に都度確認する
・docker historyコマンドで確認できる
ADDとCOPYの使い分けについて
- ADDは多機能なため、原則COPY推奨
- ADDは圧縮ファイルの展開もできる。urlを渡すとそのファイルやディレクトリをダウンロードできたりもする。ぱっと見てコードの内容がわかりにくい
イメージのキャッシュを最大限活用する
- キャッシュがある場合は以下のようにキャッシュを使用して、ビルドを飛ばして次の処理に進むのでビルドが高速
=> CACHED [stage-0 2/7] RUN apt update && apt install -y zlib1g-dev mar
- 変更頻度の高いものほどDockerfileの下部に記載する。なぜなら、変更箇所以降の後続のレイヤーのキャッシュが都度破棄される仕組みのため
マルチステージビルドを活用する
- ビルドステージとランタイムステージを分離することで最終イメージサイズを最適化させる
- 例えば、Nodeを使用する際にUbuntuのイメージを取得してそこにapt等でnodeを入れるのではなく、ビルドステージでnodeの公式イメージをビルドし、その成果物のバイナリのみをUbuntuのビルド時に使用する
- 最終的に生成されるイメージに不要なファイルはランタイムステージから分離する
- 以下は、一例として、nodeのビルドステージ(as node)でビルドし、そのバイナリファイルをCOPYコマンドでnginxに取り込んでいる
FROM node:16-alpine as node
FROM nginx:1.20-alpine
RUN apk update && \
apk add --update --no-cache --virtual=.build-dependencies g++
# マルチステージビルドでnodeとnpm
# node
COPY --from=node /usr/local/bin /usr/local/bin
# npm
COPY --from=node /usr/local/lib /usr/local/lib
COPY ./docker/nginx/*.conf /etc/nginx/conf.d/
WORKDIR /var/www
コンテナの実行ユーザー
- root権限でコンテナを実行しない
- 多くの公式イメージはrootユーザで動作する。 コンテナ内に侵入された場合、ホスト側もroot権限でアクセスが可能になってしまう。非常に危険。
- ビルド時に専用のユーザーを作成し、そのユーザーにvar/wwwなどの必要なディレクトリのみ権限を与えるようにする
# 追加
RUN useradd masayan
RUN chown -R masayan:<group> /var/www/
# 切り替え
USER masayan
- それぞれコンテナのシェルを起動した際、rootと非rootでは以下のように表示が異なる
// 非root /var/www $// root root@XXXXXXXX:/#
- なお、以下のコマンドで、ユーザー一覧を参照可能
cut /etc/passwd
- ユーザーグループの一覧はこちら
cat /etc/group
ビルド対象を認識する
- docker build を行う時にDocker デーモンへビルドコンテキストに指定したファイルを送信している
- ビルド対象を認識できていないと、意図しない大きなファイルがビルドコンテキストとして送信され、ビルドに時間がかかったり、イメージサイズが無駄に肥大化する
- .dockerignoreファイルを活用する、あるいは、ビルドコンテキストで不要なファイルをイメージに含まないようにする
- 一般に、buildのパスだけ指定しているケースが多いが、実はbuildコンテキストとDockerfileをそれぞれ分けて指定することも可能
build: context: .
dockerfile:
./docker/php/Dockerfile
コンテナについて
- エフェメラルなコンテナを作成する
・コンテナは一時的・短命であるべきとされている。必要な際にはすぐに停止・破棄・削除ができることが条件 - 1コンテナ1アプリ
・例えば、php-fpmとnginxの構成のように、webサーバーとしてのnginxと、APサーバーとしてのphp-fpmのように役割を分ける
まとめ
いかがでしたでしょうか。本記事では、Dockerのベストプラクティスについて説明しています。ベストプラクティスのポイントは大きく2つあり、セキュリティ対策とイメージサイズ最適化になります。イメージサイズは開発環境のパフォーマンスに大きく影響しますので、ぜひ参考にしてみてください。