Docker: Creating an Image and Optimizing for Size

2024-09-27

I recently created a rust application and couldn’t understand why a few lines of code turned into an image that is almost 1.5 Gb. After a bunch of tinkering, I was able to bring the size way down. Let’s take a look at the steps and what it took to get there.

Setup

Let’s go through this exercise with a really simple application that just prints out “Hello World”. I created a folder named rustacean-optimization and then ran cargo init inside the folder to create the rust files.

> cargo init

This will create a cargo.toml file that has name set as rustacean-optimization as well as a src/main.rs file that prints “Hello World.”
I can then build and run the application by running cargo run. You can see below the command that was run and the output.

> cargo run
   Compiling rustacean-optimization v0.1.0 (/Users/nfang/workspace/rust-playground/rustacean-optimization)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.72s
     Running `target/debug/rustacean-optimization`
Hello, world!

I can also build locally by using cargo build -r

> cargo build -r
   Compiling rustacean-optimization v0.1.0 (/Users/nfang/workspace/rust-playground/rustacean-optimization)
    Finished `release` profile [optimized] target(s) in 0.58s

Step 1

I started with a basic Dockerfile that takes the latest rust image, copies in the rust files and builds the application.
*You could run docker init to create Dockerfile, but that includes a lot of extra code that is outside the scope of this post

FROM rust:latest
WORKDIR /usr/src/myapp
COPY . .
RUN cargo build --release
CMD ["./target/release/rustacean-optimization"]

Now build the docker image with the -t flag to give it a tag. Make sure you include the . to use the Dockerfile in the local directory.

> docker buildx build . -t ro-image-1
Output: (expand to see)
[+] Building 43.2s (10/10) FINISHED                                                                         docker:orbstack
=> [internal] load build definition from Dockerfile                                                                   0.1s
=> => transferring dockerfile: 159B                                                                                   0.0s
=> [internal] load metadata for docker.io/library/rust:latest                                                         1.6s
=> [auth] library/rust:pull token for registry-1.docker.io                                                            0.0s
=> [internal] load .dockerignore                                                                                      0.0s
=> => transferring context: 683B                                                                                      0.0s
=> [1/4] FROM docker.io/library/rust:latest@sha256:de79647d96a14ca3336b09f402e0710eea88b3b0fcad18646a6d54813f3ba9c8  39.5s
=> => resolve docker.io/library/rust:latest@sha256:de79647d96a14ca3336b09f402e0710eea88b3b0fcad18646a6d54813f3ba9c8   0.0s
=> => sha256:a47cff7f31e941e78bf63ca19f0811b675283e2c00ddea10c57f78d93b2bc343 24.05MB / 24.05MB                       4.2s
=> => sha256:de79647d96a14ca3336b09f402e0710eea88b3b0fcad18646a6d54813f3ba9c8 7.75kB / 7.75kB                         0.0s
=> => sha256:c9c623bcf8dd793e818cb5ee959b1eb431ebb39c044456e265de8e9815923cc1 1.94kB / 1.94kB                         0.0s
=> => sha256:3217a6680ad94a88c6ac0a3f8ce547b5040f8424fdee8578a1a5dfa669ea0c5b 4.33kB / 4.33kB                         0.0s
=> => sha256:cdd62bf39133c498a16f7a7b1b6555ba43d02b2511c508fa4c0a9b1975ffe20e 49.56MB / 49.56MB                       5.2s
=> => sha256:a173f2aee8e962ea19db1e418ae84a0c9f71480b51f768a19332dfa83d7722a5 64.39MB / 64.39MB                       8.6s
=> => sha256:01272fe8adbacc44afd2b92994b31c40a151f4324ca392050d9e8d580927dd32 211.27MB / 211.27MB                    23.0s
=> => sha256:c6baa1c7db3cf519424e08f89770f4a4f540d4d3effc5816374b37bbea6088f4 184.34MB / 184.34MB                    22.1s
=> => extracting sha256:cdd62bf39133c498a16f7a7b1b6555ba43d02b2511c508fa4c0a9b1975ffe20e                              3.1s
=> => extracting sha256:a47cff7f31e941e78bf63ca19f0811b675283e2c00ddea10c57f78d93b2bc343                              0.8s
=> => extracting sha256:a173f2aee8e962ea19db1e418ae84a0c9f71480b51f768a19332dfa83d7722a5                              3.5s
=> => extracting sha256:01272fe8adbacc44afd2b92994b31c40a151f4324ca392050d9e8d580927dd32                              9.8s
=> => extracting sha256:c6baa1c7db3cf519424e08f89770f4a4f540d4d3effc5816374b37bbea6088f4                              5.0s
=> [internal] load build context                                                                                      0.1s
=> => transferring context: 445B                                                                                      0.0s
=> [2/4] WORKDIR /usr/src/myapp                                                                                       1.2s
=> [3/4] COPY . .                                                                                                     0.1s
=> [4/4] RUN cargo build --release                                                                                    0.5s
=> exporting to image                                                                                                 0.1s
=> => exporting layers                                                                                                0.1s
=> => writing image sha256:6da6f69c8a3451339fd88083105ef7428a2a9f7def4cc06d832747344563398d                           0.0s
=> => naming to docker.io/library/ro-image-1                                                                          0.0s

