A miniature Node.js image

Node.js is a JavaScript-based platform for server-side and networking applications. The node image is one of the most popular images on DockerHub, with an impressive 4.6 Billions+ pulls as of this writing.

With all it’s impressive statistics, there are a few stats which can be a bit concerning. Specially the sizes of the various tags of this image and their respective vulnerabilities. The table below will demonstrate.

Image Compressed size* Decompressed size* Vulnerabilities*
node:20 380.44 MB 1.1 GB 3 Medium, 82 Low
node:18 378.61 MB 1.09 GB 3 Medium, 82 Low
node:20-slim 76.5 MB 247 MB 17 Low
node:18-alpine 51.17 MB 177 MB

* values are collected for linux/amd64 only.

The table shows that the bigger images are more vulnerable, which makes sense. The more stuff you have in your image, the more there will be chances of vulnerabilities.

Additionally, if you are planning to run an application container based on these images, you would not want your container to be this huge if possible. You won’t probably need all the extra software and runtimes available in those images.

Take only what you need

How about having only the files you need in your container? Wouldn’t that be nice? Enter Chisel.

Chisel enables you to do exactly that without breaking any sweat. You can define custom “slices” that only contain the necessary files, install only those and disregard all of the rest. The existing slice definitions can be found here: chisel-releases.

In fact, Zixing Liu from the Foundations team at Canonical very recently added the nodejs slices for lunar. We can utilise these slices and create a much smaller and more secure container image, as the following multi-stage Dockerfile will show.

ARG UBUNTU_RELEASE=23.04
ARG USER=app UID=101 GROUP=app GID=101

FROM ubuntu:$UBUNTU_RELEASE AS builder
ARG USER UID GROUP GID TARGETARCH
SHELL ["/bin/bash", "-oeux", "pipefail", "-c"]

# install the chisel binary
ADD https://github.com/canonical/chisel/releases/download/v0.8.0/chisel_v0.8.0_linux_${TARGETARCH}.tar.gz chisel.tar.gz
RUN tar -xvf chisel.tar.gz -C /usr/bin/
RUN apt-get update \
    && DEBIAN_FRONTEND=noninteractive apt-get install -y ca-certificates \
    && apt-get clean -y \
    && rm -rf /var/lib/apt/lists/*
# add root and $USER users
RUN install -d -m 0755 -o $UID -g $GID /rootfs/home/$USER \
    && mkdir -p /rootfs/etc \
    && echo -e "root:x:0:\n$GROUP:x:$GID:" >/rootfs/etc/group \
    && echo -e "root:x:0:0:root:/root:/noshell\n$USER:x:$UID:$GID::/home/$USER:/noshell" >/rootfs/etc/passwd
# install the required slices
RUN chisel cut --root /rootfs \
    base-files_base \
    base-files_release-info \
    tzdata_zoneinfo \
    ca-certificates_data \
    openssl_config \
    openssl_data \
    libgcc-s1_libs \
    libc6_libs \
    nodejs_bins

FROM scratch
ARG USER UID GROUP GID
USER $UID:$GID

# copy the rootfs we have prepared in the previous step
COPY --from=builder /rootfs /
# Workaround for https://github.com/moby/moby/issues/38710
COPY --from=builder --chown=$UID:$GID /rootfs/home/$USER /home/$USER

ENTRYPOINT [ "node" ]

Build the image with the following command:

# NOTE: export DOCKER_BUILDKIT=1 if you're running on an older Docker version
$ docker build -t ubuntu/chiselled-node:18 .

And voila! You will have a chiselled Node.js 18 LTS image which is tiny. You can check if the image is working properly by running:

$ docker run -it ubuntu/chiselled-node:18
Welcome to Node.js v18.13.0.
Type ".help" for more information.
> .help
.break    Sometimes you get stuck, this gets you out
.clear    Alias for .break
.editor   Enter editor mode
.exit     Exit the REPL
.help     Print this help message
.load     Load JS from a file into the REPL session
.save     Save all evaluated commands in this REPL session to a file

Press Ctrl+C to abort current expression, Ctrl+D to exit the REPL
> .exit

If you want to run your JS application on this, it’s simple too. Let’s say you have an app.js application. For the sake of simplicity, let’s assume it does not do much other than printing “Hello World”. You can scratch up a very simple Dockerfile to build your application container:

FROM ubuntu/chiselled-node:18

ADD app.js /

ENTRYPOINT ["node", "app.js"]

Build and run your favorite application like below:

$ docker build -t myapp -f Dockerfile.app .
$ docker run myapp
Hello World

Of course, you can run more complex applications on this image like you would do for the official node images.

Conclusion

But did it help though? How much did you exactly gain by using the chiselled nodejs image? Let’s find out!

Image Compressed size* Decompressed size* Vulnerabilities*
node:20 380.44 MB 1.1 GB 3 Medium, 82 Low
node:18 378.61 MB 1.09 GB 3 Medium, 82 Low
node:20-slim 76.5 MB 247 MB 17 Low
node:18-alpine 51.17 MB 177 MB
ubuntu/chiselled-node:18 37 MB 102 MB 4 Medium, 2 Low

* values are collected for linux/amd64 only.

The bottom row of this table indicates that our new image is roughly 1/10th of the node:18 image in size. We can now create Ubuntu-based container images for Node.js (applications) while also maintaining a tiny size.

If this article helped you create a cool, small and secure nodejs container you would like to share, I would love to know! Let me know in the comments.


PS. Shoutout to Zixing Liu (@liushuyu-011) from the Foundations team at Canonical for his amazing work in creating the nodejs slices.

5 Likes

Hi! Thanks for this!
I’ve got some doubts:

  1. If I get it correctly, this is based on the 23.04 nodejs package, which is in universe, so how can one get security updates? I having ESM mandatory?
  2. Is there the option to use those slices with the official nodejs PPA?

Thanks again!

1 Like

Hello!

  1. Indeed, this is based on the 23.04 nodejs package in universe. I think the slices will need to be backported to an LTS release to receive security updates. That might mean downgrading the nodejs version.
    About ESM, Chisel does not yet have the functionality to support Pro FIPS archives. But, work is ongoing and I hope we will soon have that feature.

  2. Unfortunately, no. Chisel does not support PPAs yet. Please feel free to create an issue in the chisel repo.

Please let me know if you have any more questions.

Thanks for your responses! Some more doubts:

My understanding was that, even on an LTS, support for universe packages was a best effort thing, that’s why I was asking about the ESM support.

Done: https://github.com/canonical/chisel/issues/98

ESM is indeed a Pro service, which shouldn’t take much longer to be supported in Chisel: https://github.com/canonical/chisel/pull/61