feat: slim docker images
All checks were successful
ci/woodpecker/push/workflow Pipeline was successful

This commit is contained in:
hadestructhor 2025-02-23 14:09:53 +01:00
parent 8bcaf21bce
commit e7c60fa8d1
3 changed files with 123 additions and 0 deletions

View file

@ -289,6 +289,43 @@ This is the biggest optimization in image size to date! A 1 998,2 MB less than
I personally love distroless images. I've seen people say that you can debug them less, and not having basic tools like a shell is contraining, but I personally think it's not needed.
You can simply build both a distroless and alpine image of your same code, then when there's a need to debug, scitch the container image for the alpine version. Or even better, use distroless images in production environments and alpines in development environments.
### Seventh Optimisation: slim docker images
I've come across one (probably last optimisation I can find out there).
It's the use of the [slim](https://github.com/slimtoolkit/slim) tool.
It allows you to minify your docker images without even changing them !
Beware though, you need to absolutely know what is to include and not, or else you might have some files not detected at runtime during the slim image generation which can reduce your app's capabilities.
Here's the lists of commands I ran to mignify all the images built up until now:
```shell
slim build --target react-simple --tag react-simple:slim
slim build --target react-multistage:latest --tag react-multistage:slim --include-path /prod
slim build --target react-multistage-alpine:latest --tag react-multistage-alpine:slim --include-path /prod
slim build --target react-nginx:latest --tag react-nginx:slim --include-path /usr/share/nginx/html
slim build --target react-nginx-alpine:latest --tag react-nginx-alpine:slim --include-path /usr/share/nginx/html
slim build --target react-bun-rspack:latest --tag react-bun-rspack:slim --include-path /usr/share/nginx/html
slim build --target react-nginx-distroless:latest --tag react-nginx-distroless:slim --include-path /usr/share/nginx/html --include-path /var/lib/nginx
```
I've listed the images in the same order as they were created in this guide.
Here's an explanation as to why some need an `--include-path` attribute.
This is the perfect example to illustrate slim and it's complexity. To simplify, slim runs a docker container of the image you try to mignify, then tests different crawlers against it, such as http requests, finds all the files that are to keep and keeps them.
I can't completely say how it works for sure, as I need more time to learn how it works under the hood, but that's as far as I gathered.
Now here's the problems I've ran into trying to mignify some docker images.
The first ones where I ran into problems were the multistage versions, using npm serve under the hood.
The problem came from the files that weren't loaded right away by the slim http crawlers, such as the icon of the CRA default React App (RIP in peace to CRA btw).
So what would happen is that the image is generated without any issue, but the assets weren't included in the final slim image.
To avoid this, you need to know how your images work and what they need to run properly. For me it was the /prod folder that needed to be included in the case of the react-multistage and react-multistage-alpine.
The same thing happened with the nginx images, where all needed to keep the default html folder that I populated with the build react app.
For the Chainguard nginx image, some additional folders needed to be kept as they were completely ignored by default, giving me errors about missing files and folders.
Here's the final result of the slimmed images and their size reduction in percentage and times wise compared to the react-simple image:
[size_comparisons.md](./size_comparisons.md)
### Benchmark of build times using docker build --no-cache
Here's the link to the result of the benchmark made to recreate the image 10 times for each branch that contains each optimization:

9
size_comparisons.md Normal file
View file

@ -0,0 +1,9 @@
| step | image | latest_size | slim_size | latest_times_smaller | latest_percent_decrease | slim_times_smaller | slim_percent_decrease |
| ---- | ----------------------- | ----------- | --------- | -------------------- | ----------------------- | ------------------ | --------------------- |
| 0 | react-simple | 1.9 GB | 241.0 MB | 1 | 0 | 8.215767634854771 | 87.82828282828282 |
| 1 | react-multistage | 1.5 GB | 145.0 MB | 1.2452830188679245 | 19.696969696969695 | 13.655172413793103 | 92.67676767676768 |
| 2 | react-multistage-alpine | 207.0 MB | 145.0 MB | 9.565217391304348 | 89.54545454545455 | 13.655172413793103 | 92.67676767676768 |
| 3 | react-nginx | 280.0 MB | 22.5 MB | 7.071428571428571 | 85.85858585858585 | 88 | 98.86363636363636 |
| 4 | react-nginx-alpine | 67.7 MB | 17.2 MB | 29.246676514032497 | 96.58080808080808 | 115.11627906976744 | 99.13131313131314 |
| 5 | react-bun-rspack | 66.5 MB | 16.0 MB | 29.774436090225564 | 96.64141414141415 | 123.75 | 99.1919191919192 |
| 6 | react-nginx-distroless | 31.4 MB | 17.2 MB | 63.05732484076433 | 98.41414141414141 | 115.11627906976744 | 99.13131313131314 |

