Skip to content

Techniques for creating smaller Node.js Docker images

Notifications You must be signed in to change notification settings

sdavids/sdavids-node-docker-image-slimming

Repository files navigation

Node Docker Image Slimming

The final result with all techniques applied can be found in the main branch:

The other branches showcase a single technique—​the last commit of the branch contains the changes for the specific technique.

⚠️

All branches besides main will be force-pushed to in order to keep them up-to date with the main branch and remain a single commit.

# Branch Layer Count Image Size (MB) node_modules Size (MB) server.mjs Size (B)

0

000-base

33

1950.0

140.0

2139

1

001-dockerignore

35

1770.0

120.0

2139

2

002-alpine

31

382.0

119.7

2139

3

003-alpine-npm-ci

31

233.0

38.3

2139

4

004-alpine-clean-modules

31

217.0

22.4

2139

5

005-alpine-multi-stage-build

29

216.0

22.4

2139

6

006-alpine-alpine-final

24

116.0

22.4

2139

7

007-alpine-upx

26

114.0

22.4

2139

8

008-alpine-hardening

30

114.0

22.4

2139

9

009-alpine-webpack

30

114.0

22.4

1189

9

009-alpine-esbuild-external

30

114.0

22.4

1136

9

009-alpine-esbuild

29

93.4

0

4750597

  1. baseline with the default variant of the offical NodeJS Docker image for building the image and serving the example REST API with no optimizations

  2. use a .dockerignore file and specific COPY commands

    ℹ️

    The ls_extensions and ls_extensions_git functions might help fine-tuning your .dockerignore file.

  3. use the alpine variant of the offical NodeJS Docker image for both building and running

  4. use npm ci --omit dev --omit optional --omit peer instead of npm i

    ℹ️

    Depending on your project setup you might not be able to omit the peer dependencies.

  5. use the clean-modules npm package to clean up the node_modules directory

    ℹ️

    clean-modules will have more impact once there are more dependencies.

  6. use a multi-stage build

  7. use the official Alpine Docker image for serving the example REST API

    ℹ️

    Depending on your project setup you might have to install more packages via apk add --no-cache.

    The dependencies of docker run --rm alpine apk add nodejs might be a starting point.

  8. use UPX to compress the node binary

  9. harden the final alpine image

    ℹ️

    Hardening does not only decrease the image size but also makes it significantly more secure.

  10. bundle the example REST API

    ℹ️

    Bundling will have more impact once there are more source files to bundle.

    1. use webpack

    2. use esbuild; esbuild --bundle --minify --packages=external

      ℹ️

      Minified JavaScript makes debugging production issues harder.

    3. use esbuild; esbuild --bundle --minify

      ℹ️

      Minified JavaScript makes debugging production issues harder.

  1. Build the Docker image:

    $ npm run docker:build
  2. Start the image (HTTP server):

    $ npm run docker:start
  3. Stop the image:

    $ npm run docker:stop
  4. Create a self-signed certificate:

    $ npm run cert:create
  5. Start the image (HTTPS server):

    $ npm run docker:start:secure

There are also other build tasks available.

The example exposes two endpoints (OpenAPI 3 Description):

/

returns a randomly generated user in JSON format

/-/health/liveness

liveness probe

You can use several API Tools to interact with the API.

We abide by the Contributor Covenant, Version 2.1 and ask that you do as well.

For more information, please see CODE_OF_CONDUCT.adoc.

Install fnm or NVM.

ℹ️

This repository uses husky for Git hooks.

More information: Husky - Command not found

~/.zprofile
if command -v fnm > /dev/null 2>&1; then
  eval "$(fnm env --use-on-cd)"
fi
~/.config/husky/init.sh
#!/usr/bin/env sh

# vim:ft=zsh

# shellcheck shell=sh disable=SC1091

set -eu

[ -e /etc/zshenv ] && . /etc/zshenv
[ -e "${ZDOTDIR:=${HOME}}/.zshenv" ] && . "${ZDOTDIR:=${HOME}}/.zshenv"
[ -e /etc/zprofile ] && . /etc/zprofile
[ -e "${ZDOTDIR:=${HOME}}/.zprofile" ] && . "${ZDOTDIR:=${HOME}}/.zprofile"
[ -e /etc/zlogin ] && . /etc/zlogin
[ -e "${ZDOTDIR:=${HOME}}/.zlogin" ] && . "${ZDOTDIR:=${HOME}}/.zlogin"
~/.zshrc
export NVM_DIR="${HOME}/.nvm"

[ -s "${NVM_DIR}/nvm.sh" ] && . "${NVM_DIR}/nvm.sh"
[ -s "${NVM_DIR}/bash_completion" ] && . "${NVM_DIR}/bash_completion"

