Building Minimal Docker Containers for Rust Applications

Building Minimal Docker Containers for Rust Applications

Inspired by Nick Gauthier's work on building minimal Go containers and Erik Kidd's work on simple compilation of Rust binaries with no dependencies, I was curious to see how small a comparative Rust container serving HTTP via the Iron framework could be.

In this article, I will be combining an example 'Hello World' HTTP server with rust-musl-builder and a bare-bones Docker container to create a tiny self-contained Docker image and compare it to the minimal Docker containers built in Go.

A Simple 'Hello World' Server in Rust

The example code for the 'Hello World' webserver comes straight from the Iron framework starting page, with the http command changed to listen on all network interfaces:

extern crate iron;

use iron::prelude::*;
use iron::status;

fn main() {
    fn hello_world(_: &mut Request) -> IronResult<Response> {
        Ok(Response::with((status::Ok, "Hello World!")))
    }

    println!("On 3000");
    Iron::new(hello_world).http("0.0.0.0:3000").unwrap();
}

with the iron dependency added to Cargo.toml:

[dependencies.iron]
version = "*"

Using a local rust installation, we can verify that the webserver works:

$ cargo run
   [...]
   Compiling myapp v0.1.0 (file:///path/to/myapp)
     Running `target/debug/myapp`
On 3000

and in another tab, that it is responding correctly:

$ curl localhost:3000
Hello World!

Compiling Statically

Thanks to Erik Kidd's awesome rust-musl-builder, creating static Rust binaries without dependencies is a breeze. rust-musl-builder packages a Rust compiler with the minimal musl C standard library into a Docker container so that statically linked Rust binaries can be created without installing a custom compiler on your computer. Instead, a simple shell alias for running builds in the container is provided that will pull the necessary Docker image on the first invocation:

$ alias rust-musl-builder='docker run --rm -it -v "$(pwd)":/home/rust/src ekidd/rust-musl-builder'
$ rust-musl-builder cargo build --release

Since the current working directory is mapped into the compilation container, the compiled binary will be found in the target/x86_64-unknown-linux-musl/release folder. We can verify that it is indeed statically compiled - and tiny:

$ ldd target/x86_64-unknown-linux-musl/release/myapp
        not a dynamic executable
$ ls -lh target/x86_64-unknown-linux-musl/release/myapp
-rwxr-xr-x 1 seemayer seemayer 1.9M Jul 13 18:53 target/x86_64-unknown-linux-musl/release/myapp

We now have a 1.9 MB HTTP server that responds 'Hello World' to requests on port 3000 without needing any external dependencies.

Building a Minimal Docker Container

Since the binary does not contain dependencies, it will work fine when running in a Docker container that was built from scratch (i.e. not based on an existing image). The following Dockerfile will be enough to create a bare-bones container for running our app:

FROM scratch

ADD target/x86_64-unknown-linux-musl/release/myapp /
EXPOSE 3000

CMD ["/myapp"]

With the Dockerfile, building and running the app is easy and the from-scratch Docker image is virtually the same size as the binary we compiled:

$ docker build -t myapp .
[...]
$ docker images
REPOSITORY                                       TAG                 IMAGE ID            CREATED             SIZE
myapp                                            latest              b7d042250a69        About an hour ago   1.929 MB

Now the big moment of truth! We can use docker run to run our newly built image, binding port 3000 on the host to port 3000 in the container:

$ docker run --rm --name myapp -p 3000:3000 myapp
On 3000

The console message tells us that the binary is running successfully. In another terminal, we can confirm that the container is responding to requests:

$ curl localhost:3000
Hello World!

Conclusion

As the Rust ecosystem for web development matures, running bare-bone Rust services in Docker containers is a super-low-overhead and high-performance way of deploying webapps. Compared to the 6.1 MB Go container created by Nick Gauthier or the 3.6 MB Go container created by Adriaan de Jonge, our 1.9 MB Rust container is almost half as big as the smallest Go container and was easy to create.

I'm excited for the future of Rust web development!