77
size_comparisons.nu Normal file
View file

@ -0,0 +1,77 @@
let steps = [
['step', 'image', 'next_step'];
['0', 'react-simple', '1'],
['1', 'react-multistage', '2'],
['2', 'react-multistage-alpine', '3'],
['3', 'react-nginx', '4'],
['4', 'react-nginx-alpine', '5'],
['5', 'react-bun-rspack', '6'],
['6', 'react-nginx-distroless', '0'],
];
mut max_size = 0B
mut min_size = 0B
mut max_size_slim = 0B
mut min_size_slim = 0B
let all_res = docker image ls | from ssv | select REPOSITORY TAG SIZE | into filesize SIZE | where REPOSITORY starts-with "react"
for $entry in $steps {
let docker_res = $all_res | where REPOSITORY == $entry.image
let latest_res = $docker_res | where TAG == 'latest' | get 0
let slim_res = $docker_res | where TAG == 'slim' | get 0
let size: filesize = $latest_res | get SIZE
let size_slim: filesize = $slim_res | get SIZE
if $max_size == 0MiB or $size >= $max_size {
$max_size = $size
}
if $min_size == 0MiB or $size <= $min_size {
$min_size = $size
}
if $max_size_slim == 0MiB or $size_slim >= $max_size_slim {
$max_size_slim = $size_slim
}
if $min_size_slim == 0MiB or $size_slim <= $min_size_slim {
$min_size_slim = $size_slim
}
}
mut current = $steps | where step == '0' | get 0
mut result: table<step:string, image:string, latest_size:filesize, slim_size:filesize, latest_times_smaller:float, latest_percent_decrease:float, slim_times_smaller:float, slim_percent_decrease:float> = []
while $current.next_step != '0' {
let docker_res = $all_res | where REPOSITORY == $current.image
let latest_res = $docker_res | where TAG == 'latest' | get 0
let slim_res = $docker_res | where TAG == 'slim' | get 0
let size: filesize = $latest_res | get SIZE
let size_slim: filesize = $slim_res | get SIZE
let row = [
['step', 'image', 'latest_size', 'slim_size', 'latest_times_smaller', 'latest_percent_decrease', 'slim_times_smaller', 'slim_percent_decrease'];
[$current.step, $current.image, $size, $size_slim, ($max_size / $size), (($max_size - $size) * 100 / $max_size), ($max_size / $size_slim), (($max_size - $size_slim) * 100 / $max_size)]
]
$result = ($result ++ $row)
$current = $steps | where step == $current.next_step | get 0
}
let docker_res = $all_res | where REPOSITORY == $current.image
let latest_res = $docker_res | where TAG == 'latest' | get 0
let slim_res = $docker_res | where TAG == 'slim' | get 0
let size: filesize = $latest_res | get SIZE
let size_slim: filesize = $slim_res | get SIZE
let row = [
['step', 'image', 'latest_size', 'slim_size', 'latest_times_smaller', 'latest_percent_decrease', 'slim_times_smaller', 'slim_percent_decrease'];
[$current.step, $current.image, $size, $size_slim, ($max_size / $size), (($max_size - $size) * 100 / $max_size), ($max_size / $size_slim), (($max_size - $size_slim) * 100 / $max_size)]
]
$result = ($result ++ $row)
print $'Max size: ($max_size), Min size: ($min_size)'
print $'Max size slim: ($max_size_slim), Min size slim: ($min_size_slim)'
$result | to md --pretty | save -f size_comparisons.md
$result