Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9414dcb
Merge pull request #240 from jonfairbanks/develop
jonfairbanks Jul 10, 2026
c766bf3
Enable Dependabot auto-merge (#244)
jonfairbanks Jul 10, 2026
6aa3628
Bump azure/setup-helm from 4 to 5 (#241)
dependabot[bot] Jul 10, 2026
e15614c
Bump actions/checkout from 6 to 7 (#242)
dependabot[bot] Jul 10, 2026
abcb3b9
Publish Helm chart to GHCR (#245)
jonfairbanks Jul 10, 2026
43d05f4
Fix Helm chart non-root runtime (#246)
jonfairbanks Jul 10, 2026
9b26aa2
Harden container runtime and CI validation (#247)
jonfairbanks Jul 10, 2026
d8a22c8
Scope container scan to OS dependencies (#249)
jonfairbanks Jul 10, 2026
017e0a8
Bump eslint from 10.6.0 to 10.7.0 in the npm group (#250)
dependabot[bot] Jul 17, 2026
a18b35a
Bump actions/setup-node from 6 to 7 (#251)
dependabot[bot] Jul 17, 2026
8b65f28
Bump actions/checkout from 7.0.0 to 7.0.1 (#252)
dependabot[bot] Jul 24, 2026
cacc6e5
Bump docker/login-action from 4.4.0 to 4.5.0 (#253)
dependabot[bot] Jul 24, 2026
eb6c52e
Update brace-expansion lockfile (#258)
jonfairbanks Aug 1, 2026
ca5ed82
Bump docker/login-action from 4.5.0 to 4.6.0 (#256)
dependabot[bot] Aug 1, 2026
fe492b1
Bump the npm group across 1 directory with 2 updates (#254)
dependabot[bot] Aug 1, 2026
bd035ba
Bump github/codeql-action from 4 to 4.37.3 (#257)
dependabot[bot] Aug 1, 2026
821a063
Upgrade runtime to Node 26 (#259)
jonfairbanks Aug 1, 2026
6286b9c
Bump globals from 17.8.0 to 17.9.0 in the npm group (#260)
dependabot[bot] Aug 7, 2026
ff9aeb9
Bump github/codeql-action from 4.37.3 to 4.37.6 (#261)
dependabot[bot] Aug 7, 2026
b9033e9
Fix ingress URL in Helm notes (#262)
jonfairbanks Aug 9, 2026
d938f32
Configure timezone for Helm workloads (#263)
jonfairbanks Aug 9, 2026
0653d1c
Bump eslint from 10.8.0 to 10.8.1 in the npm group (#264)
dependabot[bot] Aug 14, 2026
821f274
Lower HPA minimum replicas (#266)
jonfairbanks Aug 20, 2026
444c790
Update Helm chart resource defaults (#267)
jonfairbanks Aug 20, 2026
0d6c86b
Bump the npm group with 2 updates (#268)
dependabot[bot] Aug 21, 2026
fe9f802
Bump Helm chart to 3.0.5 (#270)
jonfairbanks Aug 21, 2026
eaa4b6d
Restrict Dependabot auto-merge to non-major updates (#271)
jonfairbanks Aug 25, 2026
6b8ccf6
Bump github/codeql-action from 4.37.6 to 4.37.8 (#269)
dependabot[bot] Aug 25, 2026
e2a6e0d
Lower default memory request
jonfairbanks Aug 26, 2026
97bec0d
Merge pull request #272 from jonfairbanks/fix/lower-docker-node-app-m…
jonfairbanks Aug 26, 2026
cef28c8
Harden GitHub Actions workflows
jonfairbanks Aug 26, 2026
872a958
Restore optional Snyk configuration
jonfairbanks Aug 26, 2026
9d1fc4d
Merge pull request #273 from jonfairbanks/fix/pin-actions-and-enforce…
jonfairbanks Aug 26, 2026
b169ffd
Tune Docker Node App readiness delay
jonfairbanks Aug 26, 2026
89357e2
Merge branch 'master' into feature/probe-tuning
jonfairbanks Aug 26, 2026
c8d85cd
Remove readiness delay comment
jonfairbanks Aug 26, 2026
4e49191
Merge pull request #274 from jonfairbanks/feature/probe-tuning
jonfairbanks Aug 26, 2026
f1a5047
Add shutdown readiness handling
jonfairbanks Aug 27, 2026
5a40844
Bump chart version to 3.0.8
jonfairbanks Aug 27, 2026
4617600
Merge pull request #275 from jonfairbanks/feature/graceful-shutdown-r…
jonfairbanks Aug 27, 2026
0ae24aa
Deploy Docker Node App with immutable image tags
jonfairbanks Aug 27, 2026
68da8a9
Merge pull request #276 from jonfairbanks/fix/docker-node-app-image-r…
jonfairbanks Aug 27, 2026
320b16f
Restore version-tagged Docker Node App releases
jonfairbanks Aug 27, 2026
64f038d
Merge pull request #277 from jonfairbanks/fix/revert-image-sha-packaging
jonfairbanks Aug 27, 2026
e5f337a
Bump eslint from 10.8.1 to 10.9.1 in the npm group (#278)
dependabot[bot] Aug 28, 2026
e3f7f59
Bump docker/setup-buildx-action from 4.2.0 to 4.3.0 (#279)
dependabot[bot] Aug 31, 2026
443ba4f
fix: dispatch publish after dependabot merge (#280)
jonfairbanks Aug 31, 2026
db12ea5
Bump qs from 6.15.3 to 6.16.0 (#281)
dependabot[bot] Sep 3, 2026
5ced862
Bump docker/setup-qemu-action from 4.2.0 to 4.3.0 (#283)
dependabot[bot] Sep 4, 2026
91ae2e8
Bump github/codeql-action/init from 4.37.8 to 4.37.9 (#284)
dependabot[bot] Sep 4, 2026
12eab7b
Bump github/codeql-action/analyze from 4.37.8 to 4.37.9 (#282)
dependabot[bot] Sep 4, 2026
cbfe038
Add configurable HPA behavior (#285)
jonfairbanks Sep 10, 2026
177ecbf
Bump the npm group with 2 updates (#286)
dependabot[bot] Sep 11, 2026
a8d536a
Bump docker/build-push-action from 7.3.0 to 7.4.0 (#287)
dependabot[bot] Sep 18, 2026
b59178a
Bump docker/setup-qemu-action from 4.3.0 to 4.4.0 (#288)
dependabot[bot] Sep 18, 2026
894dbf5
Bump docker/setup-buildx-action from 4.3.0 to 4.4.0 (#290)
dependabot[bot] Sep 18, 2026
ddbb934
Bump github/codeql-action/analyze from 4.37.9 to 4.38.0 (#291)
dependabot[bot] Sep 18, 2026
a2495e1
Bump github/codeql-action/init from 4.37.9 to 4.38.0 (#289)
dependabot[bot] Sep 18, 2026
6ae5e45
Configure npm Minimum Release Age and Patch proxy-addr (#292)
jonfairbanks Sep 19, 2026
01043ba
Bump github/codeql-action/analyze from 4.38.0 to 4.38.1 (#293)
dependabot[bot] Sep 25, 2026
1be395e
Bump github/codeql-action/init from 4.38.0 to 4.38.1 (#294)
dependabot[bot] Sep 28, 2026
07f9860
Fix Dependabot publication dispatch (#295)
jonfairbanks Sep 30, 2026
a6f93cf
Bump brace-expansion from 5.0.9 to 5.0.12 (#296)
dependabot[bot] Sep 30, 2026
7fe5d9d
Enforce Production Security and Release Gates
jonfairbanks Sep 30, 2026
2c11005
Explain Release Blocking When the Branch Advances
jonfairbanks Sep 30, 2026
58d2a30
Merge pull request #299 from jonfairbanks/fix/enforce-production-secu…
jonfairbanks Sep 30, 2026
2270662
Sync Develop With Current Production and Dependency Fixes
jonfairbanks Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 21 additions & 13 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ jobs:
strategy:
fail-fast: false
matrix:
node-version: [22, 24]
node-version: [26]
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
Expand All @@ -35,10 +35,10 @@ jobs:
name: Lint and audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24
node-version: 26
cache: npm
- run: npm ci
- run: npm run lint
Expand All @@ -48,22 +48,30 @@ jobs:
name: Helm lint and render
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: azure/setup-helm@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
- run: helm lint chart
- run: helm template docker-node-app chart --set autoscaling.enabled=true --set ingress.enabled=true

container:
name: Build container
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4.4.0
- name: Build test target
uses: docker/build-push-action@v7
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
target: test
push: false
cache-from: type=gha
cache-to: type=gha,mode=max
cache-from: type=gha,scope=test
cache-to: type=gha,mode=max,scope=test
- name: Build production target
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
target: production
push: false
cache-from: type=gha,scope=production
cache-to: type=gha,mode=max,scope=production
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
name: Analyze JavaScript
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: github/codeql-action/init@v4
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: github/codeql-action/init@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
with:
languages: javascript-typescript
- uses: github/codeql-action/analyze@v4
- uses: github/codeql-action/analyze@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
62 changes: 35 additions & 27 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
if [ "$RELEASE_BRANCH" = master ]; then
required='["Test (Node 26)","Lint and audit","Helm lint and render","Build container","Open Source and Container","Analyze JavaScript"]'
else
required='["Test (Node 22)","Test (Node 24)","Lint and audit","Helm lint and render","Build container","Open Source","Analyze JavaScript"]'
required='["Test (Node 26)","Lint and audit","Helm lint and render","Build container","Open Source and Container","Analyze JavaScript"]'
fi

for attempt in {1..60}; do
Expand All @@ -76,7 +76,10 @@ jobs:
' <<< "$checks")"
if [ -z "$missing" ]; then
current_sha="$(gh api "repos/$GH_REPO/commits/$RELEASE_BRANCH" --jq .sha)"
test "$current_sha" = "$RELEASE_SHA"
if [ "$current_sha" != "$RELEASE_SHA" ]; then
echo "Branch advanced while checks were running; release blocked." >&2
exit 1
fi
echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
echo "branch=$RELEASE_BRANCH" >> "$GITHUB_OUTPUT"
exit 0
Expand All @@ -92,15 +95,15 @@ jobs:
needs: verify
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.verify.outputs.sha }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
- name: Log in to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
Expand All @@ -109,7 +112,7 @@ jobs:
run: echo "version=$(jq -r .version package.json)" >> "$GITHUB_OUTPUT"
- name: Generate image metadata
id: meta
uses: docker/metadata-action@v6
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
with:
images: jonfairbanks/docker-node-app
tags: |
Expand All @@ -118,7 +121,7 @@ jobs:
type=raw,value=latest,enable=${{ needs.verify.outputs.branch == 'master' }}
type=raw,value=${{ steps.package.outputs.version }},enable=${{ needs.verify.outputs.branch == 'master' }}
- name: Build and push
uses: docker/build-push-action@v7
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
with:
context: .
target: production
Expand All @@ -134,27 +137,32 @@ jobs:
needs: verify
if: needs.verify.outputs.branch == 'master'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.verify.outputs.sha }}
- uses: azure/setup-helm@v4
- name: Check out chart repository
uses: actions/checkout@v6
fetch-depth: 2
- name: Set up Helm
uses: azure/setup-helm@9bc31f4ebc9c6b171d7bfbaa5d006ae7abdb4310 # v5.0.1
with:
repository: jonfairbanks/helm-charts
token: ${{ secrets.HELM_CHARTS_PAT }}
path: helm-charts
- name: Package chart
run: helm package chart --destination helm-charts/_releases
- name: Publish chart package
working-directory: helm-charts
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add _releases
git diff --cached --quiet && exit 0
git commit -m "Publish docker-node-app chart ${SOURCE_SHA::7}"
git push
version: v4.2.3
- name: Publish chart to GHCR
env:
SOURCE_SHA: ${{ needs.verify.outputs.sha }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
version=$(awk '/^version:/ { print $2; exit }' chart/Chart.yaml)
chart=oci://ghcr.io/jonfairbanks/charts/docker-node-app
if helm show chart "$chart" --version "$version" >/dev/null 2>&1; then
if git diff --quiet HEAD^ HEAD -- chart; then
echo "${chart}:${version} already exists and this change does not modify the chart."
exit 0
fi
echo "${chart}:${version} already exists; increment chart/Chart.yaml." >&2
exit 1
fi
echo "$GHCR_TOKEN" | helm registry login ghcr.io --username "$GITHUB_ACTOR" --password-stdin
helm package chart --destination /tmp/charts
helm push "/tmp/charts/docker-node-app-${version}.tgz" oci://ghcr.io/jonfairbanks/charts
25 changes: 22 additions & 3 deletions .github/workflows/snyk.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ permissions:

jobs:
open-source:
name: Open Source
name: Open Source and Container
runs-on: ubuntu-latest
timeout-minutes: 15
env:
Expand All @@ -33,10 +33,10 @@ jobs:
fi

- name: Checkout
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Use Node.js 24.x
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
Expand All @@ -51,3 +51,22 @@ jobs:
--file=package.json
--package-manager=npm
--severity-threshold=high

- name: Build production image
run: docker build --target production --tag docker-node-app:snyk .

- name: Scan production image
run: |
# snyk/actions/docker invokes the legacy `snyk test --docker` path.
# Use Snyk's current container command with the current immutable
# scanner runtime so scanner updates cannot silently change CI.
docker run --rm \
--env SNYK_TOKEN \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$GITHUB_WORKSPACE:/workspace" \
--workdir /workspace \
snyk/snyk@sha256:3c6fbd3e70dea2792d6bc2a080335fafdccd243ad9c0b7faa91002caac7d7851 \
snyk container test docker-node-app:snyk \
--exclude-app-vulns \
--file=Dockerfile \
--severity-threshold=high
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
min-release-age=7
5 changes: 3 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# syntax=docker/dockerfile:1.7

FROM node:24-alpine AS base
RUN apk add --no-cache tini
FROM node:26-alpine AS base
ENV NPM_CONFIG_ENGINE_STRICT=true
RUN apk add --no-cache tini tzdata
WORKDIR /app

FROM base AS dependencies
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

## A sample Node.js app in Docker

- Uses Node.js 24 LTS
- Uses Node.js 26
- Reproducible npm installs from the committed lockfile
- Runs as a non-root user for enhanced security
- Multi-stage development, test, and production images
Expand All @@ -20,7 +20,7 @@

### Local development

With Node.js 24 installed:
With Node.js 26 installed:

```shell
npm ci
Expand Down Expand Up @@ -68,6 +68,13 @@ helm lint chart
helm upgrade --install docker-node-app chart
```

The chart sets the pod timezone to `America/Los_Angeles`. Override it with an
IANA timezone name when deploying elsewhere:

```shell
helm upgrade --install docker-node-app chart --set timezone=Europe/London
```

For testing that pods are balancing correctly, you can make multiple requests to your app to verify.

To make 50 requests and write them to a file, you can run the following with your endpoint:
Expand Down
11 changes: 10 additions & 1 deletion __tests__/app.test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
const assert = require('node:assert/strict');
const { describe, test } = require('node:test');
const request = require('supertest');
const app = require('../app');
const { app, setDraining } = require('../app');

describe('Verify the site loads', () => {
test.after(() => setDraining(false));

test('Response should equal HTTP 200', async () => {
const response = await request(app).get('/');
assert.equal(response.statusCode, 200);
Expand All @@ -14,4 +16,11 @@ describe('Verify the site loads', () => {
assert.equal(response.statusCode, 200);
assert.match(response.body.response.msg, /up and running/);
});

test('Readiness check rejects traffic while draining', async () => {
assert.equal((await request(app).get('/readyz')).statusCode, 200);

setDraining(true);
assert.equal((await request(app).get('/readyz')).statusCode, 503);
});
});
59 changes: 59 additions & 0 deletions __tests__/shutdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const assert = require('node:assert/strict');
const http = require('node:http');
const { once } = require('node:events');
const { test } = require('node:test');
const { createGracefulShutdown } = require('../index');

test('graceful shutdown drains an in-flight request before closing', async (t) => {
let markRequestStarted;
let releaseResponse;
const requestStarted = new Promise((resolve) => {
markRequestStarted = resolve;
});
const responseReleased = new Promise((resolve) => {
releaseResponse = resolve;
});

const server = http.createServer(async (_request, response) => {
markRequestStarted();
await responseReleased;
response.end('ok');
});
server.keepAliveTimeout = 10;

t.after(() => {
server.closeAllConnections?.();
if (server.listening) {
server.close();
}
});

server.listen(0, '127.0.0.1');
await once(server, 'listening');
const { port } = server.address();
const responsePromise = fetch(`http://127.0.0.1:${port}`);
await requestStarted;

const messages = [];
const logger = {
log: (message) => messages.push(message),
error: (message) => messages.push(message),
};
const shutdown = createGracefulShutdown(server, { timeoutMs: 1000, logger });
let shutdownComplete = false;
const shutdownPromise = shutdown('SIGTERM').then(() => {
shutdownComplete = true;
});

await new Promise((resolve) => setImmediate(resolve));
assert.equal(shutdownComplete, false);

releaseResponse();
const response = await responsePromise;
assert.equal(await response.text(), 'ok');
await shutdownPromise;

assert.equal(shutdownComplete, true);
assert.equal(server.listening, false);
assert.deepEqual(messages, ['Received SIGTERM; draining active connections...']);
});
16 changes: 14 additions & 2 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const app = express();
const os = require('os');
const dayjs = require('dayjs');
const advancedFormat = require('dayjs/plugin/advancedFormat');
let isDraining = false;

app.set('view engine', 'ejs');
dayjs.extend(advancedFormat);
Expand Down Expand Up @@ -49,7 +50,18 @@ app.get('/healthz', (req, res) => {
host: os.hostname(),
clientSourceIP: ip,
},
});
});
});

// During termination, keep the container alive long enough for in-flight
// requests to finish, but tell Kubernetes and load balancers not to send it
// any new requests.
app.get('/readyz', (_req, res) => {
res.sendStatus(isDraining ? 503 : 200);
});

module.exports = app;
function setDraining(value) {
isDraining = value;
}

module.exports = { app, setDraining };
Loading
Loading