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 /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 /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
andbase-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 /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