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
ENTRYPOINT ["/usr/local/bin/podbox-guest", "--entry"]
CMD ["/usr/bin/bash"]
Build Context Layout¶
Generated Quadlet Files¶
Three 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
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 |
SecurityLabelDisable |
true |
Required for Wayland socket access |
PodmanArgs |
--init |
catatonit as PID 1 (zombie reaping) |
Volume |
%h/containers/<name>:/root:Z |
Isolated home (never the host home) |
Restart |
on-failure |
Auto-restart on crash |
Volumes for Wayland, audio, D-Bus, XDG dirs, and the host-guest socket 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 |
host-exec (symlink) |
Execute command on host, relay output |
Daemon startup sequence¶
- Read
PODBOX_CONTAINERenv → derive socket paths - Create
/run/podbox/bin/directory - Connect to host socket (3 retries × 500ms)
- Handshake: send capabilities, receive accepted list
- Install interceptor symlinks in
/run/podbox/bin/ - Prepend
/run/podbox/binto$PATHvia/etc/environment.d/podbox.conf - Enter event loop (poll-based, 0% CPU when idle, 5-min 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
│ │ ├── cli.rs # clap CLI definition
│ │ ├── config.rs # TOML parsing + validation
│ │ ├── build.rs # build orchestration
│ │ ├── codegen/ # pure string generators
│ │ ├── export.rs # .desktop + bin shim
│ │ ├── quadlet_install.rs
│ │ ├── socket_host.rs # host-side socket handler
│ │ ├── podman.rs # version detection + subcommand wrappers
│ │ ├── process.rs # exec_replace, run_piped, spawn
│ │ ├── lock.rs # build lock file
│ │ ├── env.rs # host env resolution
│ │ ├── xdg.rs # XDG dir resolution
│ │ └── error.rs # error types
│ │
│ └── podbox-guest/ # static musl sidecar
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # argv[0] dispatch
│ ├── entry.rs # fork + exec
│ ├── daemon.rs # event loop
│ ├── socket.rs # socket I/O
│ ├── protocol.rs # message types + framing
│ ├── interceptors/ # notify, xdg_open, clipboard, host_exec
│ └── error.rs
│
├── 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
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. - exec_replace for TTY:
podbox shellandpodbox execuseCommandExt::exec()to replace the process — neverspawn_interactive. This preserves the TTY for readline, Ctrl+L, etc.
Exit Codes¶
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | General error |
| 2 | Configuration error |
| 3 | Container missing |
| 4 | Build or inspect failure |
| 5 | Missing dependency |