[Rust] 构建最精简的 Rust Docker 镜像
💥 内容涉及著作权,均归属作者本人。若非作者注明,默认欢迎转载:请注明出处,及相关链接。
Summary: 构建最精简的 Docker 映像,以用来部署 Rust,将会带来很多益处:不仅有利于安全(减少攻击面),而且还可以缩短部署时间、降低成本(减少带宽和存储),并降低依赖项冲突的风险。
本文摘选自 Sylvain Kerkour(Bloom.sh 站点的创建者和《黑帽 Rust(Black Hat Rust)》一书作者)的文章 How to create small Docker images for Rust。
构建最精简的 Docker 映像,以用来部署 Rust,将会带来很多益处:不仅有利于安全(减少攻击面),而且还可以缩短部署时间、降低成本(减少带宽和存储),并降低依赖项冲突的风险。
Rust 代码
我们的“应用”相当简单:将构建一个简单的命令行实用程序,用来调用 https://api.myip.com,并打印响应结果。
进行 HTTPS 调用很有趣,因为它需要一个库来与 TLS(通常使用 openssl
)交互。但是,为了构建尽可能精简的 Docker 映像,我们需要对我们的程序做静态链接,而 openssl
的静态链接并不是那么容易实现。所以,本文我们将避免使用 openssl
,而改用 Rust 生态库 rustls
。
让我们暂时忽略 Jemalloc
。
cargo new myip
Cargo.toml
[package]
name = "myip"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
serde = { version = "1", features = ["derive"] }
reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls", "blocking"] }
[target.'cfg(all(target_env = "musl", target_pointer_width = "64"))'.dependencies.jemallocator]
version = "0.3"
main.rs
use serde::Deserialize;
use std::error::Error;
// Use Jemalloc only for musl-64 bits platforms
#[cfg(all(target_env = "musl", target_pointer_width = "64"))]
#[global_allocator]
static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc;
#[derive(Deserialize, Debug)]
struct ApiRes {
ip: String,
}
fn main() -> Result<(), Box<dyn Error>> {
let res = reqwest::blocking::get("https://api.myip.com")?.json::<ApiRes>()?;
println!("{}", res.ip);
Ok(())
}
我们执行 cargo run
,看看是否正常运行:
cargo run
Running `target/debug/myip`
127.0.0.1
使用空镜像 scratch
大小:15.9 MB
为了将 docker 空镜像 scratch
作为基础镜像,我们必须静态地将程序链接到 musl libc
,因为 glibc
在 scratch
中不可用。链接 musl libc
,可以通过增加编译目标 x86_64-unknown-linux-musl
来实现。
这样做有一个问题,musl
的内存分配器没有进行速度优化,可能会降低应用程序的性能,尤其是在处理高吞吐量的应用程序时。
这就是为什么我们要使用 jemalloc
,一个为高并发应用程序设计的内存分配器。
请注意,在使用 jemalloc
时可能会产生错误,因此请注意查看日志 ;)
作为一个数据节点,我已经使用它为数百万个 HTTP 请求提供了服务,没有任何问题。
Dockerfile.scratch
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates
# Create appuser
ENV USER=myip
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR /myip
COPY ./ .
RUN cargo build --target x86_64-unknown-linux-musl --release
####################################################################################################
## Final image
####################################################################################################
FROM scratch
# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
WORKDIR /myip
# Copy our build
COPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./
# Use an unprivileged user.
USER myip:myip
CMD ["/myip/myip"]
让我们构建,以及运行镜像:
docker build -t myip:scratch -f Dockerfile.scratch .
# 省略构建时输出
# ……
docker run -ti --rm myip:scratch
127.0.0.1
使用基础镜像 alpine
大小:21.6MB
Alpine Linux 是以安全为理念的轻量级 Linux 发行版,基于 musl libc
和 busybox
。
如果使用 scratch
空镜像不满足需求,并且需要包管理器来安装依赖项,如 chromium
或者 ssh
,那么应当使用 alpine
基础镜像。
由于基础镜像 alpine
基于 musl libc
,因此它的约束条件与空镜像 scratch
相同,我们需要使用编译目标 x86_64-unknown-linux-musl
,以静态链接我们的 Rust 程序。
Dockerfile.alpine
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder
RUN rustup target add x86_64-unknown-linux-musl
RUN apt update && apt install -y musl-tools musl-dev
RUN update-ca-certificates
# Create appuser
ENV USER=myip
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR /myip
COPY ./ .
RUN cargo build --target x86_64-unknown-linux-musl --release
####################################################################################################
## Final image
####################################################################################################
FROM alpine
# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
WORKDIR /myip
# Copy our build
COPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./
# Use an unprivileged user.
USER myip:myip
CMD ["/myip/myip"]
让我们构建,以及运行镜像:
docker build -t myip:alpine -f Dockerfile.alpine .
# 省略构建时输出
# ……
docker run -ti --rm myip:alpine
127.0.0.1
使用基础镜像 buster-slim
大小:79.4MB
最后一个例子,我们将使用基础镜像 debian:buster-slim
作为基本。由于 Debian 基于 glibc
,我们不再需要使用编译目标 x86_64-unknown-linux-musl
。
Dockerfile.debian
####################################################################################################
## Builder
####################################################################################################
FROM rust:latest AS builder
RUN update-ca-certificates
# Create appuser
ENV USER=myip
ENV UID=10001
RUN adduser \
--disabled-password \
--gecos "" \
--home "/nonexistent" \
--shell "/sbin/nologin" \
--no-create-home \
--uid "${UID}" \
"${USER}"
WORKDIR /myip
COPY ./ .
# We no longer need to use the x86_64-unknown-linux-musl target
RUN cargo build --release
####################################################################################################
## Final image
####################################################################################################
FROM debian:buster-slim
# Import from builder.
COPY --from=builder /etc/passwd /etc/passwd
COPY --from=builder /etc/group /etc/group
WORKDIR /myip
# Copy our build
COPY --from=builder /myip/target/release/myip ./
# Use an unprivileged user.
USER myip:myip
CMD ["/myip/myip"]
让我们构建,以及运行镜像:
docker build -t myip:debian -f Dockerfile.debian .
# 省略构建时输出
# ……
docker run -ti --rm myip:debian
127.0.0.1
结论
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myip scratch 795604e74501 9 minutes ago 15.9MB
myip alpine 9a26400587a2 2 minutes ago 21.6MB
myip debian c388547b9486 12 seconds ago 79.4MB
虽然本文我们聚焦于 Docker,但是如果镜像对您来说仍然太大,并且您知道自己在做什么,那么请参阅这篇文章,还有一些技巧可以将 Rust 可执行文件的大小进一步精简。
例如,在 Cargo.toml
文件中:
[profile.release]
lto = true
codegen-units = 1
然后,在执行 cargo build
命令后,在 Dockerfile
文件中增加:
RUN strip -s /myip/target/release/myip
现在,大小如下:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myip scratch de26b0460262 17 minutes ago 4.2MB
myip alpine 4188ccc82662 6 minutes ago 9.81MB
myip debian 0eefb58278a8 4 seconds ago 72.8MB
谢谢您的阅读!