rules_podman
Daemonless, hermetic, Bazel-idiomatic Podman: static Linux engine + mac/win client toolchain, run/build/image-load rules, and a self-managed macOS podman machine.
| Latest | 0.0.2 |
|---|---|
| Versions | 2 |
| Category | Bazel rules |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/rules_podman/ |
| Source | github.com/tomato-bazel/rules_podman |
bazel_dep(name = "rules_podman", version = "0.0.2")
View source & releases on GitHub ↗
Hermetic, Bazel-idiomatic Podman: a pinned, daemonless Podman
toolchain plus bazel run rules for running, building, and loading
container images.
Status: v0.0.1
- A module extension that hermetically fetches Podman for the host platform, sha256-pinned (Linux amd64/arm64, macOS amd64/arm64, Windows amd64/arm64).
- A Bazel toolchain (
@rules_podman//podman:toolchain_type) so the binary is swappable viaregister_toolchains(...). - Three rules:
podman_run,podman_build,podman_image_load, with opt-in isolated container stores.
See CHANGELOG.md for what has shipped and docs/ for generated rule reference.
Daemonless on Linux
On Linux the toolchain is the fully-static, rootless
mgoltzsche/podman-static
bundle — podman plus its own OCI runtime (crun/runc), conmon, netavark,
pasta, aardvark-dns, and fuse-overlayfs. There is no daemon and no
service: podman forks conmon → crun directly. rules_podman fetches
and pins the whole bundle and generates a launcher that points podman at
the bundled runtimes, helpers, and configs. This is a genuine,
self-contained, hermetic container engine.
On macOS / Windows the toolchain is the official containers/podman
client. Those OSes have no Linux kernel, so Podman can only run Linux
containers through a podman machine VM (which runs a service inside) —
daemonless is not possible there, by the nature of the platform. The
client is still fetched + pinned hermetically and is handy for local
development; point it at a machine via CONTAINER_HOST, the rules’
url / connection attributes, or podman machine start.
So: on Linux (CI, prod) you get a hermetic daemonless engine; on a Mac or Windows dev box you get a pinned client that drives a local machine.
Self-managed machine on macOS (podman_machine)
Instead of “bring your own podman machine”, //podman/machine:machine.bzl
provides a self-managed, pinned Podman service VM on macOS by
composing rules_macvm: it
renders an Ignition file (inject an SSH key, enable podman.socket) and
EFI-boots a bootable Podman/FCOS image via vfkit, exposing the API socket
over vsock. Point CONTAINER_HOST at that socket and the rules below
drive containers inside it.
load("@rules_podman//podman/machine:machine.bzl", "podman_machine")
podman_machine(
name = "machine",
image = "//path:fcos.raw", # a bootable Podman/FCOS disk
ssh_authorized_keys = ["ssh-ed25519 AAAA… you@host"],
)
The rendered Ignition and the VM spec are golden-tested, but the live boot/connect path is not exercised in CI (Apple Virtualization.framework can’t run there) — validate on a Mac with a real bootable image. Windows still uses bring-your-own WSL2 machine.
Hermetic container stores
The rules take a storage attribute (engine toolchains only):
default— podman’s standard rootless store (shared, persists).ephemeral— a throwawayvfsstore per invocation (--root/--runroot/--storage-driver=vfs), removed on exit. Host-independent and reproducible.workspace— a persistent store under$BUILD_WORKSPACE_DIRECTORY.
(vfs is used for isolation because it needs no /dev/fuse or extra
privileges — it runs anywhere, including locked-down CI.)
Install
.bazelrc:
common --registry=https://registry.fastverk.com/
common --registry=https://bcr.bazel.build/
MODULE.bazel:
bazel_dep(name = "rules_podman", version = "0.0.1")
podman = use_extension("@rules_podman//podman:extensions.bzl", "podman")
# Optional: pin a specific Podman version (defaults to the bundled pin).
# podman.toolchain(version = "5.8.2")
use_repo(podman, "podman")
register_toolchains("@podman//:podman_toolchain_def")
Usage
load("@rules_podman//podman:defs.bzl", "podman_run", "podman_build", "podman_image_load")
# `bazel run //:podman -- ps -a` — args forwarded verbatim. Daemonless on Linux.
podman_run(name = "podman")
# `bazel run //:image` — stage the context + `podman build`, in a clean store.
podman_build(
name = "image",
srcs = ["Containerfile", "app/server.py"],
image_tags = ["registry.example.com/app:latest"],
storage = "ephemeral",
# build_args = {"VERSION": "1.2.3"},
)
# `bazel run //:load` — `podman load -i` an OCI/docker archive
# (e.g. the output of @rules_oci's oci_load).
podman_image_load(
name = "load",
image = "//path/to:image.tar",
)
Every rule accepts url / connection to target a specific service and
extra_args to bake in flags. The bare binary is available as
@podman//:podman if you don’t want the launcher ergonomics.
Linux host notes
The engine is fully self-contained, but rootless Podman still leans on a
few host primitives for some workloads: newuidmap/newgidmap (the
uidmap package) for multi-UID containers, and iptables/nsenter for
certain network modes. Single-UID containers and pasta networking work
without them. podman info / build / load need none of it.
Maintenance
Bump the pinned Podman version (rewrites podman/private/known_versions.bzl
across both upstreams):
tools/refresh_versions.py # latest stable
tools/refresh_versions.py --version 5.8.2
Regenerate the committed rule docs after editing docstrings:
bazel run //docs:update Usage#
Real usage, taken from the module’s examples/.
examples/machine/BUILD.bazel
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@rules_macvm//vm:toolchains.bzl", "vm_provider")
load("@rules_podman//podman/machine:machine.bzl", "podman_machine")
load("@rules_shell//shell:sh_binary.bzl", "sh_binary")
# Our own mock VMM (rules_macvm's is dev-only and invisible to consumers).
sh_binary(
name = "mock_vmm",
srcs = ["mock_vmm.sh"],
)
vm_provider(
name = "mock_provider",
efi_boot = True,
kind = "mock",
linux_boot = True,
nested = True,
rosetta = True,
virtiofs = True,
vmm = ":mock_vmm",
vsock = True,
)
# A Podman service VM built against the mock backend, so the provisioning
# + VM spec are tested hermetically (no Apple Virtualization.framework).
# For a real machine, drop `provider` (uses the registered @vfkit
# toolchain) and point `image` at a bootable Podman/FCOS disk, on a Mac.
podman_machine(
name = "machine",
image = "fcos.raw",
provider = ":mock_provider",
ssh_authorized_keys = ["ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAID demo@rules_podman"],
)
# Golden: the rendered Ignition (ssh key + enable podman.socket).
diff_test(
name = "machine_ignition_test",
file1 = "machine.ignition.ign.golden",
file2 = "machine.ignition.ign",
)
# Golden: the resolved vfkit-style VM command line (EFI boot + vsock +
# nat + rosetta + the ignition).
diff_test(
name = "machine_argv_test",
file1 = "machine.argv.golden",
file2 = "machine.argv",
)
build_test(
name = "machine_build_test",
targets = [
":machine",
":machine.ignition",
],
)
examples/smoke/BUILD.bazel
load("@bazel_skylib//rules:build_test.bzl", "build_test")
load("@rules_podman//podman:defs.bzl", "podman_build", "podman_image_load", "podman_run")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
# `bazel run //examples/smoke:podman -- ps -a` — drive Podman. Daemonless
# on Linux; on macOS/Windows it talks to your `podman machine`.
podman_run(
name = "podman",
)
# `bazel run //examples/smoke:daemonless` — proves the Linux engine runs
# with no service: `podman info` against a throwaway vfs store. Run-only
# (needs the Linux engine toolchain + host userns); not wired as a test
# because nested user namespaces under the Bazel sandbox are environment-
# dependent.
podman_run(
name = "daemonless",
extra_args = ["info"],
storage = "ephemeral",
)
# Hermetic smoke: `podman --version` exits 0 and reports the pinned
# version, offline — no service required. Verifies the toolchain resolves
# and the fetched binary (daemonless launcher on Linux, client elsewhere)
# executes on the host.
sh_test(
name = "podman_version_test",
srcs = ["version_smoke.sh"],
args = [
"$(rootpath @podman//:podman)",
"5.8.2",
],
data = ["@podman"],
)
# `bazel run //examples/smoke:image` — builds `rules_podman/smoke:latest`
# from the local Containerfile in a throwaway vfs store (daemonless on Linux).
podman_build(
name = "image",
srcs = [
"Containerfile",
"app.txt",
],
image_tags = ["rules_podman/smoke:latest"],
storage = "ephemeral",
)
# Tar the context into a stand-in archive so the load rule has a real
# file input to wire up. (A `FROM scratch` tarball isn't a loadable OCI
# image — running this needs a genuine archive; the build_test below only
# covers analysis + launcher generation.)
genrule(
name = "sample_tar",
srcs = ["app.txt"],
outs = ["sample.tar"],
cmd = "tar -cf $@ -C $$(dirname $(location app.txt)) app.txt",
)
# `bazel run //examples/smoke:load` — `podman load -i sample.tar`.
podman_image_load(
name = "load",
image = ":sample.tar",
)
# Daemon-free coverage for podman_build / podman_image_load: analyze the
# rules and build their launcher scripts without executing them.
build_test(
name = "rules_build_test",
targets = [
":image",
":load",
],
)Rules & providers#
Generated with Stardoc from the module's .bzl sources.
from docs/defs.md
User-facing Bazel rules for rules_podman.
Three bazel run-friendly wrappers around the Podman binary resolved
through @rules_podman//podman:toolchain_type:
podman_run: a thin launcher — exec podman with optional--connection/--url, an optional isolated storage root, and any baked-inextra_args, forwarding CLI arguments verbatim.podman_build: stage a tracked build context (the rule’ssrcs) into a temp dir andpodman buildit. Inputs are real Bazel deps, so edits to the Containerfile / context invalidate the run.podman_image_load:podman load -i <tarball>an OCI/docker archive produced elsewhere in the graph (e.g.@rules_oci’soci_load).
Daemonless on Linux: the default toolchain there is the static
mgoltzsche/podman-static engine, so these rules run containers with no
service — podman forks conmon → crun directly. On macOS/Windows the
toolchain is the official client, which needs a podman machine; point
it at one via CONTAINER_HOST, url/connection, or a running machine.
Storage isolation (storage = "ephemeral"|"workspace") injects
--root/--runroot/--storage-driver=vfs so a run gets a clean,
host-independent container store. It only applies to engine toolchains
(those flags are server-side; a remote client ignores them).
For the bare binary depend directly on @podman//:podman.
podman_build
load("@rules_podman//podman:defs.bzl", "podman_build")
podman_build(name, srcs, build_args, connection, containerfile, extra_args, image_tags, storage,
storage_dir, url)
Build an image from a tracked build context via bazel run. Stages srcs into a temp context and runs podman build (daemonless on Linux).
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| srcs | The build context: the Containerfile plus everything it COPYs/ADDs. Staged into the build context preserving each file’s path relative to this rule’s package. | List of labels | required | |
| build_args | --build-arg KEY=VALUE pairs passed to podman build. | Dictionary: String -> String | optional | {} |
| connection | Named Podman system connection to target (--connection). | String | optional | "" |
| containerfile | Path of the Containerfile within the staged context (i.e. relative to this rule’s package). Must be among srcs. | String | optional | "Containerfile" |
| extra_args | Flags appended after the rules_podman defaults but before any args passed on the CLI. | List of strings | optional | [] |
| image_tags | Image tags to apply (-t). (Named image_tags, not tags, which is Bazel’s reserved target-tags attribute.) | List of strings | optional | [] |
| storage | Container-store isolation (engine/daemonless toolchains only): * default: use podman’s standard rootless store (shared, persists). * ephemeral: a fresh $TMPDIR/podman_store.XXXXXX with --storage-driver=vfs, removed on exit. Hermetic, but images don’t persist across runs. * workspace: a persistent store under $BUILD_WORKSPACE_DIRECTORY/<storage_dir> (requires bazel run). | String | optional | "default" |
| storage_dir | Workspace-relative store path used when storage = "workspace". Defaults to .cache/rules_podman/<target name>. | String | optional | "" |
| url | Podman service URL to target (--url), e.g. unix:///run/podman/podman.sock or an ssh:// endpoint. Overrides $CONTAINER_HOST for this invocation. | String | optional | "" |
podman_image_load
load("@rules_podman//podman:defs.bzl", "podman_image_load")
podman_image_load(name, connection, extra_args, image, storage, storage_dir, url)
podman load -i <tarball> an image archive into the target store via bazel run. Use storage = "workspace" to load into a persistent store.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| connection | Named Podman system connection to target (--connection). | String | optional | "" |
| extra_args | Flags appended after the rules_podman defaults but before any args passed on the CLI. | List of strings | optional | [] |
| image | An OCI-archive or docker-archive tarball to podman load, e.g. the output of @rules_oci’s oci_load rule. | Label | required | |
| storage | Container-store isolation (engine/daemonless toolchains only): * default: use podman’s standard rootless store (shared, persists). * ephemeral: a fresh $TMPDIR/podman_store.XXXXXX with --storage-driver=vfs, removed on exit. Hermetic, but images don’t persist across runs. * workspace: a persistent store under $BUILD_WORKSPACE_DIRECTORY/<storage_dir> (requires bazel run). | String | optional | "default" |
| storage_dir | Workspace-relative store path used when storage = "workspace". Defaults to .cache/rules_podman/<target name>. | String | optional | "" |
| url | Podman service URL to target (--url), e.g. unix:///run/podman/podman.sock or an ssh:// endpoint. Overrides $CONTAINER_HOST for this invocation. | String | optional | "" |
podman_run
load("@rules_podman//podman:defs.bzl", "podman_run")
podman_run(name, connection, extra_args, storage, storage_dir, url)
Run Podman via bazel run. Daemonless on Linux; CLI arguments are forwarded verbatim (bazel run //:podman -- ps -a).
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| connection | Named Podman system connection to target (--connection). | String | optional | "" |
| extra_args | Flags appended after the rules_podman defaults but before any args passed on the CLI. | List of strings | optional | [] |
| storage | Container-store isolation (engine/daemonless toolchains only): * default: use podman’s standard rootless store (shared, persists). * ephemeral: a fresh $TMPDIR/podman_store.XXXXXX with --storage-driver=vfs, removed on exit. Hermetic, but images don’t persist across runs. * workspace: a persistent store under $BUILD_WORKSPACE_DIRECTORY/<storage_dir> (requires bazel run). | String | optional | "default" |
| storage_dir | Workspace-relative store path used when storage = "workspace". Defaults to .cache/rules_podman/<target name>. | String | optional | "" |
| url | Podman service URL to target (--url), e.g. unix:///run/podman/podman.sock or an ssh:// endpoint. Overrides $CONTAINER_HOST for this invocation. | String | optional | "" |
from docs/extensions.md
Module extension for rules_podman.
Hermetically fetches Podman for the host platform, pinned by sha256
(see private/known_versions.bzl), and exposes it as @podman//:podman
plus a ready-to-register toolchain (@podman//:podman_toolchain_def).
On Linux it fetches the fully-static mgoltzsche/podman-static bundle
and generates a launcher that wires podman to its bundled OCI
runtime (crun), conmon, netavark, and configs — a real daemonless,
rootless engine, no service to start. On macOS/Windows it fetches the
official client binary (which talks to a podman machine).
Default usage:
podman = use_extension("@rules_podman//podman:extensions.bzl", "podman")
use_repo(podman, "podman")
register_toolchains("@podman//:podman_toolchain_def")
Pin a specific Podman version:
podman = use_extension("@rules_podman//podman:extensions.bzl", "podman")
podman.toolchain(version = "5.8.2")
use_repo(podman, "podman")
podman
podman = use_extension("@rules_podman//podman:extensions.bzl", "podman")
podman.toolchain(version)
Sets up @podman: a daemonless static engine on Linux, the official client on macOS/Windows.
TAG CLASSES
toolchain
Attributes
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| version | Override the Podman version. Defaults to the value in known_versions.bzl. | String | optional | "" |
from docs/machine.md
podman_machine — a self-managed Podman service VM for macOS.
On Linux, rules_podman is daemonless (the static engine forks the OCI
runtime directly). macOS has no Linux kernel, so the podman service
must run inside a Linux VM. This macro composes rules_macvm to provide
that VM hermetically and reproducibly — the Docker-Desktop / podman machine architecture, but pinned and Bazel-native:
ignition_configrenders provisioning (inject an SSH key, enablepodman.socket) entirely from attrs.vmEFI-boots a bootable Podman/Fedora-CoreOSimagewith that Ignition, exposing the Podman API socket over virtio-vsock and NAT networking.
bazel run //:<name> boots it; point the client at the socket
(CONTAINER_HOST=unix://<socket>) and rules_podman’s podman_run /
podman_build / podman_image_load drive containers inside it.
VALIDATION BOUNDARY: the rendered Ignition and the VM spec/argv are golden-tested. The live boot + connect path needs a real Mac and a bootable Podman image, and is NOT exercised in CI (Apple Virtualization.framework can’t run in cloud CI). Treat the boot path as unvalidated until run on hardware.
podman_machine
load("@rules_podman//podman/machine:machine.bzl", "podman_machine")
podman_machine(name, image, ssh_authorized_keys, enable_units, cpus, memory, rosetta, socket,
extra_devices, provider, visibility, **kwargs)
Declare a self-managed Podman service VM.
PARAMETERS
from docs/toolchains.md
Toolchain rule for rules_podman.
podman_toolchain wraps a hermetically-fetched Podman binary as a
single Bazel toolchain. The user-facing rules (podman_run,
podman_build, podman_image_load) resolve their client through
@rules_podman//podman:toolchain_type, so a custom Podman (a
locally-built engine, a distro package, a different pinned version) can
be swapped in via register_toolchains(...) without touching rule
attributes.
The engine field tells the rules whether this is a real local engine
(Linux daemonless bundle) or a remote client (macOS/Windows). Storage
isolation (--root/--runroot/--storage-driver) is only injected for
engine toolchains — those flags are server-side and ignored by a client.
The module extension at @rules_podman//podman:extensions.bzl
generates a default toolchain (@podman//:podman_toolchain_def).
Register it from MODULE.bazel:
register_toolchains("@podman//:podman_toolchain_def")
podman_toolchain
load("@rules_podman//podman:toolchains.bzl", "podman_toolchain")
podman_toolchain(name, engine, podman, version)
Declare a Podman binary as a Bazel toolchain.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| engine | True if this binary is a local daemonless engine; False for a remote client. Gates storage-isolation flag injection in the rules. | Boolean | optional | False |
| podman | The podman executable target. | Label | required | |
| version | The Podman release version of this binary. Informational; surfaced on PodmanToolchainInfo for diagnostics. | String | optional | "" |
PodmanToolchainInfo
load("@rules_podman//podman:toolchains.bzl", "PodmanToolchainInfo")
PodmanToolchainInfo(podman, version, engine)
A Podman binary, resolved via a toolchain.
FIELDS
Conformance#
No gate findings. 8 contested atoms. See how gating works or the full report.
Contested atoms
Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.
| Atom | Resolved here | Elsewhere |
|---|---|---|
apple_support | 1.24.2 | 2.2.0 ×1 |
bazel_skylib | 1.8.2 | 1.9.0 ×2 |
nlohmann_json | 3.6.1 | 3.12.0.bcr.1 ×1 |
protobuf | 33.4 | 34.0.bcr.1 ×2 |
rules_jvm_external | 6.7 | 6.8 ×4 |
rules_python | 1.7.0 | 2.0.1 ×1 |
rules_swift | 3.1.2 | 3.6.1 ×1 |
upb | 0.0.0-20220923-a547704 | 0.0.0-20230516-61a97ef ×1 |
Dependencies#
Depends on
Versions#
2 published versions, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.0.2 latest | lJpk4PCDHQgcZM5z… | tag archive ↗ |
0.0.1 | kyvdMdv1vy6yJ7uW… | tag archive ↗ |
Changelog#
All notable changes to rules_podman. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.
0.0.2
- Fix: Linux daemonless engine couldn’t find conmon. podman locates
conmon and the OCI runtime via
containers.conf’s[engine] conmon_path/runtime, not via$PATHorCONTAINERS_HELPER_BINARY_DIR. The bundle’scontainers.confdoesn’t set them, so podman fell back to compiled-in absolute defaults (/usr/local/lib/podman/conmon, …) that don’t exist once Bazel relocates the bundle into runfiles — every container run failed withcould not find a working conmon binary. The generated launcher now emits aCONTAINERS_CONF_OVERRIDEre-pointingconmon_path/helper_binaries_dir/ the crun+runc runtimes at the relocated$ROOT. Other base-conf keys (cgroup_manager, events_logger) still apply. Adds an ubuntu-onlylinux-engineCI smoke that boots the engine (bazel run //examples/smoke:daemonless). - Fix: pull/run aborted with “no policy.json file found”. podman has no
env var or global flag for the image signature policy — it only reads
/etc/containers/policy.jsonor$HOME/.config/containers/policy.json. On a host with neither (a from-scratch CI image), the launcher now seeds the bundle’s (permissive)policy.jsonat the user-level default path, best-effort and never clobbering an existing one. Storage is left to podman’s rootless defaults (the bundle’sstorage.confis root-oriented —graphroot=/var/lib/containers).
0.0.1
- Initial scaffold via
rels scaffold. - Module extension
@rules_podman//podman:extensions.bzl%podmanthat hermetically fetches Podman (5.8.2, pinned by sha256) for Linux/macOS/Windows on amd64 + arm64, and emits@podman//:podmanplus a registerable@podman//:podman_toolchain_def.- Linux: daemonless. Fetches the fully-static, rootless
mgoltzsche/podman-staticbundle (podman + crun/runc + conmon + netavark + pasta + fuse-overlayfs) and generates a launcher wiring podman to the bundled runtimes/helpers/configs. No service required. - macOS/Windows: the official
containers/podmanclient (drives apodman machine; daemonless isn’t possible without a Linux kernel).
- Linux: daemonless. Fetches the fully-static, rootless
- Toolchain
@rules_podman//podman:toolchain_type+podman_toolchainrule (carries anengineflag), swappable viaregister_toolchains(...). - Rules
podman_run,podman_build,podman_image_loadin//podman:defs.bzl, each resolving the binary through the toolchain and acceptingurl/connection/extra_args.podman_run/_build/_image_loadalso takestorage = default|ephemeral|workspaceto isolate the container store (--root/--runroot/--storage-driver=vfs) on engine toolchains. podman_machine(//podman/machine:machine.bzl): a self-managed Podman service VM for macOS, composingrules_macvm— renders an Ignition (ssh key + enablepodman.socket) and EFI-boots a bootable Podman/FCOS image with the API socket over vsock. Provisioning + VM spec are golden-tested; the live boot/connect path needs a real Mac and is not exercised in CI. (Adds abazel_depon rules_macvm.)tools/refresh_versions.pyre-pins a version across both upstreams via the GitHub releases API; stardoc-generated reference underdocs/;//examples/smokecoverage (podman --versiontest + build_test, plus a:daemonlessrun-only demo).