Rust: Docker Image Optimization With GitLab CI

mattdark

Mario García

Posted on January 7, 2024

Rust: Docker Image Optimization With GitLab CI

As I described here, a way to optimize your Docker image when containerizing a Rust app, is by using multi-stage builds. At the first the stage, you build the app and get the binary, and at the second stage you build the final image that contains only the binary generated previously. This way, you will get a smaller Docker image. The Dockerfile for a multi-stage build looks as follows:



FROM rust:latest AS builder
WORKDIR /app

COPY Cargo.toml .
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release

COPY src src
RUN touch src/main.rs
RUN cargo build --release

RUN strip target/release/hello_rocket

FROM gcr.io/distroless/cc-debian12 as release
WORKDIR /app
COPY --from=builder /app/target/release/hello_rocket .

ENV ROCKET_ADDRESS=0.0.0.0
ENV ROCKET_PORT=8000
EXPOSE 8000

CMD ["./hello_rocket"]


Enter fullscreen mode Exit fullscreen mode

Another option is to use GitLab CI. Through this blog post, you will learn how to use GitLab CI for building an optimized Docker image of your Rust app.

GitLab CI

First, create a GitLab repository for your project.

Then, create a Hello, world! example with Rocket, in your local environment.

Create a new project:



$ cargo new hello_rocket


Enter fullscreen mode Exit fullscreen mode

Change to the project directory:



$ cd hello_rocket


Enter fullscreen mode Exit fullscreen mode

Replace the content of the src/main.rs with:



#[macro_use] extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![index])
}


Enter fullscreen mode Exit fullscreen mode

Edit the Cargo.toml file and add the corresponding dependency:



[package]
name = "hello_rocket"
version = "0.1.0"
edition = "2021"

[dependencies]
rocket = "=0.5.0-rc.3"


Enter fullscreen mode Exit fullscreen mode

The above code will display Hello, world! on the browser. Now, sync your repository with the code of your application.

Create a Dockerfile in your repository, with the following content:



FROM gcr.io/distroless/cc-debian12

WORKDIR /app

COPY /target/release/hello_rocket .

ENV ROCKET_ADDRESS=0.0.0.0
ENV ROCKET_PORT=8000
EXPOSE 8000

CMD ["./hello_rocket"]


Enter fullscreen mode Exit fullscreen mode

I'm using a distroless image to avoid getting any error when running the container.

Before creating the GitLab CI configuration file, .gitlab-ci.yml, go to SettingsCI/CD and add the following variables:

  • CI_REGISTRY_USER. Type your Docker Hub user in the Value field
  • CI_REGISTRY_PASSWORD. In the Value field, type the password of your Docker Hub user
  • CI_REGISTRY. Type docker.io in the Value field
  • CI_REGISTRY_IMAGE. In the Value, type index.docker.io/username/hello-rocket

username is your Docker Hub user. hello-rocket is the name of the Docker Hub repository where the image will be available.

And finally, create the .gitlab-ci.yml file in your repository, with the following content:



stages:
    - build
    - deploy

build-app:
    image: rust:latest
    stage: build
    script:
        - cargo build --release
        - strip target/release/hello_rocket
    artifacts:
        paths:
            - target/

docker-build:
  # Official docker image.
  image: docker:latest
  stage: deploy
  services:
    - docker:dind
  before_script:
    - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
  script:
    - docker build --pull -t "$CI_REGISTRY_IMAGE" .
    - docker push "$CI_REGISTRY_IMAGE"
  dependencies:
    - build-app


Enter fullscreen mode Exit fullscreen mode

In the first job (build-app) of your CI/CD pipeline:

  • Your Rust app is built (cargo build --release)
  • Unnecessary information from the binary is removed, reducing its size and making it more difficult to reverse engineer
  • With job artifacts, the content of the target directory is stored and passed to the next job

In the second job (docker-build), the Docker image is built, using a Dockerfile, and published on Docker Hub. For this example, you will publish the Docker image that contains your app on Docker Hub, and you can replace the instructions if you're deploying directly to any cloud platform. To use the artifacts, you must specify the first job as dependency.

When the second job is started, the artifacts from the previous job are downloaded, and with the following instruction, copied into the Docker image that will be generated with the Dockerfile:



COPY /target/release/hello_rocket .


Enter fullscreen mode Exit fullscreen mode

After the CI/CD pipeline has finished, you could use the image from Docker Hub to initialize a container and run your application.



$ docker run -p 8000:8000 --name hello-rocket username/hello-rocket


Enter fullscreen mode Exit fullscreen mode

And you can go to localhost:8000 in your browser.

Conclusion

Through this blog post, you learned how to use GitLab CI and job artifacts to build an optimized Docker image of your application.


Support me on Buy Me A Coffee

💖 💪 🙅 🚩
mattdark
Mario García

Posted on January 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related