Docker イメージ縮小オタク

おはようございます!!!coord_eです、よろしくどうぞ。

動機

Docker イメージが小さくなると → 嬉しい!1

一般的なテク

multi-stage build

ビルド環境とか最終的なイメージには必要ないんで…ビルドした後、必要なものだけ最終的なイメージに引っ張ってきます。

upx

バイナリ詰めるマン。なんでかバイナリサイズが 1/10 とかになる2。普通に怖い

スムーズに本題に入るためにまずシングルバイナリを否定します。シングルバイナリを否定してもいいですか?シングルバイナリがかわいそうですが…

シングルバイナリ作るのしんどくないですか?しんどいですね。みんながみんな Go を書いているわけではないので…あとソースコードが手元にない場合とかはもうどうしようもないですね。

実は、シングルバイナリを作らず極小 Docker イメージを作る方法があるんです!3

実行に必要なものをリストアップしよう

シェル芸をな4

大体の場合、実行に必要なものは実行ファイルそれ自体と動的リンクされたライブラリ、そして動的リンカ(プログラムインタプリタ)です5。前者は ldd(1)で、後者は readelf(1)でそれぞれ取得します。

{ \
      echo "/path/to/your/executable"; \
      readelf -l "/path/to/your/executable" \
        | grep "program interpreter" \
        | sed -e 's/^.*: \(.*\)\]$/\1/'; \
      ldd "/path/to/your/executable" \
        | awk -F'=>' '{print $2}' \
        | sed -e 's/(.*)//' -e '/^\s*$/d' \
        | awk '{$1=$1};1'; \
}

おそらくリンクが混ざっているので、次のコマンドにパイプして6リンク元とリンク先を両方ともリストに加えてやります。

xargs -I{} bash -c "echo {}; readlink -f {};"

これで実行に必要なファイルのリストができました!あとはこれを次のコマンドにパイプして、全ての必要なファイルを一つのディレクトリに詰めます。

xargs -I{} cp -r --parents {} /bundle

いいですね。では†次のステージ†へ…

FROM scratch
COPY --from=0 /bundle/ /.

必要なものは全部 /bundle/ に入ってるので、ベースイメージはscratchです7COPY でさっき/bundleにコピーしたファイル達を / に展開して…終了!

実例

HLintという Haskell の Lint ツールのイッミジを作ってみます。Haskell 製のツールで、シングルバイナリを作るのがちょっとめんどくさい8、なので今回の食材にぴったり。…では、出来上がった Dockerfile がこちらです!

FROM haskell:8

RUN cabal new-update

# install
ARG INSTALL_DIR=/usr/bin
RUN cabal new-install hlint-2.2.11 --installdir "${INSTALL_DIR}" --install-method copy

# prepare for compression
WORKDIR /tmp
ADD https://github.com/upx/upx/releases/download/v3.95/upx-3.95-amd64_linux.tar.xz upx.tar.xz
RUN tar --strip-components=1 -xf upx.tar.xz && mv upx /usr/bin/

# compress executable
RUN cp "${INSTALL_DIR}/hlint" /tmp/hlint_copy
RUN upx -q -9 --brute "${INSTALL_DIR}/hlint"

# collect runtime dependencies
RUN { \
      echo "${INSTALL_DIR}/hlint"; \
      echo "$(which git)"; \
      readelf -l /tmp/hlint_copy \
        | grep "program interpreter" \
        | sed -e 's/^.*: \(.*\)\]$/\1/'; \
      { ldd /tmp/hlint_copy; ldd $(which git); } \
        | awk -F'=>' '{print $2}' \
        | sed -e 's/(.*)//' -e '/^\s*$/d' \
        | awk '{$1=$1};1'; \
    } | xargs -I{} bash -c "echo {}; readlink -f {};" \
      | xargs -I{} cp -r --parents {} /bundle

# copy
FROM scratch
COPY --from=0 /bundle/ /.

WORKDIR /work
CMD ["${INSTALL_DIR}/hlint"]

hlintには--gitオプションがあって、git管理対象のファイルのみに lint を行うといったことができます。そこそこ便利なので、--gitオプションが使えるように上の Dockerfile ではgitを同梱しています。このように必要なものをザクザク足していくことも、できるんですね(小並感)。でも、イメージサイズは?はい、こちらになります!

$ docker image ls
…
coorde/hlint    2.2.11   662fad71d2d6    4 weeks ago    13.6MB
…

小さめで嬉しいですね。今回作ったcoorde/hlintはボクがバイト先で作っているソフトウェアの CI 上でブンブン働いています。

まとめ

参考文献


  1. よかったですね↩︎

  2. やりすぎると常に 255 を返すハリボテになったり並列実行したときに限り低確率で異常終了する不思議な物体になったりすることがあるので、オプションは-9 ぐらいで止めておくといい↩︎

  3. いかがでしたか?↩︎

  4. Dockerfile はシェル芸が正当化できるので好きです↩︎

  5. 他に必要なものがあったらこのリストに追加していけばいいです↩︎

  6. xargsbash -c コンボ嫌いなんですけどもっといい方法知りませんか?↩︎

  7. /bundle/に突っ込んだファイルを必ず使いに行くようにしているわけではないので、alpineとかにするとうまく動かないことが多い↩︎

  8. HLint のリリース、普通に動的リンクされたバイナリで配布されててそういうのもあるんだってなった↩︎