Blogpost

Published: 2020-11-18

Docker and node.js - caching node_modules efficiently

In order to avoid a full module install every time you touch the "package.json" file follow this guide. In the end docker will only build your package install layer when you really modified some of your package dependencies. I assume you use npm as package manager, I don't know if this works with yarn too.

Terminal

Table of contents

Your package files creation script

Create a file named "package.js" and put it into the project's main folder. This file will create a "docker-package.json" and a "docker-package-lock.json" containing only your dependencies (and some additional information in the case of the lock file).

package.js content:

const fs = require('fs').promises;
const LOCK_FILE_EXCLUDED_PROPS = [
    'name',
    'version',
];

(async function () {
    console.log('creating docker-package.json');

    let rawContent = await fs.readFile('./package.json');
    let parsedContent = JSON.parse(rawContent);
    let onlyDependencies = {};
    for (const prop of Object.keys(parsedContent)) {
        if (prop.toLowerCase().includes('dependencies')) {
            onlyDependencies[prop] = parsedContent[prop];
        }
    }
    await fs.writeFile('./docker-package.json', JSON.stringify(onlyDependencies, null, 3));

    console.log('creating docker-package-lock.json');

    rawContent = await fs.readFile('./package-lock.json');
    parsedContent = JSON.parse(rawContent);
    onlyDependencies = {};
    for (const prop of Object.keys(parsedContent)) {
        if (LOCK_FILE_EXCLUDED_PROPS.includes(prop)) {
            continue;
        }
        onlyDependencies[prop] = parsedContent[prop];
    }
    await fs.writeFile('./docker-package-lock.json', JSON.stringify(onlyDependencies, null, 3));

    process.exit(0);
})();

Add a postinstall script to your package.json

In your package.json add the following to your "scripts" property. Now every time you run npm i, on completion, our previously created script will run and create the slimmer docker versions of your package files.

package.json:

{
    ...
    "scripts": {
        ...
        "postinstall": "node ./package.js"
    },
    ...
}

Dockerfile

Your Dockerfile should now contain the following two lines before your RUN npm i (RUN npm ci for production).

COPY docker-package.json ./package.json
COPY docker-package-lock.json ./package-lock.json

This way docker can efficiently cache your packages and will only reinstall them when a dependency changes.

A complete simple Dockerfile could look like this.

FROM node:12 AS base
WORKDIR /app

COPY docker-package.json ./package.json
COPY docker-package-lock.json ./package-lock.json
RUN npm ci

COPY . .

# remove the following lines if you get an error
FROM node:12-alpine
WORKDIR /app
COPY --from=base /app .

Sources

Tags

#docker #nodejs