An Interest In:
Web News this Week
- April 23, 2024
- April 22, 2024
- April 21, 2024
- April 20, 2024
- April 19, 2024
- April 18, 2024
- April 17, 2024
How to create small Docker images for Rust
Building minimal Docker images to deploy Rust brings up a lot of benefits: it's not only good for security (reduced attack surface) but also to improve deployment times, reduce costs (less bandwidth and storage), and reduce the risk of dependency conflicts.
Table of contents
- Code
- FROM scratch (15.9MB)
- FROM alpine (21.6MB)
- FROM gcr.io/distroless/cc (33MB)
- FROM buster-slim (79.4MB)
- Conclusion
- The code is on GitHub
Want to learn more? Take a look at my book Black Hat Rust to learn Rust, Security and Cryptography and where we use Docker for offensive purpose.
Code
Our "app" is rather simple: we are going to build a command-line utility that calls https://api.myip.com
and prints the result.
Making HTTPS calls is interesting because it requires a library to interact with TLS, usually openssl
. But in order to build the smallest Docker image possible, we need to statically link our program, and statically linking openssl
is not that easy. That's why we will avoid openssl
and use rustls
instead.
Let's ignore the Jemalloc
thing for a moment.
$ 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 Running `target/debug/myip`127.0.0.1
FROM scratch
Size: 15.9 MB
In order to use FROM scratch
as the base image, we have to statically link our program to the musl libc
because glibc
is unavailable in scratch
. It can be achieved by using the x86_64-unknown-linux-musl
target.
A problem with this approach is that musl
's memory allocator is not optimized for speed and may reduce your app's performance, especially when dealing with high throughput applications.
This is why we used jemalloc
, a memory allocator designed for highly concurrent applications.
Be aware that some people are reporting errors using this allocator, so watch your logs ;)
As a data point, I've served millions of HTTP requests using it, without problems.
Dockerfile.scratch
###################################################################################################### Builder####################################################################################################FROM rust:latest AS builderRUN rustup target add x86_64-unknown-linux-muslRUN apt update && apt install -y musl-tools musl-devRUN update-ca-certificates# Create appuserENV USER=myipENV UID=10001RUN adduser \ --disabled-password \ --gecos "" \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ "${USER}"WORKDIR /myipCOPY ./ .RUN cargo build --target x86_64-unknown-linux-musl --release###################################################################################################### Final image####################################################################################################FROM scratch# Import from builder.COPY --from=builder /etc/passwd /etc/passwdCOPY --from=builder /etc/group /etc/groupWORKDIR /myip# Copy our buildCOPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./# Use an unprivileged user.USER myip:myipCMD ["/myip/myip"]
$ docker build -t myip:scratch -f Dockerfile.scratch .# ...$ docker run -ti --rm myip:scratch127.0.0.1
FROM alpine
Size: 21.6MB
Alpine Linux is a security-oriented, lightweight Linux distribution based on musl libc and busybox.
It should be used when FROM scratch
is not enough and you need a package manager to install dependencies such as chromium
or ssh
.
As it's based on musl libc
is has the same constraints as FROM scratch
, and we need to statically link our Rust program using x86_64-unknown-linux-musl
.
Dockerfile.alpine
###################################################################################################### Builder####################################################################################################FROM rust:latest AS builderRUN rustup target add x86_64-unknown-linux-muslRUN apt update && apt install -y musl-tools musl-devRUN update-ca-certificates# Create appuserENV USER=myipENV UID=10001RUN adduser \ --disabled-password \ --gecos "" \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ "${USER}"WORKDIR /myipCOPY ./ .RUN cargo build --target x86_64-unknown-linux-musl --release###################################################################################################### Final image####################################################################################################FROM alpine# Import from builder.COPY --from=builder /etc/passwd /etc/passwdCOPY --from=builder /etc/group /etc/groupWORKDIR /myip# Copy our buildCOPY --from=builder /myip/target/x86_64-unknown-linux-musl/release/myip ./# Use an unprivileged user.USER myip:myipCMD ["/myip/myip"]
$ docker build -t myip:alpine -f Dockerfile.alpine .# ...$ docker run -ti --rm myip:alpine127.0.0.1
FROM distroless/cc
Size: 38.3
We can also use the distroless family of images maintained by Google that use packages from debian, but remove all the useless packages in order to create minimal images. Thus, we no longer need to use the MUSL libc.
Here we use the the distroless/cc image because Rust needs libgcc1
for unwinding.
Dockerfile.distroless
###################################################################################################### Builder####################################################################################################FROM rust:latest AS builderRUN update-ca-certificates# Create appuserENV USER=myipENV UID=10001RUN adduser \ --disabled-password \ --gecos "" \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ "${USER}"WORKDIR /myipCOPY ./ .# We no longer need to use the x86_64-unknown-linux-musl targetRUN cargo build --release###################################################################################################### Final image####################################################################################################FROM gcr.io/distroless/cc# Import from builder.COPY --from=builder /etc/passwd /etc/passwdCOPY --from=builder /etc/group /etc/groupWORKDIR /myip# Copy our buildCOPY --from=builder /myip/target/release/myip ./# Use an unprivileged user.USER myip:myipCMD ["/myip/myip"]
$ docker build -t myip:distroless -f Dockerfile.distroless .# ...$ docker run -ti --rm myip:distroless127.0.0.1
FROM buster-slim
Size: 79.4MB
In this last example, we will use debian:buster-slim
as the base image. As Debian is based on glibc
we no longer need to use the x86_64-unknown-linux-musl
compilation target.
Dockerfile.debian
###################################################################################################### Builder####################################################################################################FROM rust:latest AS builderRUN update-ca-certificates# Create appuserENV USER=myipENV UID=10001RUN adduser \ --disabled-password \ --gecos "" \ --home "/nonexistent" \ --shell "/sbin/nologin" \ --no-create-home \ --uid "${UID}" \ "${USER}"WORKDIR /myipCOPY ./ .# We no longer need to use the x86_64-unknown-linux-musl targetRUN cargo build --release###################################################################################################### Final image####################################################################################################FROM debian:buster-slim# Import from builder.COPY --from=builder /etc/passwd /etc/passwdCOPY --from=builder /etc/group /etc/groupWORKDIR /myip# Copy our buildCOPY --from=builder /myip/target/release/myip ./# Use an unprivileged user.USER myip:myipCMD ["/myip/myip"]
$ docker build -t myip:debian -f Dockerfile.debian .# ...$ docker run -ti --rm myip:debian127.0.0.1
Conclusion
$ docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEmyip scratch 795604e74501 9 minutes ago 15.9MBmyip alpine 9a26400587a2 2 minutes ago 21.6MBmyip distroless b789c964a680 19 seconds ago 33MBmyip debian c388547b9486 12 seconds ago 79.4MB
Here we focused on Docker, but if the images are still too large for you and you know what you are doing, here are a few tricks to minimize Rust binary size and reduces the size of the images further.
For example, using the following in Cargo.toml:
[profile.release]lto = truecodegen-units = 1
and adding the following in the Dockerfile after the cargo build
step:
RUN strip -s /myip/target/release/myip
gives:
$ docker imagesREPOSITORY TAG IMAGE ID CREATED SIZEmyip scratch de26b0460262 17 minutes ago 4.2MBmyip alpine 4188ccc82662 6 minutes ago 9.81MBmyip distroless c46e6a0c1ac3 7 seconds ago 25.6MBmyip debian 0eefb58278a8 4 seconds ago 72.8MB
If you want to learn more from real-world Rust experience, Security and Cryptography, I wrote a book where, among other things, we create and deploy a Rust service using Docker: Black Hat Rust.
The code is on GitHub
As usual, you can find the code on GitHub: github.com/skerkour/kerkour.com (please don't forget to star the repo ).
Original Link: https://dev.to/sylvainkerkour/how-to-create-small-docker-images-for-rust-4kdp
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To