Jump to content

Building nginx from source to .deb (Debian/Ubuntu)

From MediaWiki For Hamradio Analog/Digital and Debian v13 software/Raspberrry PI v3/4/5 howto

Custom mainline nginx (HTTP/3 + Brotli + zstd + ~24 dynamic modules) built from source as Debian .deb packages, published to the local apt repo, and installed — fully automated.

This page is the hard-won, step-by-step recipe. It exists so nobody has to fight the same battles again.

Overview

[edit | edit source]
  • Where: a dedicated VM (host nginx) that does nothing but compile nginx. Because it is disposable, install every build dependency freely there.
  • What: latest mainline nginx from nginx.org, with HTTP/3 (QUIC via system OpenSSL), Brotli, zstd, njs (QuickJS), Lua, ModSecurity and the full Debian dynamic-module set — all as .deb.
  • Result → where it goes: the finished .deb files are published to the local apt repo (debrepo.sa7aux.se, on the mounted disk /mnt/sdX/mirror/mirror/local/nginx), then apt upgrade installs them on the web server (speed).
  • Driver: one orchestrator, ~/src/nginx/auto-nginx.sh, calls nginx-core.sh and the per-module *2.sh scripts.

The rules that actually matter

[edit | edit source]

These are the non-negotiables. Ignoring any one of them is what causes the endless trouble:

  1. Purge first. Before building, dpkg --purge every nginx / module / njs / libquickjs / lua-resty package. Modules are compiled against nginx-dev; stale packages ⇒ wrong ABI ⇒ nothing works. Also run dpkg --configure -a + apt-get -f install to clear any half-broken state the purge leaves.
  2. Never apt build-dep. It drags in the archive nginx (old) and wrecks the ABI. Instead install all build tools up front (see below), and build with dpkg-buildpackage.
  3. Install built packages with dpkg -i, not apt. apt's solver rejects the tight inter-module version pins (e.g. lua-resty-core ↔ the lua module). dpkg -i installs the files directly; the whole group in one call satisfies circular deps. Run /sbin/ldconfig after each install.
  4. Fetch each thing from where it actually lives — not dogmatically Salsa. Most modules: Salsa debian/latest. QuickJS: apt-get source quickjs (it lives in the Debian archive). Everything at its latest.
  5. Respect the dependency order — build → install → then the next thing that needs it (see the module order).
  6. Bump the version on every real change (and only on a real change) — otherwise the apt index shows the wrong thing and installs the wrong file.

Prerequisites on the builder

[edit | edit source]

The VM needs working DNS (nginx.org / github / salsa / the internal names). If /etc/resolv.conf is empty:

printf 'nameserver 172.16.1.1\nsearch sa7aux.se\n' | sudo tee /etc/resolv.conf

Build tools + -dev libs (installed once by install_toolstools only, never the archive nginx):

build-essential devscripts debhelper dh-exec dh-lua quilt dpkg-dev po-debconf fakeroot lintian equivs
git curl wget ca-certificates rsync pkg-config cmake autoconf automake libtool
libpcre2-dev zlib1g-dev libssl-dev uuid-dev libexpat1-dev
libzstd-dev zstd libmaxminddb-dev libgeoip-dev
libgd-dev libxml2-dev libxslt1-dev libperl-dev
libpam0g-dev libkrb5-dev libluajit-5.1-dev lua5.1 libmodsecurity-dev
help2man libedit-dev ragel

Core nginx (nginx-core.sh)

[edit | edit source]

