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

10 min読了の目安(約6500字TECH技術記事

最初に

仕事で周りみてると結構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が良い