Building nginx from source to .deb (Debian/Ubuntu)
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
.debfiles are published to the local apt repo (debrepo.sa7aux.se, on the mounted disk/mnt/sdX/mirror/mirror/local/nginx), thenapt upgradeinstalls them on the web server (speed). - Driver: one orchestrator,
~/src/nginx/auto-nginx.sh, callsnginx-core.shand the per-module*2.shscripts.
The rules that actually matter
[edit | edit source]These are the non-negotiables. Ignoring any one of them is what causes the endless trouble:
- Purge first. Before building,
dpkg --purgeevery nginx / module /njs/libquickjs/lua-restypackage. Modules are compiled againstnginx-dev; stale packages ⇒ wrong ABI ⇒ nothing works. Also rundpkg --configure -a+apt-get -f installto clear any half-broken state the purge leaves. - 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 withdpkg-buildpackage. - Install built packages with
dpkg -i, notapt. apt's solver rejects the tight inter-module version pins (e.g.lua-resty-core↔ the lua module).dpkg -iinstalls the files directly; the whole group in one call satisfies circular deps. Run/sbin/ldconfigafter each install. - 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. - Respect the dependency order — build → install → then the next thing that needs it (see the module order).
- 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_tools — tools 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:
- Clone Salsa packaging: branch
debian/trixie(compat 13 — builds on Trixie;debian/latestneeds debhelper-compat 14 which Trixie lacks). It already ships--with-http_v3_module. - Download latest mainline from
nginx.orgas the.origtarball; drop the Debiandebian/on top. - Set the ABI in
debian/libnginx-mod.abisubstvarstonginx-abi-<VER>-1. rm -f debian/patches/*(Debian's CVE patches may not apply to a newer upstream); emptyseries.- In
debian/rules: remove--override-system/release/machine(they break a plain local build). - Mainline ships no
READMEbutdh_installdocswants one:echo 'Documentation is available at http://nginx.org' >> README. - zstd (no Debian packaging): clone
tokers/zstd-nginx-moduleand add--add-module=$(CURDIR)/debian/zstd-nginx-moduleto the bin flavour. dchto<VER>-1+bj1, thendpkg-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):
- NDK (
ngx_devel_kit) — needed by set-misc & lua. - Lua trio, built + installed TOGETHER (circular: the lua module ↔
lua-resty-coredepend on each other):libnginx-mod-http-lua+lua-resty-core+lua-resty-lrucache. The lua module ends upiU(unconfigured) becauselua-resty-corepins an older lua-module version — harmless: it doesn't block anything once you stop using apt. - set-misc (needs NDK).
- QuickJS (see below) — needed by njs.
- njs (see below).
- 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-dev→libquickjs(our package ships thequickjs.hheaders). - 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-optindebian/rulesso 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 toolshelp2man,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).
- Copy the
.debfiles into that dir. - Regenerate + sign the index. For the flat sa7aux sections use
sudo /mnt/sdh/meta-sa7aux(dpkg-scanpackages+apt-ftparchive release+ gpg--clearsignofInRelease). The signing prompts for the local-mirror key passphrase (pinentry needs a TTY — it cannot run head-less). - On the target:
apt update && apt full-upgrade.
Gotchas learned the hard way
[edit | edit source]GROUPSis a reserved bash array (your unix group ids). Don't name an array that — use e.g.MODGROUPS.ldconfigis in/sbin, not on a non-interactive shell'sPATH— 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.shfrom 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 hostnginx— the scripts are the executable version of this page.- ·