LAB 09 – Dynamic modules & njs scripting¶
Story: Security wants the
Serverheader to say nothing about the software, and product wants A/B buckets computed at the edge. Neither is possible with stock directives – time for modules.
| Duration | ~40 min (module build ≈ 3–5 min) |
| You edit | env/build/edge-modules/Dockerfile, env/.env, env/nginx/modules-enabled/, env/nginx/njs/, conf.d/ |
| Solution | solutions/LAB09/ (or ./bin/ngx catchup 9) |
Part A – What is in my NGINX?¶
docker compose -f env/docker-compose.yml exec edge nginx -V 2>&1 | tr ' ' '\n' | grep -E '^--with|^--add'
docker compose -f env/docker-compose.yml exec edge ls /usr/lib/nginx/modules/
--with-http_v3_module,--with-stream, … = static modules compiled into the binary.*.sofiles = dynamic modules, loaded withload_modulein the main context.--with-compat= third-party dynamic modules can be built for this exact binary.
Part B – njs: JavaScript inside NGINX¶
njs ships with the official image (ngx_http_js_module.so).
- Load the module –
env/nginx/modules-enabled/10-njs.conf:
(nginx.conf includes modules-enabled/*.conf in the main context.)
env/nginx/njs/hello.js:
function hello(r) {
r.headersOut['Content-Type'] = 'application/json';
r.return(200, JSON.stringify({
message: 'Hello from njs',
method: r.method,
uri: r.uri,
client: r.remoteAddress,
bucket: abBucket(r),
ts: new Date().toISOString()
}) + '\n');
}
function abBucket(r) {
const id = r.headersIn['X-User'] || r.remoteAddress;
let h = 0;
for (let i = 0; i < id.length; i++) {
h = (h * 31 + id.charCodeAt(i)) >>> 0;
}
return (h % 2) ? 'B' : 'A';
}
export default { hello, abBucket };
env/nginx/conf.d/06-njs.conf:
- In the shop HTTPS server:
- Test:
./bin/ngx reload
curl -s https://shop.localtest.me/njs/hello | jq .
for u in alice bob carol dave; do curl -s -H "X-User: $u" https://shop.localtest.me/njs/hello | jq -r '"\(.bucket)"'; done
✅ Checkpoint: JSON generated by JavaScript; every user gets a stable bucket.
Your turn: pass the bucket to the API: proxy_set_header X-AB-Bucket $ab_bucket; in the api location /, then check whoami's output.
Part C – Build a third-party dynamic module¶
The headers-more module can rewrite or remove any response header (including Server). It is not in the official image, so we build it – for the exact NGINX version of the image, with --with-compat.
- Read
env/build/edge-modules/Dockerfile– a multi-stage build: - stage 1 starts from the same nginx image, downloads the matching source (
$NGINX_VERSION), runs./configure --with-compat --add-dynamic-module=...andmake modules(only the module – not the whole server), - stage 2 copies the
.sointo a clean nginx image. - Build it:
cd env
docker build -t pixelshop/edge:modules --build-arg NGINX_TAG=$(grep ^NGINX_TAG .env | cut -d= -f2) build/edge-modules
docker run --rm pixelshop/edge:modules ls /usr/lib/nginx/modules | grep headers_more
- Switch the edge to the new image – in
env/.envsetEDGE_IMAGE=pixelshop/edge:modules, then:
- Load and use the module:
echo 'load_module modules/ngx_http_headers_more_filter_module.so;' > env/nginx/modules-enabled/50-headers-more.conf
env/nginx/conf.d/07-headers-more.conf:
✅ Checkpoint: server: PixelShop-Edge (with server_tokens off alone you would still see server: nginx).
⚠️ A dynamic module must be rebuilt for every NGINX version upgrade – otherwise:
module "..." version 1030006 instead of …. Automate the build in CI next to the image.
Bonus¶
- Build
ngx_brotlithe same way (it needsgit clone --recurse-submodulesandlibbrotli-dev) and compare the size ofapp.jswithAccept-Encoding: brvsgzip. - njs can also run in
stream {}(js_preread), make subrequests (r.subrequest) and call HTTP APIs (ngx.fetch). Sketch a use case for each. - Packaged modules from the nginx.org repository:
nginx-module-njs,nginx-module-otel,nginx-module-acme,nginx-module-image-filter… – when is a package better than a custom build?
Troubleshooting¶
| Symptom | Likely cause |
|---|---|
unknown directive "js_import" |
load_module missing or not in the main context |
module ... is not binary compatible |
built without --with-compat or for a different nginx version |
unknown directive "more_set_headers" |
the edge still runs the stock image – check EDGE_IMAGE and docker compose up -d edge |
njs: SyntaxError |
./bin/ngx test shows the file and line |
Files for this lab: Think-Cube/nginx-labs/labs/LAB09-modules-njs