Configuration Reference
podbox searches for a definition file in this order:
./.podbox.toml(project-local)- Active context from
~/.config/podbox/.active(set viapodbox use <name>) ~/.config/podbox/*.toml(first file, sorted by name)- Embedded default (
fedora:44, namepodbox)
[image]
| Key | Type | Default | Description |
|---|---|---|---|
base | string | required | Base container image (e.g. "fedora:44") |
name | string | required | Image tag name (e.g. "myenv") |
image | string | — | Prebuilt image reference (e.g. "ghcr.io/user/myenv:latest"). When set, podbox uses the registry image instead of building from base |
pull_retry | int | 3 | Number of pull retries on failure |
pull_retry_delay | string | "5s" | Delay between pull retries |
[image.packages]
| Key | Type | Default | Description |
|---|---|---|---|
install | string[] | [] | Packages to install via the distro package manager |
remove | string[] | [] | Packages to remove |
manager | string | auto-detected | Package manager override: dnf, apt, pacman, apk, zypper. Auto-detected from the base image name when omitted |
[image.run]
| Key | Type | Default | Description |
|---|---|---|---|
commands | string[] | [] | Extra RUN commands in the Containerfile |
1[image]2base = "fedora:44"3name = "myenv"4 5[image.packages]6install = ["git", "gcc", "ripgrep"]7remove = ["vim-minimal"]8 9[image.run]10commands = ["dnf clean all"][container]
| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Container name (used for systemd unit names, socket paths) |
home | string | required | Host path for isolated home (~ expands) |
shell | string | "bash" | Default login shell inside the container |
memory | string | — | Memory limit (e.g. "4G", "2048M"). Passed as Memory= in Quadlet |
cpus | string | — | CPU limit (e.g. "2.0", "0.5"). Passed through to Podman as --cpus |
slice | string | "podbox.slice" | systemd slice for the container service |
cpu_weight | integer | 200 | systemd CPU contention weight (1–10000) |
reload_cmd | string | — | Command run on config reload. Passed as ReloadCmd= in Quadlet |
[container.mounts]
| Key | Type | Default | Description |
|---|---|---|---|
extra | string[] | [] | Extra Volume= lines (e.g. "~/Work:/home/user/Work:z") |
[container.env]
| Key | Type | Default | Description |
|---|---|---|---|
* | string | — | Arbitrary environment variables passed to the container; explicit values always win over derived ones below |
forward | string[] | [] | Host variables to forward for enter, exec, and run; supports exact names and PREFIX_* patterns |
Derived variables (engine-set from the rest of the config, no TOML needed):
| Variable | Derived from | Example |
|---|---|---|
SHELL | container.shell (fish → /usr/bin/fish, bash → /bin/bash, zsh → /bin/zsh, full paths verbatim) | shell = "fish" ⇒ SHELL=/usr/bin/fish |
1[container]2name = "myenv"3home = "~/containers/myenv"4shell = "zsh"5 6[container.mounts]7extra = ["~/Projects:/home/user/Projects:z"]8 9[container.env]10EDITOR = "nvim"11TERM = "xterm-256color"12forward = ["SSH_AUTH_SOCK", "AWS_*"]13 14[container.services]15redis = "redis-server /etc/redis/redis.conf"16postgres = { command = "postgres -D /home/user/pgdata", restart = "on-failure" }Services run under the guest daemon (the container's init stays Podman's).
Short form restarts on failure; long form takes restart = "never",
"on-failure", or "always", plus an env table. Logs go to
/run/podbox/services/<name>.log. Services don't block idle shutdown.
For one-shot setup inside the home — [dotfiles] and [[provision]] — see
Home Setup. Both keys are documented there with their behaviour.
[security]
| Key | Type | Default | Description |
|---|---|---|---|
apparmor | string | — | AppArmor profile name. Passed as AppArmor= in Quadlet ("unconfined" to disable) |
seccomp | string | — | Seccomp profile path, "default", or "unconfined". Passed as SeccompProfile= |
security_label_disable | bool | true | Disable SELinux process labeling. Emits SecurityLabelDisable=true when set |
no_new_privileges | bool | true | Block privilege escalation via setuid binaries (sudo, su, AUR helpers). Emits NoNewPrivileges=true in the Quadlet. Set false to allow. |
read_only_rootfs | bool | false | Make root filesystem read-only. Emits ReadOnly=true in Quadlet |
userns | string | — | User namespace mode override. Defaults to "keep-id". Supported: "keep-id", "nomap", "private" |
cap_preset | string | "default" | Capability preset. Options: "none", "default", "monitoring", "admin". Adds a predefined set of --cap-add entries alongside any cap_add list below |
cap_add | string[] | [] | Extra Linux capabilities to add (e.g. ["SYS_ADMIN"]). Combined with cap_preset caps |
secrets | table[] | [] | Secrets passed to the container without baking them into the image (see below) |
[security].secrets
Values the container needs but the image must never contain. Short form
reads a podman secret and exposes it as a same-named variable:
[security]secrets = ["openai_key"] # Secret=openai_key,type=env,target=openai_keyUse the long form for a different target, a file instead of a variable, a mode, or a systemd credential source:
1[[security.secrets]]2name = "aws_creds"3type = "mount" # env (default) or mount4target = "/run/secrets/aws" # destination name inside the container5mode = "0400"6 7[[security.secrets]]8name = "gh_token"9source = "systemd" # podman (default) or systemd10target = "GH_TOKEN" # Environment=GH_TOKEN=%d/gh_token| Key | Type | Default | Description |
|---|---|---|---|
name | string | required | Secret name to read |
type | string | "env" | env (environment variable) or mount (file in the container) |
target | string | secret name | Destination name inside the container |
mode | string | — | File mode for mount secrets |
source | string | "podman" | podman reads podman secret; systemd reads a systemd credential |
Don't mix forms: all bare strings, or all tables. podbox doctor checks
the named secrets exist.
1[security]2apparmor = "unconfined"3seccomp = "default"4read_only_rootfs = true5userns = "nomap"6cap_preset = "monitoring"7cap_add = ["SYS_ADMIN"][network]
| Key | Type | Default | Description |
|---|---|---|---|
mode | string | "pasta" | Network mode: "host", "bridge", "none", "pasta", "slirp4netns", "private". Defaults to "pasta" (user-space NAT with working networking). "private" is loopback only — no host sockets or localhost services. "host" shares the host network — choose it deliberately |
ports | string[] | [] | Port mappings ("hostPort:containerPort"). Emitted as PublishPort= in Quadlet (ignored in host mode) |
[network]mode = "pasta"ports = ["8080:80", "443:443"]offline = falseoffline = true forces Network=none while keeping your mode in the
definition. Container-wide only — no per-command override.
[storage.shared_caches]
Opt-in caches shared between podbox containers. Removing a container keeps its volumes.
1[storage.shared_caches]2cargo = true3mbx = true4rustup = false5 6[[storage.shared_caches.custom]]7name = "models"8container_path = "~/.cache/models"| Key | Type | Default | Path shared |
|---|---|---|---|
cargo | bool | false | ~/.cargo/registry, ~/.cargo/git |
npm | bool | false | ~/.npm |
pnpm | bool | false | ~/.local/share/pnpm/store |
pip | bool | false | ~/.cache/pip |
uv | bool | false | ~/.cache/uv |
yarn | bool | false | ~/.cache/yarn, ~/.yarn/berry/cache |
bun | bool | false | ~/.bun/install/cache |
composer | bool | false | ~/.cache/composer |
maven | bool | false | ~/.m2/repository |
gradle | bool | false | ~/.gradle/caches |
ccache | bool | false | ~/.cache/ccache |
go | bool | false | ~/go/pkg/mod |
rustup | bool | false | ~/.rustup |
mbx | bool | false | ~/.cache/mbx |
custom[].name | string | — | Built-in names are reserved |
custom[].container_path | string | — | Destination in the container, ~/… or absolute |
cargo shares only registry + git — ~/.cargo/bin holds binaries and stays
per-container. podbox cache list shows volumes, podbox cache prune NAME
removes one.
These volumes serve containers only. To reuse a cache you already keep on
the host, use [storage.host_caches] below.
[storage.host_caches]
Same toggles, different source: a directory you already keep on the host,
bind-mounted into the container. Use this when the host owns the cache;
use shared_caches when only containers use it.
Same built-ins as shared_caches — same relative path on both sides.
A cache whose host directory doesn't exist is skipped with a warning, not mounted: there is nothing on the host to reuse, and podman refuses to start a container whose bind source is missing. Create the directory (or run the tool once on the host) and re-enable to pick it up.
1[storage.host_caches]2npm = true3mbx = true4 5[[storage.host_caches.custom]]6name = "zig"7host_path = "~/.cache/zig"8container_path = "~/.cache/zig"| Key | Type | Default | Description |
|---|---|---|---|
cargo | bool | false | ~/.cargo/registry and ~/.cargo/git |
npm, pnpm, pip, uv, yarn, bun, composer, maven, gradle, ccache, go, rustup, mbx | bool | false | That tool's cache; see the table above for paths |
custom[].name | string | — | Label used in error messages; built-in names are reserved |
custom[].host_path | string | — | Path on the host, ~/… or absolute |
custom[].container_path | string | — | Destination in the container, ~/… or absolute |
custom mounts any host dir at any container path:
Mounts stay correct when host and container usernames differ (%h on the
host side, /home/%u on the container side). Unlike shared_caches there's
no :U remap — the host dir is already yours, and keep-id keeps it that
way inside.
Three things that bite
- One directory, two writers: a container can evict host cache entries, and any size budget applies to both.
- Share caches, not build state. Two simultaneous builds of the same workspace (cargo, mbx, …) collide on target dirs — serialise them.
- A path already in
[container.mounts].extrais refused. Two mounts for one destination would fail inside Podman with an opaque error. A hand-writtenmounts.extraentry keeps working unchanged.
[integration]
Controls which host resources are shared with the container.
| Key | Type | Default | Description |
|---|---|---|---|
wayland | bool | false | Share Wayland socket for GUI apps |
audio | bool | false | Share PipeWire/PulseAudio sockets |
gpu | string/bool | "auto" | GPU passthrough (true, false, "auto", "nvidia") |
dbus | bool | false | Enable D-Bus session bus access. Talk/own/preset rules without it are rejected |
notify | bool | false | Desktop notification forwarding |
xdg_open | bool | false | URI opening via host (xdg-open) |
clipboard | bool | false | Clipboard sharing |
sync_fonts | bool | false | Bind-mount ~/.fonts and ~/.local/share/fonts (read-only) when present on the host |
sync_icons | bool | false | Bind-mount ~/.icons and ~/.local/share/icons (read-only) when present on the host |
sync_themes | bool | false | Bind-mount ~/.themes and ~/.local/share/themes (read-only) when present on the host |
gpg_agent | bool | false | Forward GPG agent socket (S.gpg-agent). Sets GPG_TTY and GNUPGHOME |
git_identity | bool | false | Bridge host Git identity and safe.directory into the container. Needs git in the image — podbox never installs it, so this is a no-op without it |
host_exec | table | { enabled = false } | Host command execution (see [integration.host_exec] below) |
ssh_agent | bool | false | Forward SSH agent socket ($SSH_AUTH_SOCK). Requires Podman ≥ 5.6 |
GpuMode values
| TOML value | Meaning |
|---|---|
"auto" (default) | Detect available GPU devices at runtime |
true | Enable /dev/dri (Intel/AMD) |
false | Disable all GPU passthrough |
"nvidia" | Enable /dev/dri + NVIDIA device nodes |
[integration.hardware]
Device passthrough. The container has no devices until you pass them here.
Each becomes an optional AddDevice=-… — a host without it skips the device
instead of failing to start.
| Key | Type | Default | Passed through |
|---|---|---|---|
kvm | bool | false | /dev/kvm — nested virtualisation, Android emulators |
joystick | bool | false | /dev/input, /dev/uinput — gamepads and joysticks |
webcam | bool | false | /dev/video*, /dev/media* |
serial | bool | false | /dev/ttyUSB*, /dev/ttyACM* — microcontrollers |
yubikey | bool | false | pcscd socket and /dev/hidraw* — smartcards, 2FA |
[integration.hardware]webcam = truekvm = truepodbox doctor reports when the host is missing a device these would need.
[integration.host_exec]
| Key | Type | Default | Description |
|---|---|---|---|
enabled | bool | false | Allow container to execute commands on the host |
allowlist | table | (none) | Allowed commands as alias → path pairs. When set, only those commands may run; when absent, anything may run |
Example — restrict to git and systemctl:
[integration.host_exec]enabled = trueallowlist = { git = "/usr/bin/git", systemctl = "/usr/bin/systemctl" }Security note: no shell is involved (execve directly), so shell
injection can't happen. But the filter is a blocklist — an allowlisted
binary keeps its full host powers (git -C /root …, find -exec …,
python -c …). Prefer wrapper scripts that pin the exact arguments over
general-purpose tools. The filter also rejects harmless arguments containing
globs or parentheses.
[integration.xdg_dirs]
| Key | Type | Default | Description |
|---|---|---|---|
documents | bool | false | Mount host ~/Documents |
downloads | bool | false | Mount host ~/Downloads |
pictures | bool | false | Mount host ~/Pictures |
music | bool | false | Mount host ~/Music |
videos | bool | false | Mount host ~/Videos |
desktop | bool | false | Mount host ~/Desktop |
projects | bool | false | Mount host ~/Projects |
[integration.export]
| Key | Type | Default | Description |
|---|---|---|---|
apps | string[] | [] | App .desktop files to export (without .desktop suffix) |
bins | string[] | [] | Binary shims to generate in ~/.local/bin |
1[integration]2wayland = true3audio = true4gpu = "auto"5dbus = true6notify = true7xdg_open = true8clipboard = true9ssh_agent = true10sync_fonts = true11sync_icons = true12sync_themes = true13 14[integration.host_exec]15enabled = true16allowlist = { git = "/usr/bin/git" }17 18[integration.xdg_dirs]19documents = true20downloads = true21projects = true22 23[integration.export]24apps = ["gedit", "nautilus"]25bins = ["rg", "gcc"][lifecycle]
| Key | Type | Default | Description |
|---|---|---|---|
quadlet | bool | false | Generate Quadlet systemd files on podbox enable |
autostart | bool | false | Start container on user login (WantedBy=default.target) |
on_stop | string | "keep" | Container behavior on stop ("keep" or "remove") |
auto_update | bool | false | Add Label=io.containers.autoupdate=registry for auto-updates |
auto_checkpoint | bool | false | Tag the current image as checkpoint-prev before update or build --rebuild; podbox rollback restores that image |
idle_timeout | string | "off" | Idle timeout before guest daemon exits ("off", "30s", "5m", `"1h") |
1[lifecycle]2quadlet = true3autostart = true4on_stop = "keep"5auto_update = true6idle_timeout = "off"[systemd]
Custom systemd unit dependencies for the generated Quadlet.
| Key | Type | Default | Description |
|---|---|---|---|
requires | string[] | [] | Units that must be active before the container (Requires=) |
after | string[] | [] | Units the container should start after (After=) |
[systemd]requires = ["postgres.service", "redis.service"]after = ["network-online.target"][dbus]
D-Bus access control via xdg-dbus-proxy. Requires integration.dbus = true.
| Key | Type | Default | Description |
|---|---|---|---|
preset | string | "" | Preset filling talk for you: "flatpak", "gnome", "kde", "portal". Portal names are never granted via talk — they come through interface-scoped rules for notify / xdg_open (see dbus-proxy.md) |
talk | string[] | [] | D-Bus services the container can call (two-way). Adding a portal-family name re-grants the full portal surface — a warning is printed |
own | string[] | [] | D-Bus services the container can register on the host bus |
[dbus]preset = "gnome"1[dbus]2preset = "portal"3talk = [4 "org.freedesktop.Notifications",5 "org.mpris.MediaPlayer2.*",6]7own = [8 "org.mpris.MediaPlayer2.podbox_app",9]See dbus-proxy.md for the full behavior matrix.
[wayland]
The Wayland firewall. A companion service filters which protocol objects the container may use — screen capture, virtual input, and input methods are blocked unless you allow them.
| Key | Type | Default | Description |
|---|---|---|---|
firewall | bool | true | Filter Wayland protocol access through the compositor proxy |
blocked_interfaces | string[] | (see below) | Wayland globals to deny. Replaces the default list when set |
1[wayland]2firewall = true3blocked_interfaces = [4 "zwlr_screencopy_manager_v1",5 "ext_image_copy_capture_v1",6]Setting blocked_interfaces replaces the default list entirely — no merging.
Default block list
Screen capture (zwlr_screencopy_manager_v1, ext_image_copy_capture_v1),
window listing (ext_foreign_toplevel_list_v1), virtual pointers
(zwlr_virtual_pointer_manager_v1, zwlr_virtual_pointer_unstable_v1),
virtual keyboards and input methods (zwp_virtual_keyboard_manager_v1,
zwp_input_method_v1, zwp_input_method_v2, ext_input_method_v1), and
fake input (org_kde_kwin_fake_input).
Full Example
This is a reference example showing every available key with sane defaults. It is not a working config — most install lists, env vars, and mounts are placeholders. Pick only what you need; omitted keys use their defaults.
1# ── Image ──────────────────────────────────────────────2[image]3base = "fedora:44" # Base image for custom builds4name = "myenv" # Image tag name5image = "ghcr.io/user/myenv:latest" # Prebuilt ref (omit for custom builds)6pull_retry = 3 # Pull retry count7pull_retry_delay = "5s" # Delay between pull retries8 9[image.packages]10install = ["git", "gcc", "ripgrep"]11remove = ["vim-minimal"]12# manager = "pacman" # omitted = auto-detect from the image name13 14[image.run]15commands = ["dnf clean all"] # Extra RUN steps16 17# ── Container ──────────────────────────────────────────18[container]19name = "myenv" # Required; used for unit names and socket paths20home = "~/containers/myenv" # Required; isolated home directory (~ expands)21shell = "bash" # Default login shell22memory = "4G" # Memory limit (e.g. "4G", "512M", omitted = unlimited)23cpus = "2.0" # CPU limit (e.g. "2.0", "0.5", omitted = unlimited)24reload_cmd = "systemctl reload …" # systemd ReloadCmd (omitted = none)25 26[container.mounts]27extra = ["~/Work:/home/user/Work:z"]28 29[container.env]30EDITOR = "nvim"31TERM = "xterm-256color"32# forward = ["HTTP_PROXY", "AWS_*"] # Host vars copied in at exec time33 34# ── Caches ─────────────────────────────────────────────35# Both opt-in, same keys, different sources:36# shared_caches → podbox volumes, shared between podbox containers37# host_caches → a directory you already keep on the host, bind-mounted in38# rustup and the ~/.cargo/bin half of cargo hold libc-bound binaries and are39# never shared.40[storage.shared_caches]41cargo = true42npm = true43mbx = false44 45[storage.host_caches]46mbx = false # ~/.cache/mbx on both sides47 48[[storage.host_caches.custom]]49name = "zig"50host_path = "~/.cache/zig"51container_path = "~/.cache/zig"52 53# ── Security ───────────────────────────────────────────54[security]55apparmor = "unconfined" # AppArmor profile (omitted = none)56seccomp = "default" # Seccomp profile (omitted = none, "unconfined" = off)57security_label_disable = true # Disable SELinux labels (needed for Wayland)58no_new_privileges = true # Block setuid escalation (sudo, su, AUR helpers)59read_only_rootfs = false # Make rootfs read-only (requires writable volumes)60userns = "keep-id" # UserNS mode: keep-id, nomap, private (omitted = keep-id)61cap_add = ["SYS_PTRACE"] # Extra Linux capabilities (omitted = none)62secrets = ["openai_key"] # Podman secrets; no value ever lands in the image63 64# [[security.secrets]] # detailed form when you need type/target/mode65# name = "aws_creds"66# type = "mount"67# mode = "0400"68 69# ── Provisioning ───────────────────────────────────────70# One-shot setup inside the container home, after dotfiles.71[[provision]]72name = "rust-toolchain" # a-z0-9_- ; unique per container73run = "mise use -g rust@stable" # executed with sh -c from the home74timeout = "5m" # 30s / 5m / 1h (default 5m)75on_failure = "warn" # warn (default) or abort76env = { FOO = "bar" } # extra variables; no PODBOX_ keys77root = false # run as root inside the container78 79# ── Network ────────────────────────────────────────────80[network]81mode = "pasta" # host, bridge, none, pasta, slirp4netns, private (default: pasta)82ports = ["8080:80"] # Port mappings (ignored in host mode)83 84# ── Integration ────────────────────────────────────────85[integration]86wayland = true # Share Wayland socket87audio = true # Share PipeWire / PulseAudio88gpu = "auto" # GPU: true, false, "auto", "nvidia"89dbus = true # Enable D-Bus session bus90notify = true # Forward desktop notifications91xdg_open = true # Forward URI opening (xdg-open)92clipboard = true # Clipboard sharing93ssh_agent = false # Forward SSH agent (needs Podman ≥ 5.6)94gpg_agent = false # Forward GPG agent95sync_fonts = true # Sync ~/.fonts / ~/.local/share/fonts (ro)96sync_icons = true # Sync ~/.icons / ~/.local/share/icons (ro)97sync_themes = true # Sync ~/.themes / ~/.local/share/themes (ro)98 99[integration.hardware] # Host device passthrough (all opt-in)100webcam = false101kvm = false102 103[integration.host_exec]104enabled = false105allowlist = { git = "/usr/bin/git" } # Alias → absolute path (required when enabled)106 107[integration.xdg_dirs]108documents = false109downloads = false110pictures = false111music = false112videos = false113desktop = false114projects = false115 116[integration.export]117apps = ["gedit", "nautilus"] # Export .desktop files for these apps118bins = ["rg", "gcc"] # Create bin shims for these commands119 120# ── Lifecycle ──────────────────────────────────────────121[lifecycle]122quadlet = false # Generate systemd Quadlet files on enable123autostart = false # Start container on user login124on_stop = "keep" # Container behavior on stop: "keep" or "remove"125auto_update = false # Label for auto-updates (registry/local)126idle_timeout = "off" # Guest daemon idle timeout: "off", "30s", "5m", "1h"127 128# ── systemd dependencies ────────────────────────────────129[systemd]130requires = ["postgres.service", "redis.service"]131after = ["network-online.target"]132 133# ── D-Bus ──────────────────────────────────────────────134[dbus]135preset = "portal" # Named preset: flatpak, gnome, kde, portal ("" = none)136talk = ["org.freedesktop.Notifications"]137own = ["org.mpris.MediaPlayer2.podbox_app"]138 139# ── Wayland firewall ───────────────────────────────────140[wayland]141firewall = true # Enable Wayland protocol firewall142blocked_interfaces = [ # Blocked Wayland globals (default list)143 "zwlr_screencopy_manager_v1",144 "ext_image_copy_capture_v1",145]Omitted keys use their defaults. See the tables above for every supported key.