Environment Caching
How caching works
When you run a command with packages — either inline (python@3.12:requests) or via a file (-e @pip:requirements.txt) — boks:
- Ensures the base image is present locally (pulling it like any other tool image)
- Computes a hash from the base image's digest (its exact content, not its tag string) and the package list (or file contents)
- Checks whether a cached image with that hash already exists
- If yes, uses the cached image immediately
- If no, builds a new image by running the package manager inside the base image, then caches the result
Hashing the digest rather than the tag matters because tags float: python:3.13 moves forward on every base-image rebuild. A cached environment is only reused while the base it was built on is still current — after a rebuild, the next run builds a fresh environment on the new base.
The cache is stored by your container runtime (Podman's local image store). Image layers are shared, so multiple environments that share the same base image don't duplicate base layer storage.
Cleaning the cache
Remove all environment images built by boks:
boks --clean
This removes images whose names begin with boks-env-. Base images (like boks.sh/python:3.12) are not removed.
Cache invalidation
The cache is invalidated automatically when:
- The base image changes — a different version (
python@3.12→python@3.13) or the same tag re-pulled after a base rebuild (the digest moved) - The package list changes (different packages or versions in inline syntax)
- The environment file changes (different file contents for
-e @provider:file)
There is no TTL or automatic expiry — environment images remain cached until you run boks --clean or remove them manually via podman rmi. Superseded environments (built on an older base digest) are not deleted automatically; boks --clean removes them along with everything else.
What does invalidate the cache automatically is the base image moving: floating tags like python:3.13 are re-pulled on a freshness window (default 7 days), and a rebuilt base changes the digest the env hash is built on — so the next run builds a fresh environment on the new base. See Core Concepts → Always up to date and Configuration → Freshness.
Package versions
A package spec like python@3.13:jinja>=1.0 resolves at build time, and the resolved set is what the env runs. The base image carries security patches (its tag floats patch versions — python@3.12 moves 3.12.11 → 3.12.12, not to 3.13); packages deserve the same discipline — security currency without feature drift — rather than re-resolving to latest on every rebuild.
Today, a base refresh forces a rebuild (the env hash covers the base digest), and that rebuild re-runs the package install against the new base — which re-resolves the spec and can pick up newer, potentially breaking package versions. The intended behavior is the opposite: a refreshed base should reinstall the same resolved package set that worked, so you get the base's security patches without your packages drifting out from under you. boks never mutates a built container, so "updating" a package set is always a from-scratch rebuild that reinstalls a known set — never an in-place upgrade.
This is tracked as future work (a lockfile that records the resolved set after a successful build and is replayed on rebuilds, with re-resolution triggered only by a CVE scan failure naming a locked package or an explicit request). Until it ships, pin exact versions in your spec (jinja>=1.0,<1.1) if you need cross-rebuild package stability. See the design note in the project's private docs for the full direction.
For exact reproducibility of the base, pin a digest (tool@sha256:… in boks.toml) — digest-pinned images are never refreshed.