The apt sandwich

Brett Weir Mar 2, 2023 5 min read

Sandwich synopsis

When crafting Dockerfiles for Debian-based containers, you'll frequently run into snippets that look like this:

# hadolint ignore=DL3008
RUN apt-get update -y \
    && apt-get install -y --no-install-recommends \
        inkscape \
    && rm -rf /var/lib/apt/lists/*

This is what I like to call the apt sandwich. I call it that because it consists of whatever you're actually trying to do (e.g. install some packages), with apt-related boilerplate on either end.

Sandwich specifications

The prototypical apt sandwich looks like:

# hadolint ignore=DL3008
RUN apt-get update -y \
    && apt-get install -y --no-install-recommends \
        # ...
        # the sandwich filling - the packages you want to install
        # ...
    && rm -rf /var/lib/apt/lists/*

Here what is accomplished with this snippet:

  • apt-get is used instead of apt, because of a long-present warning about apt CLI stability.

  • apt-get update needs to be run, because upstream maintainers clean out the apt repositories from base images to minimize the final image size.

  • The -y and -f options of apt-get and rm respectively are included to ensure that the installation completes without user intervention in the widest possible set of scenarios.

  • --no-install-recommends is to further reduce container bloat.

  • rm -rf /var/lib/apt/lists/* cleans out the apt repository lists downloaded by apt-get update.

  • The hadolint comment on the first line prevents hadolint from complaining about linter requirements that are unsatisfiable. We don't control the versions of packages that exist in upstream distro repositories, so requiring version pinning of apt packages is generally an exercise in futility. This comment needs to precede the RUN statement.

All of this is done in a single command to keep intermediate files from being snapshotted by Docker and becoming a permanent part of the image.

Sandwich savings

Using the apt sandwich doesn't just look nice—it also makes your containers a lot smaller and build faster as well!

Don't take my word for it though. In this section, we have a list of sample Dockerfiles you can build yourself to see the impact it can have, and we'll list the image size and build time for each Dockerfile. To follow along:

  • Save a Dockerfile with the given content and build the image:

    docker build -t apt-sandwich .
  • This will print the time taken in the process:

    $ docker build -t apt-sandwich .
    [+] Building 109.5s (6/6) FINISHED
     => [2/2] RUN apt-get update -y && apt-get install -y inkscape   106.6s
  • You can then retrieve the image size separately:

    docker images apt-sandwich --format json | jq -r '.VirtualSize'
    $ docker images apt-sandwich --format json | jq -r '.VirtualSize'

Okay, now you're ready to build some images!

  • Base image:

    FROM ubuntu:22.04

    Image size: 77.8MB.

  • Install inkscape:

    FROM ubuntu:22.04
    RUN apt-get update -y \
        && apt-get install -y inkscape

    Image size: 594MB. Build time: 106.6s.

  • Clean package index after install:

    FROM ubuntu:22.04
    RUN apt-get update -y \
        && apt-get install -y inkscape \
        && rm -rf /var/lib/apt/lists/*

    Image size: 552MB. Build time: 100.8s.

  • Don't install recommended packages:

    FROM ubuntu:22.04
    RUN apt-get update -y \
        && apt-get install -y --no-install-recommends inkscape \
        && rm -rf /var/lib/apt/lists/*

    Image size: 358MB. Build time: 68.2s.

To put it all together:

CommandSizeSize ReductionTimeTime Reduction
Install inkscape594MB106s
Clean package index after install552MB42MB (7%)103s3s (3%)
Don't install recommended packages358MB236MB (40%)70s36s (34%)
Base image78MB

Cleaning the package index and avoiding installing recommended packages has a pretty dramatic impact:

  • The image size is reduced by 236MB, or 40%!

  • The build time for this instruction is reduced by 36s, or 34%!

Sandwich summary

apt commands should pretty much always be written this way in a Dockerfile. It will result in a lot of savings in size, bandwidth, and time, which translates to savings in cost as well.

Containers will build faster, take up less space, and deploy into a target environment faster. And, since you know what this idiom is supposed to look like, it'll be easy to spot when a Dockerfile doesn't get it quite right, and you'll be able to quickly improve the container performance.


#docker #linux