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.