DEV Community

Sergey Zenchenko
Sergey Zenchenko

Posted on • Updated on

Actix-Web in Docker: How to build small and secure images

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.

Official docker images for Rust

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.

Dockerfile

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 image size

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.

Security

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.

Multi-stage docker builds

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.

Dockerfile

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

Making it work with Rust

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.

Dockerfile

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

  • OpenSSL
  • sqlite3
  • curl
  • zlib
  • pg

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.

Distroless

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:

Dockerfile

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.

Top comments (3)

Collapse
 
brokenthorn profile image
Paul-Sebastian Manole

Cool! Thanks for this! The Distroless section is well worth a separate small post because it's basically the TLDR; for this. Just link to to the YT video, like you did, and mention why Distroless is the right option here when you need to get going fast and be safe at the same time.

Collapse
 
sergeyzenchenko profile image
Sergey Zenchenko

Yep. Distroless is not small area :)

Collapse
 
jasonish profile image
jasonish

If Actix has it as a feature, you could try to use Rustls instead of TLS. This made compiling with Musl much simpler for my app that uses Warp, where Rustls is an option instead of OpenSSL.