Setting PKG_PATH Before You Touch pkg_add

OpenBSD does not auto-discover mirrors. If PKG_PATH is not set, pkg_add will fail with a confusing error or try to install from a local directory. Set it for your current OpenBSD version and architecture before running any package commands.

The canonical mirror URL format uses %v for the version number and %a for the architecture. Using these variables makes your shell profile portable across OpenBSD versions.

export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/%v/packages/%a/

# Add to /etc/profile or ~/.profile for persistence
echo 'export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/%v/packages/%a/' >> /etc/profile

Basic pkg_add Usage and What the Output Means

Installing a single package is straightforward. pkg_add resolves dependencies automatically and installs them before the requested package.

The output from pkg_add is more verbose than apt but less than portage. Pay attention to lines starting with "New dependencies" - those are packages being pulled in automatically. Lines starting with "--- " after installation are post-install messages from the package maintainer. Read them. They often contain mandatory configuration steps that are not performed automatically.

For example, installing nginx on OpenBSD requires enabling the rc.d service manually. The post-install message tells you this, but many sysadmins dismiss it.

# Install a single package
pkg_add nginx

# Install multiple packages at once
pkg_add git curl wget

# Install a specific version
pkg_add nginx-1.26.2

# Simulate installation without making changes
pkg_add -n nginx

Understanding Flavors: The Critical Difference from Linux Packaging

OpenBSD packages use a flavor system instead of separate package names or virtual packages. A flavor is a build variant of the same software compiled with different options. This is the concept most Linux admins miss the first time they use pkg_add.

Flavors are appended to the package name with a hyphen. To see available flavors before installing, use pkg_info -Q to search the repository.

The most common flavor distinction is between packages with and without X11 support. On headless servers, always install the no_x11 or no_x flavor when available. This avoids pulling in an entire X11 dependency tree onto a machine that will never display graphics.

PHP is the canonical example of flavor complexity on OpenBSD. The PHP package has flavors for different thread safety models and extension sets. When you run pkg_add php and multiple flavors exist, pkg_add will prompt you to choose unless you specify the flavor explicitly.

# Search for available flavors of a package
pkg_info -Q php

# Output will show:
# php-8.3.10
# php-8.3.10-fpm
# php-8.2.23
# php-8.2.23-fpm

# Install a specific flavor
pkg_add php-fpm

# Install vim without X11 support
pkg_add vim--no_x11

# The double hyphen separates package name from flavor
# Format: packagename--flavor
// advertisement

Searching the Package Repository

pkg_info -Q performs a case-insensitive substring search against the repository index. It does not require a separate index update step because OpenBSD fetches the package list directly from PKG_PATH at query time.

For packages already installed on the system, pkg_info without arguments lists everything. Combined with grep, this becomes your primary tool for auditing what is running.

The -Q flag searches both package names and their one-line descriptions. If you are looking for something and the exact name search returns nothing, try a broader term - OpenBSD package names sometimes differ from what you expect. The redis package is just redis, but the MongoDB client tools might be under a name you would not guess.

# Search repository for packages matching a string
pkg_info -Q postgres

# List all installed packages
pkg_info

# Show detailed info about an installed package
pkg_info nginx

# Show what files a package installed
pkg_info -L nginx

# Find which installed package owns a specific file
pkg_info -E /usr/local/sbin/nginx

# Search with a broader term if the specific name returns nothing
pkg_info -Q sql

Updating Packages: pkg_add -u vs syspatch

OpenBSD separates base system patches from package updates. syspatch handles security patches to the base OS. pkg_add -u handles everything in /usr/local, which is the package tree. These are two distinct operations and you need both.

Running pkg_add -u with no arguments updates all installed packages to the latest version available in the current PKG_PATH. This respects your flavor selections from initial installation. On a production server, always run this with -n first to see what would change.

One important behavior difference from Linux: pkg_add -u on OpenBSD does not prompt for confirmation by default. It will upgrade and potentially restart services if post-install scripts trigger rc.d calls. In our experience on production servers, always use -n to preview changes before a live update run.

