Improving Docker is more than just something and accomplishes

This article is part of a series of publications where I will walk in every line of the Dockerfile bars and explain best practices and improvements.
Docker images can be improved in different ways, including, but not limited to, reduce image size, improve performance performance, safety, best practices, and application improvements. In the first article, I will only touch on improving the size of the image and explaining the reason for the importance of this.
Why to improve the image size?
As in every software development process, each developer will include his causes that he wants to make Docker build faster. I will include the most important reasons for me.
Benate and faster publishing operations
The smaller images are faster for construction because fewer files and layers must be processed. This improves developer productivity, especially during repetitive development sessions. The smaller pictures takes less time to pay to the record and withdraw from it during publishing operations. This is especially important in CI/CD pipelines where containers are built and spread frequently.
Decreased storage costs and the use of the network frequency
Smaller photos consume less storage in container records, local development machines and production servers. This reduces infrastructure costs, especially for publication on a large scale. Smaller images use a lesser -width domain of frequency when they are transferred between servers, especially when you build images locally or in CI/CD pipelines and push them to the record.
“We spent $ 3.2 million on the cloud in 2022 … We stand to save about 7 million dollars at the expenses of the server over five years of our cloud exit.” David Haname Hanson – O World
Improving performance and security
Smaller images require lower resources (for example, the CPU, RAM) to download and operate, and improve the overall performance of barefoot applications. Starting times faster means that your services are more ready quickly, which is very important for high delivery and availability. Minimum basic images such as alpine
or debian-slim
It contains a fewer pre -installed packages, which reduces the risk of exploiting unavoidable or unnecessary software.
Besides all mentioned above, removing unnecessary files and tools reduces deviations when diagnosing problems and leads to a better ability to maintain and reduce technical debts.
Docker photos
For different parameters of the image, including the size, you can either take a look on a Docker desktop or play docker images
Order at the station.
➜ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
kamal-dashboard latest 673737b771cd 2 days ago 619MB
kamal-proxy latest 5f6cd8983746 6 weeks ago 115MB
docs-server latest a810244e3d88 6 weeks ago 1.18GB
busybox latest 63cd0d5fb10d 3 months ago 4.04MB
postgres latest 6c9aa6ecd71d 3 months ago 456MB
postgres 16.4 ced3ad69d60c 3 months ago 453MB
Knowing the image size does not give you the full image. You don’t know what is inside the image, the number of layers it has, or how big each layer is. A Docker’s image layer It is only reading, The non -changing file system This is made up of a Docker image. Each layer represents a set of changes made on the image file system, such as adding files, editing configurations or installing programs.
Docker’s pictures are gradually designed, layer after another, and each layer corresponds to instructions in Dockerfile
. For photo layers, you can play docker history
He orders.
➜ docker history kamal-dashboard:latest
IMAGE CREATED CREATED BY SIZE COMMENT
673737b771cd 4 days ago CMD ["./bin/thrust" "./bin/rails" "server"] 0B buildkit.dockerfile.v0
4 days ago EXPOSE map[80/tcp:{}] 0B buildkit.dockerfile.v0
4 days ago ENTRYPOINT ["/rails/bin/docker-entrypoint"] 0B buildkit.dockerfile.v0
4 days ago USER 1000:1000 0B buildkit.dockerfile.v0
4 days ago RUN /bin/sh -c groupadd --system --gid 1000 … 54MB buildkit.dockerfile.v0
4 days ago COPY /rails /rails # buildkit 56.2MB buildkit.dockerfile.v0
4 days ago COPY /usr/local/bundle /usr/local/bundle # b… 153MB buildkit.dockerfile.v0
4 days ago ENV RAILS_ENV=production BUNDLE_DEPLOYMENT=1… 0B buildkit.dockerfile.v0
4 days ago RUN /bin/sh -c apt-get update -qq && apt… 137MB buildkit.dockerfile.v0
4 days ago WORKDIR /rails 0B buildkit.dockerfile.v0
3 weeks ago CMD ["irb"] 0B buildkit.dockerfile.v0
3 weeks ago RUN /bin/sh -c set -eux; mkdir "$GEM_HOME";… 0B buildkit.dockerfile.v0
3 weeks ago ENV PATH=/usr/local/bundle/bin:/usr/local/sb… 0B buildkit.dockerfile.v0
3 weeks ago ENV BUNDLE_SILENCE_ROOT_WARNING=1 BUNDLE_APP… 0B buildkit.dockerfile.v0
3 weeks ago ENV GEM_HOME=/usr/local/bundle 0B buildkit.dockerfile.v0
3 weeks ago RUN /bin/sh -c set -eux; savedAptMark="$(a… 78.1MB buildkit.dockerfile.v0
3 weeks ago ENV RUBY_DOWNLOAD_SHA256=018d59ffb52be3c0a6d… 0B buildkit.dockerfile.v0
3 weeks ago ENV RUBY_DOWNLOAD_URL=https://cache.ruby-lan… 0B buildkit.dockerfile.v0
3 weeks ago ENV RUBY_VERSION=3.4.1 0B buildkit.dockerfile.v0
3 weeks ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0
3 weeks ago RUN /bin/sh -c set -eux; mkdir -p /usr/loca… 19B buildkit.dockerfile.v0
3 weeks ago RUN /bin/sh -c set -eux; apt-get update; a… 43.9MB buildkit.dockerfile.v0
3 weeks ago # debian.sh --arch 'arm64' out/ 'bookworm' '… 97.2MB debuerreotype 0.15
Since I have already presented a theory about pictures and classes, it is time to explore Dockerfile
. Starting with bars 7.1, Dockerfile
It is created with the new Rails app. Below is an example of its shape.
# syntax=docker/dockerfile:1
# check=error=true
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
# Rails app lives here
WORKDIR /rails
# Install base packages
# Replace libpq-dev with sqlite3 if using SQLite, or libmysqlclient-dev if using MySQL
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Set production environment
ENV RAILS_ENV="production" \
BUNDLE_DEPLOYMENT="1" \
BUNDLE_PATH="/usr/local/bundle" \
BUNDLE_WITHOUT="development"
# Throw-away build stage to reduce size of final image
FROM base AS build
# Install packages needed to build gems
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y build-essential curl git pkg-config libyaml-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
# Install application gems
COPY Gemfile Gemfile.lock ./
RUN bundle install && \
rm -rf ~/.bundle/ "${BUNDLE_PATH}"/ruby/*/cache "${BUNDLE_PATH}"/ruby/*/bundler/gems/*/.git && \
bundle exec bootsnap precompile --gemfile
# Copy application code
COPY . .
# Precompile bootsnap code for faster boot times
RUN bundle exec bootsnap precompile app/ lib/
# Precompiling assets for production without requiring secret RAILS_MASTER_KEY
RUN SECRET_KEY_BASE_DUMMY=1 ./bin/rails assets:precompile
# Final stage for app image
FROM base
# Copy built artifacts: gems, application
COPY --from=build "${BUNDLE_PATH}" "${BUNDLE_PATH}"
COPY --from=build /rails /rails
# Run and own only the runtime files as a non-root user for security
RUN groupadd --system --gid 1000 rails && \
useradd rails --uid 1000 --gid 1000 --create-home --shell /bin/bash && \
chown -R rails:rails db log storage tmp
USER 1000:1000
# Entrypoint prepares the database.
ENTRYPOINT ["/rails/bin/docker-entrypoint"]
# Start server via Thruster by default, this can be overwritten at runtime
EXPOSE 80
CMD ["./bin/thrust", "./bin/rails", "server"]
Below I will present a list of the methods and rules that are applied to Dockerfile
Above to make the final image size effective.
Improving packages
I’m sure you only keep the necessary programs on your local development machine. The same thing should be applied to Docker. In the examples below, I will constantly increase the Dockerfile extracted from the Rails Dockerfile above. I will refer to it as creative Dockerfile
Issue.
Rule No. 1: Use the minimum basic images
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
The main image is the starting point for Dockerfile
. It is the image used to create the container. The main image is the first layer in Dockerfile
And it is the only layer that is not created by Dockerfile
itself.
The basic image is determined with FROM
It is followed by the name of the image and the brand. The mark is optional, and if not determined, latest
The mark is used. The basic image can be any image available on Docker Hub or any other record.
in Dockerfile
About, we use ruby
Image with 3.4.1-slim
sign. the ruby
The image is the official Ruby image available on Docker Hub. the 3.4.1-slim
The brand is a small version of the Ruby image based on debian-slim
image. while debian-slim
The image is the minimal release of the improved Debian Linux image. Look at the table below to get an idea about a smaller range slim
Photo.
➜ docker images --filter "reference=ruby"
REPOSITORY TAG IMAGE ID CREATED SIZE
ruby 3.4.1-slim 0bf957e453fd 5 days ago 219MB
ruby 3.4.1-alpine cf9b1b8d4a0c 5 days ago 99.1MB
ruby 3.4.1-bookworm 1e77081540c0 5 days ago 1.01GB
As of January 2024, the current Debian version is called Bookworm And the other is Polse.
219 MB instead of 1 GB – a big difference. But what if alpine
The image is smaller? the alpine
The image depends on Alpine Linux distribution, which is a very lightweight Linux distribution that is improved for size and safety. Use Alpine Mountains musl
Library (instead of glibc
) And busybox
(Compressed set of UNIX tools) instead of GNU counterparts. While it is technically possible to use alpine
A picture to run bars, I will not cover them in this article.
Rule No. 2: Reducing layers
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev && \
rm -rf /var/lib/apt/lists /var/cache/apt/archives
all RUN
and COPY
and FROM
Instructions in Dockerfile
It creates a new layer. The higher the number of layers you have, the greater the size of the image. That is why the best practices are to combine multiple orders in one RUN
directions. To clarify this point, let’s take a look at the example below.
# syntax=docker/dockerfile:1
# check=error=true
# Make sure RUBY_VERSION matches the Ruby version in .ruby-version
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION-slim AS base
RUN apt-get update -qq
RUN apt-get install --no-install-recommends -y curl
RUN apt-get install --no-install-recommends -y libjemalloc2
RUN apt-get install --no-install-recommends -y libvips
RUN apt-get install --no-install-recommends -y libpq-dev
RUN rm -rf /var/lib/apt/lists /var/cache/apt/archives
CMD ["echo", "Whalecome!"]
I have divided RUN
Instructions in multiple lines, which makes it more capable of human reading. But how will this affect the size of the image? Let’s build the picture and check this.
➜ time docker build -t no-minimize-layers --no-cache -f no-minimize-layers.dockerfile .
0.31s user 0.28s system 2% cpu 28.577 total
It took 28 seconds to build the image, while building the original version with polished layers takes only 19 seconds (Almost 33 % faster).
➜ time docker build -t original --no-cache -f original.dockerfile .
0.25s user 0.28s system 2% cpu 19.909 total
Let’s check the size of the photos.
➜ docker images --filter "reference=*original*" --filter "reference=*no-minimize*"
REPOSITORY TAG IMAGE ID CREATED SIZE
original latest f1363df79c8a 8 seconds ago 356MB
no-minimize-layers latest ad3945c8a8ee 43 seconds ago 379MB
The image with modified layers is less than 23MB of those that do not contain layers to the minimum. This is it 6 % decrease. Although it seems a slight difference in this example, the difference will be much larger if you divide each RUN
Instructions in multiple lines.
Rule No. 3: Install only what is required
By default, apt-get install
It installs the recommended beams as well as the beams that I requested to install. the --no-install-recommends
He tells the option apt-get
To install only explicitly defined beams, not recommended beams.
➜ time docker build -t without-no-install-recommends --no-cache -f without-no-install-recommends.dockerfile .
0.33s user 0.30s system 2% cpu 29.786 total
➜ docker images --filter "reference=*original*" --filter "reference=*recommends*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-no-install-recommends latest 41e6e37f1e2b 3 minutes ago 426MB
minimize-layers latest dff22c85d84c 17 minutes ago 356MB
As you can see, the image without --no-install-recommends
It is 70 MB larger than the original. This is it 16 % increase in size.
Use Diving The utility to find out the files added to the image – read more about it at the end of the article.
Rule No. 4: Cleaning after installations
origin Dockerfile
It includes rm -rf /var/lib/apt/lists/* /var/cache/apt/archives
Order apt-get install
He orders. This removes the lists of packages and archives that are no longer required after the installation. Let’s see how it affects the size of the image, to achieve this, I will create a new Dockerfile
Without the cleaning order.
RUN apt-get update -qq && \
apt-get install --no-install-recommends -y curl libjemalloc2 libvips libpq-dev
The construction of images takes almost the same time as the original image, which is logical.
➜ time docker build -t without-cleaning --no-cache -f without-cleaning.dockerfile .
0.28s user 0.30s system 2% cpu 21.658 total
Let’s check the size of the photos.
➜ docker images --filter "reference=*original*" --filter "reference=*cleaning*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-cleaning latest 52884fe50773 2 minutes ago 375MB
original latest f1363df79c8a 16 minutes ago 356MB
The image without cleaning the largest 19MB of the image that cleans, and this is a 5 % increase.
The worst scenario
What if the four improvements mentioned above are not applied? Let’s create new Dockerfile
Without any improvements and image building.
# syntax=docker/dockerfile:1
# check=error=true
ARG RUBY_VERSION=3.4.1
FROM docker.io/library/ruby:$RUBY_VERSION AS base
RUN apt-get update -qq
RUN apt-get install -y curl
RUN apt-get install -y libjemalloc2
RUN apt-get install -y libvips
RUN apt-get install -y libpq-dev
CMD ["echo", "Whalecome!"]
➜ time docker build -t without-optimizations --no-cache -f without-optimizations.dockerfile .
0.46s user 0.45s system 1% cpu 1:02.21 total
Wow, it took more than a minute to build the image.
➜ docker images --filter "reference=*original*" --filter "reference=*without-optimizations*"
REPOSITORY TAG IMAGE ID CREATED SIZE
without-optimizations latest 45671929c8e4 2 minutes ago 1.07GB
original latest f1363df79c8a 27 hours ago 356MB
The image without improvements is 714 MB of the original image, and this is a 200 % increase in size. This clearly shows how important improvement is Dockerfile
The largest pictures take more time to build and consume more disk space.
Always use .DOCKERIGNORE
the .dockerignore
The file is similar to .gitignore
A file uses GIT. It is used to exclude files and evidence from the context of construction. The context is the collection of files and evidence that is sent to Docker Daemon when creating an image. The context is sent to Docker Daemon as TarBall, so it is important to keep it as small as possible.
If, for any reason, you don’t have .dockerignore
A file in your project, you can manually create it. I suggest that you use official bars .dockerignore
The file template as a starting point. Below is an example of its shape.
# See https://docs.docker.com/engine/reference/builder/#dockerignore-file for more about ignoring files.
# Ignore git directory.
/.git/
/.gitignore
# Ignore bundler config.
/.bundle
# Ignore all environment files.
/.env*
# Ignore all default key files.
/config/master.key
/config/credentials/*.key
# Ignore all logfiles and tempfiles.
/log/*
/tmp/*
!/log/.keep
!/tmp/.keep
# Ignore pidfiles, but keep the directory.
/tmp/pids/*
!/tmp/pids/.keep
# Ignore storage (uploaded files in development and any SQLite databases).
/storage/*
!/storage/.keep
/tmp/storage/*
!/tmp/storage/.keep
# Ignore assets.
/node_modules/
/app/assets/builds/*
!/app/assets/builds/.keep
/public/assets
# Ignore CI service files.
/.github
# Ignore development files
/.devcontainer
# Ignore Docker-related files
/.dockerignore
/Dockerfile*
presence .dockerfile
The file is not allowed in the project only except for unnecessary files and evidence (for example, GitHub work tasks from .github
Folder or JavaScript dependencies from node_modules
From the context. It also helps to avoid adding sensitive information accidentally to the image. For example, and .env
The file that contains environmental variables or master.key
A file used to decompose accreditation data.
Use diving
All of the above improvements may seem clear when explained. What do you do if you already have a huge picture, and don’t know where to start?
My favorite and most useful tool is diving. Dive is a TUI tool to explore a Docker image, layer contents, and discover ways to reduce the image size. Dive can be installed with your system package manager, or you can use the official Docker image to run. Let’s use the image of our worst scenario.
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
In the screen shot above, you can see the most diseases examination. The diving shows the size of each layer, the total size of the image, and the files that were changed (added, modified or deleted) in each layer. For me, this is the most useful feature of diving. By listing files in the right panel, you can easily select the unpopular files and remove the orders you add to the image.
One of the things I really love in diving is that, along with a terminal user interface, it can also provide CI’s friendly product, which can be effective in local development as well. To use it, turn on diving with CI
The environment variable has been set to true
The output is in the screenshot below.
docker run -e CI=true --rm -it -v /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest without-optimizations
My personal preference is to use diving on a scheduled basis, for example, once a week, to ensure that your photos are still in good condition. In the next articles, I will cover the automatic work items to check for Dockkerfile, including diving and hadolint.
Do not distort classes
One of the methods to reduce the size of the image I saw is to try to crush the layers. The idea was to combine several layers in one layer to reduce the size of the image. Docker had a trial option --squash
Besides, there were third-party tools such as Docker-Squash.
Although this approach is working in the past, it is currently neglecting and is not recommended to use it. The layers destroyed the basic feature of the Docker’s message in the cache of the layer. Regardless, while using it --squash
You can include sensitive or temporary files unintentionally from the previous layers in the final image. This is the approach of everything or nothing lacks accurate control.
Instead of crushing layers, it is recommended to use multi -stage designs. Bars Dockerfile
It is already used to build multiple stages, and I will explain how it works in the following article.
Conclusions
Improving Docker, just like any other improvement, It cannot be done once and forgotten. It is a continuous process that requires regular checks and improvements. I tried to cover the basics, but it is important to know and understand. In the following articles, I will cover more technologies and advanced tools that can help make Docker build faster and more efficient.