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
1FROM fedora:442 3# [image.packages]4RUN dnf install -y git gcc ripgrep && dnf clean all5 6# [image.run] custom steps7RUN dnf clean all8 9# podbox integration layer — always last10COPY podbox-guest /usr/local/bin/podbox-guest11RUN chmod +x /usr/local/bin/podbox-guest12 13ENV PODBOX_CONTAINER=myenv14ENV PODBOX_HOST_VERSION=0.5.015ENV LANG=en_US.UTF-816ENV LC_ALL=en_US.UTF-817ENTRYPOINT ["/usr/local/bin/podbox-guest", "--entry"]18CMD ["/usr/bin/fish"]Build Context Layout
~/.local/share/podbox/<name>/├── Containerfile├── podbox-guest # static musl binary from hostGenerated Quadlet Files
Files written to ~/.config/containers/systemd/:
myenv.build
[Build]ImageTag=localhost/podbox-myenv:latestFile=/home/user/.local/share/podbox/myenv/ContainerfileThe .build unit makes myenv.service depend on the build. Images are only
rebuilt when the Containerfile changes.
myenv.socket
1[Unit]2Description=podbox host-guest socket — myenv3 4[Socket]5ListenStream=%t/podbox/myenv.sock6Service=myenv-host.service7SocketMode=06008DirectoryMode=07009 10[Install]11WantedBy=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
1podbox/2├── Cargo.toml # workspace root3├── crates/4│ ├── podbox/ # host CLI binary5│ │ └── src/6│ │ ├── main.rs / cli.rs # entry point, argument parsing7│ │ ├── codegen/ # pure string generators (Containerfile, Quadlet)8│ │ ├── commands/ # one module per subcommand9│ │ ├── config/ # TOML parsing, types, validation, defaults10│ │ ├── compositor/ # Wayland firewall proxy11│ │ ├── export/ # .desktop + bin shim export12│ │ ├── quadlet_install/ # Quadlet file installation13│ │ ├── socket_host/ # host-side socket server14│ │ ├── systemd/ # systemctl wrappers15│ │ ├── wizard/ # interactive setup wizard16│ │ └── … # podman, profiles, env, history, xdg, …17│ ├── podbox-guest/ # static musl sidecar (entry, daemon, interceptors)18│ ├── podbox-protocol/ # shared wire-format types19│ └── podbox-wasm/ # pure core compiled for the Studio20├── tests/ # integration + unit tests21├── scripts/ # install / uninstall22└── docs/ # documentationPer-file listings rot on every refactor — this one already did — so the
tree stops at directories. find crates/<crate>/src -name '*.rs' fills in
the rest.
Contributor notes
- Pure codegen:
codegen::*functions are pure — data in, string out. No I/O, no env reads. - Boundary separation: I/O lives in the thin modules (
commands/,build/,quadlet_install/,socket_host/,export/,systemd/). - Visibility: submodule internals are
pub(crate); the parent module re-exports the public surface. - musl static:
podbox-guestmust stay statically linkable — no tokio, no openssl, nothing glibc-linked.poll()+ pidfds. - TTY:
shellandexecuseCommandExt::exec()to replace the process, neverspawn_interactive— preserves readline, Ctrl+L, etc. - 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 |