DEV Community

Kamesh Sampath
Kamesh Sampath

Posted on

Simplify Your Dockerfile

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
Enter fullscreen mode Exit fullscreen mode

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 to Taskfile
  • Used the task command in RUN
  • 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.

Top comments (0)