Fast + Small Docker Image Builds for Rust Apps
转自:https://shaneutt.com/blog/rust-fast-small-docker-image-builds/
In this post I’m going to demonstrate how to create small, quickly built Docker Images for Rustapplications.
We’ll start by creating a simple test application, and then building and iterating on a Dockerfile.
Requirements
Ensure you have the following installed:
Setup: demo app setup
Make sure you have and are using the latest stable Rust with rustup:
rustup default stable
rustup update
Create a new project called “myapp”:
cargo new myapp
cd myapp/
Setup: initial dockerfile
The following is a starting place we’ll use for our docker build, create a file named Dockerfile in the current directory:
FROM rust:latest
WORKDIR /usr/src/myapp
COPY . .
RUN cargo build --release
RUN cargo install --path .
CMD ["/usr/local/cargo/bin/myapp"]
And also create a .dockerignore file with the following contents:
target/
Dockerfile
You can test building and running the app with:
docker build -t myapp .
docker run --rm -it myapp
If everything is working properly, you should see the response Hello, world!.
Problems with our initial docker build
At the time of writing this blog post, Rust’s package manager cargo has an issue where it does not have a –depedencies-only option to build depedencies independently.
The lack of an option with cargo to build the depedencies separately leads to a problem of having the dependencies for the application rebuilt on every change of the src/ contents, when we really only want dependencies to be rebuilt if the Cargo.toml or Cargo.lock files are changed (e.g. when dependencies are added or updated).
As an additional problem, while the rust:latest Docker image is great for building, it’s a fairly large image coming in at over 1.5GB in size.
Improving builds so that dependencies don’t rebuild on src/ file changes
To avoid this problem and enable docker build cache so that builds are quicker, let’s start by modifying our Cargo.toml to add a dependency:
[package]
name = "myapp"
version = "0.1.0"
[dependencies]
rand = "0.5.5"
We’ve added a new crate as a dependency to our project named rand which provides convenient random number generation utilities.
Now if we run:
docker build -t myapp .
It will build the rand dependency and add it to the cache, but changing src/main.rs will invalidate the cache for the next build:
cat <<EOF > src/main.rs
fn main() {
println!("I've been updated!");
}
EOF
docker build -t myapp .
Notice that this build again had to rebuild the rand dependency.
While we’re waiting on a --dependencies-only build options for cargo, we can overcome this problem by changing our Dockerfile to have a default src/main.rs with which the dependencies are built before we COPY any of our code into the build:
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"]
The following line from the above Dockerfile will cause the following cargo build to rebuild only our application:
RUN rm -f target/release/deps/myapp*
So now if we build:
docker build -t myapp .
And then make another change to src/main.rs:
cat <<EOF > src/main.rs
fn main() {
println!("I've been updated yet again!");
}
EOF
We’ll find that subsequent docker build runs only rebuild myapp and the depedencies have been cached for quicker builds.
Reducing the size of the image
The rust:latest image has all the tools we need to build our project, but is over 1.5GB in size. We can improve the image size by using Alpine Linux which is an excellent small Linux distribution.
The Alpine team provides a docker image which is only several megabytes in size and still has some shell functionality for debugging and can be used as a small base image for our Rust builds.
Using multi-stage docker builds we can use rust:latest to do our build work, but then simply copy the app into a final build stage based on 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"]
Now if you run:
docker build -t myapp .
docker images |grep myapp
You should see something like:
myapp latest 03a3838a37bc 7 seconds ago 8.54MB
Next: Follow up - fixing and further improving our build
If you tried to run the above example with docker run --rm -it myapp, you probably got an error like:
standard_init_linux.go:187: exec user process caused "no such file or directory"
If you’re familiar with ldd you can run the following to see that we’re missing shared libraries for our application:
docker run --rm -it myapp ldd /usr/local/bin/myapp
In the above examples we show how to avoid rebuilding depdencies on every src/ file change, and how to reduce our image footprint from 1.5GB+ to several megabytes, however our build doesn’t currently work because we need to build against MUSL Libc which is a lightweight, fast standard library available as the default in alpine:latest.
Beyond that, we also want to make sure that our application runs as an unprivileged user inside the container so as to adhere to the principle of least privilege.
Building for MUSL Libc
To build for MUSL libc we’ll need to install the x86_64-unknown-linux-musl target so that cargo can be flagged to build for it with --target. We’ll also need to flag Rust to use the musl-gcc linker.
The rust:latest image will come with rustup pre-installed. rustup allows you to install new targets with rustup target add $NAME, so we can modify our Dockerfile as such:
# ------------------------------------------------------------------------------
# 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"]
Note the following line which shows the new way in which we’re building the app for MUSL Libc:
RUSTFLAGS=-Clinker=musl-gcc cargo build --release --target=x86_64-unknown-linux-musl
Do a fresh build of the app and run it:
docker build -t myapp .
docker run --rm -it myapp
If everything worked properly you should again see I've been updated yet again!.
Running as an unprivileged user
To follow principle of least privilege, let’s create a user named “myapp” which we’ll use to run myapp as instead of as the root user.
Change the Final Stage docker build stage to the following:
# ------------------------------------------------------------------------------
# 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"]
Update src/main.rs:
cat <<EOF > 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)
}
And now build the image and run:
docker build -t myapp .
docker run --rm -it myapp
If everything worked properly you should see I've once more been updated, and now I run as the user myapp!.
Wrapup!
The complete Dockerfile we have now for building our app while we’re working on it now looks like:
# ------------------------------------------------------------------------------
# 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"]
From here see my demo on deploying Rust to Kubernetes on DC/OS with Skaffold. Utilizing some of the techniques in that demo, you could automate deployment of your application to Kubernetesfor testing on a local minikube system using Skaffold.
Happy coding!
Fast + Small Docker Image Builds for Rust Apps的更多相关文章
- Introducing Makisu: Uber’s Fast, Reliable Docker Image Builder for Apache Mesos and Kubernetes
转自:https://eng.uber.com/makisu/?amp To ensure the stable, scalable growth of our diverse tech stack, ...
- Docker Resources
Menu Main Resources Books Websites Documents Archives Community Blogs Personal Blogs Videos Related ...
- CentOSLinux安装Docker容器
Docker 使用 环境说明 CentOS 7.3(不准确地说:要求必须是 CentOS 7 64位) 不建议在 Windows 上使用 Docker 基本概念 官网:https://www.dock ...
- [docker]通过阿里云源安装docker && flannel不通问题解决(try this guy out)
docker清理容器 # 容器停止后就自动删除: docker run --rm centos /bin/echo "One"; # 杀死所有正在运行的容器: docker kil ...
- 开发漫谈:千万别说你不了解Docker!
1dotCloud到Docker:低调奢华有内涵 写在前面:放在两年前,你不认识Docker情有可原.但如果现在你还这么说,不好意思,我只能说你OUT了.你最好马上get起来,因为有可能你们公司很 ...
- Centos 下安装Docker 遇到的一些错误
1.公司的服务器的内核版本:2.6.32-431.23.3.el6_x86_64 如何升级内核请参考前一篇文章 2.在这个地址上面下载 的 https://test.docker.com/builds ...
- 一、Docker之旅
刚刚接触到docker的同事可能会一头雾水,docker到底是一个什么东西,先看看官方的定义. Docker是一个开源的引擎,可以轻松的为任何应用创建一个轻量级的.可移植的.自给自足的容器.开发者在笔 ...
- 下载、运行docker
Get the Linux binary To download the latest version for Linux, use the following URLs: https://get.d ...
- Mac 下 docker安装
http://www.th7.cn/system/mac/201405/56653.shtml Mac 下 docker安装 以及 处理错误Cannot connect to the Docker d ...
随机推荐
- 忘记token怎么加入k8s集群
一.概述 新版本的k8s,初始化生成的token,只有24小时.超过时间,就得需要重新生成token,为了避免这种情况,直接生成永久的token 二.操作步骤 1.生成一条永久有效的token kub ...
- 线程池及Executor框架
1.为什么要使用线程池? 诸如 Web 服务器.数据库服务器.文件服务器或邮件服务器之类的许多服务器应用程序都面向处理来自某些远程来源的大量短小的任务.请求以某种方式到达服务器,这种方式可能是通过 ...
- Kafka启用SASL_PLAINTEXT动态配置JAAS文件的几种方式
Kafka是广泛使用消息服务,很多情况下关于认证部分我都是默认的配置,也就是不需要用户名/密码,也不配置证书.在内网或者在项目组内部可以,但是设计的跨部门时一般处于安全考虑都需要加上认证,防止kafk ...
- nodejs的交互式解释器模式常用命令
node - 进入交互器交互器解释模式 ctrl + c - 退出当前终端 ctrl + c 按下两次 - 退出 Node REPL ctrl + d - 退出 Node REPL 向上/向下 键 - ...
- RHEL6搭建网络yum源软件仓库
RHEL的更新包只对注册用户生效,所以需要自己手动改成Centos的更新包 一.查看rhel本身的yum安装包 rpm -qa | grep yum 二.卸载这些软件包 rpm -qa | grep ...
- 【Java基础】- Java学习路线图
Java的学习路线图,整理以备自己学习和温习. 1.Java基础 具体内容: 1. 编程基础(开发环境配置.基础语法.基本数据类型.流程控制.常用工具类) 2. 面向对象(继承.封装.多态.抽象类.接 ...
- js 数据类型检测
提起数据类型检测 大多数人首先想起的应该是 typeof 'xxx' == '数据类型' 坦然这种方法对于基本数据类型的检测还是非常方便的,但是当遇到引用数据类型 Object时却爱莫能助,下面就 ...
- Resource interpreted as Document but transferred with MIME type application/json
转自:https://blog.csdn.net/just_lover/article/details/81207472 我在修改并保存后,界面返回提示“undifine”,实际我是看到有返回提示的. ...
- iOS应用安全开发,你不知道的那些事
来源:http://www.csdn.net/article/2014-04-30/2819573-The-Secret-Of-App-Dev-Security 摘要:iOS应用由于其直接运行在手机上 ...
- IDA7.0安装findcrypt插件
效果图附上 安装成功的话,快捷键Ctrl+Alt+F可以调出上图的窗口,识别一些常见的算法,上面识别出是Base64加密 插件链接放上:https://github.com/polymorf/find ...