AphrodyBun GitHub

Runtime docs · Runtime · Utilities

NGINX

Own a native NGINX process and its Rust module with Bun, with explicit source and ABI qualification.

bun:nginx and the literal spelling bun:Nginixx resolve to the same Rust process controller. NGINX's selected native executable owns its configuration, HTTP engine and compiled modules. The optional @aphrody/bun-nginx package re-exports the builtin and its types.

import Nginx from "bun:nginx";
import { homedir } from "node:os";
import { join } from "node:path";

const provider = await Nginx.probe({ binary: "/usr/sbin/nginx" });
if (!provider.available) throw new Error(provider.message);

await using nginx = await Nginx.create({
  binary: "/usr/sbin/nginx",
  prefix: join(homedir(), "apps", "my-nginx"),
  configFile: "nginx.conf",
  timeoutMs: 10_000,
});
await nginx.validate();
const started = await nginx.start();
console.log(started.pid);
await nginx.reload();

The prefix must already exist and the configuration must resolve inside it. Omit daemon and pid directives: Bun supplies foreground mode and <prefix>/bun-nginx.pid. Prepare NGINX's log and temporary directories in that private prefix, or set the corresponding paths in the configuration. Existing release-site configuration policy stays in scripts/aphrody/deploy/release-nginx.ts.

An exclusive .bun-nginx-owner prevents two controllers from claiming one prefix. Start refuses an existing PID file. Reload and quit verify both prefix ownership and the PID of the spawned master. Unix masters have an owned process group; Windows masters have a job object with kill-on-close. Operations cannot adopt, reload or terminate an unrelated NGINX service.

Validation has its own exclusive temporary PID file. Real NGINX creates an empty PID file during -t, so validation never receives the live master's PID path. Startup waits for the master's complete PID before verifying its identity.

validate(), start(), reload(), quit(), status() and close() return promises. Each operation accepts timeoutMs and signal. Deadlines are integer milliseconds between 1 and 30000. Diagnostic output has a separate bound for each stream, default 262144 bytes, maximum 1048576. Concurrent operations reject with ERR_NGINX_BUSY.

The running master keeps Bun's event loop alive through a cancellable native job. Master exit ends that job and stops remaining owned workers. A collected controller synchronously terminates its owned group; reaping happens outside the JS thread. VM cancellation also terminates the group. A failed or aborted start terminates its child. quit() waits for graceful exit and permits a later start; close() also releases ownership and forces termination when graceful quit exceeds its deadline. Repeated close succeeds. Use await using or an explicit finally block for deterministic closure.

probe() reports available: false only when the executable is unavailable. Configuration failures, redirected ownership, deadlines, aborts and output limits reject with their native error codes. Bun does not download a provider implicitly. Capabilities measured by nginx -V describe compile flags; protocol tests and dynamic module configuration are separate evidence. platform and arch describe Bun's host. Upstream Windows NGINX is beta, has one effective worker and does not provide UDP/QUIC.

Provider and engine gates

The explicit Windows helper downloads the pinned official ZIP and detached signature, verifies it with official public keys in an isolated public keyring, verifies extracted bytes against the signed archive, and records the actual PE architecture. It does not modify a user's keyring or install a global service.

bun packages/bun-nginx/scripts/prepare-provider.ts C:/bun/factory/artifacts/nginx-sdk/windows "C:/Program Files/Git/usr/bin/gpg.exe"
bun packages/bun-nginx/scripts/qualify-engine.ts C:/bun/factory/artifacts/nginx-sdk/windows/prefix/nginx-1.31.6/nginx.exe C:/bun/factory/artifacts/nginx-sdk/windows/engine-qualification.json

qualify-engine.ts also accepts an existing Linux ELF executable. Its private localhost fixture measures configuration validation, HTTP bytes, Range, reload with the same master PID, rejection of invalid configuration while the old master still serves, and graceful quit. The receipt records binary SHA256, format, architecture and the executing Bun revision. It always records nativeBunController: false: this engine gate does not exercise the builtin.

On 2026-10-13 these engine gates passed for the signed upstream Windows 1.31.6 provider (PE x86, executed on Windows x64). The portable source-built Linux provider has the additional module and ABI gates below. Neither engine receipt attests all Bun release targets, TLS/HTTP3 behavior or Rust controller execution.

