FreeBSD.software
Home/Guides/How to Build Custom FreeBSD Packages with Poudriere
tutorial·2026-04-09·9 min read

How to Build Custom FreeBSD Packages with Poudriere

Build custom FreeBSD packages with Poudriere: jail creation, ports tree setup, building packages with custom options, creating a local repository, and automating builds.

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:

sh
pkg install poudriere

The configuration file is at /usr/local/etc/poudriere.conf. Edit it:

sh
vi /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:

sh
poudriere 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:

sh
poudriere jail -l

Update the jail to the latest patch level:

sh
poudriere jail -u -j 14amd64

To build packages for a different architecture (e.g., for Raspberry Pi):

sh
poudriere 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:

sh
poudriere 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:

sh
poudriere ports -c -p default -m git+https

List ports trees:

sh
poudriere ports -l

Update the ports tree:

sh
poudriere ports -u -p default

You can maintain multiple ports trees (e.g., quarterly and latest):

sh
poudriere ports -c -p quarterly -B 2026Q1

Defining the Package List

Create a file listing the ports you want to build:

sh
mkdir -p /usr/local/etc/poudriere.d vi /usr/local/etc/poudriere.d/pkglist

List ports by their category/name:

shell
www/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:

sh
poudriere 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:

sh
poudriere 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:

sh
vi /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:

sh
poudriere bulk -j 14amd64 -p default -z custom -f /usr/local/etc/poudriere.d/pkglist

Poudriere will:

  1. Create a clean jail clone
  2. Resolve all dependencies for the listed ports
  3. Build packages in parallel (respecting dependency order)
  4. Produce a pkg repository 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:

sh
poudriere 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:

sh
ls /usr/local/poudriere/data/logs/bulk/14amd64-default-custom/latest/logs/errors/

Review individual build logs:

sh
cat /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:

sh
mkdir -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.

sh
pkg install nginx sysrc nginx_enable="YES"

Configure NGINX to serve the repository:

sh
vi /usr/local/etc/nginx/nginx.conf
nginx
server { listen 80; server_name pkg.example.com; location /packages/ { alias /usr/local/poudriere/data/packages/; autoindex on; } }
sh
service nginx start

Verify the repository is accessible:

sh
curl http://pkg.example.com/packages/14amd64-default-custom/

Configuring Clients

On each FreeBSD machine that should use your custom packages, create a repository configuration:

sh
mkdir -p /usr/local/etc/pkg/repos vi /usr/local/etc/pkg/repos/custom.conf
shell
custom: { 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:

sh
scp 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:

sh
vi /usr/local/etc/pkg/repos/FreeBSD.conf
shell
FreeBSD: { enabled: no }

Update the package catalog:

sh
pkg update -f

Install packages from your custom repository:

sh
pkg install nginx

Verify the package source:

sh
pkg info -o nginx pkg query "%R" nginx

Automating Builds

Create a script that updates the ports tree and rebuilds packages on a schedule:

sh
vi /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
sh
chmod 755 /usr/local/bin/poudriere-build.sh

Schedule it weekly with cron:

sh
crontab -e
shell
0 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):

sh
poudriere jail -d -j old-jail-name

Check disk usage:

sh
du -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:

sh
poudriere 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:

sh
BUILDER_HOSTNAME=build.example.com MAX_EXECUTION_TIME=7200

Client says "no packages available":

Verify the repository URL is correct and accessible:

sh
pkg -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:

sh
ls /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.

Get more FreeBSD guides

Weekly tutorials, security advisories, and package updates. No spam.