diff --git a/.github/workflows/dev.yml b/.github/workflows/dev.yml index 0ad32e7..4ba827a 100644 --- a/.github/workflows/dev.yml +++ b/.github/workflows/dev.yml @@ -30,6 +30,24 @@ jobs: with: file: ./VERSION id: version + - + name: Read Node.js version - minor + uses: bfren/read-file@v2 + with: + file: ./${{ matrix.node }}/overlay/tmp/NODE_MINOR + id: node_minor + - + name: Read image version + uses: bfren/read-file@v2 + with: + file: ./VERSION_MAJOR + id: version_major + - + name: Read image version + uses: bfren/read-file@v2 + with: + file: ./VERSION_MINOR + id: version_minor - name: Set up QEMU uses: docker/setup-qemu-action@v4 @@ -79,9 +97,35 @@ jobs: platforms: linux/amd64,linux/arm64 tags: | bfren/node:node${{ matrix.node }}-dev + bfren/node:node${{ matrix.node }}-${{ steps.version_major.outputs.contents }}-dev + bfren/node:node${{ matrix.node }}-${{ steps.version_minor.outputs.contents }}-dev bfren/node:node${{ matrix.node }}-${{ steps.version.outputs.contents }}-dev + bfren/node:node${{ steps.node_minor.outputs.contents }}-dev + bfren/node:node${{ steps.node_minor.outputs.contents }}-${{ steps.version.outputs.contents }}-dev quay.io/bfren/node:node${{ matrix.node }}-dev + quay.io/bfren/node:node${{ matrix.node }}-${{ steps.version_major.outputs.contents }}-dev + quay.io/bfren/node:node${{ matrix.node }}-${{ steps.version_minor.outputs.contents }}-dev quay.io/bfren/node:node${{ matrix.node }}-${{ steps.version.outputs.contents }}-dev + quay.io/bfren/node:node${{ steps.node_minor.outputs.contents }}-dev + quay.io/bfren/node:node${{ steps.node_minor.outputs.contents }}-${{ steps.version.outputs.contents }}-dev + - + name: Build and push default + id: docker_build_default + if: matrix.node == '24' + uses: docker/build-push-action@v7 + with: + context: . + file: ./${{ matrix.node }}/Dockerfile + build-args: | + BF_IMAGE=${{ env.REPOSITORY_NAME }} + BF_VERSION=${{ steps.version.outputs.contents }} + push: ${{ startsWith(github.ref, 'refs/heads/') }} + platforms: linux/amd64,linux/arm64 + tags: | + bfren/node:dev + bfren/node:${{ steps.version.outputs.contents }}-dev + quay.io/bfren/node:dev + quay.io/bfren/node:${{ steps.version.outputs.contents }}-dev - name: Image digest run: echo ${{ steps.docker_build.outputs.digest }} diff --git a/16/Dockerfile b/16/Dockerfile index 4195c86..6b8fa23 100644 --- a/16/Dockerfile +++ b/16/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.16-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.16-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/17/Dockerfile b/17/Dockerfile index fcd08c2..6c86c69 100644 --- a/17/Dockerfile +++ b/17/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.15-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.15-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/18/Dockerfile b/18/Dockerfile index 9066f16..20914d8 100644 --- a/18/Dockerfile +++ b/18/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.18-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.18-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/19/Dockerfile b/19/Dockerfile index 382ae97..7c07921 100644 --- a/19/Dockerfile +++ b/19/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.17-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.17-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/20/Dockerfile b/20/Dockerfile index 87c4a0e..3d953b2 100644 --- a/20/Dockerfile +++ b/20/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.20-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.20-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/21/Dockerfile b/21/Dockerfile index 9f203a4..e280d86 100644 --- a/21/Dockerfile +++ b/21/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.20-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.20-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/22/Dockerfile b/22/Dockerfile index aa19ace..f97c54e 100644 --- a/22/Dockerfile +++ b/22/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.22-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.22-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/23/Dockerfile b/23/Dockerfile index baccc21..1562c76 100644 --- a/23/Dockerfile +++ b/23/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.22-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.22-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/24/Dockerfile b/24/Dockerfile index 95d8605..b1b7f2e 100644 --- a/24/Dockerfile +++ b/24/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.24-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.24-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/26/Dockerfile b/26/Dockerfile index 6f1f30f..0a43455 100644 --- a/26/Dockerfile +++ b/26/Dockerfile @@ -1,4 +1,4 @@ -FROM quay.io/bfren/alpine-s6:alpine3.24-6.5.0 +FROM quay.io/bfren/alpine-s6:alpine3.24-6.6.0 LABEL org.opencontainers.image.source="https://github.com/bfren/docker-node" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a13010f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@../index/CLAUDE.md diff --git a/README.md b/README.md index b190b4e..d781021 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,146 @@ -# Docker Node.js +# Node.js ![GitHub release (latest by date)](https://img.shields.io/github/v/release/bfren/docker-node) ![Docker Pulls](https://img.shields.io/endpoint?url=https%3A%2F%2Fbfren.dev%2Fdocker%2Fpulls%2Fnode) ![Docker Image Size](https://img.shields.io/endpoint?url=https%3A%2F%2Fbfren.dev%2Fdocker%2Fsize%2Fnode) ![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/bfren/docker-node/dev.yml?branch=main) -[Docker Repository](https://hub.docker.com/r/bfren/nginx-webdav) - [bfren ecosystem](https://github.com/bfren/docker) +[Docker Hub](https://hub.docker.com/r/bfren/node) - [Quay.io](https://quay.io/repository/bfren/node) - [bfren ecosystem](https://github.com/bfren/docker) -[Node.js](https://nodejs.org), with `npm` also installed. +## Introduction -## Contents +[Node.js](https://nodejs.org) and `npm`, running your app as a supervised service. It is built on [Alpine + S6](https://github.com/bfren/docker-alpine-s6), with Node.js from Alpine's packages - each Node.js version uses the newest Alpine release that has it. -* [Ports](#ports) -* [Licence / Copyright](#licence) +The image runs `/app/live/index.js` as the `node` user. Use it as the base for a Node.js app's image (as [Send](https://github.com/bfren/docker-send) does), or mount a ready-to-run app into it. -## Ports +## Quickstart -* 3000 +With your app (including `node_modules`) in `./app`, and `index.js` listening on port 3000: + +```yaml +services: + app: + image: quay.io/bfren/node:node24-4.2 + restart: unless-stopped + ports: + - "127.0.0.1:3000:3000" + volumes: + - ./app:/app/live +``` + +Run `docker compose up -d`, then open `http://127.0.0.1:3000`. Without your own app, a welcome page is shown. + +Your app must listen on `0.0.0.0` (all interfaces), not `127.0.0.1` - otherwise it can't be reached through the published port. + +## Features + +* ๐ŸŸฉ Node.js 16 to 26 - choose with the image tag +* ๐Ÿ“ฆ `npm` installed +* ๐Ÿงญ Your app runs under S6 supervision as `node` (UID 1000), not root +* ๐Ÿ›‘ The container stops if your app crashes, so Docker can restart it +* ๐Ÿ” `/app/live` is made read-only for the app on every start +* ๐Ÿงช Built-in tests + +## Tags + +Images are published to Docker Hub (`bfren/node`) and Quay.io (`quay.io/bfren/node`) for `linux/amd64` and `linux/arm64`. + +| Tag | Example | Use when | +| ----------------- | ----------------- | ----------------------------------------------- | +| `nodeN` | `node24` | You want the latest image for a Node.js version | +| `nodeN-MAJOR` | `node24-4` | You want updates, but not breaking changes | +| `nodeN-MINOR` | `node24-4.2` | You want fixes only - **recommended** | +| `nodeN-VERSION` | `node24-4.2.6` | You want to pin this image exactly | +| `nodeN.M` | `node24.18` | You want an exact Node.js release | +| `nodeN.M-VERSION` | `node24.18-4.2.6` | You want to pin both | +| `latest`, `4.2.6` | | As above, on Node.js 24 | + +| Node.js | Release | Alpine | Security support ends | +| ------- | ------- | ------ | --------------------- | +| 26 | 26.5.1 | 3.24 | 2029-04-30 (LTS) | +| 24 | 24.18.1 | 3.24 | 2028-04-30 (LTS) | +| 23 | 23.11.1 | 3.22 | ended 2025-06-01 | +| 22 | 22.23.2 | 3.22 | 2027-04-30 (LTS) | +| 21 | 21.7.3 | 3.20 | ended 2024-06-01 | +| 20 | 20.15.1 | 3.20 | ended 2026-04-30 | +| 19 | 19.7.0 | 3.17 | ended 2023-06-01 | +| 18 | 18.20.1 | 3.18 | ended 2025-04-30 | +| 17 | 17.9.0 | 3.15 | ended 2022-06-01 | +| 16 | 16.20.2 | 3.16 | ended 2023-09-11 | + +Use an LTS version for anything long-lived - see the [Node.js release schedule](https://nodejs.org/en/about/previous-releases). Images for versions that have ended are still built, but get no Node.js fixes. + +## Configuration + +### Ports + +| Port | Purpose | +| ------ | ----------------------------------------------------- | +| `3000` | Used by the welcome page - your app can use any port. | + +### Volumes + +| Volume | Purpose | +| ----------- | -------------------------------------- | +| `/app/live` | Your app - `index.js` is run on start. | + +### Environment Variables + +This image adds none of its own. See [Alpine + S6](https://github.com/bfren/docker-alpine-s6#environment-variables) for `BF_DEBUG`, `BF_TZ` and the other base image variables. Your app can read any variables you pass to the container. + +## How It Works + +On every start the Node.js and `npm` versions are written to the log, and everything in `/app/live` is given to the `node` user and made read-only (`0400` files, `0500` folders). Then `node /app/live/index.js` runs under S6 as `node`. + +If your app crashes (exits with an error), the container stops - set a `restart` policy so Docker starts it again. If it exits cleanly, S6 starts it again inside the container. + +The health check is the base image's, which checks cron is running - it doesn't check your app. To check your app, replace `/usr/bin/bf/bf-healthcheck` in your own image. + +## Building on this image + +Copy your app into `/app/live` and install its dependencies when the image is built: + +```dockerfile +FROM quay.io/bfren/node:node24-4.2 + +COPY ./app /app/live +RUN cd /app/live && npm ci --omit=dev +``` + +`index.js` must be the entry point. If your app's entry point is elsewhere, add an `index.js` that loads it, e.g. `require('./dist/server.js');`. + +## Testing + +```sh +./test.nu # builds Node.js 24 and runs /test +./test.nu 20 # or any version directory +./test.nu --all # every version, in parallel +./run.sh 24 # builds and opens a shell +./generate-dockerfiles.sh # regenerates the Dockerfiles from Dockerfile.esh +``` + +`/test` runs this image's tests alongside the base images' tests: the app paths, the welcome page (valid JavaScript, listening on all interfaces), the installed Node.js and `npm` versions, and the service scripts. + +## Troubleshooting + +### The container stops straight after starting + +Your app crashed. Check `docker logs ` for its error - often a missing `node_modules` folder, or no `/app/live/index.js`. + +### My app can't write files (`EACCES`) + +`/app/live` is read-only on purpose. Write data to another folder, and mount a volume there. + +### The port is published but nothing answers + +Your app is probably listening on `127.0.0.1` or `localhost` - change it to `0.0.0.0`. + +## FAQs + +### How do I run `npm install` in the container? + +`docker exec sh -c 'cd /app/live && npm install'` works, but installing when the image is built (see [Building on this image](#building-on-this-image)) is more reliable, and the new files are only given to `node` on the next start. + +### Can I use a different entry file? + +Not directly - `/app/live/index.js` is always run. Make it load your real entry point. ## Licence diff --git a/VERSION b/VERSION index ad35fe0..0ce756d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -4.2.5 \ No newline at end of file +4.2.6 \ No newline at end of file diff --git a/generate-dockerfiles.sh b/generate-dockerfiles.sh index 8391949..c60520a 100755 --- a/generate-dockerfiles.sh +++ b/generate-dockerfiles.sh @@ -4,7 +4,7 @@ set -euo pipefail docker pull bfren/alpine -BASE_VERSION="6.5.0" +BASE_VERSION="6.6.0" echo "Base: ${BASE_VERSION}" NODE_VERSIONS="16 17 18 19 20 21 22 23 24 26" diff --git a/overlay/app/live/index.js b/overlay/app/live/index.js index 0068b50..c305369 100644 --- a/overlay/app/live/index.js +++ b/overlay/app/live/index.js @@ -1,8 +1,8 @@ // Import http module const http = require('http'); -// Hosting values -const hostname = '127.0.0.1'; +// Hosting values - listen on all interfaces so the port can be published +const hostname = '0.0.0.0'; const port = 3000; // Create server diff --git a/overlay/etc/bf/init.d/10-env.nu b/overlay/etc/bf/init.d/10-env.nu index 989847d..344a6c7 100644 --- a/overlay/etc/bf/init.d/10-env.nu +++ b/overlay/etc/bf/init.d/10-env.nu @@ -1,17 +1,15 @@ use bf +use bf-node bf env load # Set environment variables def main [] { - # app directories - let app = "/app" - let app_live = $"($app)/live" - bf env set NODE_APP $app - bf env set NODE_APP_LIVE $app_live - bf env set NODE_APP_PUBLISH $"($app)/publish" - - # run file - bf env set NODE_RUN $"($app_live)/index.js" + # app directories and run file + let paths = bf-node app paths + bf env set NODE_APP $paths.app + bf env set NODE_APP_LIVE $paths.live + bf env set NODE_APP_PUBLISH $paths.publish + bf env set NODE_RUN $paths.run # return nothing return diff --git a/overlay/etc/nu/scripts/bf-node/app.nu b/overlay/etc/nu/scripts/bf-node/app.nu new file mode 100644 index 0000000..ccd5728 --- /dev/null +++ b/overlay/etc/nu/scripts/bf-node/app.nu @@ -0,0 +1,11 @@ +# Paths used to run the Node.js app +export def paths []: nothing -> record { + let app = "/app" + let live = $"($app)/live" + { + app: $app + live: $live + publish: $"($app)/publish" + run: $"($live)/index.js" + } +} diff --git a/overlay/etc/nu/scripts/bf-node/mod.nu b/overlay/etc/nu/scripts/bf-node/mod.nu index e69de29..4235b35 100644 --- a/overlay/etc/nu/scripts/bf-node/mod.nu +++ b/overlay/etc/nu/scripts/bf-node/mod.nu @@ -0,0 +1 @@ +export use app.nu diff --git a/overlay/etc/nu/scripts/tests/app.nu b/overlay/etc/nu/scripts/tests/app.nu new file mode 100644 index 0000000..14d8811 --- /dev/null +++ b/overlay/etc/nu/scripts/tests/app.nu @@ -0,0 +1,38 @@ +use std/assert +use bf-node + + +#====================================================================================================================== +# paths +#====================================================================================================================== + +export def paths__run_file_is_in_live_directory [] { + let paths = bf-node app paths + + assert equal "/app/live" $paths.live + assert equal "/app/live/index.js" $paths.run + assert ($paths.run | str starts-with $paths.live) +} + + +#====================================================================================================================== +# default app +#====================================================================================================================== + +export def default_app__exists [] { + assert ((bf-node app paths).run | path exists) +} + +export def default_app__listens_on_all_interfaces [] { + # listening on 127.0.0.1 would make the published port unreachable + let source = open --raw (bf-node app paths).run + + assert ($source =~ "hostname = '0.0.0.0'") + assert ($source =~ "port = 3000") +} + +export def default_app__is_valid_javascript [] { + let result = ^node --check (bf-node app paths).run | complete + + assert equal 0 $result.exit_code $result.stderr +} diff --git a/overlay/etc/nu/scripts/tests/image.nu b/overlay/etc/nu/scripts/tests/image.nu new file mode 100644 index 0000000..30ef691 --- /dev/null +++ b/overlay/etc/nu/scripts/tests/image.nu @@ -0,0 +1,43 @@ +use std/assert +use bf + + +#====================================================================================================================== +# installation +#====================================================================================================================== + +export def install__node_user_has_uid_1000 [] { + assert equal "1000" (^id -u node | str trim) + assert equal "1000" (^id -g node | str trim) +} + +export def install__node_matches_build [] { + let build = with-env {BF_ETC: "/etc/bf"} { bf build | get "Node.js" } + let installed = ^node --version | str trim | str replace "v" "" + + assert ($build | str starts-with $installed) $"Node.js ($installed) should match the build ($build)" +} + +export def install__npm_matches_build [] { + let build = with-env {BF_ETC: "/etc/bf"} { bf build | get NPM } + let installed = ^npm --version | str trim + + assert ($build | str starts-with $installed) $"NPM ($installed) should match the build ($build)" +} + +export def install__versions_are_saved_for_startup [] { + assert ((open --raw /etc/bf/INIT_VERSIONS) =~ '^Node\.js v\d+\.\d+\.\d+ installed with NPM \d+\.\d+\.\d+\.$') +} + + +#====================================================================================================================== +# service +#====================================================================================================================== + +export def service__scripts_parse [] { + for x in [run finish] { + let path = $"/etc/s6-overlay/s6-rc.d/node/($x)" + let result = ^$nu.current-exe --no-config-file --commands $"nu-check ($path)" | complete + assert equal "true" ($result.stdout | str trim) $"($x) does not parse: ($result.stderr)" + } +} diff --git a/overlay/etc/nu/scripts/tests/mod.nu b/overlay/etc/nu/scripts/tests/mod.nu index e69de29..4bfcb6a 100644 --- a/overlay/etc/nu/scripts/tests/mod.nu +++ b/overlay/etc/nu/scripts/tests/mod.nu @@ -0,0 +1,2 @@ +export use app.nu +export use image.nu diff --git a/pull.sh b/pull.sh index 108e1db..95298cc 100644 --- a/pull.sh +++ b/pull.sh @@ -6,4 +6,4 @@ if [ -n "${1-}" ] ; then git checkout ${1} && git pull fi -chmod +x pull.sh run.sh test.sh +chmod +x pull.sh run.sh test.nu diff --git a/test.nu b/test.nu new file mode 100755 index 0000000..9bba8a2 --- /dev/null +++ b/test.nu @@ -0,0 +1,25 @@ +#!/usr/bin/env -S nu -n + +use ../index/test.nu + +const node_branches = [ + "16" + "17" + "18" + "19" + "20" + "21" + "22" + "23" + "24" + "26" +] + +# Run test suite for image +def main [ + node: string = "24" # define which Node.js version to use + --all (-a) # test all registered versions of Node.js (overrides $node) +] { + let branches = if $all { $node_branches } else { [$node] } + test "node" $branches +} diff --git a/test.sh b/test.sh deleted file mode 100644 index 97acc2a..0000000 --- a/test.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -IMAGE=node -VERSION=`cat VERSION` -NODE=${1:-24} -TAG=${IMAGE}-test - -docker buildx build \ - --load \ - --build-arg BF_IMAGE=${IMAGE} \ - --build-arg BF_VERSION=${VERSION} \ - -f ${NODE}/Dockerfile \ - -t ${TAG} \ - . \ - && \ - docker run --entrypoint /test ${TAG}