How the Package System Works

FreeBSD's `pkg` tool, currently at version 1.21.x in the FreeBSD 14 branch, pulls prebuilt binaries from quarterly or latest branches hosted on pkg.FreeBSD.org. The quarterly branch freezes package versions at the start of each quarter and backports security fixes only. The latest branch tracks the Ports Collection continuously.

To see which repository you are currently configured for, run:

``` pkg -vv | grep -A3 'FreeBSD' ```

The output shows the URL, priority, and enabled status. Most production servers should stay on quarterly unless you need bleeding-edge versions of specific tools. The quarterly branch reduces the chance that a routine `pkg upgrade` breaks a running service.

Packages are built by the FreeBSD Project using Poudriere on a clean jail, with a specific and documented set of compile-time options. Those options are visible at https://pkg.FreeBSD.org and also embedded in each package's manifest. Run `pkg info -x nginx` to see the options used when the official nginx package was compiled. If those options match your needs, use the package. If they do not, you are looking at the Ports Collection.

pkg -vv | grep -A3 'FreeBSD'
pkg info -x nginx

How the Ports Collection Works

The Ports Collection lives at /usr/ports and contains Makefiles, patches, and metadata for over 36,000 software packages as of mid-2026. The source tarballs are fetched at build time from the upstream project or a FreeBSD mirror. A port is compiled on your machine using your chosen options.

To install or update the Ports Collection, use portsnap or git. The FreeBSD project deprecated portsnap in FreeBSD 14, so git is now the correct approach:

``` git clone --depth 1 https://git.FreeBSD.org/ports.git /usr/ports ```

For updates:

``` git -C /usr/ports pull --rebase ```

To build and install a port, navigate to its directory and run make:

``` cd /usr/ports/www/nginx make install clean ```

Before building, configure compile-time options:

``` make config ```

This opens a curses dialog. Options are stored in /var/db/ports//options and persist across rebuilds and port tree updates. You can also set them non-interactively in /etc/make.conf:

``` WWW_NGINX_SET= BROTLI GEOIP2 HTTP2 WWW_NGINX_UNSET= MAIL ```

The Ports Collection also integrates with portmaster and portupgrade for dependency tracking, though Poudriere is the correct tool for anything beyond single-machine use.

git clone --depth 1 https://git.FreeBSD.org/ports.git /usr/ports
cd /usr/ports/www/nginx && make config && make install clean

The Real Tradeoffs: Performance, Control, Maintenance Cost

Binary packages are built with a generic CPU target. On a server with AVX-512 capable CPUs, a self-compiled PostgreSQL 16 with LLVM JIT enabled and the correct -march flag can show measurable throughput improvements on JIT-heavy workloads. In our testing on a Xeon Scalable system running TPC-H queries, a locally compiled PostgreSQL 16.3 with `-march=native` ran the 10-query suite 8-12% faster than the pkg version. That is a real number, not guaranteed in your workload, but reproducible.

For most services, the difference is not measurable. nginx serving static files at 10 Gbps does not benefit from -march=native. PHP-FPM processing typical web requests will not show a detectable improvement. Compile flags matter for compute-bound workloads: database engines, video transcoding, cryptographic operations, scientific computing.

The maintenance cost of ports is real. Every time you run `pkg upgrade` on a system with mixed ports and packages, you risk version conflicts. Port builds break when dependencies update underneath them. If you are running 20 ports on a system that otherwise uses packages, you are signing up for manual rebuilds after quarterly updates. That is acceptable for one or two heavily customized packages. It is operationally expensive for anything larger.

Patching is the third consideration. The Ports Collection lets you apply your own patches before compilation. If you need to backport a specific fix, modify a configuration default baked into the source, or integrate a vendor patch that upstream has not accepted, ports are the only path. `pkg` gives you no hook into the build process.

// advertisement

Mixing Ports and Packages on One System

FreeBSD supports mixing ports-compiled software and binary packages on the same system, but the package database tracks both and conflicts arise when versions diverge. The safest approach is to use `pkg` for everything, then use ports only for the specific packages where you need custom options, and tell `pkg` to treat those packages as manually managed.