Robust core = current Debian packaging + latest upstream + zstd:

  1. Clone Salsa packaging: branch debian/trixie (compat 13 — builds on Trixie; debian/latest needs debhelper-compat 14 which Trixie lacks). It already ships --with-http_v3_module.
  2. Download latest mainline from nginx.org as the .orig tarball; drop the Debian debian/ on top.
  3. Set the ABI in debian/libnginx-mod.abisubstvars to nginx-abi-<VER>-1.
  4. rm -f debian/patches/* (Debian's CVE patches may not apply to a newer upstream); empty series.
  5. In debian/rules: remove --override-system/release/machine (they break a plain local build).
  6. Mainline ships no README but dh_installdocs wants one: echo 'Documentation is available at http://nginx.org' >> README.
  7. zstd (no Debian packaging): clone tokers/zstd-nginx-module and add --add-module=$(CURDIR)/debian/zstd-nginx-module to the bin flavour.
  8. dch to <VER>-1+bj1, then dpkg-buildpackage -us -uc -b.

Install on the builder (needs the binary too, else apt pulls the archive 1.26.3):

sudo dpkg -i nginx-common_*.deb nginx_*.deb nginx-dev_*.deb libnginx-mod-stream_*.deb

nginx-dev now Provides nginx-abi-<VER>-1 — exactly what every module's rewritten Build-Depends asks for.

Modules — order + the ABI trick

[edit | edit source]

Each libnginx-mod-*2.sh clones its Salsa packaging (debian/latest) and — the key trick — rewrites the nginx-abi-X-Y dependency in debian/control + *.substvars to the ABI of our nginx (generic regex, so it matches whatever Salsa currently carries). Then build with dpkg-buildpackage, install the result with dpkg -i + ldconfig.

Order (build → install → next):

  1. NDK (ngx_devel_kit) — needed by set-misc & lua.
  2. Lua trio, built + installed TOGETHER (circular: the lua module ↔ lua-resty-core depend on each other): libnginx-mod-http-lua + lua-resty-core + lua-resty-lrucache. The lua module ends up iU (unconfigured) because lua-resty-core pins an older lua-module version — harmless: it doesn't block anything once you stop using apt.
  3. set-misc (needs NDK).
  4. QuickJS (see below) — needed by njs.
  5. njs (see below).
  6. The independent modules: echo, srcache, memc (needs ragel), nchan, fancyindex, subs, rtmp, cache-purge, geoip2, stream-geoip2, uploadprogress, dav-ext, headers-more, upstream-fair, brotli, auth-pam, auth-spnego, push-stream, modsecurity.

The two tricky ones

[edit | edit source]

QuickJS

[edit | edit source]

Debian's njs uses QuickJS. The source lives in the archive, not Salsa:

apt-get source --download-only quickjs
tar -xf quickjs_*.orig.tar.xz ; cd quickjs-*/ ; tar -xf ../quickjs_*.debian.tar.xz

The stock package installs libquickjs.so + headers under a subdir (/usr/lib/$(MULTIARCH)/quickjs/), which is not on the linker/loader path. So ship an ld.so.conf.d entry inside the libquickjs package so ldconfig registers it on any box (build + speed). Add to debian/rules:

execute_after_dh_install:
	install -d debian/libquickjs/etc/ld.so.conf.d
	echo /usr/lib/$(DEB_HOST_MULTIARCH)/quickjs > debian/libquickjs/etc/ld.so.conf.d/libquickjs.conf

Then dpkg -i libquickjs*.deb && /sbin/ldconfig ⇒ it lands in the ld cache.

njs ↔ libquickjs

[edit | edit source]

njs's ./configure probes for QuickJS in fixed paths and links + runs a test program, so both the build-link and the run must find libquickjs. In the njs build script:

  • Swap the Build-Depends: libqjs0 / libqjs-devlibquickjs (our package ships the quickjs.h headers).
  • Symlink into the dir njs probes so the link step passes:
sudo mkdir -p /usr/lib/quickjs
sudo ln -sf /usr/lib/$MA/quickjs/libquickjs.so /usr/lib/quickjs/libquickjs.so
sudo ln -sf /usr/lib/$MA/quickjs/libquickjs.a  /usr/lib/quickjs/libquickjs.a
  • Add to njs' --cc-opt/--ld-opt in debian/rules so the module both links and, via rpath, loads libquickjs at runtime on any box:
--cc-opt="... -I/usr/include/quickjs"
--ld-opt="... -L/usr/lib/$MA/quickjs -Wl,-rpath,/usr/lib/$MA/quickjs"
  • njs also Build-Depends on libnginx-mod-stream (install it from the core build) and the tools help2man, libedit-dev.

Verify success: configure prints using QuickJS library, and objdump -p ngx_http_js_module.so shows NEEDED libquickjs.so + a RUNPATH.

Publish to the apt repo

[edit | edit source]

The repo lives on the mounted disk: /mnt/sdh/mirror/mirror/local/nginx (served at https://debrepo.sa7aux.se/nginx; /var/www/html/mirror/nginx is only nginx's serving symlink into it — resolve it with readlink -f).

  1. Copy the .deb files into that dir.
  2. Regenerate + sign the index. For the flat sa7aux sections use sudo /mnt/sdh/meta-sa7aux (dpkg-scanpackages + apt-ftparchive release + gpg --clearsign of InRelease). The signing prompts for the local-mirror key passphrase (pinentry needs a TTY — it cannot run head-less).
  3. On the target: apt update && apt full-upgrade.

Gotchas learned the hard way

[edit | edit source]
  • GROUPS is a reserved bash array (your unix group ids). Don't name an array that — use e.g. MODGROUPS.
  • ldconfig is in /sbin, not on a non-interactive shell's PATH — call /sbin/ldconfig.
  • Two packages must never ship the same file (e.g. both shipping /usr/bin/svxmixer) — dpkg refuses; rename one.
  • pkill -f auto-nginx.sh from an ssh session whose command line contains that string kills its own shell (exit 255). Kill by PID.
  • Always drive remote work over one persistent SSH ControlMaster socket, batched — never a new connection per call or a background poll loop (rate-limits port 22 + a swarm of shells).

See also

[edit | edit source]
  • ~/src/nginx/ on host nginx — the scripts are the executable version of this page.
  • ·