Why container size matter?

This is a good question: storage is cheap why should we care about it? Who cares if a ‘Hello world’ NodeJS app uses 2GB of image as long as it works? Let me be a grumpy sysadmin guy and ask some question my dear developer friend:

  • Is network usage free? No.
  • Will the application start slowly on a cold start? Yes. Will you blame the environment instead of reflecting your own setup? Yes.
  • Does a larger image mean it contains more packages? Yes. Do more packages means bigger attack surface? Yes. Basically do you create extra maintenance work for you? Yes.
  • Besides storage may cheap but not infinite.

Of course, it does not mean that every single image must be a pure SCRATCH container. Image still can contain a shell for debugging or monitoring. But there some practice that can help like multi-stage build. Today, I won’t discuss multi-stage builds; instead, I’ll explain an obvious concept that everyone learns – at least theoretically – during container education: how layers work.

What are layers?

Every single statement in a Containerfile (or Dockerfile) means one layer. Layers can be cached separately. For example, having a container where only the application has been changed, and makes a re-deploy, only the changed layers would be downloaded. Great concept, right?

How don’t do it?

I never blame anybody for what they are doing. I always wonder, what they were thinking – maybe they don’t know some basics or just don’t care? I never assume anything in advance; instead, I rather start by speaking with people about what they are doing and why. Maybe I am the person who is wrong and I have something to learn.

So what happened? Removing the uninterested part, this was in the Containerfile:

WORKDIR /tmp/app
RUN wget -nv "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
    && tar -xvf "trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"

WORKDIR /app
RUN cp /tmp/app/trivy ./trivy \
    && chmod +x /app/trivy \
    rm -r /tmp/app

What is wrong with this file? It’s commendable that they considered removing the downloaded tarball. However, it does not really matter because the layer from the first RUN command remains and consumes space.

After build, let’s analyze the history of the image.

$ podman images
REPOSITORY                                       TAG               IMAGE ID      CREATED         SIZE
localhost/trivy-for-gitea                        latest            7d2c07fb0c2c  12 seconds ago  434 MB
$ podman history localhost/trivy-for-gitea:latest
ID            CREATED         CREATED BY                                     SIZE        COMMENT
79f494556b9d  31 seconds ago  /bin/sh -c #(nop) ENTRYPOINT [ "./scan.sh" ]   0B          FROM f636633dbc4d
<missing>     31 seconds ago  /bin/sh -c #(nop) LABEL summary="Trivy sca...  0B          FROM 79f494556b9d
<missing>     32 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  8.7kB       FROM 84962301ab7d
84962301ab7d  32 seconds ago  /bin/sh -c #(nop) COPY file:3e0dc7f6445563...  8.19kB      FROM 9db1ad5b3d33
9db1ad5b3d33  33 seconds ago  /bin/sh -c #(nop) COPY file:467d03d5b25988...  3.07kB      FROM b98538d538f3
175d35bb5f4f  33 seconds ago  /bin/sh -c #(nop) WORKDIR /app                 0B          FROM 175d35bb5f4f
<missing>     36 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  150MB       FROM 1229b642a3a4
b593e5530f94  38 seconds ago  /bin/sh -c #(nop) WORKDIR /app                 0B          FROM b593e5530f94
<missing>     42 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  195MB       FROM 715c1bb26193
9958fde18afb  58 seconds ago  /bin/sh -c #(nop) ARG TRIVY_VERSION dataSo...  0B          FROM 8b3fa6700c51
<missing>     58 seconds ago  /bin/sh -c #(nop) WORKDIR /tmp/app             0B          FROM 9958fde18afb
<missing>     58 seconds ago  /bin/sh -c mkdir -p /app /tmp/app              3.07kB      FROM e51ff272e7cb
e51ff272e7cb  59 seconds ago  /bin/sh -c apt-get update     && apt-get i...  11.2MB      FROM docker.io/library/debian:bookworm-slim
595d99e62673  5 weeks ago     # debian.sh --arch 'amd64' out/ 'bookworm'...  77.9MB      debuerreotype 0.15

