Simplify Your Dockerfile
Kamesh Sampath
Posted on February 20, 2023
With rustlang gaining lots of popularity, I thought to give it a try. As cloud native application developer, the first thing I thought was to build was simple REST API that greets the user by name.
After building the application locally, the next immediate was to containerise it. Though we have an official rustlang image, I want to build a customised image that will allow me to build cross platform container images namely linux/arm64
and linux/amd64
. I don't want to dwell into those details as that demands its own blog post ;).
So my Dockerfile
with all rust specific tools and dependencies looks like:
The Dockerfile
has two stages bins and final. bins is used to do all binary builds that are required by final stage. The final stage builds the final image that can be used with to build multi-arch container images out of our rust applications.
Though it is not a complicated Dockerfile
, if you notice the last RUN
instruction of the final stage, it is complex and hard to debug typically when one of the commands fails to run. It is also hard read and understand the RUN
instruction. Doing multiple RUN
instructions to split the commands, is not recommended as it creates new layers and your your final image will be bloated in size.
NOTE: rustlang builder images are usually bigger ~ 700mb(compressed) by virute of dependencies that it needs e.g gcc, cross compilation linkers etc.,
I was then thinking of ways to simplify this Dockerfile though not from size point of view but atleast making it simple to read and understand.
The target was to to have one RUN
instruction but to split the commands into individual steps without compromising on the size.
I then stumbled upon Taskfile -- Task is a task runner / build tool -- which is similar to [GNU Make](https://www.gnu.org/software/make/)
but way simpler. Taskfile helped me to make the Dockerfile
simpler.
I kind of moved the whole set RUN
instruction into a Taskfile
:
Though it is verbose, but helps in understanding commands we are running as part of the Docker build. With descriptions, comments, conditions it becomes more powerful and self documented in explaining what is being executed and when it will be executed.
Updating the Dockerfile
file results in:
#syntax=docker/dockerfile:1.3-labs
FROM --platform=$TARGETPLATFORM rust:1.67-alpine3.17 AS bins
ARG TARGETPLATFORM
RUN --mount=type=cache,target=/usr/local/cargo/registry \
apk add -U --no-cache alpine-sdk gcompat go-task \
&& cargo install cargo-zigbuild
## The core builder that can be used to build rust applications
FROM --platform=$TARGETPLATFORM alpine:3.17 as final
ARG TARGETPLATFORM
ARG rust_version=1.67.1
ARG rustup_version=1.25.2
ARG user_id=1001
ARG user=builder
ENV USER_ID=$user_id \
USER=$user \
RUST_VERSION=$rust_version \
RUSTUP_VERSION=$rustup_version \
RUSTUP_HOME=/usr/local/rustup \
CARGO_HOME=/usr/local/cargo \
PATH=/usr/local/cargo/bin:$PATH \
RUST_VERSION=1.67.1
COPY --from=bins /usr/bin/go-task /usr/local/bin/task
COPY --from=bins /usr/local/cargo/bin/cargo-zigbuild /usr/local/cargo/bin/
COPY tasks/Taskfile.root.yaml ./Taskfile.yaml
RUN task
As we moved all our commands to Taskfile
the RUN
instruction now has to just run the task
command, which will then run the default
from the Taskfile
.
To summarise we,
- Wrote a multi stage
Dockerfile
to build multi arch rust app container - Moved all the instructions from
RUN
toTaskfile
- Used the
task
command inRUN
- Moving commands to Taskfile allows us to run/test the tasks separately before using them in
Dockerfile
. For more usage check the TaskFile documentation.
For a end to end example refer to rust-greeter that uses rust-zig-builder.
Posted on February 20, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.