Mario García
Posted on January 7, 2024
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 theValue
field -
CI_REGISTRY_PASSWORD
. In theValue
field, type the password of your Docker Hub user -
CI_REGISTRY
. Typedocker.io
in theValue
field -
CI_REGISTRY_IMAGE
. In theValue
, typeindex.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, 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.
Posted on January 7, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.