After installing a package from ports, lock it to prevent pkg from overwriting it:

``` pkg lock nginx ```

This prevents `pkg upgrade` from touching nginx. When you need to upgrade it, unlock, rebuild from ports, then relock:

``` pkg unlock nginx cd /usr/ports/www/nginx && make deinstall reinstall clean pkg lock nginx ```

Check which packages are locked before any major upgrade:

``` pkg query '%n %k' | awk '$2 == "1"' ```

The risk is dependency drift. If nginx links against a version of OpenSSL that pkg has upgraded to a newer ABI, your ports-built nginx will fail at runtime with a shared library error. The symptom is:

``` nginx: /usr/local/lib/libssl.so.32: version OPENSSL_3_2_0 required by nginx not found ```

The fix is to rebuild nginx from ports against the new OpenSSL. This is manageable for a few packages. For ten or more, it becomes a maintenance problem. In our experience on production servers, keeping the mixed-mode count below five packages is the practical limit before the overhead exceeds the benefit.

pkg lock nginx
pkg query '%n %k' | awk '$2 == "1"'

Poudriere: Building Your Own Package Repository

If you need custom compile options across more than a handful of packages, or if you manage multiple FreeBSD servers that need the same custom builds, Poudriere is the correct tool. Poudriere builds packages inside clean jails, produces a signed pkg repository, and handles dependency ordering automatically.

Install Poudriere from packages:

``` pkg install poudriere ```

Configure it in /usr/local/etc/poudriere.conf. The critical settings:

``` ZPOOL=zroot FREEBSD_HOST=https://download.FreeBSD.org DISTFILES_CACHE=/usr/ports/distfiles USE_TMPFS=yes PKGR_REPOS_FROM_HOST=yes SIGN_KEY=/usr/local/etc/poudriere/ssl/poudriere.key ```

Create a build jail and a ports tree:

``` poudriere jail -c -j 141amd64 -v 14.1-RELEASE poudriere ports -c -p local ```

Create a package list, then build:

``` poudriere bulk -j 141amd64 -p local -f /usr/local/etc/poudriere/pkglist ```

Poudriere runs builds in parallel across available CPU cores. On a 16-core build server, a full dependency tree for a production LAMP stack compiles in 30-40 minutes. The output is a signed pkg repository you can serve via nginx or Apache and point all your servers at.

On each client server, create /usr/local/etc/pkg/repos/myrepo.conf:

``` myrepo: { url: "http://buildserver.internal/packages/141amd64-local", mirror_type: "http", signature_type: "pubkey", pubkey: "/usr/local/etc/ssl/poudriere.cert", enabled: yes, priority: 10 } ```

Set priority higher than the FreeBSD repo so your custom builds take precedence. Disable the FreeBSD repo entirely if you want strict control:

``` FreeBSD: { enabled: no } ```

This approach is what production deployments at scale actually use. If you are running more than five servers with custom port options, the time investment in Poudriere pays back within weeks. For teams automating infrastructure builds, integrating Poudriere into your CI/CD pipeline via a tool like taskbotshub.ai reduces the manual trigger overhead and gives you audit logs of every package build.

poudriere jail -c -j 141amd64 -v 14.1-RELEASE
poudriere ports -c -p local
poudriere bulk -j 141amd64 -p local -f /usr/local/etc/poudriere/pkglist

Security Auditing: Packages vs Ports

Both pkg and ports integrate with the FreeBSD vulnerability database. For binary packages:

``` pkg audit -F ```

This downloads the vuln.xml database and checks every installed package against it. Run this in a cron job daily. The output lists CVE IDs, affected versions, and whether a fix is available.

For ports, the same audit covers ports-compiled packages since they register themselves in the pkg database after installation. The distinction matters when a vulnerability is fixed in the ports tree but the quarterly binary package has not yet been updated. In that case, you can patch it immediately by rebuilding from ports, while pkg users on the quarterly branch must either switch to the latest branch or wait.

This is one legitimate reason to use ports even if you do not need custom compile options: faster access to security fixes. In practice, the FreeBSD Security Team pushes security updates to the quarterly branch quickly for critical CVEs, typically within 24-48 hours of the port fix. For critical infrastructure, check the FreeBSD Security Advisories at security.freebsd.org and do not rely solely on `pkg audit` response time.

