🐋

[1.59.0対応] Rustを軽量イメージ化するためのDockerfile

2022/02/22に公開
3

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点がポイントです。

  1. ビルドコンテナとpushするコンテナを分ける
  2. 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を使っています。

emk/rust-musl-builder: Docker images for compiling static Rust binaries using musl-libc and musl-gcc, with static versions of useful C libraries. Supports openssl and diesel crates.

opensslなど、Rustのメジャーなクレートをビルドするために必要なライブラリをしっかり含んでいる便利イメージです。これ自体、muslを採用しているため、ビルド時にターゲットを明示せずに済んでいます。

あとはDockerfileに記述している通りです。cargo build --releaseでバイナリを作り、alpineのほうにバイナリをコピーしてENTRYPOINTで起動します。
Google cloud runの場合はポート8080を開けておく必要があるので、それもしっかり記述しておきます。
あとはgcloud run deployでデプロイすれば(もしくは好きなサービスにデプロイすれば)、動くサーバーの出来上がりです。

おまけ 詰まったところ

ENTRYPOINT [ "./app_name" ] 

これを

ENTRYPOINT [ ./app_name ] 

としてしまったせいで、デバッグ作業に数時間が消えました。皆さんもご注意ください😵

Discussion

rithmetyrithmety

alpine から scratch にしたらもっと小さくできそうです

kyoheiukyoheiu

ご指摘ありがとうございます。それぞれの結果を本文に追記しました。ここにも貼っておきます。

# Cargo.toml
[profile.release]
strip = true

結果

image size
alpine 7.81MB
scratch 2.22MB
distroless/cc 24.8MB