Architecture¶
How It Works¶
A definition TOML is the single source of truth. Everything podbox generates — Containerfiles, Quadlet systemd units, lock files, desktop entries — derives from this one file. The user never writes a raw Containerfile or systemd unit manually.
Codegen Pipeline¶
podbox build runs these steps in order. Each codegen step is a pure function:
data in, string out, no I/O. Orchestration (file writes, podman invocations) is
separate.
Generated Containerfile¶
FROM fedora:44
# [image.packages]
RUN dnf install -y git gcc ripgrep && dnf clean all
# [image.run] custom steps
RUN dnf clean all
# podbox integration layer — always last
COPY podbox-guest /usr/local/bin/podbox-guest
RUN chmod +x /usr/local/bin/podbox-guest
ENV PODBOX_CONTAINER=myenv
ENV PODBOX_HOST_VERSION=0.5.0
ENV LANG=en_US.UTF-8
ENV LC_ALL=en_US.UTF-8
ENTRYPOINT ["/usr/local/bin/podbox-guest", "--entry"]
CMD ["/usr/bin/fish"]
Build Context Layout¶
Generated Quadlet Files¶
Files written to ~/.config/containers/systemd/:
myenv.build¶
[Build]
ImageTag=localhost/podbox-myenv:latest
File=/home/user/.local/share/podbox/myenv/Containerfile
The .build unit makes myenv.service depend on the build. Images are only
rebuilt when the Containerfile changes.
myenv.socket¶
[Unit]
Description=podbox host-guest socket — myenv
[Socket]
ListenStream=%t/podbox/myenv.sock
Service=myenv-host.service
SocketMode=0600
DirectoryMode=0700
[Install]
WantedBy=sockets.target
%t is systemd's specifier for $XDG_RUNTIME_DIR. The socket is created
before the container starts and persists across restarts.
myenv.container¶
Key Quadlet settings (see quadlet.md for full list):
| Setting | Value | Purpose |
|---|---|---|
UserNS |
keep-id |
Maps host UID/GID into container |
User |
root |
Run as root (UID mapped via UserNS) |
SecurityLabelDisable |
true |
Required for Wayland socket access |
NoNewPrivileges |
true |
Block setuid escalation (sudo, su) |
PodmanArgs |
--init |
catatonit as PID 1 (zombie reaping) |
PodmanArgs |
--workdir=/home/%u |
Default working directory |
Volume |
<context>/.flatpak-info:/.flatpak-info:ro |
Sandbox detection marker (portals) |
Volume |
%h/containers/<name>:/home/%u:Z |
Isolated home (never the host home) |
Volume |
%t/podbox/<name>.sock:%t/podbox/<name>.sock |
Host-guest socket |
Environment |
HOST_USER, HOST_UID, HOST_GID |
Host identity injected |
Environment |
PATH=/run/podbox/bin:… |
Interceptor directory prepended |
Restart |
on-failure |
Auto-restart on crash |
Volumes for Wayland, audio, D-Bus, XDG dirs, GPU devices, and theme/icon/font sync are added conditionally based on the config.
Host-Guest Socket Protocol¶
The guest daemon connects to a Unix socket on the host to bridge container capabilities. Messages are length-prefixed JSON (see protocol.md for the wire format).
Guest Daemon (podbox-guest)¶
The guest binary is a static musl binary baked into every built image.
Its behavior is determined by argv[0]:
| Invoked as | Mode |
|---|---|
podbox-guest --entry |
Fork daemon, exec user shell/command |
podbox-guest --daemon |
Event loop, interceptor setup |
notify-send (symlink) |
Parse args, forward to daemon |
xdg-open (symlink) |
Parse args, forward to daemon |
podbox-clipboard (symlink) |
Read stdin / write stdout for clipboard |
host-exec (symlink) |
Execute command on host, relay output |
Daemon startup sequence¶
- Read
PODBOX_CONTAINERenv → derive socket paths - Create
/run/podbox/bin/directory - Check version drift — compare
PODBOX_HOST_VERSIONagainst podbox-guest version - Connect to host socket (3 retries × 500ms)
- Handshake: send capabilities, receive accepted list and idle timeout
- Install interceptor symlinks in
/run/podbox/bin/for accepted capabilities - Prepend
/run/podbox/binto$PATHvia/etc/profile.d/podbox.shand/etc/fish/conf.d/podbox.fish - Enter event loop (poll + pidfd-based, 0% CPU when idle, configurable idle timeout)
If the socket is absent at startup, the daemon logs a warning and exits cleanly. The container continues running without integration — this is intentional.
UID Mapping¶
UserNS=keep-id + User=root creates an idmapped mount that shifts UIDs by 1
inside the container (host UID 1000 → container UID 999). The entrypoint reads
the actual home owner and makes the directory world-writable. No chown is
performed on bind-mounted directories — that would corrupt host ownership
through the idmapped mount.
Runtime Flow (Full Sequence)¶
Project Structure¶
podbox/
├── Cargo.toml # workspace root
├── crates/
│ ├── podbox/ # host CLI binary
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── main.rs # entry point, dispatch
│ │ ├── lib.rs # module declarations
│ │ ├── cli.rs # clap CLI definition
│ │ ├── build.rs # Containerfile generation + build orchestration
│ │ ├── cli.rs # clap CLI definition
│ │ ├── compositor.rs # Wayland firewall proxy
│ │ ├── config/ # TOML parsing, types, validation, defaults
│ │ │ ├── mod.rs
│ │ │ ├── types.rs # all config structs
│ │ │ ├── enums.rs # PackageManager, GpuMode, OnStop, XdgDirValue
│ │ │ ├── fs.rs # config discovery, active context
│ │ │ ├── defaults.rs # embedded default + helper functions
│ │ │ └── validation.rs # config validation
│ │ ├── codegen/ # pure string generators
│ │ │ ├── mod.rs
│ │ │ ├── quadlet.rs # .container, .build, .socket, .service gen
│ │ │ ├── containerfile.rs# Containerfile generation
│ │ │ └── distros.rs # distro family detection, base packages
│ │ ├── commands/ # command implementations
│ │ │ ├── mod.rs
│ │ │ ├── clone.rs
│ │ │ ├── context.rs
│ │ │ ├── create.rs
│ │ │ ├── definition.rs
│ │ │ ├── diff.rs
│ │ │ ├── export.rs
│ │ │ ├── inspect.rs
│ │ │ ├── lifecycle.rs # build, enable, disable, start, stop, remove
│ │ │ ├── pull.rs
│ │ │ ├── runtime.rs # shell, enter, exec, run, status, logs
│ │ │ ├── serve.rs
│ │ │ ├── stats.rs
│ │ │ └── translate.rs
│ │ ├── diff.rs # package drift detection
│ │ ├── editor.rs # editor resolution
│ │ ├── env.rs # host env resolution (GPU, audio, locale)
│ │ ├── error.rs # error types
│ │ ├── export.rs # .desktop + bin shim export
│ │ ├── guest.rs # guest binary installation
│ │ ├── labels.rs # image label defaults
│ │ ├── lock.rs # build lock file
│ │ ├── podman.rs # version detection + subcommand wrappers
│ │ ├── process.rs # exec_replace, run_piped, spawn
│ │ ├── profiles.rs # named config templates (bundled + custom)
│ │ ├── profiles/ # built-in profile TOMLs
│ │ │ ├── cachy.toml
│ │ │ ├── dev.toml
│ │ │ └── fedora.toml
│ │ ├── protocol.rs # host-side protocol handler
│ │ ├── quadlet_install.rs# Quadlet file installation
│ │ ├── socket_host/ # host-side socket handler
│ │ │ ├── handlers.rs
│ │ ├── systemd.rs # systemctl wrappers
│ │ ├── wizard/ # interactive setup wizard
│ │ │ ├── mod.rs
│ │ │ ├── prompts.rs
│ │ │ ├── shell.rs
│ │ │ └── summary.rs
│ │ └── xdg.rs # XDG dir resolution
│ │
│ ├── podbox-guest/ # static musl sidecar
│ │ ├── Cargo.toml
│ │ └── src/
│ │ ├── main.rs # argv[0] dispatch
│ │ ├── lib.rs # module declarations
│ │ ├── entry.rs # fork + exec
│ │ ├── daemon.rs # event loop (poll + pidfd)
│ │ ├── socket.rs # socket I/O
│ │ ├── protocol.rs # message types + framing (re-exports)
│ │ ├── interceptors/ # notify, xdg_open, clipboard, host_exec
│ │ └── error.rs
│ │
│ └── podbox-protocol/ # shared wire-format types
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs # GuestMessage, HostMessage, read/write_frame
│
├── tests/ # integration + unit tests
├── scripts/ # install / uninstall
└── docs/ # documentation
Key architectural rules¶
- Pure codegen: All
codegen::*functions are pure — data in, string out. No I/O, no env reads, no filesystem access. - Boundary separation: I/O lives only in
commands/,build.rs,quadlet_install.rs,socket_host.rs,export.rs. - musl static:
podbox-guestmust stay statically linkable. No tokio, no openssl, no crate that links against glibc. Usespoll()+ pidfds. - exec_replace for TTY:
podbox shellandpodbox execuseCommandExt::exec()to replace the process — neverspawn_interactive. This preserves the TTY for readline, Ctrl+L, etc. - pidfd-based process tracking: The guest daemon uses
pidfd_open()(Linux 5.3+) andpoll()to watch user process exits without busy-looping. - Config as single source of truth: Containerfile, Quadlet units, lock files, and desktop entries all derive from one TOML definition.
Exit Codes¶
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Configuration error |
| 3 | Container missing |
| 4 | Build or inspect failure |
| 5 | Missing dependency (podman not found) |
| 6 | Pull or tag failure |