The changed-binary controller gate is separate:

$env:BUN_NGINX_TEST_BINARY = "C:/bun/factory/artifacts/nginx-sdk/windows/prefix/nginx-1.31.6/nginx.exe"
bun bd test ./test/js/bun/nginx/test.ts

Core tests use an explicit process-protocol fixture to cover isolation, deadlines, abort, crash, GC, keepalive, VM exit and worker cleanup. The optional real engine case skips only when no selected executable exists. A missing builtin or failed core test remains a failed gate.

References: NGINX switches, Windows limitations, official signing keys.

Rust module and source ownership

The canonical sources are vendor/nginx at 0a5db852e18d1c22c62a5ede1412ac3f416111a0 (1.31.6) and vendor/ngx-rust at cf58b5270dc5e9b855dab765e9f13ba2a12c7709 (ngx/nginx-sys 0.5.0). packages/bun-nginx/native/sources.json and the explicit source acquisition command define this contract:

bun packages/bun-nginx/scripts/prepare-sources.ts
bun packages/bun-nginx/scripts/verify-sources.ts factory/artifacts/nginx-sdk/source-inventory.json

The helper reuses an existing matching clean vendor and refuses a changed revision or tracked edits. It creates the two canonical vendors when absent. No implicit Git link is added to Bun's repository. Both complete upstream source trees, licenses, Git tree IDs and file SHA256 inventory remain available locally. The optional ngx vendored feature and its nested nginx-src/nginx submodule stay disabled, so there is one NGINX source owner on each build host.

The native module crate is packages/bun-nginx/native. NGINX's official --add-module build integration compiles its Rust static library into the actual NGINX executable. It reuses vendor/ngx-rust/examples/auto/rust, the exact configured NGINX headers and the module's own Cargo.lock. Cargo does not fetch or build another NGINX source. NGINX_SOURCE_DIR, NGINX_BUILD_DIR and the configured-header digest identify the selected ABI.

flowchart LR
    Bun[Bun VM] --> Controller[Native Rust process controller]
    Controller --> Master[Owned NGINX master]
    Master --> Worker[NGINX worker and Rust HTTP module]
    Worker --> Pool[Request pool and cleanup]
    Worker --> Timer[NGINX event timer]
    Worker --> HTTP[Bounded JSON over local HTTP]
    HTTP --> Bun

bun_nginx; enables the module's metadata response in a location. bun_nginx_delay 1..1000; enables a bounded asynchronous response using an NGINX timer. Buffers and timer context belong to the request pool. Cleanup removes pending timers; a request reference keeps the pool alive through the callback. A cycle-owned counter makes pending timers observable without crossing worker threads. C callbacks contain Rust panics. The module creates no Tokio runtime and transfers no NGINX pointer or signal into JavaScriptCore. The timer context starts with the exact configured ngx_connection_t binding, checked at offset zero, so NGINX's C debug logger reads the correct connection identifier. The Linux module gate enables that logger while testing timers.

events { worker_connections 32; }
http {
    server {
        listen 127.0.0.1:8080;
        location = /info { bun_nginx; }
        location = /delay { bun_nginx_delay 500; }
    }
}

The optional SDK readModuleInfo(nginx, url) reads this module's bounded loopback HTTP response and verifies its schema and master PID against the existing native controller. It can check the configuration digest from a selected build receipt. HTTP metadata alone does not attest a binary hash; the build and qualification receipts provide that evidence separately.

import { readModuleInfo } from "@aphrody/bun-nginx";
const module = await readModuleInfo(nginx, "http://127.0.0.1:8080/info", {
  expectedConfigurationSha256: buildReceipt.configurationSha256,
});
console.log(module.moduleSignature, module.pendingTimers);

Native module gates

build-provider.ts plan <absolute-owned-output> verifies source and module inputs and emits the configured native commands without executing them. Its executable Linux recipe holds /home/ubuntu/build/locks/heavy.lock and uses at most two build jobs. Windows source and toolchain qualification are separate gates; a plan keeps them explicit. It does not restart a system NGINX service or modify production configuration.

