Rust Binary Release with GitLab CI
Published: 2023-12-03
Intro
In this post I will show you how to use GitLab CI to build and release a Rust binary.
The pipeline will:
- Run tests
- Build a statically linked binary for Linux x86_64
- Build a Docker image
- Create a GitLab release with the binary and Docker image attached
The code for this post can be found on GitLab.
Config File
A GitLab CI pipeline is defined in a .gitlab-ci.yml file in the root of the repository. The config file is written in YAML and defines the stages, jobs, and scripts that will be executed.
Below is the .gitlab-ci.yml file we will be using. I will break down each section in the following sections.
# Reference: https://docs.gitlab.com/ee/ci/yaml/
stages:
- prepare
- test
- build
- release
variables:
# Environment variable for the Cargo home directory
CARGO_HOME: $CI_PROJECT_DIR/.cargo
# Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
# Define image name with environment variables
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
cache:
# Cache Cargo dependencies between jobs for all pipelines
key: "$CI_JOB_NAME"
paths:
- .cargo/
- target/
cargo-version:
stage: prepare
image: rust:latest
script:
- rustc --version && cargo --version
cargo-test:
stage: test
image: rust:latest
script:
- cargo test --verbose
cargo-build:
stage: build
image: rust:latest
script:
# Install musl target
- rustup target add x86_64-unknown-linux-musl
# Build a statically linked binary
- cargo build --release --target x86_64-unknown-linux-musl
# Create a releases directory
- mkdir releases
# Copy the binary to the releases directory and append the platform to the filename
- cp target/x86_64-unknown-linux-musl/release/rust-binary-release-gitlab-ci releases/rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl
artifacts:
paths:
- releases/
only:
- tags
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
# Login to the GitLab container registry
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
# Build and tag the Docker image
- docker build --pull -t $IMAGE_NAME:$CI_COMMIT_TAG -t $IMAGE_NAME:latest .
# Push the Docker image to the GitLab container registry
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
- docker push $IMAGE_NAME:latest
after_script:
# Logout from the GitLab container registry
- docker logout $CI_REGISTRY
only:
- tags
gitlab-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating GitLab release for $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
name: "Release $CI_COMMIT_TAG"
description: "Release $CI_COMMIT_TAG"
assets:
links:
- name: "rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl"
url: "$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/raw/releases/rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl?job=cargo-build"
link_type: package
- name: "Docker Image"
url: "$CI_PROJECT_URL/container_registry"
link_type: image
only:
- tagsStages
The pipeline is divided into 4 stages:
- prepare - Verify the Rust version
- test - Run tests
- build - Build the binary and Docker image
- release - Create a GitLab release
stages:
- prepare
- test
- build
- releasePrepare
The prepare stage verifies the Rust version.
cargo-version:
stage: prepare
image: rust:latest
script:
- rustc --version && cargo --versionTest
The test stage runs tests.
cargo-test:
stage: test
image: rust:latest
script:
- cargo test --verboseBuild
The build stage builds the binary and Docker image.
Binary Build
The cargo-build job builds a statically linked binary for Linux x86_64.
cargo-build:
stage: build
image: rust:latest
script:
# Install musl target
- rustup target add x86_64-unknown-linux-musl
# Build a statically linked binary
- cargo build --release --target x86_64-unknown-linux-musl
# Create a releases directory
- mkdir releases
# Copy the binary to the releases directory and append the platform to the filename
- cp target/x86_64-unknown-linux-musl/release/rust-binary-release-gitlab-ci releases/rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl
artifacts:
paths:
- releases/
only:
- tagsThe artifacts section of the job saves the binary as an artifact for use in the gitlab-release job later on. The only section ensures the job only runs when a tag is pushed.
Docker Build
The docker-build job builds a Docker image and pushes it to the GitLab container registry.
docker-build:
stage: build
image: docker:latest
services:
- docker:dind
before_script:
# Login to the GitLab container registry
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
script:
# Build and tag the Docker image
- docker build --pull -t $IMAGE_NAME:$CI_COMMIT_TAG -t $IMAGE_NAME:latest .
# Push the Docker image to the GitLab container registry
- docker push $IMAGE_NAME:$CI_COMMIT_TAG
- docker push $IMAGE_NAME:latest
after_script:
# Logout from the GitLab container registry
- docker logout $CI_REGISTRY
only:
- tagsThe services section of the job runs Docker in Docker (DinD) which is required to build and push Docker images. The before_script section logs into the GitLab container registry. The after_script section logs out of the GitLab container registry. The only section ensures the job only runs when a tag is pushed.
The Dockerfile is shown below:
FROM rust:latest as builder
WORKDIR /app
COPY . .
RUN rustup target add x86_64-unknown-linux-musl && \
cargo build --release --target x86_64-unknown-linux-musl
FROM alpine:latest
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/rust-binary-release-gitlab-ci /usr/local/bin/rust-binary-release-gitlab-ci
CMD ["rust-binary-release-gitlab-ci"]Release
The gitlab-release job creates a GitLab release with the binary and Docker image attached.
gitlab-release:
stage: release
image: registry.gitlab.com/gitlab-org/release-cli:latest
script:
- echo "Creating GitLab release for $CI_COMMIT_TAG"
release:
tag_name: $CI_COMMIT_TAG
name: "Release $CI_COMMIT_TAG"
description: "Release $CI_COMMIT_TAG"
assets:
links:
- name: "rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl"
url: "$CI_PROJECT_URL/-/jobs/artifacts/$CI_COMMIT_TAG/raw/releases/rust-binary-release-gitlab-ci-x86_64-unknown-linux-musl?job=cargo-build"
link_type: package
- name: "Docker Image"
url: "$CI_PROJECT_URL/container_registry"
link_type: image
only:
- tagsThe release section of the job defines the release metadata. The assets section defines the assets that will be attached to the release. The first asset is the binary built in the cargo-build job. The second asset is a link to the Docker image in the GitLab container registry.
Bonus Round
The pipeline also includes some additional configuration for caching and environment variables.
Caching
The pipeline caches the Cargo dependencies between jobs for all pipelines. This speeds up the pipeline by not having to download the dependencies every time.
cache:
# Cache Cargo dependencies between jobs for all pipelines
key: "$CI_JOB_NAME"
paths:
- .cargo/
- target/Environment Variables
The pipeline defines some environment variables for the Cargo home directory and Docker configuration.
variables:
# Environment variable for the Cargo home directory
CARGO_HOME: $CI_PROJECT_DIR/.cargo
# Use TLS https://docs.gitlab.com/ee/ci/docker/using_docker_build.html#tls-enabled
DOCKER_HOST: tcp://docker:2376
DOCKER_TLS_CERTDIR: "/certs"
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
# Define image name with environment variables
IMAGE_NAME: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUGImprovements
There are a few improvements that could be made to this pipeline:
- Build binaries for multiple platforms (macOS, Windows, etc.)
- Use a matrix to build multiple binaries in parallel
- Use a more descriptive release description
- Add a changelog to the release
Outro
In this post I showed you how to use GitLab CI to build and release a Rust binary. The pipeline runs tests, builds a statically linked binary for Linux x86_64, builds a Docker image, and creates a GitLab release with the binary and Docker image attached.
Below is a diagram of the pipeline stages:
For a GitHub Actions version of this pipeline, see my post on Rust Binary and Docker Releases using GitHub Actions.