Removing the uninterested lines, this two remains. Layer of first run:

<missing>     42 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  195MB       FROM 715c1bb26193

Layer of second run (copy of binary):

<missing>     36 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  150MB       FROM 1229b642a3a4

It is visible that, although the remove has been done, it does not reduce the size of previous layer.

How to fix it?

It is not difficult to fix it, just need to merge the two RUN statement.

WORKDIR /tmp/app
RUN wget -nv "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
    && tar -xvf "trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
    && cp /tmp/app/trivy /app/trivy \
    && chmod +x /app/trivy \
    && rm -r /tmp/app

After its build, we can see that the image size has been reduced.

podman images
REPOSITORY                                       TAG               IMAGE ID      CREATED         SIZE
localhost/trivy-for-gitea                        latest            c6281027b217  6 seconds ago   239 MB
podman history localhost/trivy-for-gitea:latest
ID            CREATED         CREATED BY                                     SIZE        COMMENT
c6d0dd60d7c4  18 seconds ago  /bin/sh -c #(nop) ENTRYPOINT [ "./scan.sh" ]   0B          FROM c397c8d6547f
<missing>     19 seconds ago  /bin/sh -c #(nop) LABEL summary="Trivy sca...  0B          FROM c6d0dd60d7c4
<missing>     19 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  8.7kB       FROM 9316d745be98
9316d745be98  20 seconds ago  /bin/sh -c #(nop) COPY file:3e0dc7f6445563...  8.19kB      FROM 9d3530a4c297
9d3530a4c297  20 seconds ago  /bin/sh -c #(nop) COPY file:467d03d5b25988...  3.07kB      FROM 20ffb695bf5b
cec9cb87d99a  20 seconds ago  /bin/sh -c #(nop) WORKDIR /app                 0B          FROM cec9cb87d99a
<missing>     24 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  150MB       FROM 715c1bb26193
9958fde18afb  10 minutes ago  /bin/sh -c #(nop) ARG TRIVY_VERSION dataSo...  0B          FROM 8b3fa6700c51
<missing>     10 minutes ago  /bin/sh -c #(nop) WORKDIR /tmp/app             0B          FROM 9958fde18afb
<missing>     10 minutes ago  /bin/sh -c mkdir -p /app /tmp/app              3.07kB      FROM e51ff272e7cb
e51ff272e7cb  10 minutes ago  /bin/sh -c apt-get update     && apt-get i...  11.2MB      FROM docker.io/library/debian:bookworm-slim
595d99e62673  5 weeks ago     # debian.sh --arch 'amd64' out/ 'bookworm'...  77.9MB      debuerreotype 0.15

We can see that the 195MB big layer just disappeared. With this RUN statement we handle everything at one place, we only have just one 150MB layer (which is size of the binary).

<missing>     24 seconds ago  |3 TRIVY_VERSION=0.61.0 dataSource=gihub-r...  150MB       FROM 715c1bb26193

Another example for RUN:

RUN apt-get update \
    && apt-get install --no-install-recommends -y wget=* ca-certificates=* jq=* \
    && apt-get clean  \
    && rm -rf /var/lib/apt/lists/*

Final words

I always like to find out why the problem existed at first place. It is not judgement, not mocking or something, but I always like to hear and see feedback why something bad happened. It may help to improve learning materials or documentation.

In this special case, the root cause was, that they thought it works like a shell. So, they told, if they would do it in shell, like this:

$ cd /tmp/app
$ wget -nv "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" \
    && tar -xvf "trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz"
$ cd /app
$ cp /tmp/app/trivy ./trivy
$ rm /tmp/app

And they just copied this method, by changing the cd with RUN. It is not bad that happened, or wrong. I believe that everybody in the IT learning until the very end. There are a lot of thing that I also don’t know, maybe doing things not perfectly, but always open to learn.