Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/sdavids/sdavids-node-docker-image-slimming
Techniques for creating smaller Node.js Docker images
https://github.com/sdavids/sdavids-node-docker-image-slimming
docker express node
Last synced: 3 days ago
JSON representation
Techniques for creating smaller Node.js Docker images
- Host: GitHub
- URL: https://github.com/sdavids/sdavids-node-docker-image-slimming
- Owner: sdavids
- Created: 2020-04-06T10:04:14.000Z (almost 5 years ago)
- Default Branch: main
- Last Pushed: 2024-12-13T18:42:42.000Z (about 1 month ago)
- Last Synced: 2024-12-13T19:36:14.514Z (about 1 month ago)
- Topics: docker, express, node
- Language: Shell
- Homepage:
- Size: 2.35 MB
- Stars: 0
- Watchers: 3
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.adoc
- Contributing: CONTRIBUTING.adoc
- License: LICENSES/Apache-2.0.txt
- Code of conduct: CODE_OF_CONDUCT.adoc
- Codeowners: .github/CODEOWNERS
- Security: SECURITY.md
- Authors: AUTHORS
Awesome Lists containing this project
README
// SPDX-FileCopyrightText: © 2020 Sebastian Davids
// SPDX-License-Identifier: Apache-2.0
= Node Docker Image Slimming
Sebastian Davids
// Metadata:
:description: Techniques for creating a smaller Node.js Docker image.
// Settings:
:sectnums:
:sectanchors:
:sectlinks:
:toc: macro
:toclevels: 3
:toc-placement!:
:hide-uri-scheme:
:source-highlighter: rouge
:rouge-style: github
:experimental:
// Refs:
:uri-contributor-covenant: https://www.contributor-covenant.org
:uri-apache-license: https://www.apache.org/licenses/LICENSE-2.0
:uri-google-style: https://github.com/google/gts
:docker-install-url: https://docs.docker.com/install/
:nvm-install-url: https://github.com/nvm-sh/nvm#installing-and-updating
:fnm-install-url: https://github.com/Schniz/fnm#installationifdef::env-browser[:outfilesuffix: .adoc]
ifdef::env-github[]
:outfilesuffix: .adoc
:note-caption: :information_source:
:important-caption: :heavy_exclamation_mark:
:warning-caption: :warning:
:badges:
endif::[]ifdef::badges[]
image:https://img.shields.io/github/license/sdavids/sdavids-node-docker-image-slimming[Apache License,Version 2.0,link={uri-apache-license}]
image:https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg[Contributor Covenant,Version 2.1,link={uri-contributor-covenant}]
image:https://img.shields.io/badge/code%20style-google-blueviolet.svg[Code Style: Google,link={uri-google-style}]
image:https://img.shields.io/osslifecycle/sdavids/sdavids-node-docker-image-slimming[OSS Lifecycle]
image:https://img.shields.io/maintenance/yes/2024[Maintenance]
image:https://img.shields.io/github/last-commit/sdavids/sdavids-node-docker-image-slimming[GitHub last commit]
image:http://isitmaintained.com/badge/resolution/sdavids/sdavids-node-docker-image-slimming.svg[Resolution Time]
image:http://isitmaintained.com/badge/open/sdavids/sdavids-node-docker-image-slimming.svg[Open Issues]
endif::[]toc::[]
This repository showcases several techniques for creating smaller Node.js Docker images:
. .dockerignore file
. multi-stage build
. smaller base image
. image hardening
. https://upx.github.io[UPX]
. cleaning up `node_modules`
. JavaScript bundlingThe link:Dockerfile[example] is an https://expressjs.com[Express]-based REST API packaged in a Docker image with a https://docs.docker.com/reference/dockerfile/#healthcheck[HEALTHCHECK].
****
*TLDR*1950.0 MB => 93.4 MB (-95.21%)
****== Techniques
The final result with all techniques applied can be found in the `main` branch:
* link:Dockerfile[]
* link:scripts/docker-build.sh[]The other branches showcase a single technique--the last commit of the branch contains the changes for the specific technique.
[WARNING]
====
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.
======= Results
[%header,cols=">1,4,^2m,>2m,>2m,>2m"]
|===|#
|Branch
| Layer Count
|Image Size (MB)
|node_modules Size (MB)
|server.mjs Size (B)|0
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/000-base?tab=readme-ov-file#node-docker-image-slimming[000-base]
|33
|1950.0
|140.0
|2139|1
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/001-dockerignore?tab=readme-ov-file#node-docker-image-slimming[001-dockerignore]
|35
|1770.0
|120.0
|2139|2
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/002-alpine?tab=readme-ov-file#node-docker-image-slimming[002-alpine]
|31
|382.0
|119.7
|2139|3
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/003-alpine-npm-ci?tab=readme-ov-file#node-docker-image-slimming[003-alpine-npm-ci]
|31
|233.0
|38.3
|2139|4
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/004-alpine-clean-modules?tab=readme-ov-file#node-docker-image-slimming[004-alpine-clean-modules]
|31
|217.0
|22.4
|2139|5
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/005-alpine-multi-stage-build?tab=readme-ov-file#node-docker-image-slimming[005-alpine-multi-stage-build]
|29
|216.0
|22.4
|2139|6
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/006-alpine-alpine-final?tab=readme-ov-file#node-docker-image-slimming[006-alpine-alpine-final]
|24
|116.0
|22.4
|2139|7
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/007-alpine-upx?tab=readme-ov-file#node-docker-image-slimming[007-alpine-upx]
|26
|114.0
|22.4
|2139|8
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/008-alpine-hardening?tab=readme-ov-file#node-docker-image-slimming[008-alpine-hardening]
|30
|114.0
|22.4
|2139|9
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/009-alpine-webpack?tab=readme-ov-file#node-docker-image-slimming[009-alpine-webpack]
|30
|114.0
|22.4
|1189|9
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/009-alpine-esbuild-external?tab=readme-ov-file#node-docker-image-slimming[009-alpine-esbuild-external]
|30
|114.0
|22.4
|1136|9
|https://github.com/sdavids/sdavids-node-docker-image-slimming/tree/009-alpine-esbuild?tab=readme-ov-file#node-docker-image-slimming[009-alpine-esbuild]
|29
|93.4
|0
|4750597|===
[start=0]
. baseline with the default variant of the https://hub.docker.com/_/node/[offical NodeJS Docker image] for building the image and serving the example REST API with no optimizations
. use a https://docs.docker.com/reference/dockerfile/#dockerignore-file[.dockerignore file] and specific `COPY` commands
+
[NOTE]
====
The https://github.com/sdavids/sdavids-shell-misc?tab=readme-ov-file#212-ls_extensions[ls_extensions] and https://github.com/sdavids/sdavids-shell-misc?tab=readme-ov-file#ls_extensions_git[ls_extensions_git] functions might help fine-tuning your `.dockerignore` file.
====
. use the `alpine` variant of the https://hub.docker.com/_/node/[offical NodeJS Docker image] for both building and running
. use `npm ci --omit dev --omit optional --omit peer` instead of `npm i`
+
[NOTE]
====
Depending on your project setup you might not be able to omit the https://nodejs.org/en/blog/npm/peer-dependencies[peer dependencies].
====
. use the https://www.npmjs.com/package/clean-modules[clean-modules] npm package to clean up the `node_modules` directory
+
[NOTE]
====
`clean-modules` will have more impact once there are more dependencies.
====
. use a multi-stage build
. use the https://hub.docker.com/_/alpine/[official Alpine Docker image] for serving the example REST API
+
[NOTE]
====
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.
====
. use https://upx.github.io[UPX] to compress the `node` binary
. harden the final `alpine` image
+
[NOTE]
====
Hardening does not only decrease the image size but also makes it significantly more secure.
====
. bundle the example REST API
+
[NOTE]
====
Bundling will have more impact once there are more source files to bundle.
====
.. use https://webpack.js.org[webpack]
.. use https://esbuild.github.io[esbuild]; `esbuild --bundle --minify --packages=external`
+
[NOTE]
====
Minified JavaScript makes debugging production issues harder.
====
.. use https://esbuild.github.io[esbuild]; `esbuild --bundle --minify`
+
[NOTE]
====
Minified JavaScript makes debugging production issues harder.
======= Running the Example
. Build the Docker image:
+
[source,shell]
----
$ npm run docker:build
----. Start the image (HTTP server):
+
[source,shell]
----
$ npm run docker:start
----
+
=> `http://localhost:3000[http://localhost:3000]`. Stop the image:
+
[source,shell]
----
$ npm run docker:stop
----. Create a self-signed certificate:
+
[source,shell]
----
$ npm run cert:create
----. Start the image (HTTPS server):
+
[source,shell]
----
$ npm run docker:start:secure
----
+
=> `https://localhost:3000[https://localhost:3000]`There are also <> available.
=== Example REST API
The example exposes two endpoints (link:api/openapi.yaml[OpenAPI 3 Description]):
`/`::
returns a randomly generated user in JSON format`/-/health/liveness`::
liveness probeYou can use several <> to interact with the API.
=== More Information
* https://docs.docker.com/reference/dockerfile/#dockerignore-file[.dockerignore file]
* https://docs.docker.com/reference/dockerfile/#copy[Dockerfile - COPY]
* https://hub.docker.com/_/node[node:-alpine]
* https://docs.npmjs.com/cli/v10/commands/npm-ci[npm-ci]
* https://docs.npmjs.com/cli/v10/commands/npm-ci#omit[npm-ci --omit]
* https://docs.npmjs.com/cli/v10/commands/npm-cache[npm-cache]
* https://www.npmjs.com/package/clean-modules[clean-modules]
* https://docs.docker.com/build/building/multi-stage/[Multi-stage builds]
* https://hub.docker.com/_/alpine/[Official Alpine Docker Image]
* https://upx.github.io[UPX]
* https://github.com/ironpeakservices/iron-alpine[iron-alpine]
* https://webpack.js.org[webpack]
* https://esbuild.github.io/getting-started/#bundling-for-node[esbuild - Bundling for node]== License
Apache License, Version 2.0 (link:LICENSE[] or {uri-apache-license}).
=== Contribution
See link:CONTRIBUTING{outfilesuffix}[].
== Code of Conduct
We abide by the {uri-contributor-covenant}[Contributor Covenant, Version 2.1] and ask that you do as well.
For more information, please see link:CODE_OF_CONDUCT{outfilesuffix}[].
== Development Environment Setup
=== Installation
==== Docker
Install {docker-install-url}[Docker].
==== Node Version Manager
Install {fnm-install-url}[fnm] or {nvm-install-url}[NVM].
[NOTE]
====
This repository uses https://typicode.github.io/husky/[husky] for Git hooks.More information:
https://typicode.github.io/husky/troubleshooting.html#command-not-found[Husky - Command not found]
========= fnm
.~/.zprofile
[source,shell]
----
if command -v fnm > /dev/null 2>&1; then
eval "$(fnm env --use-on-cd)"
fi
----.~/.config/husky/init.sh
[source,shell]
----
#!/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"
----===== nvm
.~/.zshrc
[source,shell]
----
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
[source,shell]
----
#!/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
----==== shellcheck
===== Linux
[source,shell]
----
$ sudo apt-get install shellcheck
----===== Mac
[source,shell]
----
$ brew install shellcheck
----==== yamllint
===== Linux
[source,shell]
----
$ sudo apt-get install yamllint
----===== Mac
[source,shell]
----
$ brew install yamllint
----==== hadolint
===== Linux
Install https://github.com/hadolint/hadolint?tab=readme-ov-file#install[hadolint].
===== Mac
[source,shell]
----
$ brew install hadolint
----[#api-tools]
=== API Tools==== OpenAPI
Open:
link:api/openapi.yaml[OpenAPI 3 Description]
==== Postman
Install https://www.postman.com/downloads/[Postman].
Import:
* link:api/api.postman_collection.json[Postman Collection]
* link:api/local.postman_environment.json[Postman 'local' Environment]
* link:api/local-secure.postman_environment.json[Postman 'local secure' Environment]===== More Information
* https://blog.postman.com/self-signed-ssl-certificate-troubleshooting/[Troubleshooting Self-signed SSL Certificate Issues and More in Postman]
==== RapidAPI
Install https://paw.cloud[RapidAPI].
Open:
* link:api/api.paw[RapidAPI Project]
==== JetBrains IDEs
Install and enable the https://plugins.jetbrains.com/plugin/13121-http-client[HTTP Client] plugin.
Open:
* link:api/api.http[HTTP requests file]
use with the `local` or `local-secure` environments defined in:
* link:api/http-client.private.env.json[HTTP environment file]
===== More Information
* https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html#run_request[Execute HTTP requests]
* https://www.jetbrains.com/help/idea/http-client-in-product-code-editor.html#disable_certificate_verification[Disable certificate verification]==== Visual Studio Code
Install and enable the https://github.com/Huachao/vscode-restclient[REST Client] extension.
Add the following snippet to `.vscode/settings.json`:
[source,json]
----
"rest-client.environmentVariables": {
"local": {
"host": "http://localhost",
"port": "3000"
},
"local-secure": {
"host": "https://localhost",
"port": "3000"
}
}
----Open:
* link:api/api.http[HTTP requests file]
https://github.com/Huachao/vscode-restclient?tab=readme-ov-file#environments[Switch] (kbd:[Ctrl+Alt+E] / macOS: kbd:[⌘ Сmd+⌥ Opt+E]) to the `local` or `local-secure` environment.
===== More Information
* https://github.com/Huachao/vscode-restclient?tab=readme-ov-file#environments[REST Client - Environments]
==== cURL
[source,shell]
----
$ curl http://localhost:3000/
$ curl http://localhost:3000/-/health/liveness
----[source,shell]
----
$ curl --insecure https://localhost:3000/
$ curl --insecure https://localhost:3000/-/health/liveness
----===== More Information
* https://github.com/Huachao/vscode-restclient?tab=readme-ov-file#environments[REST Client - Environments]
[#build-tasks]
== Build Tasks=== Development
==== start
Runs the app from the source files (`src/js/`).
[source,shell]
----
$ npm start
----=> `http://localhost:3000`
==== start:dev
Runs the app from the source files (`src/js/`); restarting on file changes.
[source,shell]
----
$ npm run start:dev
----=> `http://localhost:3000`
=== Building
[#build]
==== buildBuilds the app.
[source,shell]
----
$ npm run build
----=> `dist/`
==== start:build
Runs the app generated by <> (`dist/`).
[source,shell]
----
$ npm run start:build
----=> `http://localhost:3000`
==== clean
Deletes `dist/` generated by <>.
[source,shell]
----
$ npm run clean
----=== Code Quality
==== format
Format files with https://prettier.io[prettier].
[source,shell]
----
$ npm run format
----==== format:checks
Checks the formatting of the files with https://prettier.io[prettier].
[source,shell]
----
$ npm run format:check
----==== lint
Find problems via https://eslint.org[ESLint].
[source,shell]
----
$ npm run lint
----==== lint:fix
Fix problems via https://eslint.org[ESLint].
[source,shell]
----
$ npm run lint:fix
----=== Docker
==== docker:build
Builds the app's image.
[source,shell]
----
$ npm run docker:build
----==== docker:cleanup
Removes all containers, volumes, and images previously created by this project.
[source,shell]
----
$ npm run docker:cleanup
----
==== docker-healthDisplays the https://docs.docker.com/reference/dockerfile/#healthcheck[health status] of the app's container.
[source,shell]
----
$ npm run docker:health
----==== docker-logs
Displays the logs of the app's container.
[source,shell]
----
$ npm run docker:logs
----==== docker:sh
Opens a shell into the running app's container.
[source,shell]
----
$ npm run docker:sh
----==== docker:start
Starts the app in a container exposing an HTTP port.
[source,shell]
----
$ npm run docker:start
----=> `http://localhost:3000[http://localhost:3000]`
==== docker:start:secure
Starts the app in a container exposing an HTTPS port.
[source,shell]
----
$ npm run docker:start:secure
----=> `https://localhost:3000[https://localhost:3000]`
[IMPORTANT]
====
One needs to create the necessary private key and certificate via <>.
======== docker:stop
Stops the app's container.
[source,shell]
----
$ npm run docker:stop
----=== Misc
==== clean:node
Deletes `node_modules/` and `package-lock.json`.
[source,shell]
----
$ npm run clean:node
----[[cert_create]]
==== cert:createCreates a private key and a self-signed certificate.
[source,shell]
----
$ npm run cert:create
----=> `docker/certs/cert.pem` and `docker/certs/key.pem`
[NOTE]
====
The generated certificate is valid for 30 days.
========= MacOS
Check your login keychain in _Keychain Access_; _Secure Sockets Layer (SSL)_ should be set to "Always Trust":
image::src/docs/asciidoc/images/self-signed-macos.png[]
[NOTE]
====
Chrome and Safari need no further configuration.
========= Firefox (MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT)
You need to bypass the https://support.mozilla.org/en-US/kb/error-codes-secure-websites#w_self-signed-certificate[self-signed certificate warning] by clicking on "Advanced" and then "Accept the Risk and Continue":
image::src/docs/asciidoc/images/self-signed-firefox.png[]
===== Related Scripts
. <>
[#cert_delete]
==== cert:deleteDeletes the private key and the self-signed certificate.
===== Usage
[source,shell]
----
$ npm run cert:delete
----===== Firefox
You can delete the certificate via `Firefox > Preferences > Privacy & Security > Certificates`; click "View Certificates...":
image::src/docs/asciidoc/images/self-signed-firefox-delete-1.png[]
Click on the "Servers" tab:
image::src/docs/asciidoc/images/self-signed-firefox-delete-2.png[]
===== Related Scripts
. <>