View build details: docker-desktop://dashboard/build/orbstack/orbstack/sgs71mmw0llw73rcnh1vmqx61

What's next:
  View a summary of image vulnerabilities and recommendations → docker scout quickview

Run the docker image.

> docker run ro-image
Hello, world!

If we take a look at the image, the size is about 1.5GB

> docker images ro-image-1
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
ro-image-1   latest    6da6f69c8a34   2 minutes ago   1.44GB

Step 2

The first optimization was to use multiple stages so the final image only has the compiled binary and does not include the codebase. This updated Dockerfile uses the AS keyword to define the different stages. This also allows copy to use the —from tag to copy files between stages.

FROM rust:latest AS build
WORKDIR /usr/src/myapp
COPY . .
RUN cargo build --release

FROM rust:latest AS final
COPY --from=build /usr/src/myapp/target/release/rustacean-optimization /usr/local/bin/rustacean-optimization
CMD ["rustacean-optimization"]

Build the docker image again using a different tag so we can compare sizes easily.

> docker buildx build . -t ro-image-2
Output: (expand to see)
[+] Building 1.2s (11/11) FINISHED                                                                          docker:orbstack
 => [internal] load build definition from Dockerfile                                                                   0.0s
 => => transferring dockerfile: 289B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/rust:latest                                                         0.8s
 => [auth] library/rust:pull token for registry-1.docker.io                                                            0.0s
 => [internal] load .dockerignore                                                                                      0.0s
 => => transferring context: 683B                                                                                      0.0s
 => [internal] load build context                                                                                      0.0s
 => => transferring context: 114B                                                                                      0.0s
 => CACHED [build 1/4] FROM docker.io/library/rust:latest@sha256:de79647d96a14ca3336b09f402e0710eea88b3b0fcad18646a6d  0.0s
 => CACHED [build 2/4] WORKDIR /usr/src/myapp                                                                          0.0s
 => CACHED [build 3/4] COPY . .                                                                                        0.0s
 => CACHED [build 4/4] RUN cargo build --release                                                                       0.0s
 => [final 2/2] COPY --from=build /usr/src/myapp/target/release/rustacean-optimization /usr/local/bin/rustacean-optim  0.1s
 => exporting to image                                                                                                 0.1s
 => => exporting layers                                                                                                0.0s
 => => writing image sha256:b1a4f6152d64b01879ec6aa384dfa4a663047c12bfa0b26e4b2a1bc9b0f64aea                           0.0s
 => => naming to docker.io/library/ro-image-2                                                                          0.0s

View build details: docker-desktop://dashboard/build/orbstack/orbstack/su60l8zue994va5nuzb2liao1

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview

Run the image to make sure the rust application is still working.

> docker run ro-image-2
Hello, world!

Looking at the size, there is not a noticable difference, but the size of the codebase is too small to see the decrease in size here.

> docker images
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
ro-image-2   latest    b1a4f6152d64   2 minutes ago   1.44GB
ro-image-1   latest    6da6f69c8a34   7 minutes ago   1.44GB

Step 3

Try using a smaller base image. I heard alpine was one of the smallest ones. This updated Dockerfile uses alpine:latest instead of rust:latest as teh base image

FROM rust:latest AS build
WORKDIR /usr/src/myapp
COPY . .
RUN cargo build --release

FROM alpine:latest AS final
COPY --from=build /usr/src/myapp/target/release/rustacean-optimization /usr/local/bin/rustacean-optimization
CMD ["rustacean-optimization"]

When building the docker image, everything seems to build correctly.

