diff --git a/.github/workflows/build-test-windows.yml b/.github/workflows/build-test-windows.yml new file mode 100644 index 000000000..bd7daf0ee --- /dev/null +++ b/.github/workflows/build-test-windows.yml @@ -0,0 +1,114 @@ +name: build-test-windows + +on: + push: + paths: + - "**/windowsservercore-ltsc2019/**" + - "**/windowsservercore-ltsc2022/**" + - ".github/workflows/build-test-windows.yml" + + pull_request: + paths: + - "**/windowsservercore-ltsc2019/**" + - "**/windowsservercore-ltsc2022/**" + - ".github/workflows/build-test-windows.yml" + +jobs: + build-windows-2019: + name: build-windows-2019 + runs-on: windows-2019 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + version: [ "22.9.0" ] + variant: [ "windowsservercore-ltsc2019" ] + + steps: + - name: Get short node version + uses: actions/github-script@v7 + id: short-version + with: + result-encoding: string + script: return "${{ matrix.version }}".split('.')[0] + + - name: Checkout + uses: actions/checkout@v4 + + # We cannot use docker/build-push-action here because it requires buildx, which is not available on Windows + - name: Build image + run: | + docker build --tag node:${{ matrix.version }}-${{ matrix.variant }} ./${{ steps.short-version.outputs.result }}/${{ matrix.variant }} + + - name: Test for node version + shell: pwsh + run: | + $image_node_version = (docker run --rm node:${{ matrix.version }}-${{ matrix.variant }} node --print "process.versions.node").Trim() + Write-Host "Expected: '${{ matrix.version }}', Got: '$image_node_version'" + if ($image_node_version -ne "${{ matrix.version }}") { + exit 1 + } + + - name: Verify node runs regular files + shell: pwsh + run: | + $tempDir = New-Item -ItemType Directory -Path $env:TEMP -Name "tempNodeApp" + $tmp_file = Join-Path $tempDir "index.js" + "console.log('success')" | Out-File -FilePath $tmp_file -Encoding utf8 + $output = (docker run --rm -w /app --mount "type=bind,src=$tempDir,target=c:\app" node:${{ matrix.version }}-${{ matrix.variant }} node C:/app/index.js) + if ($output -ne 'success') { + exit 1 + } + + - name: Test for npm + run: docker run --rm node:${{ matrix.version }}-${{ matrix.variant }} powershell.exe npm --version + + build-windows-2022: + name: build-windows-2022 + runs-on: windows-2022 + timeout-minutes: 60 + strategy: + fail-fast: false + matrix: + version: [ "22.9.0" ] + variant: [ "windowsservercore-ltsc2022" ] + + steps: + - name: Get short node version + uses: actions/github-script@v7 + id: short-version + with: + result-encoding: string + script: return "${{ matrix.version }}".split('.')[0] + + - name: Checkout + uses: actions/checkout@v4 + + # We cannot use docker/build-push-action here because it requires buildx, which is not available on Windows + - name: Build image + run: | + docker build --tag node:${{ matrix.version }}-${{ matrix.variant }} ./${{ steps.short-version.outputs.result }}/${{ matrix.variant }} + + - name: Test for node version + shell: pwsh + run: | + $image_node_version = (docker run --rm node:${{ matrix.version }}-${{ matrix.variant }} node --print "process.versions.node").Trim() + Write-Host "Expected: '${{ matrix.version }}', Got: '$image_node_version'" + if ($image_node_version -ne "${{ matrix.version }}") { + exit 1 + } + + - name: Verify node runs regular files + shell: pwsh + run: | + $tempDir = New-Item -ItemType Directory -Path $env:TEMP -Name "tempNodeApp" + $tmp_file = Join-Path $tempDir "index.js" + "console.log('success')" | Out-File -FilePath $tmp_file -Encoding utf8 + $output = (docker run --rm -w /app --mount "type=bind,src=$tempDir,target=c:\app" node:${{ matrix.version }}-${{ matrix.variant }} node C:/app/index.js) + if ($output -ne 'success') { + exit 1 + } + + - name: Test for npm + # We need to use powershell.exe to run npm because docker needs to attach to process and npm is a batch file/powershell script + run: docker run --rm node:${{ matrix.version }}-${{ matrix.variant }} powershell.exe npm --version diff --git a/22/windowsservercore-ltsc2019/Dockerfile b/22/windowsservercore-ltsc2019/Dockerfile new file mode 100644 index 000000000..642a660de --- /dev/null +++ b/22/windowsservercore-ltsc2019/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/windows/servercore:ltsc2019 + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# PATH isn't actually set in the Docker image, so we have to set it from within the container +RUN $newPath = ('C:\nodejs;{0};{0}' -f $env:PATH); \ + [Environment]::SetEnvironmentVariable('PATH', $newPath, [EnvironmentVariableTarget]::Machine) +# doing this first to share cache across versions more aggressively + +ENV NODE_VERSION 22.9.0 +ENV NODE_CHECKSUM 8af226c0aa71fefe5228e881f4b5c5d90a8b41c290b96f44f56990d8dc3fac1c + +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \ + Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $env:NODE_CHECKSUM) { Write-Error 'SHA256 mismatch' } ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' ; \ + Remove-Item node.zip -Force ; \ + node --version; \ + npm --version; diff --git a/22/windowsservercore-ltsc2022/Dockerfile b/22/windowsservercore-ltsc2022/Dockerfile new file mode 100644 index 000000000..c6c5365ec --- /dev/null +++ b/22/windowsservercore-ltsc2022/Dockerfile @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/windows/servercore:ltsc2022 + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# PATH isn't actually set in the Docker image, so we have to set it from within the container +RUN $newPath = ('C:\nodejs;{0};{0}' -f $env:PATH); \ + [Environment]::SetEnvironmentVariable('PATH', $newPath, [EnvironmentVariableTarget]::Machine) +# doing this first to share cache across versions more aggressively + +ENV NODE_VERSION 22.9.0 +ENV NODE_CHECKSUM 8af226c0aa71fefe5228e881f4b5c5d90a8b41c290b96f44f56990d8dc3fac1c + +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \ + Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $env:NODE_CHECKSUM) { Write-Error 'SHA256 mismatch' } ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' ; \ + Remove-Item node.zip -Force ; \ + node --version; \ + npm --version; diff --git a/Dockerfile-windows.template b/Dockerfile-windows.template new file mode 100644 index 000000000..05da6f405 --- /dev/null +++ b/Dockerfile-windows.template @@ -0,0 +1,20 @@ +FROM mcr.microsoft.com/windows/servercore:version + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# PATH isn't actually set in the Docker image, so we have to set it from within the container +RUN $newPath = ('C:\nodejs;{0};{0}' -f $env:PATH); \ + [Environment]::SetEnvironmentVariable('PATH', $newPath, [EnvironmentVariableTarget]::Machine) +# doing this first to share cache across versions more aggressively + +ENV NODE_VERSION 0.0.0 +ENV NODE_CHECKSUM CHECKSUM_x64 + +RUN [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 ; \ + Invoke-WebRequest $('https://nodejs.org/dist/v{0}/node-v{0}-win-x64.zip' -f $env:NODE_VERSION) -OutFile 'node.zip' -UseBasicParsing ; \ + if ((Get-FileHash node.zip -Algorithm sha256).Hash -ne $env:NODE_CHECKSUM) { Write-Error 'SHA256 mismatch' } ; \ + Expand-Archive node.zip -DestinationPath C:\ ; \ + Rename-Item -Path $('C:\node-v{0}-win-x64' -f $env:NODE_VERSION) -NewName 'C:\nodejs' ; \ + Remove-Item node.zip -Force ; \ + node --version; \ + npm --version; diff --git a/README.md b/README.md index fc778a31e..e60d03139 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ The official Node.js docker image, made with love by the node community. - [`node:bullseye`](#nodebullseye) - [`node:bookworm`](#nodebookworm) - [`node:slim`](#nodeslim) + - [`node:windowsservercore-ltsc<2019|2022>`](#nodewindowsservercore-ltsc20192022) - [License](#license) - [Supported Docker versions](#supported-docker-versions) - [Supported Node.js versions](#supported-nodejs-versions) @@ -189,12 +190,12 @@ One common issue that may arise is a missing shared library required for use of `process.dlopen`. To add the missing shared libraries to your image: - For Alpine v3.18 and earlier, adding the -[`libc6-compat`](https://pkgs.alpinelinux.org/package/v3.18/main/x86/libc6-compat) -package in your Dockerfile is recommended: `apk add --no-cache libc6-compat` + [`libc6-compat`](https://pkgs.alpinelinux.org/package/v3.18/main/x86/libc6-compat) + package in your Dockerfile is recommended: `apk add --no-cache libc6-compat` - Starting from Alpine v3.19, you can use the -[`gcompat`](https://pkgs.alpinelinux.org/package/v3.19/main/x86/gcompat) package -to add the missing shared libraries: `apk add --no-cache gcompat` + [`gcompat`](https://pkgs.alpinelinux.org/package/v3.19/main/x86/gcompat) package + to add the missing shared libraries: `apk add --no-cache gcompat` To minimize image size, it's uncommon for additional related tools (such as `git` or `bash`) to be included in Alpine-based images. Using this @@ -224,6 +225,16 @@ in an environment where *only* the Node.js image will be deployed and you have space constraints, we highly recommend using the default image of this repository. +### `node:windowsservercore-ltsc<2019|2022>` + +This image is based on Windows Server Core and is the recommended image +for users who require Windows-based environments. +It is available in two versions:`node:windowsservercore-ltsc2019` and `node:windowsservercore-ltsc2022`. +You can run this image on Windows Server 2019 or Windows Server 2022 or on Windows desktop versions +that support Windows containers. +Keep in mind that these images are significantly larger than the Linux-based +variants due to the Windows Server Core base. + ## License [License information](https://github.com/nodejs/node/blob/master/LICENSE) for diff --git a/architectures b/architectures index 1cb4bf352..c2b3c0213 100644 --- a/architectures +++ b/architectures @@ -1,5 +1,5 @@ bashbrew-arch variants -amd64 alpine3.19,alpine3.20,bookworm,bookworm-slim,bullseye,bullseye-slim +amd64 alpine3.19,alpine3.20,bookworm,bookworm-slim,bullseye,bullseye-slim,windowsservercore-ltsc2019,windowsservercore-ltsc2022 arm32v6 alpine3.19,alpine3.20 arm32v7 alpine3.19,alpine3.20,bookworm,bookworm-slim,bullseye,bullseye-slim arm64v8 alpine3.19,alpine3.20,bookworm,bookworm-slim,bullseye,bullseye-slim diff --git a/functions.sh b/functions.sh index bee3dafe0..c2e861b20 100755 --- a/functions.sh +++ b/functions.sh @@ -193,6 +193,16 @@ function is_debian_slim() { return 1 } +function is_windows() { + local variant + variant=$1 + shift + + if [ "${variant}" = "${variant#windows}" ]; then + return 1 + fi +} + function get_fork_name() { local version version=$1 diff --git a/genMatrix.js b/genMatrix.js index 9f57ea509..30f1588e5 100644 --- a/genMatrix.js +++ b/genMatrix.js @@ -8,23 +8,25 @@ const testFiles = [ ]; const nodeDirRegex = /^\d+$/; +// Directories starting with 'windowsservercore-' are excluded from the matrix windows-2019 are excluded for example +const windowsDirRegex = /^windowsservercore-/; const areTestFilesChanged = (changedFiles) => changedFiles .some((file) => testFiles.includes(file)); -// Returns a list of the child directories in the given path +// Returns a list of the child directories in the given path, excluding those starting with 'windows-' const getChildDirectories = (parent) => fs.readdirSync(parent, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) + .filter((directory) => directory.isDirectory()) .map(({ name }) => path.resolve(parent, name)); -const getNodeVerionDirs = (base) => getChildDirectories(base) +const getNodeVersionDirs = (base) => getChildDirectories(base) .filter((childPath) => nodeDirRegex.test(path.basename(childPath))); // Returns the paths of Dockerfiles that are at: base/*/Dockerfile const getDockerfilesInChildDirs = (base) => getChildDirectories(base) .map((childDir) => path.resolve(childDir, 'Dockerfile')); -const getAllDockerfiles = (base) => getNodeVerionDirs(base).flatMap(getDockerfilesInChildDirs); +const getAllDockerfiles = (base) => getNodeVersionDirs(base).flatMap(getDockerfilesInChildDirs); const getAffectedDockerfiles = (filesAdded, filesModified, filesRenamed) => { const files = [ @@ -69,7 +71,8 @@ const getDockerfileMatrixEntry = (file) => { const generateBuildMatrix = (filesAdded, filesModified, filesRenamed) => { const dockerfiles = [...new Set(getAffectedDockerfiles(filesAdded, filesModified, filesRenamed))]; - const entries = dockerfiles.map(getDockerfileMatrixEntry); + let entries = dockerfiles.map(getDockerfileMatrixEntry); + entries = entries.filter((entry) => !windowsDirRegex.test(entry.variant)); // Return null if there are no entries so we can skip the matrix step return entries.length diff --git a/update.sh b/update.sh index 0b6aaf69d..bbdd4e34e 100755 --- a/update.sh +++ b/update.sh @@ -5,22 +5,24 @@ set -ue function usage() { cat << EOF - Update the node docker images. + Update the node Docker images. Usage: - $0 [-s] [MAJOR_VERSION(S)] [VARIANT(S)] + $0 [-s] [-w] [MAJOR_VERSION(S)] [VARIANT(S)] Examples: - - update.sh # Update all images - - update.sh -s # Update all images, skip updating Alpine and Yarn - - update.sh 8,10 # Update all variants of version 8 and 10 - - update.sh -s 8 # Update version 8 and variants, skip updating Alpine and Yarn - - update.sh 8 alpine # Update only alpine's variants for version 8 - - update.sh -s 8 bullseye # Update only bullseye variant for version 8, skip updating Alpine and Yarn - - update.sh . alpine # Update the alpine variant for all versions + - update.sh # Update all images + - update.sh -s # Update all images, skip updating Alpine and Yarn + - update.sh -w # Update only Windows images + - update.sh 8,10 # Update all variants of version 8 and 10 + - update.sh -s 8 # Update version 8 and variants, skip updating Alpine and Yarn + - update.sh 8 alpine # Update only Alpine variants for version 8 + - update.sh -w 8 windowsservercore-2022 # Update only Windows Server Core 2022 variant for version 8 + - update.sh . alpine # Update the Alpine variant for all versions OPTIONS: - -s Security update; skip updating the yarn and alpine versions. + -s Security update; skip updating the Yarn and Alpine versions. + -w Windows images update only -b CI config update only -h Show this message @@ -28,12 +30,17 @@ EOF } SKIP=false -while getopts "sh" opt; do +WINDOWS_ONLY=false +while getopts "swh" opt; do case "${opt}" in s) SKIP=true shift ;; + w) + WINDOWS_ONLY=true + shift + ;; h) usage exit @@ -70,6 +77,10 @@ if [ "${SKIP}" != true ]; then yarnVersion="$(curl -sSL --compressed https://yarnpkg.com/latest-version)" fi +if [ "${WINDOWS_ONLY}" = true ]; then + echo "Updating Windows images only..." +fi + function in_versions_to_update() { local version=$1 @@ -122,6 +133,10 @@ function update_node_version() { shift fi + if [ "${WINDOWS_ONLY}" = true ] && ! is_windows "${variant}"; then + return + fi + fullVersion="$(curl -sSL --compressed "${baseuri}" | grep ' /dev/null; then @@ -223,9 +262,15 @@ for version in "${versions[@]}"; do template_file="${parentpath}/Dockerfile-slim.template" elif is_alpine "${variant}"; then template_file="${parentpath}/Dockerfile-alpine.template" + elif is_windows "${variant}"; then + template_file="${parentpath}/Dockerfile-windows.template" + fi + + # Copy .sh only if not is_windows + if ! is_windows "${variant}"; then + cp "${parentpath}/docker-entrypoint.sh" "${version}/${variant}/docker-entrypoint.sh" fi - cp "${parentpath}/docker-entrypoint.sh" "${version}/${variant}/docker-entrypoint.sh" if [ "${update_version}" -eq 0 ] && [ "${update_variant}" -eq 0 ]; then update_node_version "${baseuri}" "${versionnum}" "${template_file}" "${version}/${variant}/Dockerfile" "${variant}" & pids+=($!)