原文地址html
这篇文章我会介绍如何快速为Rust应用体积小的Docker镜像。我会从建立一个小的测试应用开始,而后不断构建迭代Dockerfile。linux
首先确保你已经安装了下面的应用:git
使用rustup进行设置,确保使用最新稳定版的Rust。github
rustup default stable
rustup update
复制代码
建立一个myapp
的新项目docker
cargo new myapp
cd myapp/
复制代码
如下是咱们用于docker构建的起点,在当前目录中建立一个名为Dockerfile的文件:shell
FROM rust:latest
WORKDIR /usr/src/myapp COPY . . RUN cargo build --release RUN cargo install --path . CMD ["/usr/local/cargo/bin/myapp"] 复制代码
一样建立一个.dockerignore
的文件写入如下内容:缓存
target/
Dockerfile
复制代码
尝试构建并运行应用:bash
docker build -t myapp .
docker run --rm -it myapp
复制代码
若是一切都能工做的话,你能够在控制台看到Hello, world!
。app
当我写这篇文章的时候, Rust的包管理器cargo有一个issue是它尚未一个dependencies-only
的选项,用来单独构建依赖。工具
cargo缺乏这样单独构建依赖的选项使得咱们在每次改动src下面的内容时都会从新构建依赖项,但咱们只想在Cargo.toml或者Cargo.lock文件改变时才从新构建依赖项,比方说添加或者更新依赖。
另外一个问题是,虽然rust:latest Docker镜像很是适合构建,但它的体积至关大,超过1.6GB。
为了不这些问题而且开启docker构建缓存让构建变得更快,首先咱们开始改动Cargo.toml来添加一个依赖:
[package]
name = "myapp"
version = "0.1.0"
[dependencies]
rand = "0.5.5"
复制代码
咱们添加rand
包做为项目依赖,它提供了方便地生成随机数的工具。
如今若是运行:
docker build -t myapp .
复制代码
它将构建rand
依赖关系并将其添加到缓存,可是更改src/main.rs
将使得生成的缓存无效:
cat src/main.rs
fn main() {
println!("I've been updated!");
}
docker build -t myapp .
复制代码
请注意,这次构建必须再次重建rand依赖项。
在等待cargo的only-dependencies
构建选项时,将任何代码复制到构建环境以前,咱们能够经过将Dockerfile
更改成默认的src/main.rs
来解决此问题:
FROM rust:latest
WORKDIR /usr/src/myapp
COPY Cargo.toml Cargo.toml
RUN mkdir src/
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs
RUN cargo build --release
RUN rm -f target/release/deps/myapp*
COPY . .
RUN cargo build --release
RUN cargo install --path .
CMD ["/usr/local/cargo/bin/myapp"]
复制代码
上面的Dockerfile中的如下几行将致使Cargo构建时仅从新构建咱们的应用程序:
RUN rm -f target/release/deps/myapp*
复制代码
若是咱们构建一次:
docker build -t myapp .
复制代码
而后对src/main.rs
作一点小小的改动:
cat src/main.rs
fn main() {
println!("I've been updated yet again!");
}
复制代码
咱们将会发现接下来docker只会在咱们的应用逻辑改变时从新构建,而依赖项目则被缓存起来用来快速构建。
rust:latest镜像具备构建项目所需的全部工具,但它的大小超过1.6GB。咱们可使用Alpine Linux(一种出色的小型Linux发行版)来改善镜像大小。
Alpine团队提供了一个只有几兆字节大小的docker映像,而且仍然具备一些用于调试的shell功能,而且能够用做Rust构建的小型基础镜像。
经过多阶段docker构建,咱们可使用rust:latest来完成构建工做,只需将应用复制到基于alpine:latest的最终构建阶段便可:
# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------
FROM rust:latest as cargo-build
WORKDIR /usr/src/myapp COPY Cargo.toml Cargo.toml RUN mkdir src/ RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs RUN cargo build --release RUN rm -f target/release/deps/myapp* COPY . . RUN cargo build --release RUN cargo install --path . # ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------
FROM alpine:latest
COPY --from=cargo-build /usr/local/cargo/bin/myapp /usr/local/bin/myapp CMD ["myapp"] 复制代码
如今若是你运行:
docker build -t myapp .
docker images |grep myapp
复制代码
你能够看到这些东西:
myapp latest 03a3838a37bc 7 seconds ago 8.54MB
复制代码
若是你尝试使用docker run --rm -it myapp
运行以上示例,则可能会遇到相似下面的错误:
standard_init_linux.go:187: exec user process caused "no such file or directory"
复制代码
若是您熟悉ldd则能够运行如下命令,以查看咱们缺乏的应用程序共享库:
docker run --rm -it myapp ldd /usr/local/bin/myapp
复制代码
在上面的例子中我演示了如何经过避免每次src/main.rs
改动从新构建依赖提高构建速度,以及如何将镜像大小从1.6GB+减小到几兆字节,然而咱们的构建仍是不能生效,由于咱们须要针对MUSL Libc进行构建,这是一个轻量级、快速的标准库,在alpine:latest中是默认库。
除此以外,咱们还但愿确保咱们的应用程序以容器内的非特权用户身份运行,从而遵照最小特权原则。
要针对MUSL libc进行构建,咱们须要安装x86_64-unknown-linux-musl构建目标,以即可以将Cargo标记为使用--target为其构建。咱们还须要标记Rust使用musl-gcc连接器。
rust:latest镜像预安装rustup。 rustup容许咱们使用rustup target add $NAME
安装新的构建目标,所以咱们能够像这样修改Dockerfile:
# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------
FROM rust:latest as cargo-build
RUN apt-get update
RUN apt-get install musl-tools -y
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /usr/src/myapp
COPY Cargo.toml Cargo.toml
RUN mkdir src/
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
RUN rm -f target/x86_64-unknown-linux-musl/release/deps/myapp*
COPY . .
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------
FROM alpine:latest
COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp /usr/local/bin/myapp
CMD ["myapp"]
复制代码
请注意如下行,它显示了咱们为MUSL Libc构建应用程序的新方式:
RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-mus
复制代码
从新构建应用程序并运行它:
docker build -t myapp .
docker run --rm -it myapp
复制代码
若是一切正常,你应该再次看到应用已被更新了!
为了遵循最小特权原则,咱们建立一个名为myapp
的用户,避免用户以root
用户的身份运行应用。
将Final Stage docker build阶段更改成如下内容:
# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------
FROM alpine:latest
RUN addgroup -g 1000 myapp
RUN adduser -D -s /bin/sh -u 1000 -G myapp myapp
WORKDIR /home/myapp/bin/
COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp .
RUN chown myapp:myapp myapp
USER myapp
CMD ["./myapp"]
复制代码
更新src/main.rs
为:
use std::process::Command;
fn main() {
let mut user = String::from_utf8(Command::new("whoami").output().unwrap().stdout().unwrap();
user.pop();
println!("I've once more been updated, and now I run as the user {}!", user)
}
复制代码
如今构建并运行应用:
docker build -t myapp .
docker run --rm -it myapp
复制代码
若是一切正常,你应该会看到应用已再次更新,如今应用以用户myapp运行。
如今咱们构建应用程序的完整Dockerfile以下所示:
# ------------------------------------------------------------------------------
# Cargo Build Stage
# ------------------------------------------------------------------------------
FROM rust:latest as cargo-build
RUN apt-get update
RUN apt-get install musl-tools -y
RUN rustup target add x86_64-unknown-linux-musl
WORKDIR /usr/src/myapp
COPY Cargo.toml Cargo.toml
RUN mkdir src/
RUN echo "fn main() {println!(\"if you see this, the build broke\")}" > src/main.rs
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
RUN rm -f target/x86_64-unknown-linux-musl/release/deps/myapp*
COPY . .
RUN RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
# ------------------------------------------------------------------------------
# Final Stage
# ------------------------------------------------------------------------------
FROM alpine:latest
RUN addgroup -g 1000 myapp
RUN adduser -D -s /bin/sh -u 1000 -G myapp myapp
WORKDIR /home/myapp/bin/
COPY --from=cargo-build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/myapp .
RUN chown myapp:myapp myapp
USER myapp
CMD ["./myapp"]
复制代码
从这里观看有关使用Skaffold在DC / OS上将Rust部署到Kubernetes的演示。利用该演示中的一些技术,你能够将应用程序自动部署到Kubernetes,以使用Skaffold在本地minikube系统上进行测试。
Happy coding!