Optimized Docker config for Actix Web, Diesel and Postgres
Interestingly, the resources are quite limited or rather sparse for this topic/setup currently. I learnt quite a few lessons the hard way and I hope that this article will be helpful to some of you out there 😆
You can find the working sample in this GitHub repo. The rest of the article is going to base upon that repo. Go ahead and give that repo a star ⭐ if it helped 😆🙏
- A working Dockerfile (~2.5GB image size)
- The optimized Dockerfile (~60MB image size)
- .env and dotenv
- Lessons Learnt
- Things to find out next
The biggest challenge or confusion when setting up a Dockerfile for Rust web application is that, there's almost always some issues related to missing packages/libs during the build. And we'll have to start Googling to install some missing packages, etc. Not very fun tbh haha!
In our case, we're setting up an Actix Web application that uses Diesel as our ORM to connect to Postgres. Hmm.. Sounds pretty straightforward, but trust me it's nerve wrecking.
I'll first go through a simple setup that gets the application running successfully in Docker (~2.5GB), then followed by another setup that optimizes its image size (~60MB).
Okay, enough talking. Let's get to work!
To start it off, using an
alpine image base is almost never gonna work out-of-box, not until we've configured all sorts of missing lib dependencies required to build our Rust app "stack" (actix-web, diesel, postgres). Trust me, I've spent hours on this.
After Googling for quite a bit, I've finally came across a repo containing a
Dockerfile that builds and runs our app successfully. Yessshhh!!!
FROM rust:slim-buster RUN apt-get update && \ apt-get -y upgrade && \ apt-get -y install libpq-dev WORKDIR /app COPY . /app/ COPY .env.docker /app/.env RUN cargo build --release EXPOSE 8080 ENTRYPOINT ["/bin/bash", "-c", "cargo run --release"]
If we pay attention to line 1, that base image is actually based on
debian instead of
alpine, which contains more "tools" than the lightweight counterpart.
It works fine. However, if we inspect the image size, we'd be surprised that it's actually a freaking 2.55GB. Holy cow, we're talking about Gigabytes here for a relatively simple application! There must be something that we can do to reduce the image size!
There's a common build strategy to optimize a Docker image size - Muti-stage builds. It generally refers to using different images to handle
deploy tasks respectively.
"build" - usually a bigger image, loaded with all sorts of libs/tools to build the app. In our case, it's ideally an image that's capable of performing
cargo build. For that, we can just use an official Rust Docker image, such as
"deploy" - usually a minimalist image just enough to run the app. After some explorations, I came across something called Distroless image. It's a list of bare minimum images that are language focused. In our case, we should be using gcr.io/distroless/cc-debian11 for Rust.
However, this image alone is not enough to run our app. It'll throw some errors complaining that "libpq.so.5 missing..." or something similar along the line. The culprit is actually
diesel, whom requires
libpq. Not entirely sure about the details but it has something to do with Postgres as well.
The solution here is actually to install libpq during the build stage, then copy the necessary dependency lib files over to the deploy image.
Here is the GitHub issue thread that gave me a clue about the solution at first. It points to this Dockerfile here. However, I wasn't able to achieve direct success with the image suggested there "i0nw/distroless:libpq5-debian11" 🙁. Somehow, the specified path
x86_64-linux-gnu was not found during my build. When I inspect the image, there's instead another directory called
After tweaking the configs to accommodate the path issues, voila! Finally, we get a working version of the Dockerfile!
Below is the working Dockerfile with multi-stage build.
# --------------------------------------------------- # 1 - Build Stage # # Use official rust image to for application build # --------------------------------------------------- FROM rust:1.66.1 as build # Setup working directory WORKDIR /usr/src/codefee-works-api COPY . . COPY .env.docker .env # Install dependency (Required by diesel) RUN apt-get update && apt-get install libpq5 -y # Build application RUN cargo install --path . # --------------------------------------------------- # 2 - Deploy Stage # # Use a distroless image for minimal container size # - Copy `libpq` dependencies into the image (Required by diesel) # - Copy application files into the image # --------------------------------------------------- FROM gcr.io/distroless/cc-debian11 # libpq related (required by diesel) COPY --from=build /usr/lib/aarch64-linux-gnu/libpq.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libgssapi_krb5.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libldap_r-2.4.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libkrb5.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libk5crypto.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libkrb5support.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/liblber-2.4.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libsasl2.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libgnutls.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libp11-kit.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libidn2.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libunistring.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libtasn1.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libnettle.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libhogweed.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libgmp.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /usr/lib/aarch64-linux-gnu/libffi.so* /usr/lib/aarch64-linux-gnu/ COPY --from=build /lib/aarch64-linux-gnu/libcom_err.so* /lib/aarch64-linux-gnu/ COPY --from=build /lib/aarch64-linux-gnu/libkeyutils.so* /lib/aarch64-linux-gnu/ # Application files COPY --from=build /usr/local/cargo/bin/codefee-works-api /usr/local/bin/codefee-works-api COPY --from=build /usr/src/codefee-works-api/.env /.env CMD ["codefee-works-api"]
The result is mind-blowing. Our image size is now down to only 62.68MB from the earlier 2.55GB. That's almost a 98% in size reduction. Woohoooo~!!!
Not sure if we can further reduce the size here. 62.68MB still sounds a lil bit big to me tbh. However, I'll leave it at that for now. If any of you readers found out some ways to reduce it, please do not hesitate to leave me a message! I'd be really grateful about it ❤️
If your application uses a
.env file to configure environment variables, REMEMBER to copy over the
.env file into your final image. Otherwise,
dotenv is gonna panic.
You can just copy the intended
.env file into the image's root. Yep, easy peasy. How or why? Well, if you inspect the
dotenv crate's source, you'll notice that there is a function called try_parent in there. It works by recursively finding its parent directory until a
.env file is found. Therefore, leaving the
.env file at root directory will still work for this.
In the sample repo, you'll see that we just copy the
.env file from
build image into the
deploy image's root directory.
- Use a
distrolessimage to minimize your image size
- If you use
diesel, you need to copy the required
libpqfiles into the final image as well
- If you use
dotenv, remember to copy your intended
.envfile into the image. Otherwise, your
Here are some of the things that I felt can be improved and requires further research:
- Incremental build (running fresh
cargo installeverytime is super slow)
- Further image size reduction
I find that the resources are generally quite sparse on using Docker with the Rust stack covered here
(Actix Web, Diesel and Postgres). It took me some time to consolidate the different moving parts and finally nailing down the configurations.
From my perspective, it tells us one thing, i.e. there aren't too many people who are actively doing this in comparison to node, golang, etc. other popular ecosystems for web service. Learning curve is probably one of the major factors here. After all, Rust is not the easiest language to pick up 😆 Well, I'm still struggling hard too! haha..
I decided to first check out
Actix Web instead of
Rocket, solely due to the promising performance benchmarks being displayed by the former. However, it gives me a feeling that the latter might have a larger community around it (might be easier to find "answers"). Not entirely sure though, just a feeling.
Hopefully, more and more folks will start writing web services in Rust. Definitely hoping and looking forward to the time when Rust finally enters the "mainstream" for this area of programming!
After working with Rust on and off for a few months, I think this language is quite verbose, yet beautiful at the same time. It's definitely low level and reminds me of my university days, studying data structures and algorithms with C++ and C haha! Well, it feels akin to C# too (feels at home 😆)!
Since there's a big wave around authoring Frontend toolings with more performant languages, e.g. Golang and Rust, it really excites me and motivated me to pick up either one of them. I tried Go some time ago, but sadly did not find the syntax enjoyable in contrast to many other folks. Rust on the other hand, feels kinda natural (perhaps more akin to C#).
Man, it's been quite a while since my last article. So many stuffs happened in the past "almost one year". Since Covid has finally tuned down a lil, I've finally gotten the chance to travel back to my hometown, visiting my parents, some old friends, etc.
Took the chance to go for Registration of Marriage too! 😆 Haha.. yep, officially married with my beloved partner.
I have also started working at ByteDance recently too. Definitely one of the wildest ride in my life haha.. For me, the journey from preparations, tryouts, failing interviews, failing lesser, burnout... till I finally landed this job has truly shaped me into a better engineer through and through. Thank God I'm still alive! Perhaps, I should dedicate an article to demystify the entire interview journey as well 😆
The coffee? It's the legendary Kopi O Kosong today! 😆