bun packages/bun-nginx/scripts/build-provider.ts plan /absolute/checkout/factory/artifacts/nginx-sdk/linux-x64-portable /absolute/sdk-receipt.json
bun packages/bun-nginx/scripts/qualify-engine.ts --module-build /absolute/build-receipt.json /absolute/module-engine-qualification.json

The final product is a source-built NGINX with its Rust module statically included. A separate temporary module built with abi-mismatch-fixture deliberately changes its ngx_module_t signature. The real NGINX loader must reject it as incompatible. That fixture is not a release module.

qualify-module.ts requires a changed Bun binary exposing bun:nginx, the explicit source-build receipt and the incompatible fixture. Its gates check HTTP/HEAD bytes, source/configuration identity, a concurrent response during a pending timer, client abort, invalid configuration, reload with the same master, ABI mismatch rejection and graceful quit. A runtime lacking the builtin fails that bridge gate. Separate engine-only receipts continue to record nativeBunController: false.

Source preparation and planning do not qualify module execution or all Bun release targets. Shared libraries, libc baseline, TLS/HTTP3 behavior, target architecture and module ABI require their own native proofs.

Portable GNU x64 build

packages/bun-nginx/scripts/build-portable-gnu-x64.sh builds in the existing checkout with LLVM 23, the focal GNU sysroot and the shared heavy lock. It limits native compilation to two jobs. The recipe selects the existing canonical BoringSSL and zlib sources, and extracts the exact PCRE2 development package from signed Ubuntu archive metadata into its private SDK prefix. Headers and libraries from the build host cannot enter through inherited compiler or linker search paths.

timeout 3600 bash packages/bun-nginx/scripts/build-portable-gnu-x64.sh
bun packages/bun-nginx/scripts/qualify-portable.ts /absolute/build-receipt.json /absolute/module-engine-qualification.json /absolute/portable-qualification.json

The SDK receipt records source pins, signed metadata, file hashes and the canonical Bun compatibility object. The module uses the optimized ngx-release profile while NGINX retains --with-debug for its actual C logger gate. The executable has a relative $ORIGIN/../lib search path. The incompatible shared fixture has a separate private unwinder and its notice, both excluded from release artifacts. Its dependency graph is checked alongside the engine's graph.

qualify-portable.ts reuses scripts/aphrody/media-sdk-abi.ts to reject wrong architecture, loader, search paths, private libc symbols, unresolved dependencies and required GLIBC versions above 2.17. Diagnostic streams have a 64 MiB hard bound, retain at most 64 KiB of text, and record the complete stream's byte count and SHA256. Build logs are truncated before each command.

On 2026-10-13, the source-built Linux x64 NGINX 1.31.6 executable passed HTTP, HEAD, Range, reload, invalid Rust configuration, graceful quit and the real loader's incompatible-module rejection. A 500 ms native timer allowed a concurrent metadata response, and an aborted request's pending timer returned to zero. The executable SHA256 is 7c4b83794387538549a46b91c71bbd01980c87b685b6636adcd91bb39438823a. That receipt records nativeRustModuleQualified: true and nativeBunController: false; the selected Bun binary predates this builtin. Windows Rust-module execution and the changed-binary bridge remain unqualified. The engine and fixture require at most GLIBC 2.17; the fixture's private unwinder requires GLIBC 2.14. These are measured ELF requirements, with native execution on the VPS; execution on an actual GLIBC 2.17 host remains unqualified. The receipt keeps portableReleaseQualified and allReleasePlatformsQualified false until the remaining runtime and protocol gates pass.

test/integration/glibc-compat/run.sh qualifies the canonical compatibility owner against the GNU x64/arm64 and musl x64/arm64 sysroots. GNU probes cover entropy bounds, interruption, the ENOSYS device fallback and its errors, descriptor closure, fortified erasure and glob64 callback/layout behavior. Both GNU probe binaries meet GLIBC 2.17; musl binaries have no GNU libc imports. The ARM64 runs use QEMU user emulation, and the musl probe covers the shared gettid wrapper rather than GNU adapters. A real old kernel or early boot entropy environment is not attested by these probes.

References: NGINX development guide, official ngx-rust, source configuration.