Dockerfile書くときに意識してること

Dockerfile書くときに意識してること

July 17, 2020
Docker
docker, Dockerfile

最初に

仕事で周りみてると結構 Dockerfile 適当にかいてる?って思うことがあったので 自分の知識整理も含めて、整理

2018 年くらいまでは Docker ちゃんと追えてたけど、最近は k8s の方くらいしか見てないので新しい便利機能追えてなかったり、古い推奨とかあるかも..

ある程度 Docker build に関する知識ある前提 また Dockerfile も用途(開発用 Image か, 運用 Image なのか)で良い方法は変わってくると思う

  • 共通事項
  • 開発時の Dockerfile で意識すること(docker build の速度をあげる)
  • 運用時の Dockerfile で意識すること(docker image のサイズを落とす)

で書ければ良いかと

共通事項

なるべく Docker Official Images をベースにする

ベースイメージは Docker Hub が公式で提供しているイメージを使いましょう

どれが公式イメージ?かというと, image 名だけのパスです
例えばnginx:latest, redis:latestなど

Docker の image パスは <registry host>/<user>/<image name>:<tag>となっています

Docker Hub の場合はdocker.ioという registry host 名があるのですが、これは省略できます
また Docker Hub の公式 image はlibraryという user 名が付いてますが、これも省略できます
結局残るのが、image name と tag だけになります
(nginx:latestもフルパスはdocker.io/library/nginx:latestというわけです)

また Docker Hub で Official Images にチェックをつけて検索かけるとかでもいいかと思います

image

サードパーティのイメージ使う場合はちゃんと整備されているか確認する

公式で提供されていない場合(例えば kafka とかは公式提供なし) 次の選択肢が出てきます

  • サードパーティの image を使う
  • centosなどのベースイメージにして、ライブラリインストール手順を自分で組み込む

サードパーティが信用できない場合は後者にします。

サードパーティの信頼基準ですが、自分の場合は以下をみてます

  • サードパーティ(ユーザー)がそのイメージの開発者か?(例えば kafka というイメージだったら GitHub の kafka リポジトリのメンテナか)
  • サードパーティが個人でなく組織か?(会社だと安心感)
  • DockerHub に Dockerfile の登録や、GitHub の紐付けがされているか
  • Pull 数が少なすぎないか?

これらと、案件のセキュリティに対する考え方とかを合わせて選びます。
(個人があげてるようなイメージはよっぽどのことがない限りは使いません、仕事では)

タグはちゃんと指定しよう

タグを指定しない場合、latestタグを勝手に Docker がつけます。
しかしながら、latestタグは基本的に可変であることを意識しないといけません

Github の release の latest release と同じくらいに思っておけばいい

latest 指定で開発を続けていると、昨日まで動いたのに、今日は動かないとかが発生する可能性があります
(latest イメージが使っているライブラリがメジャーバージョンアップして、利用してた機能が廃止されたとか)

そういうのがあるので、基本的にタグはちゃんと指定しましょう
(指定したところで、それが上書きされる可能性はありますが、latest よりは更新頻度少ないですし, ちゃんとしたリポジトリなら破壊的更新はしないはず…)

選び方ですが、例えば redis イメージを使うとして
使うバージョンが確定しているのであれば、tag でそのバージョンを選びましょう

特にバージョンは指定ない場合は、latest が紐づいてるタグを選ぶと良いでしょう
Image には tag と合わせて DIGEST という HASH 値が割り振られてます。
latest は基本的に今の一番安定している tag の別名的なものなので、latest との DIGEST で検索かければ、同じ DIGEST でバージョンなどがちゃんとついてる tag が見つかると思います。
特になければこれを使うと良いです。

またバージョン決まってても redis:5.5, redis:5.5-streach みたいに色々あるかと思います。
この辺はだいたい util 系(vim とか)を入れてるか、削除しているかの違いだと思います。
特に希望なければ、イメージサイズが小さいのを選んでおくと良いと思います。

これは Dockerfile の書き方に関するナレッジなので、深く書きませんが
もちろん build するときもちゃんと tag 指定するようにしましょう(アプリとかの場合はバージョンつけたり)
全部 latest で上書き push は…

Docker は layer ごとに保存されるので、別タグで push しても差分情報だけが保存されるためさほどストレージは圧迫しません

Docker のストレージの仕組みはここを参照

ENV と ARG をちゃんと使い分けよう

ENV と ARG はどちらも Docker image の中に環境変数を突っ込むものですが 違いは, docker build 時のみ利用するのか、build 後、RUN するときも残ってて欲しいかが違いです

よくある例として、Dockerfile の中で、社内の git repository から clone するときに認証情報を渡さないといけない、ただハードコーディングはしたくないから環境変数で渡したいというパターン

このような場合は ARG を使うようにしましょう

どういうときに ENV を使うかというと、Docker image で動かすアプリが環境変数をパラメータでもち、かつそれにデフォルト値を指定しておきたい場合などです。 ENV で指定された環境変数ですが、docker run 時に-eオプションをつけることで、上書きすることができるので

複雑なインストール手順などは別途スクリプトファイルを書こう

Dockerfile では、やろうと思えば以下のように RUN で改行して、長い処理もかけます

RUN wget foobar.com/hoge && \
    cd hoge && \
    make && \
    make install

しかし、bash の構文使うような複雑な処理は書きづらいですし、ミスも発生しやすくなるので
長い shell コマンドはファイルに切り出して、Dockerfile に入れる形にした方がいいです。

#!/bin/bash

wget foobar.com/hoge
cd hoge
make
make install

