Simplify Your Dockerfile
Tags: docker, rust, taskfile, opensource
Published: 2023-02-20 09:19:23

With [rustlang](https://www.rust-lang.org/) 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](https://hub.docker.com/_/rust/) 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:

{% embed https://gist.github.com/kameshsampath/db6962db374d4483d5bdf90715a5da96 %}

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](https://taskfile.dev/) -- 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`: 

{%embed https://gist.github.com/kameshsampath/4a009b71a11c0d5e3d8c7e8578860673 %}

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:

```dockerfile
#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` 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](https://taskfile.dev/usage/). 

For a end to end example refer to [rust-greeter](https://github.com/kameshsampath/rust-greeter) that uses [rust-zig-builder](https://github.com/kameshsampath/rust-zig-builder).