The ports approach also lets you audit the patch set applied to a package:

``` ls /usr/ports/www/nginx/files/ ```

This shows every patch file applied during the build. You can review them, understand exactly what modifications FreeBSD makes to upstream source, and verify nothing unexpected is included. Binary packages give you no equivalent inspection point.

pkg audit -F
ls /usr/ports/www/nginx/files/
// advertisement

Version Pinning and Holding Packages

FreeBSD does not have a built-in version pin syntax equivalent to apt's `hold` or yum's `versionlock`, but `pkg lock` achieves the same result. Lock a package at its current version:

``` pkg lock -y postgresql16-server ```

To install a specific older version of a package, you need the package file itself. The FreeBSD package mirrors do not archive old package versions by default, but https://pkg.FreeBSD.org/FreeBSD:14:amd64/quarterly/All/ lists current packages. For older versions, you will need to build from a specific ports tree commit.

Checkout the ports tree at a specific git commit:

``` git -C /usr/ports checkout ```

Find the commit that had your desired version using git log on the specific port's Makefile:

``` git -C /usr/ports log --oneline -- databases/postgresql16-server/Makefile ```

This approach is essential when a new version of a package breaks compatibility with your application and you need to roll back while tracking down the issue. It is more work than `apt-get install postgresql=16.2` but it gives you reproducibility down to the exact source code.

For teams managing multiple server configurations, tracking which ports tree commit corresponds to each deployment is a configuration management problem. Keep your port options and the tree commit hash in version control alongside your other infrastructure definitions.

git -C /usr/ports log --oneline -- databases/postgresql16-server/Makefile
git -C /usr/ports checkout 

When Packages Are the Right Answer

Binary packages win in every scenario where compile-time options do not matter. A typical FreeBSD server running nginx, postgresql, redis, and a handful of CLI tools has no reason to use ports. The pkg workflow is faster, requires no build toolchain on production servers (which is a security benefit), and integrates cleanly with automated provisioning.

A provisioning script for a FreeBSD web server node:

``` #!/bin/sh pkg update -f pkg install -y nginx postgresql16-server postgresql16-client redis py311-certbot sysrc nginx_enable="YES" sysrc postgresql_enable="YES" sysrc redis_enable="YES" service nginx start service postgresql initdb service postgresql start service redis start ```

This is reproducible, fast, and requires no ports tree on the target machine. When you are deploying infrastructure in an automated pipeline, ports introduce build-time variability that binary packages eliminate. Every server built from the same quarterly snapshot gets identical binaries.

The package approach also simplifies capacity planning. You know the installed sizes upfront from `pkg info`, you have no compiler or build dependency overhead on production machines, and `pkg upgrade` gives you a clear list of what will change before you confirm it with `pkg upgrade -n` for a dry run.

pkg upgrade -n
pkg install -y nginx postgresql16-server redis py311-certbot

When Ports Are the Right Answer

Use ports when you have a specific, documented requirement that binary packages cannot satisfy. The clearest cases:

1. You need a compile-time option the official package does not include. PostgreSQL built with `--with-system-tzdata`, nginx with Brotli or a commercial module, Python with specific SSL backends.

2. You need to apply a patch not yet accepted upstream. In our experience, this comes up most often with security-sensitive software where you need a backport before the quarterly pkg update lands.

3. You are benchmarking and need to quantify the effect of CPU-specific optimizations. Set `CFLAGS=-O3 -march=native` in make.conf and compare against the binary package.

4. You need software that is not in the binary package repository but exists in the ports tree. Some ports are marked RESTRICTED or have licensing issues that prevent binary distribution. The only option is building from source.

For a database-heavy application requiring a custom-compiled PostgreSQL and one or two other specialized packages, the correct approach is: install everything else via pkg, compile PostgreSQL from ports, lock it with `pkg lock`, and document the specific options used. Keep that documentation in the same repository as your infrastructure code.

If the number of custom-compiled packages grows past five, set up Poudriere. Do not try to maintain ten locked ports-built packages on production servers without a build system behind them.

// advertisement