Atsushi2022の日記

データエンジニアリングに関連する記事を投稿してます

Docker ベストプラクティス

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
  • ENVRUN同様レイヤーを作成することになる。ENV移行のレイヤーで環境変数unsetしてもレイヤーに維持され、値がダンプされる。これを防いで、環境変数unsetするには、1つのRUNsetunsetを実行し、環境変数setunsetする必要がある。
# syntax=docker/dockerfile:1
FROM alpine
RUN export ADMIN_USER="mark" \
    && echo $ADMIN_USER > ./mark \
    && unset ADMIN_USER
CMD sh

ADD or COPY

COPYbuild 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

より確実にビルドキャッシュするには、wgettarによる手動でのリモートファイル追加よりも、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を使用すること

参考

Best practices for Dockerfile instructions

Dockerfile reference