if command -v nvm > /dev/null 2>&1; then
  autoload -U add-zsh-hook
  load-nvmrc() {
    local nvmrc_path="$(nvm_find_nvmrc)"
    if [ -n "${nvmrc_path}" ]; then
      local nvmrc_node_version=$(nvm version "$(cat "${nvmrc_path}")")
      if [ "${nvmrc_node_version}" = "N/A" ]; then
        nvm install
      elif [ "${nvmrc_node_version}" != "$(nvm version)" ]; then
        nvm use
      fi
    elif [ -n "$(PWD=$OLDPWD nvm_find_nvmrc)" ] && [ "$(nvm version)" != "$(nvm version default)" ]; then
      echo "Reverting to nvm default version"
      nvm use default
    fi
  }

  add-zsh-hook chpwd load-nvmrc
  load-nvmrc
fi
~/.config/husky/init.sh
#!/usr/bin/env sh

# vim:ft=zsh

# shellcheck shell=sh disable=SC1091

set -eu

[ -e /etc/zshenv ] && . /etc/zshenv
[ -e "${ZDOTDIR:=${HOME}}/.zshenv" ] && . "${ZDOTDIR:=${HOME}}/.zshenv"
[ -e /etc/zprofile ] && . /etc/zprofile
[ -e "${ZDOTDIR:=${HOME}}/.zprofile" ] && . "${ZDOTDIR:=${HOME}}/.zprofile"
[ -e /etc/zlogin ] && . /etc/zlogin
[ -e "${ZDOTDIR:=${HOME}}/.zlogin" ] && . "${ZDOTDIR:=${HOME}}/.zlogin"

export NVM_DIR="${HOME}/.nvm"

if [ -f "${NVM_DIR}/nvm.sh" ]; then
  . "${NVM_DIR}/nvm.sh"

  if [ -f ".nvmrc" ]; then
    nvm use
  fi
fi
$ sudo apt-get install shellcheck
$ brew install shellcheck
$ sudo apt-get install yamllint
$ brew install yamllint

Install hadolint.

$ brew install hadolint

Install and enable the HTTP Client plugin.

Open:

use with the local or local-secure environments defined in:

Install and enable the REST Client extension.

Add the following snippet to .vscode/settings.json:

    "rest-client.environmentVariables": {
        "local": {
            "host": "http://localhost",
            "port": "3000"
          },
          "local-secure": {
            "host": "https://localhost",
            "port": "3000"
          }
    }

Open:

Switch (Ctrl+Alt+E / macOS: ⌘ Сmd+⌥ Opt+E) to the local or local-secure environment.

$ curl http://localhost:3000/
$ curl http://localhost:3000/-/health/liveness
$ curl --insecure https://localhost:3000/
$ curl --insecure https://localhost:3000/-/health/liveness

Runs the app from the source files (src/js/).

$ npm start

Runs the app from the source files (src/js/); restarting on file changes.

$ npm run start:dev

Builds the app.

$ npm run build

dist/

Runs the app generated by build (dist/).

$ npm run start:build

Deletes dist/ generated by build.

$ npm run clean

Format files with prettier.

$ npm run format

Checks the formatting of the files with prettier.

$ npm run format:check

Find problems via ESLint.

$ npm run lint

Fix problems via ESLint.

$ npm run lint:fix

Builds the app’s image.

$ npm run docker:build

Removes all containers, volumes, and images previously created by this project.

$ npm run docker:cleanup

Displays the health status of the app’s container.

$ npm run docker:health

Displays the logs of the app’s container.

$ npm run docker:logs

Opens a shell into the running app’s container.

$ npm run docker:sh

Starts the app in a container exposing an HTTP port.

$ npm run docker:start

Starts the app in a container exposing an HTTPS port.

$ npm run docker:start:secure

One needs to create the necessary private key and certificate via cert:create.

Stops the app’s container.

$ npm run docker:stop

Deletes node_modules/ and package-lock.json.

$ npm run clean:node

Creates a private key and a self-signed certificate.

$ npm run cert:create

docker/certs/cert.pem and docker/certs/key.pem

ℹ️

The generated certificate is valid for 30 days.

Check your login keychain in Keychain Access; Secure Sockets Layer (SSL) should be set to "Always Trust":

self signed macos
ℹ️

Chrome and Safari need no further configuration.

You need to bypass the self-signed certificate warning by clicking on "Advanced" and then "Accept the Risk and Continue":

self signed firefox

Deletes the private key and the self-signed certificate.

$ npm run cert:delete

You can delete the certificate via Firefox > Preferences > Privacy & Security > Certificates; click "View Certificates…​":

self signed firefox delete 1

Click on the "Servers" tab:

self signed firefox delete 2

About

Techniques for creating smaller Node.js Docker images

Topics

Resources

Security policy

Stars

Watchers

Forks