├── .dockerignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── build.sh ├── patch.sh └── patches ├── v8-cppgc-shared-no-lto.patch └── v8-missing-elf-arm32v6-7.patch /.dockerignore: -------------------------------------------------------------------------------- 1 | * 2 | !build.sh 3 | !patch.sh 4 | !patches 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16.2 as builder 2 | 3 | RUN apk update 4 | RUN apk add make g++ python3 gnupg curl file flex patch rsync texinfo 5 | 6 | ARG arch= 7 | ENV BUILD_ARCH=$arch 8 | 9 | COPY build.sh / 10 | 11 | RUN curl -Lsq -o musl-cross-make.zip https://git.zv.io/toolchains/musl-cross-make/-/archive/ed72f5171e3d4a9e92026823cbfe93e795105763/musl-cross-make-ed72f5171e3d4a9e92026823cbfe93e795105763.zip \ 12 | && unzip -q musl-cross-make.zip \ 13 | && mv musl-cross-make-ed72f5171e3d4a9e92026823cbfe93e795105763 musl-cross-make \ 14 | && $(/build.sh config_mak ${BUILD_ARCH:-""} /musl-cross-make/config.mak) \ 15 | && cd /musl-cross-make \ 16 | && make install -j$(getconf _NPROCESSORS_ONLN) V= \ 17 | && rm -rf /musl-cross-make 18 | 19 | ARG version=0.0.0 20 | ENV NODE_VERSION=$version 21 | 22 | # gpg keys listed at https://github.com/nodejs/node#release-keys 23 | RUN for key in \ 24 | 4ED778F539E3634C779C87C6D7062848A1AB005C \ 25 | 141F07595B7B3FFE74309A937405533BE57C7D57 \ 26 | 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 27 | 74F12602B6F1C4E913FAA37AD3A89613643B6201 \ 28 | 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ 29 | 61FC681DFB92A079F1685E77973F295594EC4689 \ 30 | 8FCCA13FEF1D0C2E91008E09770F7A9A5AE15600 \ 31 | C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ 32 | 890C08DB8579162FEE0DF9DB8BEAB4DFCF555EF4 \ 33 | C82FA3AE1CBEDC6BE46B9360C43CEC45C17AB93C \ 34 | DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ 35 | A48C2BEE680E841632CD4E44F07496B3EB3C1762 \ 36 | 108F52B48DB57BB0CC439B2997B01419BD92F80A \ 37 | B9E2F5981AA6E0CD28160D9FF13993A75599653C \ 38 | ; do \ 39 | gpg --batch --keyserver hkps://keys.openpgp.org --recv-keys "$key" || \ 40 | gpg --batch --keyserver keyserver.ubuntu.com --recv-keys "$key" ; \ 41 | done \ 42 | && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION.tar.xz" \ 43 | && curl -fsSLO --compressed "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ 44 | && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ 45 | && grep " node-v$NODE_VERSION.tar.xz\$" SHASUMS256.txt | sha256sum -c - 46 | 47 | ADD patch.sh / 48 | ADD patches /patches 49 | 50 | RUN tar -xf "node-v$NODE_VERSION.tar.xz" \ 51 | && cd "node-v$NODE_VERSION" \ 52 | && /patch.sh ${BUILD_ARCH} ${NODE_VERSION} \ 53 | && export TARGET=$(/build.sh target ${BUILD_ARCH:-""}) \ 54 | && export CC=$TARGET-gcc \ 55 | && export CXX=$TARGET-g++ \ 56 | && export AR=$TARGET-ar \ 57 | && export NM=$TARGET-nm \ 58 | && export RANLIB=$TARGET-ranlib \ 59 | && export LINK=$TARGET-g++ \ 60 | && export CXXFLAGS="-O3 -ffunction-sections -fdata-sections" \ 61 | && export LDFLAGS="-Wl,--gc-sections,--strip-all $(/build.sh ld_flags ${BUILD_ARCH:-""})" \ 62 | && ln -snf libc.so /usr/local/$TARGET/lib/ld-musl-*.so.1 \ 63 | && ln -snf /usr/local/$TARGET/lib/ld-musl-*.so.1 /lib \ 64 | && ./configure \ 65 | --partly-static \ 66 | --with-intl=small-icu \ 67 | --without-dtrace \ 68 | --without-inspector \ 69 | --without-etw \ 70 | $(/build.sh node_config ${BUILD_ARCH:-""}) \ 71 | && make -j$(getconf _NPROCESSORS_ONLN) V= 72 | 73 | RUN echo 'node:x:1000:1000:Linux User,,,:/home/node:/bin/sh' > /tmp/passwd 74 | 75 | FROM scratch 76 | 77 | ARG version=0.0.0 78 | 79 | LABEL org.opencontainers.image.source="https://github.com/astefanutti/scratch-node" 80 | 81 | COPY --from=builder node-v$version/out/Release/node /bin/node 82 | COPY --from=builder /lib/ld-musl-*.so.1 /lib/ 83 | COPY --from=builder /tmp/passwd /etc/passwd 84 | 85 | USER node 86 | 87 | ENTRYPOINT ["node"] 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Antonin Stefanutti 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=astefanutti 2 | REPOSITORIES?=quay.io/$(PREFIX) ghcr.io/$(PREFIX) 3 | VERSION?=18.10.0 4 | 5 | # This option is for running docker manifest command 6 | export DOCKER_CLI_EXPERIMENTAL := enabled 7 | 8 | ARCHITECTURES=amd64 arm32v6 arm32v7 arm64v8 9 | 10 | tag-images: 11 | @for arch in $(ARCHITECTURES); do docker tag $(PREFIX)/scratch-node:${VERSION}-$${arch} $(PREFIX)/scratch-node:${TAG}-$${arch}; done 12 | 13 | move-images: 14 | @for repo in $(REPOSITORIES); do \ 15 | for arch in $(ARCHITECTURES); do \ 16 | docker tag $(PREFIX)/scratch-node:${VERSION}-$${arch} $${repo}/scratch-node:${VERSION}-$${arch}; \ 17 | done \ 18 | done 19 | 20 | build-images: 21 | @for arch in $(ARCHITECTURES); do docker build --progress=auto --build-arg version=${VERSION} --build-arg arch=$${arch} -t $(PREFIX)/scratch-node:${VERSION}-$${arch} .; done 22 | 23 | push-images: 24 | @for arch in $(ARCHITECTURES); do docker push $(PREFIX)/scratch-node:${VERSION}-$${arch}; done 25 | 26 | create-manifest: push-images 27 | docker manifest create --amend $(PREFIX)/scratch-node:$(VERSION) $(shell echo $(ARCHITECTURES) | sed -e "s~[^ ]*~$(PREFIX)/scratch-node:$(VERSION)\-&~g") 28 | docker manifest annotate --os linux --arch amd64 $(PREFIX)/scratch-node:${VERSION} $(PREFIX)/scratch-node:${VERSION}-amd64 29 | # TODO: set the CPU features as soon as the CLI exposes a --features option 30 | docker manifest annotate --os linux --arch arm --variant v6 $(PREFIX)/scratch-node:${VERSION} $(PREFIX)/scratch-node:${VERSION}-arm32v6 31 | docker manifest annotate --os linux --arch arm --variant v7 $(PREFIX)/scratch-node:${VERSION} $(PREFIX)/scratch-node:${VERSION}-arm32v7 32 | docker manifest annotate --os linux --arch arm64 --variant v8 $(PREFIX)/scratch-node:${VERSION} $(PREFIX)/scratch-node:${VERSION}-arm64v8 33 | 34 | push-manifest: create-manifest 35 | docker manifest push --purge $(PREFIX)/scratch-node:${VERSION} 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Distroless Node.js Docker Images 2 | 3 | Multi-architecture distroless Node.js Docker images. 4 | 5 | ## Content 6 | 7 | * The Node.js binary, statically linked using [_musl_](https://musl.libc.org), with opt-in support for i18n data 8 | * The _musl_ dynamic linker, to support native modules 9 | * A `/etc/passwd` entry for a `node` user 10 | 11 | ## Images 12 | 13 | Multi-architecture images for `amd64`, `arm32v6`, `arm32v7` and `arm64v8`: 14 | 15 | * `latest`, `18`, `18.10`, `18.10.0` – 18.7 MB / 47.5 MB 16 | * `17`, `17.7`, `17.7.2` – 17.9 MB / 46.4 MB 17 | * `16`, `16.14`, `16.14.2` – 17.1 MB / 43.0 MB 18 | * `15`, `15.14`, `15.14.0` – 16.7 MB / 42.7 MB 19 | * `14`, `14.17`, `14.17.0` – 15.9 MB / 41.7 MB 20 | * `13`, `13.14`, `13.14.0` – 14.8 MB / 39.0 MB 21 | * `12`, `12.22`, `12.22.1` – 15.2 MB / 39.8 MB 22 | * `10`, `10.22`, `10.22.0` – 13.3 MB / 34.1 MB 23 | * `8`, `8.17`, `8.17.0` – 11.2 MB / 30.1 MB 24 | 25 | The image sizes are _compressed_ / _unpacked_. 26 | They are published to the following repositories: 27 | * [docker.io/astefanutti/scratch-node](https://hub.docker.com/r/astefanutti/scratch-node) 28 | * [ghcr.io/astefanutti/scratch-node](https://github.com/users/astefanutti/packages/container/package/scratch-node) 29 | * [quay.io/astefanutti/scratch-node](https://quay.io/repository/astefanutti/scratch-node) 30 | 31 | ## Usage 32 | 33 | ```dockerfile 34 | FROM node as builder 35 | 36 | WORKDIR /app 37 | 38 | COPY package.json package-lock.json index.js ./ 39 | 40 | RUN npm install --prod 41 | 42 | FROM astefanutti/scratch-node 43 | 44 | COPY --from=builder /app / 45 | 46 | ENTRYPOINT ["node", "index.js"] 47 | ``` 48 | 49 | ### Native modules 50 | 51 | Native modules need to be statically compiled with _musl_ to be loadable. 52 | This can easily be achieved by updating the above example with: 53 | 54 | ```dockerfile 55 | FROM node:alpine as builder 56 | 57 | RUN apk update && apk add make g++ python 58 | 59 | WORKDIR /app 60 | 61 | COPY package.json package-lock.json index.js ./ 62 | 63 | RUN LDFLAGS='-static-libgcc -static-libstdc++' npm install --build-from-source= 64 | 65 | FROM astefanutti/scratch-node 66 | 67 | COPY --from=builder /app / 68 | 69 | ENTRYPOINT ["node", "index.js"] 70 | ``` 71 | 72 | ### Internationalization 73 | 74 | The Node binaries are linked against the ICU library statically, and include a subset of ICU data (typically only the English locale) to keep the image sizes small. 75 | Additional locales data can be provided if needed, so that methods work for all ICU locales. 76 | It can be made available to ICU by retrieving the locales data from the ICU sources, e.g.: 77 | 78 | ```dockerfile 79 | FROM alpine as builder 80 | 81 | RUN apk update && apk add curl 82 | 83 | # Note the exact version of icu4c that's compatible depends on the Node version! 84 | RUN curl -Lsq -o icu4c-71_1-src.zip https://github.com/unicode-org/icu/releases/download/release-71-1/icu4c-71_1-src.zip \ 85 | && unzip -q icu4c-71_1-src.zip 86 | 87 | FROM astefanutti/scratch-node:18.10.0 88 | 89 | COPY --from=builder /icu/source/data/in/icudt71l.dat /icu/ 90 | 91 | ENV NODE_ICU_DATA=/icu 92 | ``` 93 | 94 | More information can be found in the [Providing ICU data at runtime](https://nodejs.org/api/intl.html#intl_providing_icu_data_at_runtime) from the Node.js documentation. 95 | 96 | ## Build 97 | 98 | The image can be built by executing the following commands: 99 | 100 | ```console 101 | $ git clone https://github.com/astefanutti/scratch-node 102 | $ cd scratch-node 103 | $ docker build --build-arg version= --build-arg arch= . 104 | ``` 105 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | target() { 6 | local arch="$1" 7 | case "${arch}" in 8 | "x64" | "x86_64" | "amd64" | "") 9 | echo "x86_64-linux-musl" 10 | ;; 11 | "arm32v6") 12 | echo "armv6-linux-musleabihf" 13 | ;; 14 | "arm32v7") 15 | echo "armv7-linux-musleabihf" 16 | ;; 17 | "arm64v8") 18 | echo "aarch64-linux-musl" 19 | ;; 20 | *) 21 | >&2 echo Unsupported architecture: "${arch}" 22 | exit 1 23 | ;; 24 | esac 25 | } 26 | 27 | ld_flags() { 28 | local arch="$1" 29 | case "${arch}" in 30 | "arm32v6") 31 | echo "-Wl,-Bstatic,-latomic,-rpath=/usr/local/armv6-linux-musleabihf/lib" 32 | ;; 33 | "arm32v7") 34 | echo "-Wl,-Bstatic,-latomic,-rpath=/usr/local/armv7-linux-musleabihf/lib" 35 | ;; 36 | "" | *) 37 | echo "" 38 | ;; 39 | esac 40 | } 41 | 42 | gcc_config() { 43 | local arch="$1" 44 | case "${arch}" in 45 | "arm32v6") 46 | echo "--with-arch=armv6zk+fp --with-tune=arm1176jzf-s" 47 | ;; 48 | "arm32v7") 49 | echo "--with-arch=armv7-a+neon-vfpv4 --with-tune=generic-armv7-a" 50 | ;; 51 | "" | *) 52 | echo "" 53 | ;; 54 | esac 55 | } 56 | 57 | config_mak() { 58 | local arch="$1" 59 | cat > $2 <<-EOF 60 | MUSL_VER = 1.2.3 61 | GCC_VER = 11.3.0 62 | BINUTILS_VER = 2.38 63 | GMP_VER = 6.2.1 64 | MPC_VER = 1.2.1 65 | MPFR_VER = 4.1.0 66 | LINUX_VER = 5.15.2 67 | TARGET=$(target ${BUILD_ARCH:-""}) 68 | OUTPUT=/usr/local 69 | GCC_CONFIG=$(gcc_config $arch) --enable-languages=c,c++ 70 | BINUTILS_CONFIG=--enable-gold --enable-lto 71 | EOF 72 | } 73 | 74 | node_config() { 75 | local arch="$1" 76 | case "${arch}" in 77 | "x64" | "x86_64" | "amd64") 78 | echo "--enable-lto" 79 | ;; 80 | "arm32v6") 81 | echo "--with-arm-float-abi=hard --with-arm-fpu=vfp" 82 | ;; 83 | "arm32v7") 84 | echo "--with-arm-float-abi=hard --with-arm-fpu=vfpv3" 85 | ;; 86 | "arm64v8") 87 | echo "--with-arm-float-abi=hard --with-arm-fpu=neon --enable-lto" 88 | ;; 89 | "" | *) 90 | echo "" 91 | ;; 92 | esac 93 | } 94 | 95 | call() { 96 | local func="$1" 97 | shift 98 | ${func} "$@" 99 | } 100 | 101 | call "$@" 102 | -------------------------------------------------------------------------------- /patch.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -eu 4 | 5 | apply() { 6 | local arch="$1" 7 | local version="$2" 8 | 9 | case "${version}" in 10 | 13.10.*) 11 | if [[ "$arch" = "arm32v6" ]] || [[ "$arch" = "arm32v7" ]]; then 12 | patch -p0 < /patches/v8-missing-elf-arm32v6-7.patch 13 | fi 14 | ;; 15 | 15.*.* | 16.*.* | 17.*.* | 18.*.*) 16 | patch -p0 < /patches/v8-cppgc-shared-no-lto.patch 17 | ;; 18 | *) 19 | ;; 20 | esac 21 | } 22 | 23 | apply "$@" 24 | -------------------------------------------------------------------------------- /patches/v8-cppgc-shared-no-lto.patch: -------------------------------------------------------------------------------- 1 | --- tools/v8_gypfiles/v8.gyp 2 | +++ tools/v8_gypfiles/v8.gyp 3 | @@ -1592,9 +1592,9 @@ 4 | }, # v8_bigint 5 | { 6 | 'target_name': 'v8_heap_base', 7 | - 'type': 'none', 8 | + 'type': 'static_library', 9 | 'toolsets': ['host', 'target'], 10 | - 'direct_dependent_settings': { 11 | + # 'direct_dependent_settings': { 12 | 'sources': [ 13 | '<(V8_ROOT)/src/heap/base/active-system-pages.cc', 14 | '<(V8_ROOT)/src/heap/base/stack.cc', 15 | @@ -1678,7 +1678,7 @@ 16 | ], 17 | }], 18 | ], 19 | - }, 20 | + # }, 21 | }, # v8_heap_base 22 | 23 | ############################################################################### 24 | -------------------------------------------------------------------------------- /patches/v8-missing-elf-arm32v6-7.patch: -------------------------------------------------------------------------------- 1 | --- deps/v8/src/base/cpu.orig.cc 2020-03-07 14:44:14.000000000 +0100 2 | +++ deps/v8/src/base/cpu.cc 2020-03-07 14:44:36.000000000 +0100 3 | @@ -17,7 +17,7 @@ 4 | #if V8_OS_QNX 5 | #include // cpuinfo 6 | #endif 7 | -#if (V8_OS_LINUX && V8_HOST_ARCH_PPC) || V8_OS_ANDROID 8 | +#if V8_OS_LINUX || V8_OS_ANDROID 9 | #include 10 | #endif 11 | #if V8_OS_AIX 12 | --------------------------------------------------------------------------------