Skip to main content
podbox/docs/architecture.md
On this page

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.

How podbox works

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.

Codegen pipeline

Generated Containerfile

dockerfile
1FROM fedora:44
2 
3# [image.packages]
4RUN dnf install -y git gcc ripgrep && dnf clean all
5 
6# [image.run] custom steps
7RUN dnf clean all
8 
9# podbox integration layer — always last
10COPY podbox-guest /usr/local/bin/podbox-guest
11RUN chmod +x /usr/local/bin/podbox-guest
12 
13ENV PODBOX_CONTAINER=myenv
14ENV PODBOX_HOST_VERSION=0.5.0
15ENV LANG=en_US.UTF-8
16ENV LC_ALL=en_US.UTF-8
17ENTRYPOINT ["/usr/local/bin/podbox-guest", "--entry"]
18CMD ["/usr/bin/fish"]

Build Context Layout

bash
~/.local/share/podbox/<name>/
├── Containerfile
├── podbox-guest # static musl binary from host

Generated Quadlet Files

Files written to ~/.config/containers/systemd/:

myenv.build

ini
[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

ini
1[Unit]
2Description=podbox host-guest socket — myenv
3 
4[Socket]
5ListenStream=%t/podbox/myenv.sock
6Service=myenv-host.service
7SocketMode=0600
8DirectoryMode=0700
9 
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):

SettingValuePurpose
UserNSkeep-idMaps host UID/GID into container
UserrootRun as root (UID mapped via UserNS)
SecurityLabelDisabletrueRequired for Wayland socket access
NoNewPrivilegestrueBlock setuid escalation (sudo, su)
PodmanArgs--initcatatonit as PID 1 (zombie reaping)
PodmanArgs--workdir=/home/%uDefault working directory
Volume<context>/.flatpak-info:/.flatpak-info:roSandbox detection marker (portals)
Volume%h/containers/<name>:/home/%u:ZIsolated home (never the host home)
Volume%t/podbox/<name>.sock:%t/podbox/<name>.sockHost-guest socket
EnvironmentHOST_USER, HOST_UID, HOST_GIDHost identity injected
EnvironmentPATH=/run/podbox/bin:…Interceptor directory prepended
Restarton-failureAuto-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).

Host-guest socket protocol

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 asMode
podbox-guest --entryFork daemon, exec user shell/command
podbox-guest --daemonEvent 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_CONTAINER env → derive socket paths
  • Create /run/podbox/bin/ directory
  • Check version drift — compare PODBOX_HOST_VERSION against 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/bin to $PATH via /etc/profile.d/podbox.sh and /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)

Runtime flow

Project Structure

bash
1podbox/
2├── Cargo.toml # workspace root
3├── crates/
4│ ├── podbox/ # host CLI binary
5│ │ └── src/
6│ │ ├── main.rs / cli.rs # entry point, argument parsing
7│ │ ├── codegen/ # pure string generators (Containerfile, Quadlet)
8│ │ ├── commands/ # one module per subcommand
9│ │ ├── config/ # TOML parsing, types, validation, defaults
10│ │ ├── compositor/ # Wayland firewall proxy
11│ │ ├── export/ # .desktop + bin shim export
12│ │ ├── quadlet_install/ # Quadlet file installation
13│ │ ├── socket_host/ # host-side socket server
14│ │ ├── systemd/ # systemctl wrappers
15│ │ ├── wizard/ # interactive setup wizard
16│ │ └── … # podman, profiles, env, history, xdg, …
17│ ├── podbox-guest/ # static musl sidecar (entry, daemon, interceptors)
18│ ├── podbox-protocol/ # shared wire-format types
19│ └── podbox-wasm/ # pure core compiled for the Studio
20├── tests/ # integration + unit tests
21├── scripts/ # install / uninstall
22└── docs/ # documentation

Per-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-guest must stay statically linkable — no tokio, no openssl, nothing glibc-linked. poll() + pidfds.
  • TTY: shell and exec use CommandExt::exec() to replace the process, never spawn_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

CodeMeaning
0Success
1General error
2Configuration error
3Container missing
4Build or inspect failure
5Missing dependency (podman not found)
6Pull or tag failure
podbox — declarative Linux container environments
Licensed under MIT. Open Source by bethropolis.