Rust: Docker Image Optimization With GitLab CI
Tags: tutorial, rust, docker, gitlab
Published: 2024-01-07 06:51:58

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"]
```

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
```

Change to the project directory:

```
$ cd hello_rocket
```

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])
}
```

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"
```

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"]
```

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 `Settings` ⇾ `CI/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
```

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](https://docs.gitlab.com/ee/ci/jobs/job_artifacts.html), 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 .
```

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
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nzx9uenqvwqv1vp8ub5d.png)](https://buymeacoffee.com/letstalkoss)