はじめに
Rustで作ったアプリケーションを本番環境にデプロイするとき、Dockerは欠かせないツールです。しかし、何も考えずにDockerfileを書くと、ビルドツールチェーン一式を含んだ巨大なイメージができてしまいます。
この記事ではマルチステージビルドを使って、Rustアプリを小さく・安全にコンテナ化する方法を解説します。
単純なDockerfileの問題点
まず「よくある失敗例」を見てみましょう。
# NG例:ビルドツールがそのまま残る
FROM rust:1.78
WORKDIR /app
COPY . .
RUN cargo build --release
CMD ["./target/release/myapp"]
このイメージをビルドすると、サイズは1GB以上になることがあります。rust公式イメージにはコンパイラ・Cargo・各種ライブラリがすべて含まれているためです。本番環境で必要なのはビルド済みのバイナリだけなのに、不要なものを大量に抱えることになります。
マルチステージビルドとは
マルチステージビルドとは、1つのDockerfileに複数のFROM命令を書き、ステージをまたいでファイルをコピーできる仕組みです。
- ビルドステージ:Rustのツールチェーンを使ってバイナリを生成する
- 実行ステージ:バイナリだけをコピーして最小限のイメージを作る
この2段階構成により、最終イメージからビルドツールを完全に排除できます。
サンプルアプリの準備
まず、簡単なHello WorldのRustプロジェクトを用意します。
cargo new myapp
cd myapp
src/main.rs はデフォルトのままでOKです。
fn main() {
println!("Hello, Docker!");
}
マルチステージビルドのDockerfileを書く
# ---- ビルドステージ ----
FROM rust:1.78-slim AS builder
WORKDIR /app
# 依存関係のキャッシュ(後述)
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/myapp*
# 実際のソースコードをコピーしてビルド
COPY src ./src
RUN cargo build --release
# ---- 実行ステージ ----
FROM debian:bookworm-slim AS runtime
# 最低限の実行依存を入れる
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# ビルドステージからバイナリだけコピー
COPY --from=builder /app/target/release/myapp ./myapp
# 非rootユーザーで実行(セキュリティ向上)
RUN useradd -m appuser
USER appuser
CMD ["./myapp"]
ポイント解説
依存関係のキャッシュ
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
ダミーのmain.rsを置いてから依存クレートだけを先にビルドします。Cargo.tomlが変わらない限り、このレイヤーはキャッシュされるため、ソースコードを変更しただけのリビルドが大幅に速くなります。
実行ステージのベースイメージ
debian:bookworm-slimは最小限のDebianイメージです。Rustのバイナリはデフォルトでglibcにリンクされているため、glibc互換のイメージが必要です。ca-certificatesはHTTPS通信が必要なアプリで使います。
非rootユーザー
RUN useradd -m appuser
USER appuser
コンテナをrootで実行すると、コンテナエスケープ時のリスクが高まります。専用ユーザーを作って切り替えるのがベストプラクティスです。
ビルドと実行
# イメージをビルド
docker build -t myapp:latest .
# コンテナを実行
docker run --rm myapp:latest
# => Hello, Docker!
# イメージサイズを確認
docker images myapp
マルチステージビルドを使うと、最終イメージは100MB前後に収まります(単純なアプリなら80MB程度)。rust公式イメージの1/10以下です。
さらに小さくするには:scratch や musl を使う
もし外部ライブラリへの依存をなくせるなら、scratch(空のイメージ)をベースにできます。そのためには静的リンクバイナリが必要です。
# ---- ビルドステージ(musl static link)----
FROM rust:1.78-slim AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt-get update && apt-get install -y musl-tools
WORKDIR /app
COPY . .
RUN cargo build --release --target x86_64-unknown-linux-musl
# ---- 実行ステージ ----
FROM scratch
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/myapp /myapp
CMD ["/myapp"]
muslでビルドしたバイナリはlibcを静的にリンクするため、scratchイメージで動きます。最終イメージはバイナリサイズのみ(数MB〜十数MB)となり、攻撃面も最小限になります。
まとめ
| 方法 | イメージサイズ | 手軽さ |
|---|---|---|
| シングルステージ(rust公式) | 1GB以上 | ◎ |
| マルチステージ + debian-slim | 〜100MB | ○ |
| マルチステージ + scratch (musl) | 数MB〜 | △ |
Rustアプリのコンテナ化では、マルチステージビルドがほぼ必須のテクニックです。
- 依存キャッシュでビルドを速く
- 最小ベースイメージでイメージを軽く
- 非rootユーザーでセキュアに
まずはdebian:bookworm-slimベースのマルチステージビルドから始めて、必要に応じてmusl + scratchに挑戦してみてください。