最も小さいOCamlのDockerコンテナイメージの作り方(2022版)
目次
はじめに
OCamlではかつてjbuilder
というビルドツールが利用されていた。
しかし2018年以降はdune
というプロジェクトに変わったため、
dune
でアプリケーションを構築し且つ最小のDockerコンテナイメージを生成する方法を示す。
参照: dune migration
opamのインストール
opam
はOCamlのパッケージ管理ツールである。
OCamlのライブラリ(モジュール)やdune
などのツールをインストールにはこれを利用するため、
予めインストールしておく。
以下のコマンドを実行しopam
をインストールする。
なお、インストール先の環境はUbuntu 20.04
となっている。
sh <(curl -sL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)
次に初期化処理を行う。
opam init
opam init
を実行した際、以下のように推奨パッケージを要求される場合がある。
root@90e8251e3431:/# opam init
[WARNING] Running as root is not recommended
[NOTE] Will configure from built-in defaults.
Checking for available remotes: none.
- you won't be able to use rsync and local repositories unless you install the rsync command on your system.
- you won't be able to use git repositories unless you install the git command on your system.
- you won't be able to use mercurial repositories unless you install the hg command on your system.
- you won't be able to use darcs repositories unless you install the darcs command on your system.
[WARNING] Recommended dependencies -- most packages rely on these:
- make
- m4
- cc
[ERROR] Missing dependencies -- the following commands are required for opam to operate:
- patch
- unzip
- bwrap: Sandboxing tool bwrap was not found. You should install 'bubblewrap'. See https://opam.ocaml.org/doc/FAQ.html#Why-does-opam-require-bwrap.
上記の案内に従ってパッケージ群をインストールする。
apt install git hg darcs rsync pkg-config make m4 gcc patch unzip bubblewrap
インストールが完了したら、改めてopam init
を実行する。
以下のように設定ファイルを上書きするかどうかを求められる場合があるが、
初めて利用する場合はyes
を選択しておけばよい。
Do you want opam to modify ~/.profile? [N/y/f]
(default is 'no', use 'f' to choose a different file) y
A hook can be added to opam's init scripts to ensure that the shell remains in sync with the opam environment when they are loaded. Set that up? [y/N] y
なお、opam init
ではdefault
というスイッチが作成される。
スイッチとは、コンパイラ及びパッケージ群を1つのまとまりと管理するためのopam
の機構である。
これ有効にするためには、eval $(opam env)
を求められる場合があるため、その際はこのコマンドを実行する。
プロジェクト毎に異なるパッケージを利用するための設定
OCamlには様々なバージョンがあり、いくつかのバージョンを使い分けたいケースもある。
その場合、opam switch
というコマンドを利用し、コンパイラ及びパッケージ群を1つのまとまり(スイッチと呼ぶ)に集約できる。
例えば、以下のコマンドを実行すると、コンパイラ4.12.0を利用するmy-app
というスイッチを作成できる。
opam switch create my-app ocaml-base-compiler.4.12.0
作成したスイッチはopam switch
コマンドで確認できる。その例を以下に示す。
opam switch
# switch compiler description
default ocaml-base-compiler.4.12.0 default
-> my-app ocaml-base-compiler.4.12.0 my-app
参照: opam switch
OCamlアプリケーションの作成
まずは、OCamlでアプリケーション開発のためのビルドツールdune
をインストールする。
その他、本稿ではWebアプリケーションを開発する予定であるため、
Webアプリケーションに必要なライブラリ(モジュール)も合わせてインストールする。
opam install dune lwt cohttp-lwt-unix
dune
を利用するためには、dune
という設定ファイルを利用する。
通常利用の場合は、実行ファイル名や利用するライブラリを記載するだけでよいが、
今回はDockerコンテナで利用することを想定するため、静的リンクでバイナリを構成するよう指定する必要がある。
参照: dune quickstart
本稿ではmain
というアプリケーション名で生成することにする。
以下に、dune
ファイルの内容を示す。
;; https://discuss.ocaml.org/t/linking-several-so-libraries-produced-by-dune/6133
(executable
(name main)
(link_flags :standard -linkall)
(libraries lwt cohttp-lwt-unix)
)
次にアプリケーション本体を作成する、
本稿では、cohttp
ライブラリ(モジュール)を利用しサンプルアプリケーションを作成する。
アプリケーションファイル名をmain.ml
とし、以下の内容で作成する。
open Lwt
open Cohttp
open Cohttp_lwt_unix
let server =
let callback _conn req body =
let uri = req |> Request.uri |> Uri.to_string in
let meth = req |> Request.meth |> Code.string_of_method in
let headers = req |> Request.headers |> Header.to_string in
( body |> Cohttp_lwt.Body.to_string >|= fun body ->
Printf.sprintf "Uri: %s\nMethod: %s\nHeaders\nHeaders: %s\nBody: %s" uri
meth headers body )
>>= fun body -> Server.respond_string ~status:`OK ~body ()
in
Server.create ~mode:(`TCP (`Port 8000)) (Server.make ~callback ())
let () = ignore (Lwt_main.run server)
参照: cohttp basic server tutorial
バイナリファイルが動作するかどうかは、以下のいづれかのコマンドで確認できる。
dune build main.exe
または、
_build/default/main.exe
Dockerfileの作成
次にDockerfile
を作成する。
Dockerコンテナはマルチステージビルドに対応させるため3つのステージに分けることにした。
以下に各ステージとその役割を示す。
- init-opam
- opamが同梱されたコンテナイメージのパッケージを最新にするステージ
- ocmal-app-base
- アプリケーションのビルドに必要なパッケージの導入及びアプリケーションのビルドを行うステージ
- ocaml-app
- ocaml-app-baseで作成したバイナリファイルを最小コンテナイメージ内にコピーしエントリポイントを設定するステージ
上記のステージを構成するためのDockerfile
の内容を以下に示す。
FROM ocaml/opam:alpine AS init-opam
RUN set -x && \
: "Update and upgrade default packagee" && \
sudo apk update && sudo apk upgrade && \
sudo apk add gmp-dev
# --- #
FROM init-opam AS ocaml-app-base
COPY . .
RUN set -x && \
: "Install related pacakges" && \
opam install . --deps-only --locked && \
eval $(opam env) && \
: "Build applications" && \
dune build main.exe && \
sudo cp ./_build/default/main.exe /usr/bin/main.exe
# --- #
FROM alpine AS ocaml-app
COPY --from=ocaml-app-base /usr/bin/main.exe /home/app/main.exe
RUN set -x && \
: "Update and upgrade default packagee" && \
apk update && apk upgrade && \
apk add gmp-dev && \
: "Create a user to execute application" && \
adduser -D app && \
: "Change owner to app" && \
chown app:app /home/app/main.exe
WORKDIR /home/app
USER app
ENTRYPOINT ["/home/app/main.exe"]
なお、今回はロックファイルを利用して本プログラムに依存するパッケージを管理することにした。
dune-project
というファイルを作成し、その中に本プログラムが利用するパッケージを記載する。
dune-project
に(generate_opam_files true)
及び(package ...)
が含まれていれば、
dune build @install
コマンドにて、パッケージ名.opam
ファイルを自動で生成する。
dune-project
の内容は以下のとおりとなる。
(lang dune 2.7)
(name main)
(version 1.0.0)
(generate_opam_files true)
(license MIT)
(authors "Toshiki Kawai")
(maintainers "Toshiki Kawai")
(package
(name main)
(synopsis "The First architecture on OCaml")
(description "The First architecture style when startup project.")
(depends
(dune (> 1.5))
(lwt (>= 5.4.0))
(cohttp-lwt-unix (>= 4.0.0))
(yojson (>= 1.7.0))))
dune-project
から.opam
ファイルを作成するには、下記のコマンドを実行する。
なお、ここで生成されるファイル名は、main.opam
となる。
dune build @install
以降は、Dockerfileに記載しているように、
opam install . --deps-only --locked
を利用して依存パッケージ群をインストールできる。
このdune-project
及びDockerfile
と前章で作成したdune
ファイル、main.ml
を同一ディレクトリに配置する。
Dockerfile dune dune-project main.ml
そして、docker build .
コマンドを実行し、Dockerコンテナイメージを作成する。
今回は docker build --tag 20210530-ocaml-micro-service .
で実行し、
Dockerコンテナイメージのタグに20210530-ocaml-micro-serviceという名称をつけた。
ビルド後のイメージサイズは25MBで、かなり小さいコンテナイメージとなった。
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
20210530-ocaml-micro-service latest 589420cffa3a 3 days ago 25MB
まとめ
OCamlアプリケーションを作成する際に役立つ、opamやduneなどの便利なツールを利用すると効率よく開発できる。
その際にDockerイメージ内にopamやduneといったツールを含めてしまうとイメージサイズが大きくなり、
本来アプリケーションに必要でないものまでインストールされた状態となってしまう。
そこでマルチステージビルドを利用し、ビルド前まではopamやduneなどの便利ツールを利用しつつ。
最終的にはalpineなどの軽量OSにアプリケーションのみデプロイすることで最小イメージの作成に成功した。