DockerでCLIツールのイメージを作ってみる

はじめに

dockerはあまりつかったことなかったんですがECSを使ってみたいなと考えてたのと ビルドするのに時間がかかるツールなどをdockerイメージにしておくと手軽に色々なところから使えていいかなと思いやってみました。

Docker覚え書き

インストール

https://docs.docker.com/install/linux/docker-ce/ubuntu/

docker-ceというのを入れるようです。

コマンド

  • build Dockerfileからイメージを作成する
  • run イメージからコンテナを立ち上げる
  • images イメージの情報を表示する
  • system df ディスクの使用状況
  • system prune 未使用データの削除
  • rm コンテナを削除する
  • rmi イメージを削除する
  • tag イメージに別の名前をつける
  • pull イメージをdockerhubなどから取得する
  • push イメージをdockerhubなどにアップロードする

Dockerfile

  • FROM ベースのイメージ。ASでステージに名前をつけて後のステージから参照できる。
  • COPY ファイルやディレクトリをホストまたは別のステージからコピーする。 コピーされるファイルの変更検知するようにして以降のビルドをやり直すなどキャッシュの制御にも使えるようです。
  • ADD コピーと似ていてファイルやディレクトリを追加する。URLを指定することもできる。
  • RUN 色々なコマンドを実行する。
  • ENV 環境変数を変更する。パスの追加など。
  • ENTRYPOINT runしたときに実行されるコマンドのようです。上書きするにはオプションで--entrypoint commandのように指定する必要があります。
  • CMD runしたときに実行されるコマンドのようです。ENTRYPOINTと似ていますが少し違いがあるようです。 run実行時にコマンドにオプションや引数を渡すにはコマンドを省略せずに書く必要があります。

haskellプログラムのdockerビルド

イメージを作りたいプログラムというのはhaskellで書いたものでした。

色々調べていてhaskellプログラムのdockerビルドについていくつか知見が得られたため書いておきます。

ベースイメージ

https://hub.docker.com/_/haskell/

haskellのイメージがありました。ただ少し古いようでした。

https://hub.docker.com/r/fpco/stack-build/

もう一つstack-buildというイメージもあったんですがこちらは3GBぐらいあってちょっと大きすぎる感じがしました。

https://github.com/freebroccolo/docker-haskell

docker-haskellgithubを見ると最新にするプルリクが出ていてこれを参考にDockerfileを書けばイメージを作れそうでした。 下のDockerfileでイメージがつくれました。

## Dockerfile for a haskell environment
FROM       debian:stretch

## ensure locale is set during build
ENV LANG            C.UTF-8

