[1.59.0対応] Rustを軽量イメージ化するためのDockerfile
TL;DR
# Dockerfile
FROM ekidd/rust-musl-builder:stable as builder
WORKDIR /home/rust
COPY . .
RUN cargo build --release
FROM alpine:latest
WORKDIR /app_name
COPY --from=builder /home/rust/target/x86_64-unknown-linux-musl/release/app_name .
EXPOSE 8080
ENTRYPOINT [ "./app_name" ]
# .dockerignore
Dockerfile
.dockerignore
.gitignore
target
解説
Google cloud runなどのサーバーレスバックエンドをRustで書くとき、通常のrust
オフィシャルイメージを使ってビルドし、そのままpushするとサイズが膨大になってしまいます。
仮にDockerfileを以下のようにした場合…
FROM rust
WORKDIR /home/rust
COPY . .
RUN cargo build --release
最終イメージの容量は1.82GBです。
なるべく軽量にしたいのであれば、以下の2点がポイントです。
- ビルドコンテナとpushするコンテナを分ける
- pushするコンテナには軽量OSを選択
冒頭のDockerfileを使うと、最終イメージはわずか12.1MBと、大きな違いが生まれます。
(追記)
コメントで指摘のあったscratchとdistroless/ccでも試してみたところ、
image | size |
---|---|
scratch | 6.22MB |
distroless/cc | 28.8MB |
という結果になりました。distrolessが意外に重くて驚いています。
さらに、つい最近リリースされた1.59.0で追加されたstrip
を使ってみます。
これはdebuginfoなど、実使用のバイナリに不要な情報を剥ぎ取ることのできる新機能です。
# Cargo.toml
[profile.release]
strip = true
結果
image | size |
---|---|
alpine | 7.81MB |
scratch | 2.22MB |
distroless/cc | 24.8MB |
順位はもちろん変わりませんが、さらにサイズが小さくなっています。Rustの進化はすごい。
(追記ここまで)
ちなみに今回はサンプルとして自作のAPIサーバーをビルドしています。依存関係はこんな感じ。
axum = "0.4"
tokio = { version = "1.0", features = ["full"] }
log = "0.4.14"
env_logger = "0.9.0"
serde = {version = "1.0.133", features = ["derive"]}
serde_json = "1.0.74"
thiserror = "1.0.30"
futures = "0.3.19"
anyhow = "1.0.53"
tower-http = { version = "0.2.3", features = ["cors"]}
http = "0.2.6"
serde_yaml = "0.8.23"
axum-macros = "0.1.0"
chrono = "0.4.19"
ビルドとpush
上記の通り、ビルドしたコンテナでそのままpushすると、コンパイルされたライブラリなどでその分容量を食います。
これを避けるために、FROM image as builder
として、ビルドのためのコンテナを別途用意します。Rustはクロスプラットフォームですから、ビルド用コンテナの仕様を決めるために、まずpush用コンテナについて考え、そこから逆算していきましょう。
push用コンテナのOS
軽量で有名なalpineはPythonを動かす場合に考慮しなくてはいけないポイントがあるそうですが、Rustではそういったことは今のところなさそうです。ここは素直にalpineを選択しておきます。
ビルド用コンテナ
alpineはCライブラリとしてmuslを使っているので、ビルド時にx86_64-unknown-linux-musl
をターゲットにする必要があります。
オフィシャルrustイメージでターゲットを明示してもよいのですが、このDockerfileではekidd/rust-musl-builder
を使っています。
opensslなど、Rustのメジャーなクレートをビルドするために必要なライブラリをしっかり含んでいる便利イメージです。これ自体、muslを採用しているため、ビルド時にターゲットを明示せずに済んでいます。
あとはDockerfileに記述している通りです。cargo build --release
でバイナリを作り、alpineのほうにバイナリをコピーしてENTRYPOINT
で起動します。
Google cloud runの場合はポート8080を開けておく必要があるので、それもしっかり記述しておきます。
あとはgcloud run deploy
でデプロイすれば(もしくは好きなサービスにデプロイすれば)、動くサーバーの出来上がりです。
おまけ 詰まったところ
ENTRYPOINT [ "./app_name" ]
これを
ENTRYPOINT [ ./app_name ]
としてしまったせいで、デバッグ作業に数時間が消えました。皆さんもご注意ください😵
Discussion
alpine から scratch にしたらもっと小さくできそうです
distroless/ccはどうでしょうか
ご指摘ありがとうございます。それぞれの結果を本文に追記しました。ここにも貼っておきます。
結果