こんな感じで shell ファイルに切り出して

COPY install.sh install.sh
RUN chmod +X install.sh
RUN ./install.sh

のようにCOPYで Dockerfile の中に入れて、動かす感じにした方が
管理もしやすくなります

開発時の Dockerfile で意識すること(docker build の速度をあげる)

開発時に意識していること(Docker の build 速度を上げるためにすること)を書いていきます.

あまり更新されないレイヤーを前の方に、頻繁に更新されるレイヤーほど後ろにしてキャッシュを聞かせる

docker build は dockerfile を上からなめて行って、前回と異なるレイヤーが出たところ以降がキャッシュが使われなくなります。
そのためなるべく頻繁に更新される部分(例えばソースコードの COPY とか)は Dockerfile の後ろにおくようにします。

packge 管理用のファイルとアプリのファイル COPY レイヤーを分ける

前の節と被りますが

$ ls
src pom.xml Dockerfile

みたいなフォルダ構成で

FROM java:8
WORKDIR /app
COPY . .
RUN 依存ライブラリダウンロード処理
RUN build処理

みたいな Dockerfile にしてしまうと、ソースコードを書き換えただけで、pom.xml をいじっていない場合でも毎回依存ライブラリのダウンロードが入ってしまう。

これの回避策として以下のようにすれば

FROM java:8
WORKDIR /app
COPY pom.xml .
RUN 依存ライブラリダウンロード処理
COPY . .
RUN build処理

ソースコードのみ変更時はRUN 依存ライブラリダウンロード処理レイヤーまでがキャッシュ利用されるので、build が高速になる
java で書いたけど、他の言語もそう(node だったら packege.json とか、go だったら go.mod とかを先に COPY する)

なるべくレイテンシーが少ないミラーリポジトリを使う

何か大きめのファイルを取得するような Dockerfile(例えば Spark など)
海外ミラーリポジトリとかでなく国内のファイルサーバを使うようにした方がいいです。

帯域細い場合は AWS 内で Build するのもあり

Dockerfile の書き方ではないけど、開発環境の回線がしょぼい場合は
AWS の vpc の中で Docker build した方が速かったりします

複数の Dockerfile で共通のベースイメージを用意しておく

複数のアプリ開発で、それぞれが共通のライブラリをあらかじめ apt-get や yum なりでインストールしておく必要がある場合は
事前にそれらだけを行う Dockerfile を作成し、共通 repository に push し、それをアプリ開発の base にすれば、ビルド時間の短縮につながります。

1 ファイルに完結しなくなるため、多用は良くない気がする..
プロジェクト内で完結する Image であれば、試してみると良いと思います。

Docker BuildKit 使う

Dockerfile じゃないけど
https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/build_enhancements/

最近はデフォで有効化されるのかな?

build が並列で行われるので、並列な build を意識した Dockerfile を書くと速くなる
普通の Dockerfile でも使うだけで多少速くなったと思うので、使うと良い

運用時の Dockerfile で意識すること(docker image のサイズを落とす)

運用時に意識していること(Docker イメージのサイズを落とすこと)を書いていきます.

Multi Stage Build

Docker の機能で Multi Stage Build というのがあります。
分かりやすいのが Go アプリの Docker Image を作るときなんかですが

Go はビルドするとシングルバイナリが生成されます。
このビルド処理には Go のランタイムがいるのですが、バイナリの実行にはランタイム不要です。
Go のランタイムを入れるだけで、ディスクが 100MB くらい埋まってしまうので運用イメージに使いたくないなと思います。

前であれば、Dockerfile の外で Go のビルドをしてバイナリを生成し、Dockerfile の中で COPY 使って、バイナリだけ運ぶとかやってました
これだと Dockerfile だけで完結しないため、ポータビリティ性が低いしビルドが各自の環境に依存してしまいます。そのためなるべく Dockerfile 内で Build したい..

これを解決するのが、Multi-Stage Build です

FROM golang as builder

RUN git clone github.com/hoge/hoge.git
WORKDIR hoge
RUN go build

FROM alpine
COPY --from=builder /go/hoge/app /app
CMD ["/app"]

こんな感じで、1 つの Dockerfile 内に 2 回FROMが登場します。
最初のFROM golang as builderで Go のランタイムをつかって Go アプリのリポジトリをクローンしてきて build を実行してバイナリを生成してます.

2 回目のFROMで alpine のイメージで build をしていきます。
次の行でCOPY --from=builder /go/hoge/app /app とありますが
ここで、前の build 処理で生成した、バイナリファイルだけを取得してきてます。

削除処理は取得処理と同一レイヤー内でする

最近は逆に分かりづらくなるってことでアンチパターンにもなってるかも?

RUN apt-get update && apt-get install -y \
    vim \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/*

こんな感じで、apt-get をしたら同じRUNの中で削除処理もしてしまう。

Docker はレイヤー(Dockerfile の 1 行)ごとに保存されるので、レイヤーが増えてもイメージサイズが減ることはありません

例えば、以下のように分けても 2 行目以降は, 削除されるファイルに参照しないというマーク付をするだけで、実体の削除は行われません。
そのためイメージサイズも減りません。
(この辺もこちらを参照)

RUN apt-get update && apt-get install -y vim
RUN apt-get clean
RUN rm -rf /var/lib/apt/lists/*

削除処理をする場合は、同一レイヤ内でやるようにしましょう

なるべく小さい baseimage を選ぶ

なるべく alpine ベースが良い
centos や ubuntu はそれだけど 100MB とかあるけど
alpine だと 10MB 程度

utility 的な使い方しないのであれば、alpine が良い