After a major OpenBSD version upgrade (for example, moving from 7.5 to 7.6), you must update PKG_PATH to point to the new version's packages before running pkg_add -u. The packages directory structure is version-specific and you will get dependency conflicts if you try to update against the wrong version's repository.

# Preview all pending package updates without applying them
pkg_add -nu

# Update all installed packages
pkg_add -u

# Apply base system security patches (separate from packages)
syspatch

# Update PKG_PATH after a major version upgrade, then update packages
export PKG_PATH=https://cdn.openbsd.org/pub/OpenBSD/7.6/packages/amd64/
pkg_add -u

Removing Packages and Handling Dependencies

pkg_delete removes a package. By default it leaves behind any package that was installed as a dependency and is still required by something else. The -a flag removes orphaned dependencies - packages that were installed automatically and are no longer needed by anything.

Be careful with pkg_delete -a on systems where you have manually installed packages that happen to be dependencies of other things. The orphan detection looks at the dependency graph, not at your intentions. If you installed curl manually but it is also a dependency of git, removing git and then running pkg_delete -a will not remove curl because curl still satisfies the manual installation record.

The -f flag forces deletion even if other packages depend on the target. Use this only when you are about to reinstall the dependent packages or when removing a package chain you know is going away entirely.

# Remove a single package
pkg_delete nginx

# Remove a package and any orphaned dependencies left behind
pkg_delete nginx
pkg_delete -a

# Force remove a package even if others depend on it
pkg_delete -f nginx

# List packages that would be removed as orphans without removing them
pkg_delete -an
// advertisement

Using pkg_add in Scripts and Automated Provisioning

pkg_add is not fully non-interactive by default. When multiple flavors exist and you have not specified one, it prompts. When a package is already installed, it asks about upgrades. This breaks unattended provisioning scripts.

For scripted installations, use the -I flag to suppress interactive prompts. Combined with explicit flavor selection and version pinning, you can make pkg_add fully deterministic in a provisioning context.

If you are building OpenBSD provisioning workflows for multiple servers, a requirements-style file approach works well. Write a plain text file with one package per line, then pipe it through a loop. This is simpler than anything ports-based and works reliably in CI environments.

For teams automating OpenBSD server builds at scale, tools like taskbotshub.ai can manage multi-step provisioning pipelines that include pkg_add runs as discrete steps with proper error handling, retry logic, and audit logging - which raw shell scripts do not give you out of the box.

# Non-interactive installation, suppress prompts
pkg_add -I nginx git curl

# Install from a package list file
# packages.txt contains one package name per line
while read pkg; do
  pkg_add -I "$pkg"
done < /etc/packages.txt

# Check exit code - pkg_add returns non-zero on failure
pkg_add -I somepackage || { echo "pkg_add failed"; exit 1; }

# Quiet output for cleaner logs
pkg_add -Iq nginx

Mirror Selection and Troubleshooting PKG_PATH

The CDN mirror at cdn.openbsd.org is anycast and usually the fastest option globally. If you are in a network environment with restrictive egress filtering, you may need to use a specific regional mirror or an HTTP mirror instead of HTTPS.

OpenBSD maintains an official mirror list at https://www.openbsd.org/ftp.html. For air-gapped or restricted environments, you can mirror the packages directory with rsync and point PKG_PATH at a local path or internal HTTP server.

A common failure mode: you set PKG_PATH correctly but pkg_add still fails with "Can't open package". Check that the architecture subdirectory is correct. On amd64 hardware it is amd64, not x86_64. On ARM64 it is arm64. Run arch to confirm your platform if you are unsure.

Another failure: SSL certificate errors when using HTTPS mirrors. OpenBSD uses LibreSSL, not OpenSSL, and its certificate store is in /etc/ssl/cert.pem. If this file is missing or your base install did not complete cleanly, HTTPS connections from pkg_add will fail. In that case, temporarily use an HTTP mirror to install ca_root_nss or verify the base install integrity.

# Check your architecture
arch
uname -m

