Dockerのベストプラクティスについて調べたことをまとめる。
一般的なガイドライン
- ベースイメージのバージョンを固定する。ダイジェスト参照を使用するとなお良し
- ビルドキャッシュを活用する
RUN
でバックスラッシュを使用して、引数を複数行に並べる- アプリケーションを分離する。複数のアプリケーションを単一のコンテナに入れない
- 不要なパッケージはインストールしない。例えばDBイメージにテキストエディタはいらない
- マルチステージビルドを使用して最終イメージサイズを縮小する
- .dockerignoreファイルでビルドに関係ないファイルを除外する
イメージ
- ベースイメージにはオフィシャルイメージを使用すること(自前のイメージをベースイメージにしない)
- サイズが小さいAlpineを推奨
LABEL
- ラベルをつけることでイメージやコンテナを検索できる
Dockerfile
dockerfile LABEL python_version="3.8.12"
検索
> docker images --filter "label=python_version=3.8.12" REPOSITORY TAG IMAGE ID CREATED SIZE mmtest latest b2cf0b43f1dc About a minute ago 2.39GB > docker container ls --filter "label=python_version=3.8.12" CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b15779dec25a mmtest "/bin/bash" 13 seconds ago Up 12 seconds blissful_chebyshev
RUN
RUN
に記述するステートメントが長くなった場合は、可読性のためバックスラッシュで複数行にするRUN apt-get
にはいくつか直観に反する挙動があるapt-get update
を実行するときは常にapt-get install
を同じRUN
ステートメントに入れる(キャッシュ無効化"cache busting"と呼ばれるテクニック)。apt-get update
だけ実行するとキャッシュ問題を引き起こし、後続のapt-get install
で問題が起こるOK
RUN apt-get update && RUN apt-get install -y curl
バージョン固定化「version pinning」でもキャッシュ無効化できる
RUN apt-get update && RUN apt-get install -y curl=7.74.0
- NG
buildすると、Dockerでは全レイヤーをDockerキャッシュに保存する。buildを複数回行った場合、Dockerは、最初の命令と2回目以降の命令を同一とみなし、前のステップのキャッシュを再利用する。その結果、apt-get updateは実行されず、古いバージョンのcurlおよびnginxパッケージを取得する可能性がある。
RUN apt-get update RUN apt-get install -y curl
- aptキャッシュの削除
/var/lib/apt/lists配下のファイルを削除し、イメージサイズを減らす
RUN apt-get update && RUN apt-get install -y curl && rm -rf /var/lib/apt/lists/*
- パイプのエラー処理
RUN
では/bin/sh -c
で実行する。パイプの最後のオペレーションのみでexitコードを評価するため、パイプの途中の処理でエラーになっていてもエラーにならないケースがあるエラーで失敗させたい場合は
set -o pipefail &&
を使用するRUN set -o pipefail && wget -O - https://some.site | wc -l > /number
ENV
- バージョン番号を
ENV
でセットすると保守が楽になる
ENV PG_MAJOR=9.3
ENV PG_VERSION=9.3.4
RUN curl -SL https://example.com/postgres-$PG_VERSION.tar.xz | tar -xJC /usr/src/postgres && â¦
ENV PATH=/usr/local/postgres-$PG_MAJOR/bin:$PATH
ENV
はRUN
同様レイヤーを作成することになる。ENV
移行のレイヤーで環境変数をunset
してもレイヤーに維持され、値がダンプされる。これを防いで、環境変数をunset
するには、1つのRUN
でset
、unset
を実行し、環境変数をset
、unset
する必要がある。
# syntax=docker/dockerfile:1 FROM alpine RUN export ADMIN_USER="mark" \ && echo $ADMIN_USER > ./mark \ && unset ADMIN_USER CMD sh
- または、すべてのコマンドをシェルスクリプトに入れて、
RUN
コマンドで実行する。
ADD or COPY
COPY
はbuild context、またはmulti-staged buildのステージからファイルをコンテナにコピーする。
一方、ADD
はリモートのHTTPSまたはGitリポジトリからファイルをフェッチして追加する。
COPY
の代わりにbind mountを使うこともできる。bind mount はbuild contextからファイルをincludeできるため COPY
よりも効率的である。
bind mountされたファイルは単一のRUN
実行のために一時的にマウントされ、最終イメージには維持されない。最終イメージにファイルを維持したい場合はCOPY
を使用すること。
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install --requirement /tmp/requirements.txt
より確実にビルドキャッシュするには、wget
やtar
による手動でのリモートファイル追加よりも、ADD
の方が望ましい。
ENTRYPOINT
ENTRYPOINT
でコマンドを指定し、コマンドのパラメータをCMD
に設定するのがベストプラクティス。docker run
でパラメータを渡してCMD
の引数を上書きすることができる。
FROM buildpack-deps:bullseye ENTRYPOINT ["/bin/bash", "-c"] CMD ["ls"]
USER
- 非特権ユーザでサービスを実行する場合には
USER
で非ルートユーザーに変更する。USER
で非ルートユーザーになる場合は、事前にグループとユーザーを作成しておく
RUN groupadd -r postgres && useradd --no-log-init -r -g postgres postgres
- 但し、複雑さ回避のため、
USER
の頻繁な使用は避ける sudo
のインストールと使用は避ける。予期しないTTYと信号転送動作問題を引き起こす可能性があるらしい。どうしてもsudo
を使用する必要がある場合、例えばデーモンをルートとして初期化して非ルートとして実行する場合などは、gosuの使用を検討すること
WORKDIR
- 明確さと可読性のために、
WORKDIR
には常に絶対パスを使用すること - 可読性と保守性の観点から
RUN cd … && do-something
と記述することは避けるべき。そういった場合はWORKDIR
を使用すること