An administrator would like nginx with the traffic status module, the small one that shows requests and bytes per virtual host on a page of its own. Debian 13 packages thirty-three nginx modules, and this one is not among them. So the evening goes the way such evenings tend to go on Linux these days, into a container. The nginx team publish a recipe for precisely this case: a Dockerfile of 79 lines that needs BuildKit, installs fifteen packages of build tools into a builder image in its first step, clones the packaging sources from GitHub and compiles the requested modules against the official image. It works, as it happens. At the end of the evening there is an image with a name like my-nginx-with-vts, and a daemon running as root to start it.
On FreeBSD the same wish is a tick in a menu. The nginx port offers 71 third-party modules as build options, VTS among them, and Poudriere builds the package with whatever has been ticked:
poudriere jail -c -j 151amd64 -v 15.1-RELEASE
poudriere ports -c
poudriere options -j 151amd64 www/nginx
poudriere bulk -j 151amd64 -b latest www/nginx
The first command fetches FreeBSD 15.1 into a jail, the second fetches a ports tree, the third opens the familiar options dialogue for nginx, and the fourth builds. Beforehand two lines in poudriere.conf name the ZFS pool and the download mirror, and that is the whole of the configuration. The switch -b latest takes every package that need not be built from the official repository, provided that its version, ABI, dependencies and options match what the build expects, a list poudriere-bulk(8) spells out in full; so the evening's compiling is nginx and very little else.
What is left at the end is a directory under /usr/local/poudriere/data/packages/ that is an ordinary pkg repository, signed if a key is set. One small file in /usr/local/etc/pkg/repos/ points pkg at it, and pkg upgrade installs nginx with its new module like any other package, on this machine or on forty others that fetch the directory over HTTP. That is the evening sorted.
The container, in its best light
One file describes the build and produces the same image on any Linux and in any CI, with the runtime inside it, so the server needs nothing but a container engine. For a team that ships one application to many hosts, that is a sound bargain, and the nginx team maintain the recipe so that nobody has to invent it.
For an administrator who wanted a package with one more option, the bargain comes with a fair amount of luggage. The engine itself is the first piece: on Debian 13, docker.io, containerd, runc, docker-cli and tini add up to 255,554 kilobytes of installed size, against 3.8 megabytes for Poudriere. The daemon, says Docker's own security page, "requires root privileges unless you opt-in to Rootless mode". Then there is the cache. Docker's documentation explains that every instruction becomes a layer, and "once a layer changes, then all downstream layers need to be rebuilt as well. Even if they wouldn't build anything differently, they still need to re-run." When OpenSSL receives its next security release, the image is rebuilt and redeployed, which is rather a faff when the only thing that changed was a library.
Debian builds its own packages without any Docker, with sbuild, which works "in a dedicated clean build environment", installs in 285 kilobytes plus its Perl library, and can isolate a build with nothing more than user namespaces. Fedora has mock for the same purpose, and both are lean. What they build, though, is one source package per call: sbuild "rebuilds Debian binary packages from the corresponding Debian source, installing any missing source dependencies" from the archive as finished binaries. A whole set with options of one's own, built in dependency order, needs a build service on top, and the repository at the end comes from reprepro or a similar tool. Poudriere does all of it in one run, and that one run is the reason the FreeBSD evening is four commands long.
A fresh jail for every port
Behind the four commands, Poudriere first prepares a reference jail and takes a snapshot of it. Each parallel builder is then a ZFS clone of that snapshot, created in clonefs() with properties that suit a room nobody will keep, sync=disabled and atime=off among them. Before the builder takes its next port, rollbackfs prepkg returns it to the exact state of the snapshot, so nothing a build leaves lying about reaches the following one. A port that quietly depends on something it never declared fails in Poudriere, where on a well-used build host it would have found the missing piece by accident.
Inside the jail the build is kept on a short lead. It runs and stages as the user nobody by default, and the network is open only while the sources are fetched; the sample configuration gives the reason in plain words: "This is a security restriction to prevent random things ran during a build from accessing the network." What arrives is checked against the checksums in the port's distinfo, and from then on the build has only what it brought.
poudriere testport goes one step further for people who maintain ports. It marks the file system before configure and reports afterwards, under "Filesystem touched during build:", every file a port wrote outside its own working area. Missing dependencies are, as the manual says, "first built in parallel", and an updated dependency triggers the rebuild of the packages that depend on it, unless one switches that off with -S and accepts the warning about "broken packages".
rollbackfs() carries a workaround for a race in ZFS, in which a rollback that runs concurrently with a new snapshot can report success and leave the dataset on the wrong one (illumos issue 7600). Poudriere drops a marker file that cannot exist in the expected snapshot, rolls back and checks whether the file has gone, and tries again up to twenty times, with a second's pause in between, before it gives up. Without ZFS at all, NO_ZFS=yes makes it copy the reference jail with cpdup instead, which works and takes rather longer.
Shell, with a shell of its own
All of this is written in shell. The src/share/poudriere directory holds 22,949 lines of sh in 42 files, without comments and blank lines, and the command that starts it adds 238. The README states the aim in one sentence: Poudriere is meant "to use modern facilities present in FreeBSD (such as ZFS, jails), to be easy to use and to depend only on base". The package in the FreeBSD 15 repository installs 3,809,299 bytes, depends on two small packages (the release manifests and the options dialogue), and links against nothing outside the base system.
Shell has one weakness for a program that keeps the books on thousands of ports: every mkdir or stat called as an external command costs a fork and an exec. Poudriere's answer is a copy of FreeBSD's own /bin/sh, compiled with 45 additional builtins in C, among them mapfile, stat, rename, mkdir and sleep, so that those operations run inside the shell process. The C for it comes to 2,731 lines, and 2,162 more serve as small helpers. Poudriere is still a shell script, run by a shell it builds for itself.
Baptiste Daroussin began it on 19 July 2010 with a commit called "Initial directory structure." Bryan Drewery has since written the larger part, 5,576 of the commits on GitHub against Daroussin's 955, and the repository counts 7,507 commits in all. The Porter's Handbook calls it "one of the most important and helpful testing and build tools" for anyone who contributes to the ports tree.
The sheet
Efficiency. Poudriere 3.4.8 installs 3,809,299 bytes on FreeBSD 15 for amd64 (922,764 to download), depends on freebsd-release-manifests and portconfig, and links only against libraries of the base system: libc, libcrypto, librt, libsbuf, libthr and libutil. Source at commit 5ba8498 of 26 October 2026: 22,949 lines of sh in 42 files, 238 for the entry point, 2,731 lines of C for the builtins and 2,162 for helpers. The working configuration is two lines. For comparison, Debian 13's container stack of five packages takes 255,554 kilobytes, and the official nginx recipe for extra modules is a Dockerfile of 79 lines.
Security. Of the four questions: Poudriere runs as root and creates jails; it takes data from the network only during the fetch phase, checked against distinfo; it parses port Makefiles through make inside the jail; and it drops privileges for the build itself, which runs as nobody by default.
Longevity. First commit 19 July 2010, 7,507 commits to date, maintained in the FreeBSD organisation and distributed as ports-mgmt/poudriere.
Stability. The manual of Poudriere 3.0, dated 16 May 2013, lists the five commands used above, bulk, jail, ports, testport and options, and its example poudriere jail -c -v 8.2-RELEASE -a amd64 -j 82amd64 has the same form as the one in today's manual, with only the version moved on. A working configuration needs two active lines.
Measured on 30 October 2026: source lines without comments and blank lines in freebsd/poudriere at commit 5ba8498; package data from packagesite.yaml in FreeBSD:15:amd64/latest; installed sizes from Debian 13's Packages index for amd64; third-party modules counted in www/nginx/Makefile.extmod (71) and libnginx-mod-* packages in Debian 13 (33); the Dockerfile from nginx/docker-nginx, directory modules.
The limit
It builds for FreeBSD. Poudriere makes FreeBSD packages, for other architectures through emulation with QEMU; a team that ships to every Linux distribution needs another tool for that job, and Poudriere never set out to be one.
Building from source takes time and room. A large set compiles for hours, the manual reckons about 3 GB for each jail, and the sample configuration asks for at least 7 GB free in the pool. -b latest narrows the work to whatever really differs from the official packages.
The fetch switch knows only what it can see. poudriere-bulk(8) warns that it has "no way of determining differences" outside its list, so a package with its own patches or special knobs has to be named in PACKAGE_FETCH_BLACKLIST.
It runs as root. The builds inside drop to nobody, but Poudriere itself creates jails and datasets, and the queue for ordinary users is still marked experimental.
The cleanest room on the machine is the one Poudriere throws away after every port.