How to Build Custom FreeBSD Packages with Poudriere
The official FreeBSD package repository provides binary packages built with default options. When you need packages compiled with specific options -- PostgreSQL with different PL languages, NGINX with additional modules, or Python with a particular SSL backend -- you have two choices: build from ports manually on every server, or use Poudriere to build custom packages once and distribute them as a proper repository.
Poudriere is the same tool the FreeBSD project uses to build its official packages. It creates clean jail environments, builds ports in isolation, and produces a signed package repository that any FreeBSD machine can use with pkg. This guide covers the complete workflow on FreeBSD 14.x: installation, jail creation, ports tree management, setting custom build options, building packages, hosting the repository, and automating the entire process.
For background on when to use packages versus ports, see our FreeBSD pkg vs ports comparison.
Why Poudriere
Building ports directly on a production server is messy. Build dependencies pollute the system. Builds can fail midway and leave partial state. Different servers may end up with different compile options for the same port. Poudriere solves all of this:
- Clean builds: each build runs in a fresh jail. No build artifacts or dependencies leak to the host.
- Reproducibility: the same ports tree version and options always produce the same packages.
- Parallelism: Poudriere builds multiple ports simultaneously, utilizing all CPU cores.
- Repository output: the result is a
pkg-compatible repository with a proper catalog and optional signing. - Audit trail: logs for every build, clearly showing success, failure, and skipped ports.
Installing Poudriere
Install Poudriere from packages:
shpkg install poudriere
The configuration file is at /usr/local/etc/poudriere.conf. Edit it:
shvi /usr/local/etc/poudriere.conf
Key settings to configure:
sh# Where Poudriere stores everything (jails, ports, packages, logs) BASEFS=/usr/local/poudriere # Where to store jail filesystems ZPOOL=zroot # Use ZFS for jail filesystems (strongly recommended) USE_TMPFS=yes TMPFS_LIMIT=8 # Use all available CPU cores PARALLEL_JOBS=auto # Where package output goes PKG_REPO_SIGNING_KEY=/usr/local/etc/poudriere.d/keys/pkg.key # FreeBSD mirror for jail creation FREEBSD_HOST=https://download.FreeBSD.org # Build log directory POUDRIERE_DATA=${BASEFS}/data
If your system uses ZFS (recommended), Poudriere creates ZFS datasets for jails and ports trees, enabling fast cloning and rollback. If you do not use ZFS, set NO_ZFS=yes and Poudriere will use directory-based storage instead.
Creating a Build Jail
A Poudriere jail is a minimal FreeBSD installation used as the build environment. Create one matching your target FreeBSD version:
shpoudriere jail -c -j 14amd64 -v 14.2-RELEASE -a amd64
This downloads FreeBSD 14.2-RELEASE and sets up a jail named 14amd64. The name is arbitrary but should be descriptive.
List jails:
shpoudriere jail -l
Update the jail to the latest patch level:
shpoudriere jail -u -j 14amd64
To build packages for a different architecture (e.g., for Raspberry Pi):
shpoudriere jail -c -j 14arm64 -v 14.2-RELEASE -a arm64
Setting Up the Ports Tree
Poudriere needs a copy of the FreeBSD ports tree. Create one:
shpoudriere ports -c -p default
This fetches the latest ports tree using portsnap or Git (depending on your configuration). By default, Poudriere uses Git.
To use a specific method:
shpoudriere ports -c -p default -m git+https
List ports trees:
shpoudriere ports -l
Update the ports tree:
shpoudriere ports -u -p default
You can maintain multiple ports trees (e.g., quarterly and latest):
shpoudriere ports -c -p quarterly -B 2026Q1
Defining the Package List
Create a file listing the ports you want to build:
shmkdir -p /usr/local/etc/poudriere.d vi /usr/local/etc/poudriere.d/pkglist
List ports by their category/name:
shellwww/nginx databases/postgresql16-server databases/postgresql16-client lang/python311 lang/php83 security/sudo sysutils/tmux editors/neovim shells/zsh net/rsync security/openssh-portable sysutils/htop www/node20 devel/git
Setting Custom Build Options
This is where Poudriere shines. You can set compile-time options for any port without building it manually.
Set options interactively for a single port:
shpoudriere options -j 14amd64 -p default -z custom www/nginx
This opens a dialog where you can enable/disable NGINX modules, select features, etc.
Set options for all ports in your list:
shpoudriere options -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist
Options are stored in /usr/local/etc/poudriere.d/14amd64-default-custom-options/.
Set options non-interactively via make.conf:
Create a make.conf for your build set:
shvi /usr/local/etc/poudriere.d/14amd64-make.conf
make# Build NGINX with specific modules www_nginx_SET=HTTP_GZIP_STATIC HTTP_REALIP HTTP_V2 STREAM THREADS MAIL www_nginx_UNSET=HTTP_PERL HTTP_REWRITE # PostgreSQL with PL/Python databases_postgresql16-server_SET=PLPYTHON PLPERL databases_postgresql16-server_UNSET=NLS # Global options OPTIONS_UNSET=DOCS EXAMPLES X11 NLS DEFAULT_VERSIONS+=python3=3.11 php=8.3 pgsql=16
The naming convention is category_port_SET / category_port_UNSET with slashes replaced by underscores and hyphens preserved.
Building Packages
Run the build:
shpoudriere bulk -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist
Poudriere will:
- Create a clean jail clone
- Resolve all dependencies for the listed ports
- Build packages in parallel (respecting dependency order)
- Produce a
pkgrepository in the output directory
Monitor the build progress. Poudriere shows a real-time table of building, succeeded, failed, and queued ports.
For a web-based build monitor:
shpoudriere status -j 14amd64 -p default -z custom
Or enable the built-in web UI by pointing a web server at /usr/local/poudriere/data/logs/bulk/.
After the build completes, the package repository is at:
shell/usr/local/poudriere/data/packages/14amd64-default-custom/
Check for failed builds:
shls /usr/local/poudriere/data/logs/bulk/14amd64-default-custom/latest/logs/errors/
Review individual build logs:
shcat /usr/local/poudriere/data/logs/bulk/14amd64-default-custom/latest/logs/www_nginx.log
Signing the Repository
Sign your repository so client machines can verify package integrity.
Generate an RSA key pair:
shmkdir -p /usr/local/etc/poudriere.d/keys openssl genrsa -out /usr/local/etc/poudriere.d/keys/pkg.key 4096 chmod 400 /usr/local/etc/poudriere.d/keys/pkg.key openssl rsa -in /usr/local/etc/poudriere.d/keys/pkg.key -pubout -out /usr/local/etc/poudriere.d/keys/pkg.pub
Ensure PKG_REPO_SIGNING_KEY in poudriere.conf points to the private key. Poudriere signs the repository automatically during builds.
Distribute pkg.pub to all client machines.
Hosting the Repository
Serve the package repository over HTTP using NGINX or any web server.
shpkg install nginx sysrc nginx_enable="YES"
Configure NGINX to serve the repository:
shvi /usr/local/etc/nginx/nginx.conf
nginxserver { listen 80; server_name pkg.example.com; location /packages/ { alias /usr/local/poudriere/data/packages/; autoindex on; } }
shservice nginx start
Verify the repository is accessible:
shcurl http://pkg.example.com/packages/14amd64-default-custom/
Configuring Clients
On each FreeBSD machine that should use your custom packages, create a repository configuration:
shmkdir -p /usr/local/etc/pkg/repos vi /usr/local/etc/pkg/repos/custom.conf
shellcustom: { url: "http://pkg.example.com/packages/14amd64-default-custom", signature_type: "pubkey", pubkey: "/usr/local/etc/pkg/repos/pkg.pub", enabled: yes, priority: 100 }
Copy the public key to the client:
shscp build-server:/usr/local/etc/poudriere.d/keys/pkg.pub /usr/local/etc/pkg/repos/pkg.pub
Optionally disable the default FreeBSD repository to use only your custom packages:
shvi /usr/local/etc/pkg/repos/FreeBSD.conf
shellFreeBSD: { enabled: no }
Update the package catalog:
shpkg update -f
Install packages from your custom repository:
shpkg install nginx
Verify the package source:
shpkg info -o nginx pkg query "%R" nginx
Automating Builds
Create a script that updates the ports tree and rebuilds packages on a schedule:
shvi /usr/local/bin/poudriere-build.sh
sh#!/bin/sh set -e JAIL="14amd64" PORTS="default" SET="custom" PKGLIST="/usr/local/etc/poudriere.d/pkglist" # Update ports tree poudriere ports -u -p "$PORTS" # Update jail to latest patch level poudriere jail -u -j "$JAIL" # Build packages poudriere bulk -j "$JAIL" -p "$PORTS" -z "$SET" -f "$PKGLIST" # Log completion echo "$(date): Build completed" >> /var/log/poudriere-auto.log
shchmod 755 /usr/local/bin/poudriere-build.sh
Schedule it weekly with cron:
shcrontab -e
shell0 3 * * 0 /usr/local/bin/poudriere-build.sh >> /var/log/poudriere-auto.log 2>&1
This runs every Sunday at 3 AM, updating the ports tree, jail, and rebuilding all packages.
Managing Multiple Build Sets
You can maintain multiple build configurations for different purposes:
sh# Production: stable, tested options poudriere bulk -j 14amd64 -p quarterly -z production -f /usr/local/etc/poudriere.d/pkglist-prod # Development: latest ports, extra debug packages poudriere bulk -j 14amd64 -p default -z dev -f /usr/local/etc/poudriere.d/pkglist-dev
Each combination of jail, ports tree, and set name produces a separate repository. Configure clients to use the appropriate one.
Cleaning Up
Poudriere accumulates build data over time. Clean old packages and logs:
sh# Remove packages for ports no longer in the list poudriere pkgclean -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist # Remove old log files poudriere logclean -j 14amd64 -p default -z custom 30
Clean unused jail snapshots (ZFS):
shpoudriere jail -d -j old-jail-name
Check disk usage:
shdu -sh /usr/local/poudriere/data/packages/ du -sh /usr/local/poudriere/data/logs/ zfs list -r zroot/poudriere
Troubleshooting
Build fails with "checksum mismatch":
The ports tree distfile cache may be stale. Clean and retry:
shpoudriere distclean -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist poudriere bulk -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist
Build hangs on a specific port:
Check the build log for the stuck port. Some ports have interactive prompts that block in batch mode. Ensure BATCH=yes is set (Poudriere sets this by default). If a port genuinely hangs, you can set a build timeout in poudriere.conf:
shBUILDER_HOSTNAME=build.example.com MAX_EXECUTION_TIME=7200
Client says "no packages available":
Verify the repository URL is correct and accessible:
shpkg -vv | grep -A5 custom curl http://pkg.example.com/packages/14amd64-default-custom/meta.conf
Ensure pkg update -f was run after creating the repository configuration.
Packages built but options do not match:
Verify options are stored in the correct directory:
shls /usr/local/etc/poudriere.d/14amd64-default-custom-options/
Check that the set name (-z custom) matches between poudriere options and poudriere bulk.
FAQ
How much disk space does Poudriere need?
It depends on the number of ports. A jail takes about 500 MB. The ports tree is about 1 GB. Building 50-100 ports with dependencies typically requires 5-15 GB for the work directory and 2-5 GB for the output packages. Use ZFS with compression enabled to reduce disk usage significantly.
Can I build packages for a different FreeBSD version?
Yes. Create a jail with the target version: poudriere jail -c -j 13amd64 -v 13.4-RELEASE -a amd64. The packages will be compatible with FreeBSD 13.4 systems.
How do I add a new port to the build list?
Add the port to your pkglist file and run poudriere bulk again. Poudriere only builds new or updated packages; existing up-to-date packages are reused.
Can Poudriere use ccache to speed up rebuilds?
Yes. Install ccache on the host, then set CCACHE_DIR=/var/cache/ccache in poudriere.conf. Poudriere mounts the ccache directory inside build jails. This significantly speeds up rebuilds when only port options change but the source code is the same.
How does Poudriere handle dependency chains?
Poudriere resolves the full dependency tree for every port in your list. If www/nginx depends on security/openssl, Poudriere builds OpenSSL first, even if it is not in your list. All dependencies are included in the output repository.
Can I use Poudriere inside a jail?
Poudriere needs access to ZFS (or significant disk space), devfs, and the ability to mount filesystems. Running Poudriere inside a jail is possible but requires jail configuration that allows mounting (allow.mount, allow.mount.devfs, allow.mount.zfs, etc.) and a delegated ZFS dataset.