Slimming a Rails Application with DockerSlim
Mar 15, 2022
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 DockerSlim
In this tutorial, we will explore some practical ways to slim a container using DockerSlim. DockerSlim 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.
Slimming a Simple Rails Application Container
DockerSlim 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 184.108.40.206 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:
# 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:
#!/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 DockerSlim 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 dslim/docker-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 dslim/docker-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 DockerSlim tool 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, DockerSlim 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 dslim/docker-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, DockerSlim 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
Now that you know the basics of DockerSlim and HTTP probes, we’ll explain how to control the execution of the build command using the --continue-after flag.
Controlling When to Finish Executing the Build Command
HTTP probes give DockerSlim 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:
- --http-probe-retry-count: This controls the number of retries for each HTTP probe. If you want to make sure that the probe will hit a valid endpoint after a certain period of time, give this a higher number and control the wait period with the --http-probe-retry-wait flag.
- --http-probe-ports: This allows you to specify a list of ports to probe. By default, DockerSlim will use the EXPOSE ports as defined in the DockerFile image.
- --http-probe-apispec: This runs a probe for an API specification like Swagger, which improves discoverability of endpoints and resources.
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 dslim/docker-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 DockerSlim. Next, we will show you how to include extra files in the finished image.
Including Extra Files and Directories in Your Minified Image
Sometimes your application container needs to contain specific files, directories, executables, or binary images in the final form. If DockerSlim 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, DockerSlim offers an extensive list of flags to control which resources should be included. For example, it offers the following –include-* flags:
- --include-path: This includes all contents of the specified folder path into the final image. For instance, to include the migrations folder in our example, you would run:
$ docker run -e "DATABASE_URL=postgres://postgres:somePassword@172.17.0.1" -it \--rm -v /var/run/docker.sock:/var/run/docker.sock dslim/docker-slim build --include-path /myapp/db/migrate app
- --include-path-file: This includes a specific file into the final image.
- --include-bin: This includes a specific binary into the final image. DockerSlim will check its dependencies and include them into the final build.
- --include-exe: This includes a specific executable into the final image. DockerSlim will check its dependencies and include them into the final build.
- --include-cert-all: This will try to keep any certificates installed in the image. For example, it will use detectors to copy certificates from known locations. If you’re using an Ubuntu image, it will copy them from the /etc/ssl/certs folder.
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. DockerSlim might not always deem them necessary when you include them in the Dockerfile, so it might filter them out from the final image.
Other Useful Flags
You can also try experimenting with the following flags:
- --http-probe-crawl: This will transform your HTTP probe into a web crawler that will follow all the links it finds in the target endpoint.
- --pull: This pulls an image from a registry instead of the local image list.
- --new-entrypoint, --new-cmd, --new-expose, --new-workdir, --new-env, --new-label, and --new-volume: Each one of these flags lets you customize the DockerFile’s respective instructions (entrypoint, cmd, expose, workdir, env, label, and volume).
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 DockerSlim a must-use tool for modern teams using containers in production.
Next Steps with DockerSlim
DockerSlim 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 DockerSlim and have deep knowledge of container development and optimization. You should also review DockerSlim’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.
About the Author
Theo Despoudis is a Senior Software Engineer, a consultant and an experienced mentor. He has a keen interest in Open Source software Architectures, Cloud Computing, best practices and functional programming. He occasionally blogs on several publishing platforms and enjoys creating projects from inspiration. Follow him on Twitter @nerdokto.