Skip to main content
On this page

Configuration Reference

podbox searches for a definition file in this order:

  • ./.podbox.toml (project-local)
  • Active context from ~/.config/podbox/.active (set via podbox use <name>)
  • ~/.config/podbox/*.toml (first file, sorted by name)
  • Embedded default (fedora:44, name podbox)

[image]

KeyTypeDefaultDescription
basestringrequiredBase container image (e.g. "fedora:44")
namestringrequiredImage tag name (e.g. "myenv")
imagestring—Prebuilt image reference (e.g. "ghcr.io/user/myenv:latest"). When set, podbox uses the registry image instead of building from base
pull_retryint3Number of pull retries on failure
pull_retry_delaystring"5s"Delay between pull retries

[image.packages]

KeyTypeDefaultDescription
installstring[][]Packages to install via the distro package manager
removestring[][]Packages to remove
managerstringauto-detectedPackage manager override: dnf, apt, pacman, apk, zypper. Auto-detected from the base image name when omitted

[image.run]

KeyTypeDefaultDescription
commandsstring[][]Extra RUN commands in the Containerfile
toml
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]

KeyTypeDefaultDescription
namestringrequiredContainer name (used for systemd unit names, socket paths)
homestringrequiredHost path for isolated home (~ expands)
shellstring"bash"Default login shell inside the container
memorystring—Memory limit (e.g. "4G", "2048M"). Passed as Memory= in Quadlet
cpusstring—CPU limit (e.g. "2.0", "0.5"). Passed through to Podman as --cpus
slicestring"podbox.slice"systemd slice for the container service
cpu_weightinteger200systemd CPU contention weight (1–10000)
reload_cmdstring—Command run on config reload. Passed as ReloadCmd= in Quadlet

[container.mounts]

KeyTypeDefaultDescription
extrastring[][]Extra Volume= lines (e.g. "~/Work:/home/user/Work:z")

[container.env]

KeyTypeDefaultDescription
*string—Arbitrary environment variables passed to the container; explicit values always win over derived ones below
forwardstring[][]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):

VariableDerived fromExample
SHELLcontainer.shell (fish → /usr/bin/fish, bash → /bin/bash, zsh → /bin/zsh, full paths verbatim)shell = "fish" ⇒ SHELL=/usr/bin/fish
toml
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]

KeyTypeDefaultDescription
apparmorstring—AppArmor profile name. Passed as AppArmor= in Quadlet ("unconfined" to disable)
seccompstring—Seccomp profile path, "default", or "unconfined". Passed as SeccompProfile=
security_label_disablebooltrueDisable SELinux process labeling. Emits SecurityLabelDisable=true when set
no_new_privilegesbooltrueBlock privilege escalation via setuid binaries (sudo, su, AUR helpers). Emits NoNewPrivileges=true in the Quadlet. Set false to allow.
read_only_rootfsboolfalseMake root filesystem read-only. Emits ReadOnly=true in Quadlet
usernsstring—User namespace mode override. Defaults to "keep-id". Supported: "keep-id", "nomap", "private"
cap_presetstring"default"Capability preset. Options: "none", "default", "monitoring", "admin". Adds a predefined set of --cap-add entries alongside any cap_add list below
cap_addstring[][]Extra Linux capabilities to add (e.g. ["SYS_ADMIN"]). Combined with cap_preset caps
secretstable[][]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:

toml
[security]
secrets = ["openai_key"] # Secret=openai_key,type=env,target=openai_key

Use the long form for a different target, a file instead of a variable, a mode, or a systemd credential source:

toml
1[[security.secrets]]
2name = "aws_creds"
3type = "mount" # env (default) or mount
4target = "/run/secrets/aws" # destination name inside the container
5mode = "0400"
6 
7[[security.secrets]]
8name = "gh_token"
9source = "systemd" # podman (default) or systemd
10target = "GH_TOKEN" # Environment=GH_TOKEN=%d/gh_token
KeyTypeDefaultDescription
namestringrequiredSecret name to read
typestring"env"env (environment variable) or mount (file in the container)
targetstringsecret nameDestination name inside the container
modestring—File mode for mount secrets
sourcestring"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.

toml
1[security]
2apparmor = "unconfined"
3seccomp = "default"
4read_only_rootfs = true
5userns = "nomap"
6cap_preset = "monitoring"
7cap_add = ["SYS_ADMIN"]

[network]

KeyTypeDefaultDescription
modestring"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
portsstring[][]Port mappings ("hostPort:containerPort"). Emitted as PublishPort= in Quadlet (ignored in host mode)
toml
[network]
mode = "pasta"
ports = ["8080:80", "443:443"]
offline = false

offline = 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.

toml
1[storage.shared_caches]
2cargo = true
3mbx = true
4rustup = false
5 
6[[storage.shared_caches.custom]]
7name = "models"
8container_path = "~/.cache/models"
KeyTypeDefaultPath shared
cargoboolfalse~/.cargo/registry, ~/.cargo/git
npmboolfalse~/.npm
pnpmboolfalse~/.local/share/pnpm/store
pipboolfalse~/.cache/pip
uvboolfalse~/.cache/uv
yarnboolfalse~/.cache/yarn, ~/.yarn/berry/cache
bunboolfalse~/.bun/install/cache
composerboolfalse~/.cache/composer
mavenboolfalse~/.m2/repository
gradleboolfalse~/.gradle/caches
ccacheboolfalse~/.cache/ccache
goboolfalse~/go/pkg/mod
rustupboolfalse~/.rustup
mbxboolfalse~/.cache/mbx
custom[].namestring—Built-in names are reserved
custom[].container_pathstring—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.

toml
1[storage.host_caches]
2npm = true
3mbx = true
4 
5[[storage.host_caches.custom]]
6name = "zig"
7host_path = "~/.cache/zig"
8container_path = "~/.cache/zig"
KeyTypeDefaultDescription
cargoboolfalse~/.cargo/registry and ~/.cargo/git
npm, pnpm, pip, uv, yarn, bun, composer, maven, gradle, ccache, go, rustup, mbxboolfalseThat tool's cache; see the table above for paths
custom[].namestring—Label used in error messages; built-in names are reserved
custom[].host_pathstring—Path on the host, ~/… or absolute
custom[].container_pathstring—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].extra is refused. Two mounts for one destination would fail inside Podman with an opaque error. A hand-written mounts.extra entry keeps working unchanged.

[integration]

Controls which host resources are shared with the container.

KeyTypeDefaultDescription
waylandboolfalseShare Wayland socket for GUI apps
audioboolfalseShare PipeWire/PulseAudio sockets
gpustring/bool"auto"GPU passthrough (true, false, "auto", "nvidia")
dbusboolfalseEnable D-Bus session bus access. Talk/own/preset rules without it are rejected
notifyboolfalseDesktop notification forwarding
xdg_openboolfalseURI opening via host (xdg-open)
clipboardboolfalseClipboard sharing
sync_fontsboolfalseBind-mount ~/.fonts and ~/.local/share/fonts (read-only) when present on the host
sync_iconsboolfalseBind-mount ~/.icons and ~/.local/share/icons (read-only) when present on the host
sync_themesboolfalseBind-mount ~/.themes and ~/.local/share/themes (read-only) when present on the host
gpg_agentboolfalseForward GPG agent socket (S.gpg-agent). Sets GPG_TTY and GNUPGHOME
git_identityboolfalseBridge 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_exectable{ enabled = false }Host command execution (see [integration.host_exec] below)
ssh_agentboolfalseForward SSH agent socket ($SSH_AUTH_SOCK). Requires Podman ≥ 5.6

GpuMode values

TOML valueMeaning
"auto" (default)Detect available GPU devices at runtime
trueEnable /dev/dri (Intel/AMD)
falseDisable 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.

KeyTypeDefaultPassed through
kvmboolfalse/dev/kvm — nested virtualisation, Android emulators
joystickboolfalse/dev/input, /dev/uinput — gamepads and joysticks
webcamboolfalse/dev/video*, /dev/media*
serialboolfalse/dev/ttyUSB*, /dev/ttyACM* — microcontrollers
yubikeyboolfalsepcscd socket and /dev/hidraw* — smartcards, 2FA
toml
[integration.hardware]
webcam = true
kvm = true

podbox doctor reports when the host is missing a device these would need.

[integration.host_exec]

KeyTypeDefaultDescription
enabledboolfalseAllow container to execute commands on the host
allowlisttable(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:

toml
[integration.host_exec]
enabled = true
allowlist = { 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]

KeyTypeDefaultDescription
documentsboolfalseMount host ~/Documents
downloadsboolfalseMount host ~/Downloads
picturesboolfalseMount host ~/Pictures
musicboolfalseMount host ~/Music
videosboolfalseMount host ~/Videos
desktopboolfalseMount host ~/Desktop
projectsboolfalseMount host ~/Projects

[integration.export]

KeyTypeDefaultDescription
appsstring[][]App .desktop files to export (without .desktop suffix)
binsstring[][]Binary shims to generate in ~/.local/bin
toml
1[integration]
2wayland = true
3audio = true
4gpu = "auto"
5dbus = true
6notify = true
7xdg_open = true
8clipboard = true
9ssh_agent = true
10sync_fonts = true
11sync_icons = true
12sync_themes = true
13 
14[integration.host_exec]
15enabled = true
16allowlist = { git = "/usr/bin/git" }
17 
18[integration.xdg_dirs]
19documents = true
20downloads = true
21projects = true
22 
23[integration.export]
24apps = ["gedit", "nautilus"]
25bins = ["rg", "gcc"]

[lifecycle]

KeyTypeDefaultDescription
quadletboolfalseGenerate Quadlet systemd files on podbox enable
autostartboolfalseStart container on user login (WantedBy=default.target)
on_stopstring"keep"Container behavior on stop ("keep" or "remove")
auto_updateboolfalseAdd Label=io.containers.autoupdate=registry for auto-updates
auto_checkpointboolfalseTag the current image as checkpoint-prev before update or build --rebuild; podbox rollback restores that image
idle_timeoutstring"off"Idle timeout before guest daemon exits ("off", "30s", "5m", `"1h")
toml
1[lifecycle]
2quadlet = true
3autostart = true
4on_stop = "keep"
5auto_update = true
6idle_timeout = "off"

[systemd]

Custom systemd unit dependencies for the generated Quadlet.

KeyTypeDefaultDescription
requiresstring[][]Units that must be active before the container (Requires=)
afterstring[][]Units the container should start after (After=)
toml
[systemd]
requires = ["postgres.service", "redis.service"]
after = ["network-online.target"]

[dbus]

D-Bus access control via xdg-dbus-proxy. Requires integration.dbus = true.

KeyTypeDefaultDescription
presetstring""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)
talkstring[][]D-Bus services the container can call (two-way). Adding a portal-family name re-grants the full portal surface — a warning is printed
ownstring[][]D-Bus services the container can register on the host bus
toml
[dbus]
preset = "gnome"
toml
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.

KeyTypeDefaultDescription
firewallbooltrueFilter Wayland protocol access through the compositor proxy
blocked_interfacesstring[](see below)Wayland globals to deny. Replaces the default list when set
toml
1[wayland]
2firewall = true
3blocked_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.

toml
1# ── Image ──────────────────────────────────────────────
2[image]
3base = "fedora:44" # Base image for custom builds
4name = "myenv" # Image tag name
5image = "ghcr.io/user/myenv:latest" # Prebuilt ref (omit for custom builds)
6pull_retry = 3 # Pull retry count
7pull_retry_delay = "5s" # Delay between pull retries
8 
9[image.packages]
10install = ["git", "gcc", "ripgrep"]
11remove = ["vim-minimal"]
12# manager = "pacman" # omitted = auto-detect from the image name
13 
14[image.run]
15commands = ["dnf clean all"] # Extra RUN steps
16 
17# ── Container ──────────────────────────────────────────
18[container]
19name = "myenv" # Required; used for unit names and socket paths
20home = "~/containers/myenv" # Required; isolated home directory (~ expands)
21shell = "bash" # Default login shell
22memory = "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 time
33 
34# ── Caches ─────────────────────────────────────────────
35# Both opt-in, same keys, different sources:
36# shared_caches → podbox volumes, shared between podbox containers
37# host_caches → a directory you already keep on the host, bind-mounted in
38# rustup and the ~/.cargo/bin half of cargo hold libc-bound binaries and are
39# never shared.
40[storage.shared_caches]
41cargo = true
42npm = true
43mbx = false
44 
45[storage.host_caches]
46mbx = false # ~/.cache/mbx on both sides
47 
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 image
63 
64# [[security.secrets]] # detailed form when you need type/target/mode
65# 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 container
73run = "mise use -g rust@stable" # executed with sh -c from the home
74timeout = "5m" # 30s / 5m / 1h (default 5m)
75on_failure = "warn" # warn (default) or abort
76env = { FOO = "bar" } # extra variables; no PODBOX_ keys
77root = false # run as root inside the container
78 
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 socket
87audio = true # Share PipeWire / PulseAudio
88gpu = "auto" # GPU: true, false, "auto", "nvidia"
89dbus = true # Enable D-Bus session bus
90notify = true # Forward desktop notifications
91xdg_open = true # Forward URI opening (xdg-open)
92clipboard = true # Clipboard sharing
93ssh_agent = false # Forward SSH agent (needs Podman ≥ 5.6)
94gpg_agent = false # Forward GPG agent
95sync_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 = false
101kvm = false
102 
103[integration.host_exec]
104enabled = false
105allowlist = { git = "/usr/bin/git" } # Alias → absolute path (required when enabled)
106 
107[integration.xdg_dirs]
108documents = false
109downloads = false
110pictures = false
111music = false
112videos = false
113desktop = false
114projects = false
115 
116[integration.export]
117apps = ["gedit", "nautilus"] # Export .desktop files for these apps
118bins = ["rg", "gcc"] # Create bin shims for these commands
119 
120# ── Lifecycle ──────────────────────────────────────────
121[lifecycle]
122quadlet = false # Generate systemd Quadlet files on enable
123autostart = false # Start container on user login
124on_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 firewall
142blocked_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.

podbox — declarative Linux container environments
Licensed under MIT. Open Source by bethropolis.