When you're using (many) jails on FreeBSD, you may end up consuming a lot of storage for the pkg(8) cache. You may be keeping multiple copies of the base, ports and kmods pkg repositories and generating additional load on the FreeBSD infrastructure updating them. I sought to optimize local storage requirements and minimize the pkg repo updates.

Why?
Regular use of pkg(8) with jails leads to each jail having its own copy of the package repository (usually only FreeBSD-ports).
Additionally, packages that get installed in multiple jails will be downloaded multiple times.
This was a bigger problem than it is now with FastlyCDN.
I sought to re-use the pkg repositories and the cached packages across my jails.
The default locations of repositories are directories in /var/db/pkg/repos.
The default location of the package cache is /var/cache/pkg.
Installed packages are registered in /var/db/pkg/local.sqlite.
The rest of this document uses /jails as the base directory where all jails are installed.
Mine are created as thin jails using ezjail but the concept will work regardless.
I use the latest branch for FreeBSD-ports, not quarterly, everywhere. Even with a mix of latest and quarterly this should work.
# Example directory layout
/
├── var
│ ├── db
│ │ └── pkg
│ │ ├── local.sqlite
│ │ └── repos
│ │ └── FreeBSD-base
| │ └── FreeBSD-ports
| │ └── FreeBSD-ports-kmods
| ├── cache
| └── pkg
└── jails
├── jail1
| └── var
| └── db
| └── pkg
| └── local.sqlite
├── jail2
| └── var
| └── db
| └── pkg
| └── local.sqlite
├── jailN ... etc.
The latest version of these utilities are hosted in the pkg_utils git repo on Codeberg.
Base for utilities
You can install packages in another directory using pkg -r /jails/jailex1.
This requires write access to the pkg repos and cache directories under that path.
The current version of pkg has a pkg clean command, but that only works for the context it is operating in.
If you share the cache between jails, packages that are not installed on the host will get purged from the cache.
The solution to this is to nullfs-mount the repositories directory and the cache directory inside the jail (or whatever context you're working in).
mount -t nullfs /var/db/pkg/repos/FreeBSD-ports /jails/jailex1/var/db/pkg/repos/FreeBSD-ports
mount -t nullfs /var/cache/pkg /jails/jailex1/var/cache/pkg
Typing this time-and-again is tedious. So I started wrapping this in a script.
pkg wrapper(s)
Shell scripts, should all be POSIX compliant (no bashisms)
This is still very crude but works well for me. In all jails, I've disabled automatic updating of pkg repositories. On the host system, repos will auto-update as per default behavior.
# /usr/local/etc/pkg.conf
REPO_AUTOUPDATE = false;
The pkg-lib.sh script has shared functions for other scripts to use.
Mounting the FreeBSD-ports repo and the pkg cache into a jail by name is one of them.
The mount_jail_pkg and umount_jail_pkg take care of (un)mounting.
The pkg-upgrade.sh script mounts the ports repo and cache into the jail, and runs pkg upgrade in the jail and unmounts again.
pkg_clean
This Python script cleans up the shared /var/cache/pkg dir on the host system.
The only external package required is for zstandard (until the Python default switches to 3.14+) and on FreeBSD you have to install the Python sqlite package.
Takes into account all the packages installed in all the jails.
The README provides more detail.
Combine all SQLite database files in found under ${PKG_DBDIR}/repos. These are used to check if a pkg file in cache is up-to-date.
Combine all local.sqlite database files under ${localdb_glob}/var/db/pkg. These are used to check if a package is installed in any of the jails or vms.
Any package file (or symlink) that's not found in the local.sqlite files, it isn't installed anywhere, will be deleted.
