There is a number of great guides on "containerizing" NodeJS applications, including this one from Snyk. However, I am yet to see a resource recommending to omit NPM from the final container image.
Let's say I have the following "dummy" application:
index.js
const express = require('express')
const app = express()
app.get('*', function (req, res) {
res.send('bla bla bla')
})
app.listen(3000)
package.json
{
"name": "test",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "^4.17.2"
}
}
One common way to structure a Dockerfile for this app would be using two stage build. First stage, installing dependencies; and second creating the final image. Both stages are using Alpine image with pre-installed NodeJS and NPM. With our simple app, we can even omit the first step, but let's pretend we need it.
bad.Dockerfile
# Build stage
FROM node:16-alpine3.15 as build
# Install dependencies
WORKDIR /
COPY package-lock.json .
COPY package.json .
RUN npm ci --production
# Final stage
FROM node:16-alpine3.15 as final
# Setup application
RUN mkdir -p /app/simple-server
WORKDIR /app/simple-server
COPY . .
COPY --from=build node_modules node_modules
# Run application
ENTRYPOINT ["node", "index.js"]
As you can see NPM will be shipped with the final container image. So what's the problem here?
The issue is the final image will have the dependency that is not used, but you would have to maintain it.
Not a big deal? It actually is, and can potentially become a blocker preventing to ship your application to production (or other environment depending on security controls in place). A good example is CVE-2021-3807. There is a GitHub Issue open, where engineers complaining how vulnerability presented in NPM blocks them in one or another way.
The solution here is simple - omit NPM from your final image. In Docker multi-stage build, it would look very similar to the bad example. The main difference is the final image is bare Alpine, and only NodeJS is installed as build step.
good.Dockerfile
# Build stage
FROM node:16-alpine3.15 as build
# Install dependencies
WORKDIR /
COPY package-lock.json .
COPY package.json .
RUN npm ci --production
# Final stage
FROM alpine:3.15 as final
# Upgrade APK
RUN apk --no-cache add --upgrade nodejs~16
# Setup application
RUN mkdir -p /app/simple-server
WORKDIR /app/simple-server
COPY . .
COPY --from=build node_modules node_modules
# Run application
ENTRYPOINT ["node", "index.js"]
Another benefit of excluding NPM from the final image is reduced size. The "dummy" server without NPM is 53.9MB, while with the package manager 112MB!
Not much else to say here. If you still have NPM in your final container image, ask yourself why!
Thank you for reading this article, and I would like to see the feedback on this one! Please let me know in comments what are YOUR legitimate reasons for having NPM in the final container image.
Top comments (0)