> docker buildx build . -t ro-image-3
Output: (expand to see)
[+] Building 2.8s (14/14) FINISHED                                                                          docker:orbstack
 => [internal] load build definition from Dockerfile                                                                   0.0s
 => => transferring dockerfile: 291B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/rust:latest                                                         0.8s
 => [internal] load metadata for docker.io/library/alpine:latest                                                       1.6s
 => [auth] library/rust:pull token for registry-1.docker.io                                                            0.0s
 => [auth] library/alpine:pull token for registry-1.docker.io                                                          0.0s
 => [internal] load .dockerignore                                                                                      0.0s
 => => transferring context: 683B                                                                                      0.0s
 => [final 1/2] FROM docker.io/library/alpine:latest@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16  0.7s
 => => resolve docker.io/library/alpine:latest@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06  0.0s
 => => sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd1367effd16dc0d06d 1.85kB / 1.85kB                         0.0s
 => => sha256:33735bd63cf84d7e388d9f6d297d348c523c044410f553bd878c6d7829612735 528B / 528B                             0.0s
 => => sha256:91ef0af61f39ece4d6710e465df5ed6ca12112358344fd51ae6a3b886634148b 1.47kB / 1.47kB                         0.0s
 => => sha256:43c4264eed91be63b206e17d93e75256a6097070ce643c5e8f0379998b44f170 3.62MB / 3.62MB                         0.5s
 => => extracting sha256:43c4264eed91be63b206e17d93e75256a6097070ce643c5e8f0379998b44f170                              0.1s
 => [build 1/4] FROM docker.io/library/rust:latest@sha256:de79647d96a14ca3336b09f402e0710eea88b3b0fcad18646a6d54813f3  0.0s
 => [internal] load build context                                                                                      0.0s
 => => transferring context: 114B                                                                                      0.0s
 => CACHED [build 2/4] WORKDIR /usr/src/myapp                                                                          0.0s
 => CACHED [build 3/4] COPY . .                                                                                        0.0s
 => CACHED [build 4/4] RUN cargo build --release                                                                       0.0s
 => [final 2/2] COPY --from=build /usr/src/myapp/target/release/rustacean-optimization /usr/local/bin/rustacean-optim  0.1s
 => exporting to image                                                                                                 0.1s
 => => exporting layers                                                                                                0.0s
 => => writing image sha256:159460fb679916a8cb67f1c407fbb0f6df91c36c2584c77e7016f6ceda83f017                           0.0s
 => => naming to docker.io/library/ro-image-3                                                                          0.0s

View build details: docker-desktop://dashboard/build/orbstack/orbstack/jk7oyuotpzbafvoyd4z7dsx3g

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview

The size is significantly smaller,

> docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
ro-image-3   latest    159460fb6799   59 seconds ago   8.21MB
ro-image-2   latest    b1a4f6152d64   5 minutes ago    1.44GB
ro-image-1   latest    6da6f69c8a34   10 minutes ago   1.44GB

but when we try to run the application, we get the error below

> docker run ro-image-3
exec /usr/local/bin/rustacean-optimization: no such file or directory

Why is it not working? This took me a long time to figure out.


Step 4

It wasn’t until I ran ldd rustaceon-optimization in the Dockerfile that I found out I was missing a dynamically linked library. After a bunch of debugging and searching, I found a few things I needed to change.

  • build against the same base image that I am trying to run the application on
  • add musl-dev and base-build
  • use the x86_64-unknown-linux-musl target when building
  • add a build-dependency for cc in cargo.toml

Here is the updated Dockerfile. I left in and commented out the debugging command I used to help figure this out.

FROM rust:1.79.0-alpine AS build
# Install missing dependencies because of alpine
RUN apk add --no-cache musl-dev build-base
WORKDIR /usr/src/myapp
COPY . .
# Add target for building on alpine also added cc build-depenencies in cargo.toml
RUN cargo build --release --target x86_64-unknown-linux-musl

FROM alpine:latest AS final
# Copy from the correct target directory in the build stage
COPY --from=build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/rustacean-optimization /usr/local/bin/
WORKDIR /usr/local/bin/

# DEBUG: Check for missing libraries
# RUN ldd rustacean-optimization
# Enable dynamic linker tracing
# ENV LD_TRACE_LOADED_OBJECTS=1
CMD ["rustacean-optimization"]

In the updated cargo.toml, I added the cc build-dependency.

[package]
name = "rustacean-optimization"
version = "0.1.0"
edition = "2021"

[build-dependencies]
cc = "1.0.117"

[dependencies]

Again, when I build the docker image, everything builds correctly, but you can see in the output that more was built this time.

