Hey folks. Yesterday I've deployed my first Rust service to production at AppSpector. Actix-Web and Rust was pleasure to work with, but Docker image building process was not so obvious. In this post I will show you how to build small and secure docker images for Rust services. The stuff that I will cover is pretty basic, but if you are doing it for the first time it will save you several hours of searching on google and github. I ignore build time optimizations for Docker images. The goal is to show how to build small and secure image.
Rust has officially supported docker images that you can find at Docker Hub. They contain everything you need to build and run typical Rust project. You may want to install additional system dependencies that you need for your project.
FROM rust:1.43.1 WORKDIR /usr/src/api-service COPY . . RUN cargo install --path . CMD ["api-service"]
You can put this docker file into you project, fix project names and run using
docker build -t api-service . docker run -it —rm —name api-service-instance api-service
You can even deploy it to production using Kubernetes, Docker Compose, Swarm or whatever else you use for deployment. The fact that you can deploy it doesn’t mean you should. There are several issues with this docker image.
The resulting image is pretty big (~1.2 Gb). It means that every time when you deploy it server need to pull this image from Docker images registry.
api-service latest a72004cb9a35 2 seconds ago 1.24GB
You can use smaller image like rust:1.43.1-slim, which is smaller, but still it’s 624 Mb.
api-service latest ada242f40855 46 seconds ago 624MB
Docker can cache images locally, but still it will slowdown deployment and usually it’s a bad practice to use such large images for real deployments.
Official images contains whole Rust dev toolset. Cargo, rustc, bash shell, etc. You probably don’t need any of that to run your web service.
All these tools available inside your container significantly increases attack surfaces of your system. If intruders will get access to a running container they will be super happy to see all these tools available for them. Restricting what’s in your runtime container to precisely what’s necessary for your app is a best security practice used by top companies in production for many years.
Docker allows you to separate image build process into different stages. You can use official Rust image to build the app and another image to run it.
FROM rust:1.43.1 as build WORKDIR /usr/src/api-service COPY . . RUN cargo install --path . FROM alpine:latest COPY --from=build /usr/local/cargo/bin/api-service /usr/local/bin/api-service CMD ["api-service"]
In this case you won’t have any components of Rust development toolchain in final image. It has clear separation between build process and runtime container.
I use Alpine images. Alpine is a lightweight Linux distribution. It’s widely used in for Docker deployments. It’s small and secure.
The resulting image is just 35.4 MB in size! Don’t forget this size also includes size of my service binary.
api-service latest 96d575188ba9 5 minutes ago 35.4MB
If you try to run image created in the example above using
docker run -rm -t api-service you should get an error:
standard_init_linux.go:187: exec user process caused “no such file or directory”
It happens because Rust binary that you’ve build is dynamically linked against libc and it’s missing from shared libraries inside Alpine image.
Alpine linux is using MUSL Libc instead of default Libc library. It's alternative Libc implementation
musl is an implementation of the C standard library built on top of the Linux system call API, including interfaces defined in the base language standard, POSIX, and widely agreed-upon extensions. musl is lightweight, fast, simple, free, and strives to be correct in the sense of standards-conformance and safety.
You can build Rust binary with x86_64-unknown-linux-musl target and link it with Musl library.
FROM rust:1.43.1 as build RUN apt-get update RUN apt-get install musl-tools -y RUN rustup target add x86_64-unknown-linux-musl WORKDIR /usr/src/api-service COPY . . RUN RUSTFLAGS=-Clinker=musl-gcc cargo install -—release —target=x86_64-unknown-linux-musl FROM alpine:latest COPY --from=build /usr/local/cargo/bin/api-service /usr/local/bin/api-service CMD ["api-service"]
It will work fine until you start linking with system libraries linked against Libc. For example OpenSSL. This is exactly that happened to me. For Actix-Web based service I need OpenSSL.
One of the ways to solve it is to rebuild OpenSSL manually with Musl support. It’s possible, but I was looking for something simpler.Also what if I need to link with another library that linked with Libc?.
There a few Docker images like GitHub - clux/muslrust: Docker environment for building musl based static rust binaries specially designed for Musl support.
They supposed to be tested for popular system libraries like
However, this whole idea of introducing third-party docker image into our infrastructure just to build Rust with Musl feels not so good. It’s less secure because third-party image can lead to additional attack areas. I would prefer to stick with official images.
Let’s just use something else instead of Alpine so we don’t have to build with Musl support. We need something small and secure.
The best candidate here is Distroless images for Google. These images are specially designed to contain only your application and its runtime dependencies.
They don’t have package managers, shells or any other program that you can find in a standard Linux. You can watch this video about why and how they are made. These are probably most secure docker images that you can find. Best practices for production usage at Google are applied to them.
Distroless images support many languages and we need to find an image that best for Rust.
I’ve tested several of them and looks like the one that we need is distroless/cc-debian10: This image contains a minimal Linux, glibc runtime for “mostly-statically compiled” languages like Rust and D
How to use it with Rust:
FROM rust:1.43.1 as build ENV PKG_CONFIG_ALLOW_CROSS=1 WORKDIR /usr/src/api-service COPY . . RUN cargo install --path . FROM gcr.io/distroless/cc-debian10 COPY --from=build /usr/local/cargo/bin/api-service /usr/local/bin/api-service CMD ["api-service"]
Just replace alpine with gcr.io/distroless/cc-debian10 and nothing else. No need to use Musl target. This image contains Libc.
api-service latest d8c818e1e1e1 19 hours ago 50.9MB
The size is 15 Mb larger than alpine, but still small enough.
It’s a good practice to use distroless images in production even if you don't have issues with Musl builds.
I hope this article will help you to deploy better.