Skip to content
FreeBSDHow-To July 11, 2026 4 min read

How to Set Up a FreeBSD Poudriere Build Server for Custom Packages

Building your own package repository with Poudriere, from jail creation through a working repo other machines can actually pull packages from.

Poudriere builds FreeBSD packages from ports inside clean, disposable jails, which is what makes it fundamentally different from building ports directly on a running system: every build starts from a known-clean base, and a broken build never leaves behind partial state that contaminates the next one. Setting up a Poudriere server that produces a repository other machines can actually use is a few distinct steps, each with its own failure modes.

Install Poudriere and create the ports tree

pkg install poudriere
poudriere ports -c

poudriere ports -c creates and checks out a ports tree specifically managed by Poudriere, separate from any /usr/ports checkout that might already exist on the system — Poudriere deliberately keeps its own copy so that jail builds are reproducible against a known snapshot rather than whatever state a system-wide ports tree happens to be in at build time.

Create the build jail

poudriere jail -c -j 141amd64 -v 14.1-RELEASE -a amd64

This creates a jail environment (141amd64 is just a label — name it however makes sense for your setup) based on FreeBSD 14.1-RELEASE for amd64. Poudriere fetches the actual base system for this jail from the FreeBSD release distribution, not from the host’s own installed system, which is what guarantees builds happen against a clean, specific release rather than whatever the build server itself happens to be running. You can maintain multiple jails at different FreeBSD versions or architectures simultaneously — this is one of Poudriere’s genuine advantages over building ports directly, since a single host can produce packages for several target releases without needing separate physical or virtual machines for each.

Configure which ports to build

Poudriere builds from a list file rather than “everything in the ports tree” by default, which is almost always what you want — building the entire ports tree is enormously time-consuming and rarely necessary:

mkdir -p /usr/local/etc/poudriere.d
cat > /usr/local/etc/poudriere.d/mylist <<'EOF'
www/nginx
databases/postgresql16-server
security/sudo
shells/bash
EOF

List one port origin per line, matching the same category/portname format used elsewhere in the ports tree. This file is what you’ll reference on every build invocation, and maintaining it under version control (even a simple git repo) is worth doing on any build server used by more than one person, since it becomes the actual source of truth for what the repository is supposed to contain.

Run the actual build

poudriere bulk -j 141amd64 -f /usr/local/etc/poudriere.d/mylist

This builds every port in the list (and their dependencies, automatically resolved) inside the specified jail, producing packages and, crucially, a full build log per port under Poudriere’s data directory (/usr/local/poudriere/data/logs/ by default) — when a build fails, the per-port log is the first place to look, since it contains the actual compiler/configure output rather than just a pass/fail summary.

Poudriere’s web-based monitoring interface (a set of static files plus a small JSON status feed, servable from any web server pointed at Poudriere’s data directory) gives real-time build progress across all ports in the batch, which matters on larger lists where a single bulk run might take hours — watching raw terminal output for that duration is impractical, and the web UI is specifically designed to make long batch builds observable without needing to tail dozens of parallel log files by hand.

Serving the resulting repository

A successful bulk build produces a package repository on disk (/usr/local/poudriere/data/packages/141amd64-mylist/ by default) in the exact layout pkg expects — repository metadata plus the actual .pkg files. Serve this directory over HTTP(S) with any web server (nginx is a common, simple choice, and conveniently is often one of the first things built with Poudriere itself):

server {
    listen 80;
    server_name pkg.internal.example.com;
    root /usr/local/poudriere/data/packages/141amd64-mylist;
    autoindex on;
}

Pointing client machines at the new repository

On machines that should pull packages from this repository instead of (or alongside) the official FreeBSD package mirrors, add a repository configuration file:

mkdir -p /usr/local/etc/pkg/repos
cat > /usr/local/etc/pkg/repos/local.conf <<'EOF'
local: {
  url: "http://pkg.internal.example.com",
  enabled: yes
}
EOF

If you want to replace the default FreeBSD repository entirely rather than adding a second one, also disable the built-in FreeBSD repo definition in the same directory (FreeBSD: { enabled: no }), otherwise pkg will consider packages from both sources, which can create confusing version conflicts if the same port exists in both with different build options or versions.

Keeping it current

Poudriere builds are a snapshot, not a continuously updated feed — running poudriere bulk again with an updated ports tree (poudriere ports -u first, to update the tree Poudriere tracks) is what actually produces newer package versions. Most production Poudriere setups run this on a schedule (a cron job invoking ports -u followed by bulk against the maintained list file) rather than manually, since a package repository that silently stops updating is a worse failure mode than one that simply doesn’t exist — machines pulling from it will happily keep installing stale packages indefinitely without any obvious signal that the repository itself has gone stale.