One of the best ways to improve the build times of container images is to analyze the final image and trim off unwanted and unnecessary files. Traditionally, this process is performed with a .dockerignore file by combining RUN commands and squashing layers.
Sometimes, however, you don’t want to change anything in your Docker container image (or Dockerfile instructions), but you do want to minify it more intelligently. In that case, you want to use SlimToolkit (previously DockerSlim).
In this tutorial, we will explore some practical ways to slim a container using SlimToolkit. SlimToolkit is like a Swiss Army knife for dissecting container internals, and the insights that it provides enable you to analyze, optimize, and deploy your product quickly. We will start by showing you how to containerize a simple Rails application. Then, we’ll include extra files and directories while handling the rest of the final build. Finally, we will introduce you to some additional options for slimming containers.
Let’s get started.
SlimToolkit is very versatile. It can be used in containers running Node.js, Python, Ruby, Java, Golang, Rust, Elixir, or PHP on Ubuntu, Debian, CentOS, Alpine, or even Distroless. In this tutorial, we will show you how to slim a simple Rails application.
First, you need to create a new Rails app. We’ll use the following command:
$ env RBENV_VERSION=2.7.4 rbenv exec rails 6.1.4.1 new /Users/theo.despoudis/Workspace/hello-world --webpack=react --skip --database=postgresql
We also specified a Ruby and Rails version to use and created a new React + Rails app with Postgres database provider.
Next, you need to delete and then create an empty Gemfile.lock: (This will fix issues when you run the bundler install command inside the container.)
$ cd hello-world $ rm Gemfile.lock $ touch Gemfile.lock
Then, create a Dockerfile that uses the official Ruby image to install Rails dependencies:
Dockerfile
# syntax=docker/dockerfile:1 FROM ruby:2.7.4 RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list RUN apt-get update -qq && apt-get install -y nodejs postgresql-client yarn WORKDIR /myapp COPY . /myapp/ COPY Gemfile /myapp/Gemfile COPY Gemfile.lock /myapp/Gemfile.lock RUN bundle install EXPOSE 3000 ENTRYPOINT ["./scripts/server.sh"]
We used the following starting script:
scripts/server.sh
#!/bin/bash set -e bundle exec puma -C config/puma.rb
Now, build the image:
$ docker build -t app .
Use the official Postgres image to provision the database:
$ docker run --name postgresql-container -p 5432:5432 -e POSTGRES_PASSWORD=somePassword -d postgres $ psql --u postgres -h 0.0.0.0 Password for user postgres: psql (13.4, server 14.1 (Debian 14.1-1.pgdg110+1)) WARNING: psql major version 13, server major version 14. Some psql features might not work. Type "help" for help. postgres=# create database hello_world_development postgres=# exit;
Then, run the application to verify that it works:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -p 3000:3000 app
Using docker inspect, you can see that the image takes up 1.1GB of storage:
$ docker inspect app | jq '.[] | .Size' | numfmt --to iec --format "%8.4f" 1.0981G
Now you can use SlimToolkit to reduce the image size. For this tutorial, we will use the official Docker image to run the experiments. First, you need to pull the latest image:
$ docker pull slim/slim
Then, run the build command to slim the container image down in just one step:
$ docker run -it --rm -v /var/run/docker.sock:/var/run/docker.sock slim/slim build app --http-probe=false
We disabled http-probing using the --http-probe=false flag because we just wanted to minify the image without probing the live container. Later on, we will enable http-probe with the --continue-after flag to continue executing the build command. This will help us control the completion of the build.
Now you can use the newly created image to inspect your cost savings:
$ docker inspect app.slim | jq '.[] | .Size' | numfmt --to iec --format "%8.4f" 79.7828M
Wow! Now it's only 80MB total!
You can also run the application in the container to verify that it works:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -p 3000:3000 app.slim … ! Unable to load application: LoadError: cannot load such file -- zlib
Oops! It looks like we need to enable the http-probe in this case, since the Rails application lazy-loads native modules like zlib and the Postgres shared libraries at runtime. Not all applications behave this way, but some (such as Rails and Django) do. The SlimToolkit does not know this unless it probes the application to verify that it is up and running and that all of its runtime modules were loaded properly. If a module was not loaded, SlimToolkit can remove it from the final build.
Now, you need to run the full-fledged build command to make it work with Rails:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -it --rm -v /var/run/docker.sock:/var/run/docker.sock slim/slim build app … cmd=build state=http.probe.starting message=WAIT FOR HTTP PROBE TO FINISH … cmd=build info=http.probe.summary total='2' failures='1' successful='1' …
In the above command, SlimToolkit created a temp container and probed the containerized application so that it knows which essential modules to keep in the final image. You can inspect the size again at this point (notice that it’s just a few MB larger):
$ docker inspect app.slim | jq '.[] | .Size' | numfmt --to iec --format "%8.4f" 80.8828M
Now, run the slimmed image:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -p 3000:3000 app.slim
Figure 1: Rails App Running in a Minified Image
It works!
Now that you know the basics of SlimToolkit and HTTP probes, we’ll explain how to control the execution of the build command using the --continue-after flag.
HTTP probes give SlimToolkit the ability to detect lazy loaded modules that are required for the application at runtime. There are various flags that can control its behavior, including:
Although these flags are useful, it takes a lot of time to figure them out, which is particularly inefficient when you are still developing the application or when you want to test specific scenarios. If you want to have better control over when the build process completes, you can just manually send a keystroke or a signal to finish building the image with the --continue-after flag.
Let’s run the previous build command with the --continue-after flag:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -it --rm -v /var/run/docker.sock:/var/run/docker.sock slim/slim build --copy-meta-artifacts . --continue-after=enter app … cmd=build info=continue.after message='provide the expected input to allow the container inspector to continue its execution' mode='enter'
(You will be able to review the info message describing the procedure.) It will start probing the endpoints as specified, but now it will wait for a trigger to complete. We used the enter flag in this case, which means that we have to press the enter key to continue. If you’ve been following along, press it now to see the build process finish:
<enter> cmd=build state=container.inspection.finishing …
The --continue-after flag allows for different combinations of flags out of enter | signal | probe | exec | timeout-number-in-seconds | container.probe.
You can emit an OS signal from the command line, for example, or wait for an executable to finish or a specified timeout (in seconds) to be reached. In this way, you will gain finer control over the build process and the accuracy of the minification. In practice, you will find that you must use flags very efficiently to deliver the most optimized image using SlimToolkit. Next, we will show you how to include extra files in the finished image.
Sometimes your application container needs to contain specific files, directories, executables, or binary images in the final form. If SlimToolkit trims them from the final build as part of the minification process for some reason, you’ll want to put them back. For example, it might remove erb templates or migrations folders if it detects that are unused.
For that, SlimToolkit offers an extensive list of flags to control which resources should be included. For example, it offers the following –include-* flags:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -it \--rm -v /var/run/docker.sock:/var/run/docker.sock slim/slim build --include-path /myapp/db/migrate app
External modules or shared libraries that the application needs (such as elastic search modules or a Postgres shared library that must be loaded) are examples of included files. SlimToolkit might not always deem them necessary when you include them in the Dockerfile, so it might filter them out from the final image.
You can also try experimenting with the following flags:
Moreover, you can also specify the container runtime (via the --cro-runtime flag) or any step of the Docker build process. This fine-grained control is extremely valuable, making SlimToolkit a must-use tool for modern teams using containers in production.
SlimToolkit offers many other flags and tools for inspecting, optimizing, and refining container images. You can start by reviewing some of the Slim.AI tutorials, as they are the main providers of SlimToolkit and have deep knowledge of container development and optimization. You should also review SlimToolkit’s official README, since it is essentially a reference guide for the tool. Finally, you can join the Slim SaaS Early Access program to analyze thousands of public container images or scan your own using the online panel.