Containerise rust applications on Ubuntu & Alpine, with Github Actions

Emilia Jaser
3 min readOct 14, 2023

--

If you have an application acting as something like a backend REST service, you most likely want to put it into a Docker container for easy deployment.

There are two approaches to building your application: Outside the Dockerfile (with CI/CD like Github Actions) and inside using a builder image.

Example: https://github.com/schitcrafter/ruscalimat (here the rust project is inside the backend folder, hence some modifications)

Building outside the Dockerfile

For debian-slim based containers

FROM debian:12-slim
RUN apt update && apt install -y openssl ca-certificates && rm -rf /var/lib/apt/lists/*

WORKDIR /usr/local/application
COPY target/release/<app-name> /usr/local/bin/application
# <app-name> here is package.name in your Cargo.toml

EXPOSE 8080
CMD ["application"]

With the corresponding Github Action:

jobs:
build:
runs-on: ubuntu-latest
steps:
# this caches (some) dependencies
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3

- uses: actions/checkout@v3

- run: cargo build --release

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

# this will push the docker images to the github container registry
# you will need to give actions permission to push there first
- name: Build and push
id: docker_build
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile
push: true
tags: |
ghcr.io/<username>/<project>:dev-latest
ghcr.io/<username>/<project>:dev-${{ github.run_number }}

Note: Because this is based on debian slim, there is very little included out of the box — if you run into problems with missing dependencies, either use raw debian, or install it (like with ca-certificates here).

For alpine-based containers

FROM alpine:3
RUN apk update && apk add ca-certificates && apk cache clean

WORKDIR /usr/local/application
COPY target/x86_64-unknown-linux-musl/release/<app-name> /usr/local/bin/application
# <app-name> here is package.name in your Cargo.toml

EXPOSE 8080
CMD ["application"]

With the corresponding github action:

jobs:
build-musl:
runs-on: ubuntu-latest
steps:
- name: Run sccache-cache
uses: mozilla-actions/sccache-action@v0.0.3

- uses: actions/checkout@v3

- run: rustup target add x86_64-unknown-linux-musl

- name: Install musl build tools
run: sudo apt update && sudo apt install musl-tools -y

- run: cargo build --release --target x86_64-unknown-linux-musl

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
context: .
file: Dockerfile.alpine
push: true
tags: |
ghcr.io/<username>/<project>:dev-latest-alpine
ghcr.io/<username>/<project>:dev-${{ github.run_number }}-alpine

OpenSSL/any dynamically linked library

The openssl crate usually links openssl dynamically (which is why it needs to be installed in your final container) — musl doesn’t support this. So, if your crate depends on openssl (you can check with “cargo tree -i openssl”), you need to statically link it. Luckily, this can be fixed with this small snippet at the end of your Cargo.toml:


[target.'cfg(target_env = "musl")'.dependencies]
openssl = { version = "*", features = ["vendored"] }

What this does is turn on the “vendored” feature when building for musl. With this enabled, openssl will be compiled from source and statically linked directly into your binary.

Building inside the Dockerfile using builder images

Another approach would be that, instead of building the binary inside your CI/CD, you can simply use a docker container to do that, too. Rust provides an image with everything needed to compile pre-installed (https://hub.docker.com/_/rust), so you can build your app in there, and extract only the binary later. There’s a great tutorial for this already over at https://blog.logrocket.com/optimizing-ci-cd-pipelines-rust-projects/#using-multi-stage-builds, so I won’t repeat that here. One drawback with this approach is that caching dependencies is way harder. For an example of how to do it anyway, see the link below / cargo-chef.

To see a fully working Github Action that does this, go to https://github.com/schitcrafter/ruscalimat/blob/5843195df8bfb37b5604c3f298a53a3a1542dc4f/.github/workflows/backend.yaml. Mind you, this also uses cargo-chef and caches docker layers in the github actions cache (tries to, at least. it doesn’t always work, probably due to the gha type cache still being experimental).

--

--