# Use a regional mirror (example: European mirror)
export PKG_PATH=https://ftp.eu.openbsd.org/pub/OpenBSD/%v/packages/%a/

# Use local mirror for air-gapped environments
export PKG_PATH=http://192.168.1.50/openbsd/%v/packages/%a/

# Use a local directory (e.g., from USB with packages)
export PKG_PATH=/mnt/usb/packages

# Verbose mode to debug connection issues
pkg_add -v nginx

Ports vs Packages: When pkg_add Is Not Enough

The OpenBSD ports tree gives you source-based builds with custom options. About 95% of the time, pkg_add against the binary repository is the right choice. Ports exist for the remaining 5%: when you need a specific build flag not exposed by flavors, when you are packaging software not yet in the tree, or when you need to patch upstream source before building.

Installing from ports requires checking out the ports tree at the correct tag matching your OpenBSD version. Do not mix ports checkouts with mismatched binary package installations. Dependencies will break.

For production servers, stick with binary packages. The OpenBSD project security-audits packages before committing them, and updates propagate to the binary tree quickly after patches land. Building from ports on production machines introduces operational complexity that rarely justifies itself.

If you are packaging your own software for internal distribution, pkg_create produces .tgz package files compatible with pkg_add. This is the correct approach for distributing internal tools across a fleet of OpenBSD machines. If you are naming a new internal tool or open-source project and need to check domain availability alongside project naming, nicename.me provides fast namespace and domain checks without requiring an account.

# Check out the ports tree matching your OpenBSD version
cd /usr
cvs -qd anoncvs@anoncvs.ca.openbsd.org:/cvs checkout -rOPENBSD_7_6 -P ports

# Build and install a port
cd /usr/ports/www/nginx
make install

# Create a binary package from a built port
make package

# The resulting .tgz can be installed on other machines
pkg_add /usr/ports/packages/amd64/all/nginx-1.26.2.tgz
// advertisement

Locking Packages and Preventing Unwanted Upgrades

OpenBSD does not have a built-in package hold mechanism equivalent to apt-mark hold. The practical alternative is to pin to a specific version by name when installing and to avoid running pkg_add -u without reviewing the -n output first.

For packages where version stability is critical - database servers, language runtimes, security tools - document the installed version and treat any upgrade as a deliberate change requiring a maintenance window. On our test servers running PostgreSQL 16.3, we treat major version upgrades as migrations requiring pg_upgrade, not routine pkg_add -u runs.

The pkg_info -m flag lists packages you installed manually, as opposed to those pulled in as dependencies. This is your audit list for what should be on the system. Comparing this list across servers in a fleet reveals configuration drift.

# List manually installed packages (excludes auto-installed dependencies)
pkg_info -m

# Show the installed version of a specific package
pkg_info nginx

# Save your manual package list to a file for documentation or replication
pkg_info -m | awk '{print $1}' > /root/installed-packages.txt

# Replicate that package list on another machine
cat /root/installed-packages.txt | xargs pkg_add -I

Post-Install Configuration: rc.d and rcctl

Installing a package does not start or enable the service. This is intentional OpenBSD design. After pkg_add, use rcctl to enable and start services. This is different from systemctl enable --now on Linux but the concept maps directly.

rcctl enable adds the service to /etc/rc.conf.local. rcctl start actually starts it. rcctl check verifies the current running state. The service name usually matches the package name but not always - check the post-install message from pkg_add, which will specify the exact rc.d service name.

Some packages install multiple rc.d scripts. PostgreSQL installs postgresql, not postgres. nginx installs nginx. PHP-FPM installs php83_fpm for PHP 8.3. Always read the post-install output.

# Enable a service to start at boot
rcctl enable nginx

# Start the service immediately
rcctl start nginx

# Check service status
rcctl check nginx

# Enable and configure a service with flags
rcctl enable mysqld
rcctl set mysqld flags --user=_mysql
rcctl start mysqld

# List all enabled services
rcctl ls on

# Disable and stop a service
rcctl disable nginx
rcctl stop nginx