RUN apt-get update && apt-get install -y --no-install-recommends gnupg dirmngr && \
    echo 'deb http://ppa.launchpad.net/hvr/ghc/ubuntu xenial main' > /etc/apt/sources.list.d/ghc.list && \
    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys F6F88286 && \
    apt-get update && \
    apt-get install -y --no-install-recommends cabal-install-2.0 ghc-8.2.2 happy-1.19.5 alex-3.1.7 \
            zlib1g-dev libtinfo-dev libsqlite3-0 libsqlite3-dev ca-certificates g++ git curl && \
    curl -fSL https://github.com/commercialhaskell/stack/releases/download/v1.6.1/stack-1.6.1-linux-x86_64-static.tar.gz -o stack.tar.gz && \
    curl -fSL https://github.com/commercialhaskell/stack/releases/download/v1.6.1/stack-1.6.1-linux-x86_64-static.tar.gz.asc -o stack.tar.gz.asc && \
    apt-get purge -y --auto-remove curl && \
    export GNUPGHOME="$(mktemp -d)" && \
    gpg --keyserver hkps://hkps.pool.sks-keyservers.net --recv-keys C5705533DA4F78D8664B5DC0575159689BEFB442 && \
    gpg --batch --verify stack.tar.gz.asc stack.tar.gz && \
    tar -xf stack.tar.gz -C /usr/local/bin --strip-components=1 && \
    /usr/local/bin/stack config set system-ghc --global true && \
    rm -rf "$GNUPGHOME" /var/lib/apt/lists/* /stack.tar.gz.asc /stack.tar.gz

ENV PATH /root/.cabal/bin:/root/.local/bin:/opt/cabal/2.0/bin:/opt/ghc/8.2.2/bin:/opt/happy/1.19.5/bin:/opt/alex/3.1.7/bin:$PATH

## run ghci by default unless a command is specified
CMD ["ghci"]

https://hub.docker.com/r/tkaaad97/haskell/

dockerhubにもpushしてみました。

indexファイルのダウンロードが遅い

stackが依存ライブラリの解決するときにindexファイルというのをダウンロードするようなんですが、 https://s3.amazonaws.com/hackage.fpcomplete.com/ から取得していて非常に時間がかかることがありました。

https://github.com/commercialhaskell/stack/issues/2240 https://github.com/commercialhaskell/stack/issues/3088

いくつかissueにもなっていました。s3からのダウンロードではなくCloudFrontなどCDNを使ったらどうかという話が上がっていました。

stackの設定でstackageやhackageのサイトから取得するようにしたら速くなったというコメントも書かれていたので真似してみました。

Dockerfileに下のように書いて設定を変更してみました。

RUN printf "\npackage-indices:\n- name: Stackage\n  download-prefix: https://www.stackage.org/lts-10.4/package/\n  http: https://www.stackage.org/lts-10.4/00-index.tar.gz\n- name: HackageOrig\n  download-prefix: https://hackage.haskell.org/package/\n  http: https://hackage.haskell.org/00-index.tar.gz" >> ~/.stack/config.yaml

下の内容がconfig.yamlに追加されてここからindex取得するようになります。

package-indices:
- name: Stackage
  download-prefix: https://www.stackage.org/lts-10.4/package/
  http: https://www.stackage.org/lts-10.4/00-index.tar.gz
- name: HackageOrig
  download-prefix: https://hackage.haskell.org/package/
  http: https://hackage.haskell.org/00-index.tar.gz

依存ライブラリのビルドをキャッシュしたい

プログラム開発途中などビルドが失敗することはよくあります。 しかしビルド時に依存ライブラリのビルドも全て毎回実行されると非常に時間がかかってしまいます。 依存ライブラリのビルドは対象プログラムのビルドとは分けてキャッシュして必要があれば更新したいです。

https://github.com/freebroccolo/docker-haskell/issues/54

こちらによさそうなやり方が書かれていました。

まず依存ライブラリのビルドを分けて行うためcabalファイルとstack.yamlのみをCOPYします。 これはソース全体をコピーしてしまうとソースコードに少しでも変更があった場合にキャッシュが使われなくなってしまうためです。 それからinstall --only-dependenciesで依存ライブラリのみビルドします。 次にソースコード全体をCOPYしてビルドを行います。

これで依存ライブラリビルドにキャッシュが使われるようになりました。

staticビルド

実行用イメージのサイズを小さくするには必要なライブラリのみstaticリンクして実行ファイルに含めてしまうのがいいと思います。 ただstaticリンクにするというのは思ったより難しくて色々と試行錯誤されているようです。

https://www.fpcomplete.com/blog/2016/10/static-compilation-with-stack https://vadosware.io/post/static-binaries-for-haskell-a-convoluted-approach/

あまりよくわかってませんがcabalファイルにld-options: staticを追加するのがいいようです。

pandocビルド

haskellCLIツールとしてはpandocが有名だと思います。 pandocのdockerビルドをしている方が他にもいたため自分でもやってみました。

pandocのcabalファイルを見るとstaticフラグとembed_data_filesフラグを有効にすると実行ファイルだけで動かせそうでした。

FROM haskell:8.2.2
RUN stack --system-ghc --resolver lts-10.4 --local-bin-path /sbin install pandoc pandoc-citeproc --ghc-options '-fPIC' --flag pandoc:static --flag pandoc:embed_data_files

FROM alpine:latest
COPY --from=0 /sbin/pandoc /sbin/
COPY --from=0 /sbin/pandoc-citeproc /sbin/
ENTRYPOINT ["pandoc"]

https://hub.docker.com/r/tkaaad97/pandoc/

ビルドしたイメージです。 簡単なコマンド実行してみたところは動作しているようでした。

作成したイメージ

https://github.com/bigsleep/ImagePacker

今回イメージ作成したプログラムはこちらです。

これは画像ファイルを読み込んでパッキングしてまとめた画像を出力するというプログラムです。

マルチディスタンスフィールドでのフォント描画の記事でもフォント画像を一ファイルにまとめるのに使いました。

以下のDockerfileでビルドすることができました。

FROM haskell:8.2.2 AS fetch-source
ADD https://api.github.com/repos/bigsleep/ImagePacker/branches/master /repository-hash
RUN git clone --depth 1 -b master https://github.com/bigsleep/ImagePacker /source

FROM haskell:8.2.2 AS build
RUN printf "\npackage-indices:\n- name: Stackage\n  download-prefix: https://www.stackage.org/lts-10.4/package/\n  http: https://www.stackage.org/lts-10.4/00-index.tar.gz\n- name: HackageOrig\n  download-prefix: https://hackage.haskell.org/package/\n  http: https://hackage.haskell.org/00-index.tar.gz" >> ~/.stack/config.yaml
RUN stack update
RUN mkdir /work
COPY --from=fetch-source /source/ImagePacker.cabal /work/
COPY --from=fetch-source /source/stack.yaml /work/
RUN cd /work && stack --system-ghc --resolver lts-10.4 --local-bin-path /sbin install --only-dependencies
COPY --from=fetch-source /source /work
RUN cd /work && stack --system-ghc --resolver lts-10.4 --local-bin-path /sbin install --flag ImagePacker:static

FROM alpine:latest
COPY --from=build /sbin/ImagePacker /sbin/
COPY --from=build /sbin/ImageGen /sbin/
ENTRYPOINT ["ImagePacker"]

https://hub.docker.com/r/tkaaad97/imagepacker/

まとめ

dockerコマンドとDockerfileの書き方を学びました。 haskellプログラムのdockerビルドについて学びました。 自作プログラムのdockerイメージを作ってdockerhubに上げることができました。