Custom Providers
boks ships with built-in providers for the common package managers (pip, npm,
apk, …). If you need one it doesn't cover — a niche manager, an internal tool,
or a different install convention — you can declare it in boks.toml without
writing Rust or opening a PR.
The idea
A provider is almost entirely data: the only thing that varies between built-in providers is the install command. A custom provider is therefore just two templated commands, and boks runs them through the same build path a built-in provider uses — same base image, same vulnerability scan, same content-hash cache. There is no plugin system, no WASM, no separate process.
Declaring a provider
Add a [package_providers.<name>] table to boks.toml:
[package_providers.mypm]
install_command_inline = "RUN mypm install {packages}"
install_command_file = "RUN mypm install -r /tmp/{file}"
Two placeholders are substituted at build time:
| Placeholder | In | Replaced by |
|---|---|---|
{packages} | install_command_inline | the space-joined inline package list (e.g. numpy pandas from tool:numpy,pandas) |
{file} | install_command_file | the copied manifest's filename (e.g. requirements.txt) |
Each value is a Dockerfile RUN instruction, run as root during podman build
— exactly like a built-in provider's install command. The manifest file is
COPY'd to /tmp/ automatically before the file-based RUN runs.
Using it
A custom provider is invoked explicitly with -e @<name>:
# File-based: install from a manifest
boks -e @mypm:project.deps alpine@3.21 cat /tmp/installed.txt
# Inline packages: the explicit provider selects mypm for the install
boks -e @mypm alpine@3.21:pkg1,pkg2 cat /tmp/installed.txt
Note the @<version> form (tool@3.21) when you also pass inline packages:
tool:3.21 would be parsed as image tool with package 3.21, since :
introduces the package list. The bare tool:pkg1,pkg2 form (without -e)
infers a built-in provider from the image name, so a custom provider is
always selected explicitly via -e @name.
The tool named here is always an indexed tool or one you mapped in your own
boks.toml — boks resolves from those two places and nowhere else, so a raw
image name (alpine, ubuntu, or a registry path) is not a usable base.
Auto-detection (-e @name:. or -e @name with no file) does not apply to custom
providers in this version — a custom provider has no default manifest filenames,
so you always name the file (or the inline packages) explicitly.
The base image
A custom provider builds on the tool's resolved base image — the same image
boks <tool> would pull, with the same freshness window (image_max_age_secs)
and the same digest-based cache invalidation. When the base image is rebuilt
upstream (a CVE fix, a new minor version), the cached environment rebuilds
automatically on the new base.
You cannot name an arbitrary FROM image in a custom provider. This is
deliberate: an arbitrary base would bypass boks's pull, freshness, and scan
path. If you need a different base, pin the tool image in [tools] and the
provider builds on top of it:
[tools]
mytool = "my-registry.example.com/mytool:custom"
[package_providers.mypm]
install_command_inline = "RUN mypm install {packages}"
install_command_file = "RUN mypm install -r /tmp/{file}"What a custom provider cannot do
These are deliberate v1 limits, not bugs:
- No
package_rulesmatching. Built-in providers can declare rules like "mount~/.cache/huggingfacewhenhuggingface_hubis installed" because they ship a manifest parser. A custom provider has no parser, so package-driven dotfile mounts and capability auto-enablement don't apply to it — the same as the built-ingem/hex/luarocksproviders, whose manifests are DSLs rather than data. If you need that, use the matching built-in provider (e.g.pip). - No auto-detection. A custom provider is never inferred from an image name
and has no default manifest filenames. Always invoke it with
-e @name. - No custom base image. See above.
Security model
A custom provider does not weaken boks's security posture:
- The scan gate still runs. A custom-provider environment is scanned like
any other built image. The only way to skip it is the existing
security_scan_enabled = falseor--skip-scan, which apply to every provider equally. - No aliasing of built-in names. A custom provider's name must be ASCII
alphanumeric and may not shadow a built-in (
pip,npm, …). Both are enforced at config load, so a[package_providers.pip]table is rejected immediately rather than silently overriding the curated provider. - No host-path backdoor. A custom provider has no index entry, so it
participates in no
package_rules— it cannot request dotfile mounts or capabilities. The install command runs in the build container, not on the host.
The install command itself is yours to author: boks does not sanitize the
template (it's your provider, run as RUN in podman build exactly like a
built-in). The inline package list is whitespace-validated upstream, so
{packages} cannot carry a newline or shell metacharacter injected through the
package list — but the template string is taken verbatim, by design.