> docker buildx build . -t ro-image-4
Output: (expand to see)
[+] Building 27.9s (14/14) FINISHED                                                                         docker:orbstack
 => [internal] load build definition from Dockerfile                                                                   0.0s
 => => transferring dockerfile: 730B                                                                                   0.0s
 => [internal] load metadata for docker.io/library/alpine:latest                                                       0.4s
 => [internal] load metadata for docker.io/library/rust:1.79.0-alpine                                                  1.0s
 => [internal] load .dockerignore                                                                                      0.0s
 => => transferring context: 683B                                                                                      0.0s
 => [build 1/5] FROM docker.io/library/rust:1.79.0-alpine@sha256:cc9b42c44d37caccb8f7c366f19f5a41ca0f20f826fb043be07  20.8s
 => => resolve docker.io/library/rust:1.79.0-alpine@sha256:cc9b42c44d37caccb8f7c366f19f5a41ca0f20f826fb043be073167308  0.0s
 => => sha256:9ba2f8a946d42f568cf4cd29d1b9b4178e6c9ed0729b03b8c30d86173872eca3 219.20MB / 219.20MB                    14.4s
 => => sha256:cc9b42c44d37caccb8f7c366f19f5a41ca0f20f826fb043be073167308b6073d 2.63kB / 2.63kB                         0.0s
 => => sha256:4507e352e63be31659483b8b8d76eab2683341bfa00375e5f405098a0d87a9ef 1.54kB / 1.54kB                         0.0s
 => => sha256:e206797bf146aa03fa50885a0ca4d52b5b3d4027d66ec28fe19eb65a849b7289 2.50kB / 2.50kB                         0.0s
 => => sha256:c6a83fedfae6ed8a4f5f7cbb6a7b6f1c1ec3d86fea8cb9e5ba2e5e6673fde9f6 3.62MB / 3.62MB                         0.4s
 => => sha256:88c7fbd688f478563cb61c0e573df586d1576845a6c45b57ade3233ee2d19f62 55.31MB / 55.31MB                       4.9s
 => => extracting sha256:c6a83fedfae6ed8a4f5f7cbb6a7b6f1c1ec3d86fea8cb9e5ba2e5e6673fde9f6                              0.1s
 => => extracting sha256:88c7fbd688f478563cb61c0e573df586d1576845a6c45b57ade3233ee2d19f62                              1.5s
 => => extracting sha256:9ba2f8a946d42f568cf4cd29d1b9b4178e6c9ed0729b03b8c30d86173872eca3                              5.6s
 => [internal] load build context                                                                                      0.0s
 => => transferring context: 114B                                                                                      0.0s
 => CACHED [final 1/3] FROM docker.io/library/alpine:latest@sha256:beefdbd8a1da6d2915566fde36db9db0b524eb737fc57cd136  0.0s
 => [build 2/5] RUN apk add --no-cache musl-dev build-base                                                             4.5s
 => [build 3/5] WORKDIR /usr/src/myapp                                                                                 0.1s
 => [build 4/5] COPY . .                                                                                               0.2s
 => [build 5/5] RUN cargo build --release --target x86_64-unknown-linux-musl                                           0.7s
 => [final 2/3] COPY --from=build /usr/src/myapp/target/x86_64-unknown-linux-musl/release/rustacean-optimization /usr  0.1s
 => [final 3/3] WORKDIR /usr/local/bin/                                                                                0.1s
 => exporting to image                                                                                                 0.1s
 => => exporting layers                                                                                                0.1s
 => => writing image sha256:f9f9b1d588de82967b76db2ff692779a70d59e2b82dc28fbf8fc67b9804521ff                           0.0s
 => => naming to docker.io/library/ro-image-4                                                                          0.0s

View build details: docker-desktop://dashboard/build/orbstack/orbstack/q4ixk9bvxb5y057vuqn6v8won

What's next:
    View a summary of image vulnerabilities and recommendations → docker scout quickview

Now when the application is run, it output’s the correct string.

> docker run ro-image
Hello, world!

The image is also only about 8.5MB which is tiny compared to the original image.

> docker images
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
ro-image-4   latest    f9f9b1d588de   2 minutes ago    8.32MB
ro-image-3   latest    159460fb6799   5 minutes ago    8.21MB
ro-image-2   latest    b1a4f6152d64   9 minutes ago    1.44GB
ro-image-1   latest    6da6f69c8a34   14 minutes ago   1.44GB