🏃➡️ TL;DR
- ❌ Avoid "bloated" image in production:
node:22
,node:latest
,node:lts
,node:current
- ✅ Use "slim" variant image in production, i.e.,
node:lts-slim
,node:22-slim
- ❌ Avoid "non-LTS" and odd-numbered releases, i.e.,
node:slim
,node:current-slim
,node:bookworm-slim
,node:23
,node:21
- ✅ Use even-numbered "LTS" release, i.e.,
node:20-slim
,node:22-slim
,node-lts-slim
🤦🏻♂️ Bad choice
As per Docker Hub, Node.js image gets 9 million weekly image pulls with 1+ Billion image already pulled and counting.
Unfortunately, use of buildpack-deps
based version of NodeJS image by default leads to unnecessarily bloated image, full of dev packages, compilers and riddled with CVEs. Avoid using "bloated" image at all cost in production, but why?
- Slower deployment times: "bloated" images are significantly larger, slows down deployment times.
- Disk spaces: "bloated" image due to it's size, consumes more disk space when uncompressed.
- Security risks: With more packages included, higher risk to new security vulnerabilities and supply chain attacks.
- Slow startup-times: A larger image can slow down startup times.
Ivan from iximiuz Labs did a post mortem analysis of node:22
image and highlights the buildpack-deps:stable
and buildpack-deps:scm
layer image on-top of Debian base image (bookworm) is where all the "bloat" comes from, including a full Python installation and the GNU Compiler Collection (GCC), which contributes to larger image size.
# 🤯 >1GB in image size
$ docker images node
REPOSITORY TAG IMAGE ID CREATED SIZE
node 22 c9d4a6dda881 4 days ago 1.12GB
node bookworm 8c96be300ba8 4 days ago 1.12GB
node current 8c96be300ba8 4 days ago 1.12GB
node latest 8c96be300ba8 4 days ago 1.12GB
node lts 85f76d7c2b89 2 weeks ago 1.1GB
# 🪲 Riddle with vulnerable libraries
$ trivy image -q node:22
node:22 (debian 12.7)
=====================
Total: 997 (UNKNOWN: 4, LOW: 492, MEDIUM: 419, HIGH: 76, CRITICAL: 6)
# 🤨 Do you need GCC compiler?
$ docker run --rm -it node:22 gcc --version
gcc (Debian 12.2.0-14) 12.2.0
Copyright (C) 2022 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# or 🐍 full Python installation?
$ docker run --rm -it node:22 python3 --version
Python 3.11.2
😌 Be calm and use "slim" variant
As you can see from above image, "bloated" image is perfect as a builder image during build stage and "slim" variant as a runtime image for production use.
"Slim" variant is 80% thinner with lot less build/dev packages, that means faster deploys, less disk space, more secure, faster start up times.
For most Node.js projects running "slim" variant in production is a safer and optimal choice due to it's balanced of size and installed packages.
# Use "slim" variant for production
FROM node:lts-slim
FROM node:22-slim
# 👌🏻 Thinner and smaller image size
$ docker images node
REPOSITORY TAG IMAGE ID CREATED SIZE
node 22-slim ddf2ab152dc9 4 days ago 240MB
node 22-alpine e906dc0e8219 4 days ago 153MB
node lts-slim 1658b30e8115 2 weeks ago 220MB
⚠️ Be careful with "Alpine" variant
Even though "alpine" variant is 30% thiner than "slim" variant, avoid using it for mission-critical NodeJS applications.
- Alpine is considered experimental and not officially supported target platform for Node.JS.
- "Slim" variant uses Debian, while "Alpine" variant uses Alpine Linux.
- Alpine uses "Musl" C library instead of widespread GNU C Library (glibc) which leads to compatibility issue and unexpected behaviors or bugs.
# Avoid "alpine" variant for mission-critical applications
FROM node:lts-alpine
FROM node:current-alpine
FROM node:22-alpine
🤔 But, when should I use "bloated" image?
A "bloated", "full", "debug" image variant includes a full set of development tools and libraries, making it an ideal choice during development but not for production.
After all, just like a chef who brings every spice to the kitchen, it's great for cooking up ideas but not so much for serving dinner! - AI
A "bloated" image serve as a builder image for the multi-stage builds, and slim image for the final runtime stage, resulting in an optimized, small, secure image that is ready for production use.
# Build stage
FROM node:22-lts as builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
# Runtime stage
FROM node:22-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/index.js"]
🙃 Wait, what about Distroless variant?
Well…distroless variant is ~30% thinner compared to "slim" variant, and is more secure with no shell, no package manager. But…
- "Distroless" variant is not officially supported by Node.js team.
- "Distroless" variant do not maintain latest LTS version, unless you pay.
Google provides "distroless" variant but you will get outdated Node.js version that is currently in maintenance mode.
# You get maintenance mode version
$ docker run -it --rm gcr.io/distroless/nodejs --version
v18.15.0
# Slightly smaller than "slim" variant
$ docker images gcr.io/distroless/nodejs
REPOSITORY TAG IMAGE ID CREATED SIZE
gcr.io/distroless/nodejs latest 5fafa8030b0b 19 months ago 161MB
Chainguard also offers distroless variant but only "latest" tag is free to use which uses non-LTS version, and other tags node:22 is only available for paid users.
# Free to use
$ docker run -it --rm cgr.dev/chainguard/node:latest --version
v23.0.0
If you need to run your mission-critical Node.js applications in highly regulated, secure environment then Chainguard is your best option.
👋 Looking for more?
Feel free to follow me on Twitter or LinkedIn for more insightful contents.
P.S. Don't forget to checkout this insightful tutorial by Ivan and get hands-on on iximiuz Labs.
Top comments (0)