Dockerfile good practices for Node and NPM

Layers of containers, that will make sense later.

NodeJS and NPM examples

Here I’ll be using NodeJS and NPM in examples, but most of those patterns can be applied to other runtimes as well.

Laverage non-root user

Default NodeJS images have node user, but it has to be enabled. The best option is to use it before any NPM dependencies or code are added.

# Copy files as a non-root user. The `node` user is built in the Node image.
WORKDIR /usr/src/app
RUN chown node:node ./
USER node

Set NODE_ENV=production by default

This is the most important one, as it affects NPM described below. In short NODE_ENV=production switch middlewares and dependencies to efficient code path and NPM installs only packages in dependencies. Packages in devDependencies and peerDependencies are ignored.

# Defaults to production, docker-compose overrides this to development on build and run.
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
version: '3'
services:
myapp:
build:
args:
- NODE_ENV=development
context: ./
environment:
- NODE_ENV=development

Install NPM dependencies before adding code

The reason is simple: dependencies change way less often than code, so we can leverage build cache. The biggest difference can be seen if you have any C++ modules that require compiling during install.

# Install dependencies first, as they change less often than code.
COPY package.json package-lock.json* ./
RUN npm ci && npm cache clean --force
COPY ./src ./src
$ docker build .
Sending build context to Docker daemon
Step 2/5 : COPY package.json package-lock.json* ./
---> Using cache
---> 6fb28308975d
Step 3/5 : RUN npm ci && npm cache clean --force
---> Using cache
---> 0a6bd71d2c2d

Use node (not NPM) to start the server

Last, but not least, is to avoid npm start as command to start application in container. Using NPM seems reasonable, because this is how you used to run the application locally. However with Docker and Kubernetes it's a bit more complicated.

# Execute NodeJS (not NPM script) to handle SIGTERM and SIGINT signals.
CMD ["node", "./src/index.js"]
const http = require('http');
const port = process.env.PORT || 8000;
http.createServer(function (req, res) {
res.end(req.url);
}).listen(port);
console.log(`Server running at http://localhost:${port}/ ...`);
// Signal handling
process.on('SIGTERM', function() {
console.log('SIGTERM: shutting down...');
});

Builder pattern

Let’s say your use case is to turn SASS/SCSS into plain CSS using Ruby Compass compiler. It has different stack than the rest of Node app, so we will need separate Docker image. Here’s how to use such separate temporary image for compilation step.

FROM rubygem/compass AS builder
COPY ./src/public /dist
WORKDIR /dist
RUN compass compile
# Output: css/app.css
# Copy compiled CSS styles from builder image.
COPY --from=builder /dist/css ./dist/css

Putting it all together

Here’s an example Dockerfile for easy copy&paste for your project. It covers all the good practices we’ve discussed earlier.

# Separate builder stage to compile SASS, so we can copy just the resulting CSS files.
FROM rubygem/compass AS builder
COPY ./src/public /dist
WORKDIR /dist
RUN compass compile
# Output: css/app.css
# Use NodeJS server for the app.
FROM node:12
# Copy files as a non-root user. The `node` user is built in the Node image.
WORKDIR /usr/src/app
RUN chown node:node ./
USER node
# Defaults to production, docker-compose overrides this to development on build and run.
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
# Install dependencies first, as they change less often than code.
COPY package.json package-lock.json* ./
RUN npm ci && npm cache clean --force
COPY ./src ./src
# Copy compiled CSS styles from builder image.
COPY --from=builder /dist/css ./dist/css
# Execute NodeJS (not NPM script) to handle SIGTERM and SIGINT signals.
CMD ["node", "./src/index.js"]

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Adam Brodziak

Adam Brodziak

154 Followers

Father. Husband. Solutions developer profesionally (software quite often). Arsenal supporter. Cyclist.